From c3b2a8cb2755c1456746c03c9935a38970ad1503 Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Thu, 8 Feb 2024 22:37:52 -0500 Subject: [PATCH 001/411] Quick Seamless Fixes --- invokeai/backend/model_management/seamless.py | 87 ++++++++----------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/invokeai/backend/model_management/seamless.py b/invokeai/backend/model_management/seamless.py index bfdf9e0c53..3ab2db1d90 100644 --- a/invokeai/backend/model_management/seamless.py +++ b/invokeai/backend/model_management/seamless.py @@ -28,68 +28,49 @@ def _conv_forward_asymmetric(self, input, weight, bias): def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]): try: to_restore = [] + skipped_layers = 0 + skip_second_resnet = True + skip_conv2 = True for m_name, m in model.named_modules(): - if isinstance(model, UNet2DConditionModel): - if ".attentions." in m_name: + if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): + continue + + if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name: + # down_blocks.1.resnets.1.conv1 + _, block_num, _, resnet_num, submodule_name = m_name.split(".") + block_num = int(block_num) + resnet_num = int(resnet_num) + + # if block_num >= seamless_down_blocks: + if block_num >= len(model.down_blocks) - skipped_layers: continue - if ".resnets." in m_name: - if ".conv2" in m_name: - continue - if ".conv_shortcut" in m_name: - continue - - """ - if isinstance(model, UNet2DConditionModel): - if False and ".upsamplers." in m_name: + if resnet_num > 0 and skip_second_resnet: continue - if False and ".downsamplers." in m_name: + if submodule_name == "conv2" and skip_conv2: continue - if True and ".resnets." in m_name: - if True and ".conv1" in m_name: - if False and "down_blocks" in m_name: - continue - if False and "mid_block" in m_name: - continue - if False and "up_blocks" in m_name: - continue + m.asymmetric_padding_mode = {} + m.asymmetric_padding = {} + m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" + m.asymmetric_padding["x"] = ( + m._reversed_padding_repeated_twice[0], + m._reversed_padding_repeated_twice[1], + 0, + 0, + ) + m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" + m.asymmetric_padding["y"] = ( + 0, + 0, + m._reversed_padding_repeated_twice[2], + m._reversed_padding_repeated_twice[3], + ) - if True and ".conv2" in m_name: - continue - - if True and ".conv_shortcut" in m_name: - continue - - if True and ".attentions." in m_name: - continue - - if False and m_name in ["conv_in", "conv_out"]: - continue - """ - - if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): - m.asymmetric_padding_mode = {} - m.asymmetric_padding = {} - m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" - m.asymmetric_padding["x"] = ( - m._reversed_padding_repeated_twice[0], - m._reversed_padding_repeated_twice[1], - 0, - 0, - ) - m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" - m.asymmetric_padding["y"] = ( - 0, - 0, - m._reversed_padding_repeated_twice[2], - m._reversed_padding_repeated_twice[3], - ) - - to_restore.append((m, m._conv_forward)) - m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) + to_restore.append((m, m._conv_forward)) + m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) yield From 3339ad4df80f250c0f19779fdb8561f0353c5fc4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:34:06 +1100 Subject: [PATCH 002/411] feat(nodes): seamless.py minor cleanup --- invokeai/backend/model_management/seamless.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/backend/model_management/seamless.py b/invokeai/backend/model_management/seamless.py index 3ab2db1d90..e145c6f481 100644 --- a/invokeai/backend/model_management/seamless.py +++ b/invokeai/backend/model_management/seamless.py @@ -1,10 +1,11 @@ from __future__ import annotations from contextlib import contextmanager -from typing import List, Union +from typing import Callable, List, Union import torch.nn as nn -from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel def _conv_forward_asymmetric(self, input, weight, bias): @@ -26,12 +27,9 @@ def _conv_forward_asymmetric(self, input, weight, bias): @contextmanager def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]): + # Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor + to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = [] try: - to_restore = [] - skipped_layers = 0 - skip_second_resnet = True - skip_conv2 = True - for m_name, m in model.named_modules(): if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): continue @@ -42,14 +40,16 @@ def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axe block_num = int(block_num) resnet_num = int(resnet_num) - # if block_num >= seamless_down_blocks: - if block_num >= len(model.down_blocks) - skipped_layers: + # Could be configurable to allow skipping arbitrary numbers of down blocks + if block_num >= len(model.down_blocks): continue - if resnet_num > 0 and skip_second_resnet: + # Skip the second resnet (could be configurable) + if resnet_num > 0: continue - if submodule_name == "conv2" and skip_conv2: + # Skip Conv2d layers (could be configurable) + if submodule_name == "conv2": continue m.asymmetric_padding_mode = {} From 8a147bd6e6ccb336ca5eeecf5a70c8b667724d4b Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Tue, 13 Feb 2024 11:53:49 -0500 Subject: [PATCH 003/411] added sortable to linear view, not saving yet --- invokeai/frontend/web/package.json | 1 + invokeai/frontend/web/pnpm-lock.yaml | 15 +++++++ .../features/dnd/components/AppDndContext.tsx | 2 + .../web/src/features/dnd/types/index.ts | 14 ++++++- .../Invocation/fields/LinearViewField.tsx | 13 +++++++ .../sidePanel/workflow/WorkflowLinearTab.tsx | 39 +++++++++++++------ .../src/features/nodes/store/workflowSlice.ts | 7 ++++ 7 files changed, 78 insertions(+), 13 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index e583169af7..cd95183c7a 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -52,6 +52,7 @@ "@chakra-ui/react-use-size": "^2.1.0", "@dagrejs/graphlib": "^2.1.13", "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.0.16", "@invoke-ai/ui-library": "^0.0.18", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index ba76a61275..1d9083d1b4 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -22,6 +22,9 @@ dependencies: '@dnd-kit/core': specifier: ^6.1.0 version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@18.2.0) @@ -2884,6 +2887,18 @@ packages: tslib: 2.6.2 dev: false + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + /@dnd-kit/utilities@3.2.2(react@18.2.0): resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} peerDependencies: diff --git a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx index f800e5c869..a28b865366 100644 --- a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx +++ b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx @@ -19,6 +19,7 @@ const AppDndContext = (props: PropsWithChildren) => { const handleDragStart = useCallback( (event: DragStartEvent) => { + console.log('handling drag start', event.active.data.current); log.trace({ dragData: parseify(event.active.data.current) }, 'Drag started'); const activeData = event.active.data.current; if (!activeData) { @@ -31,6 +32,7 @@ const AppDndContext = (props: PropsWithChildren) => { const handleDragEnd = useCallback( (event: DragEndEvent) => { + console.log('handling drag end', event.active.data.current); log.trace({ dragData: parseify(event.active.data.current) }, 'Drag ended'); const overData = event.over?.data.current; if (!activeDragData || !overData) { diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 6e680b4ba9..d4dc59de98 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -80,6 +80,14 @@ export type NodeFieldDraggableData = BaseDragData & { }; }; +export type LinearViewFieldDraggableData = BaseDragData & { + payloadType: 'LINEAR_VIEW_FIELD'; + payload: { + nodeId: string; + fieldName: string; + }; +}; + export type ImageDraggableData = BaseDragData & { payloadType: 'IMAGE_DTO'; payload: { imageDTO: ImageDTO }; @@ -90,7 +98,11 @@ export type GallerySelectionDraggableData = BaseDragData & { payload: { boardId: BoardId }; }; -export type TypesafeDraggableData = NodeFieldDraggableData | ImageDraggableData | GallerySelectionDraggableData; +export type TypesafeDraggableData = + | NodeFieldDraggableData + | LinearViewFieldDraggableData + | ImageDraggableData + | GallerySelectionDraggableData; export interface UseDroppableTypesafeArguments extends Omit { data?: TypesafeDroppableData; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 3ec4ab1b42..49a676008a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -1,3 +1,5 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; @@ -25,6 +27,13 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); }, [dispatch, fieldName, nodeId]); + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: nodeId }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + return ( { w="full" p={4} flexDir="column" + ref={setNodeRef} + style={style} + {...attributes} + {...listeners} > diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx index 375b271c66..fa6bfff41d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx @@ -1,11 +1,15 @@ + +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { Box, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import IAIDroppable from 'common/components/IAIDroppable'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import type { TypesafeDroppableData } from 'features/dnd/types'; import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField'; import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; @@ -16,20 +20,31 @@ const WorkflowLinearTab = () => { const { isLoading } = useGetOpenAPISchemaQuery(); const { t } = useTranslation(); + const droppableData = useMemo( + () => ({ + id: 'current-image', + actionType: 'SET_CURRENT_IMAGE', + }), + [] + ); + return ( + - - {isLoading ? ( - - ) : fields.length ? ( - fields.map(({ nodeId, fieldName }) => ( - - )) - ) : ( - - )} - + field.nodeId)} strategy={verticalListSortingStrategy}> + + {isLoading ? ( + + ) : fields.length ? ( + fields.map(({ nodeId, fieldName }) => ( + + )) + ) : ( + + )} + + ); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 73802da54e..2418f65ceb 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -42,6 +42,13 @@ export const workflowSlice = createSlice({ state.exposedFields = state.exposedFields.filter((field) => !isEqual(field, action.payload)); state.isTouched = true; }, + workflowExposedFieldsReordered: (state, action: PayloadAction) => { + state.exposedFields = action.payload.split(',').map((id) => { + const [nodeId, fieldName] = id.split('.'); + return { nodeId, fieldName }; + }); + state.isTouched = true; + }, workflowNameChanged: (state, action: PayloadAction) => { state.name = action.payload; state.isTouched = true; From 3726293258b5c4e062e3c2bef9d2e0304a3ffdae Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:27:31 +1100 Subject: [PATCH 004/411] feat(nodes): improve types in graph.py Methods `get_node` and `complete` were typed as returning a dynamically created unions `InvocationsUnion` and `InvocationOutputsUnion`, respectively. Static type analysers cannot work with dynamic objects, so these methods end up as effectively un-annotated, returning `Unknown`. They now return `BaseInvocation` and `BaseInvocationOutput`, respectively, which are the superclasses of all members of each union. This gives us the best type annotation that is possible. Note: the return types of these methods are never introspected, so it doesn't really matter what they are at runtime. --- invokeai/app/services/shared/graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 80f56b49d3..1acf165aba 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -540,7 +540,7 @@ class Graph(BaseModel): except NodeNotFoundError: return False - def get_node(self, node_path: str) -> InvocationsUnion: + def get_node(self, node_path: str) -> BaseInvocation: """Gets a node from the graph using a node path.""" # Materialized graphs may have nodes at the top level graph, node_id = self._get_graph_and_node(node_path) @@ -891,7 +891,7 @@ class GraphExecutionState(BaseModel): # If next is still none, there's no next node, return None return next_node - def complete(self, node_id: str, output: InvocationOutputsUnion): + def complete(self, node_id: str, output: BaseInvocationOutput) -> None: """Marks a node as complete""" if node_id not in self.execution_graph.nodes: From 85bbf65967d14bc270dfbbfd2a18e929007c1d30 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 13 Feb 2024 12:29:38 -0500 Subject: [PATCH 005/411] only refetch intermediates on modal open if it is enabled --- .../system/components/SettingsModal/SettingsModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index cc937170a3..b7232ae33f 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -101,9 +101,11 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { const clearStorage = useClearStorage(); const handleOpenSettingsModel = useCallback(() => { - refetchIntermediatesCount(); + if (shouldShowClearIntermediates) { + refetchIntermediatesCount(); + } _onSettingsModalOpen(); - }, [_onSettingsModalOpen, refetchIntermediatesCount]); + }, [_onSettingsModalOpen, refetchIntermediatesCount, shouldShowClearIntermediates]); const handleClickResetWebUI = useCallback(() => { clearStorage(); From 9d6e4ff1fb7ee55024d70e9b11cdca1408d30e60 Mon Sep 17 00:00:00 2001 From: Mary Hipp Rogers Date: Wed, 14 Feb 2024 09:02:07 -0500 Subject: [PATCH 006/411] workflow tab (#5680) * new workflow tab UI - still using shared state with workflow editor tab * polish workflow details * remove workflow tab, add edit/view mode to workflow slice and get that working to switch between within editor tab * UI updates for view/edit mode * cleanup * add warning to view mode * lint * start with isTouched false * working on styling mode toggle * more UX iteration * lint * cleanup * save original field values to state, add indicator if they have been changed and give user choice to reset * lint * fix import and commit translation * dont switch to view mode when loading a workflow * warns before clearing editor * use folder icon * fix(ui): track do not erase value when resetting field value - When adding an exposed field, we need to add it to originalExposedFieldValues - When removing an exposed field, we need to remove it from originalExposedFieldValues - add `useFieldValue` and `useOriginalFieldValue` hooks to encapsulate related logic * feat(ui): use IconButton for workflow view/edit button * feat(ui): change icon for new workflow It was the same as the workflow tab icon, confusing bc you think it's going to somehow take you to the tab. * feat(ui): use render props for NewWorkflowConfirmationAlertDialog There was a lot of potentially sensitive logic shared between the new workflow button and menu items. Also, two instances of ConfirmationAlertDialog. Using a render prop deduplicates the logic & components * fix(ui): do not mark workflow touched when loading workflow This was occurring because the `nodesChanged` action is called by reactflow when loading a workflow. Specifically, it calculates and sets the node dimensions as it loads. The existing logic set `isTouched` whenever this action was called. The changes reactflow emits have types, and we can use the change types and data to determine if a change should result in the workflow being marked as touched. * chore(ui): lint * chore(ui): lint * delete empty file --------- Co-authored-by: Mary Hipp Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com> --- invokeai/frontend/web/public/locales/en.json | 4 + .../listeners/workflowLoadRequested.ts | 2 - .../fields/FieldLinearViewToggle.tsx | 7 +- .../Invocation/fields/LinearViewField.tsx | 15 +++- .../flow/panels/TopPanel/TopPanel.tsx | 14 ++- .../panels/TopPanel/UpdateNodesButton.tsx | 1 + .../flow/panels/TopPanel/WorkflowName.tsx | 15 ---- .../panels/TopRightPanel/TopRightPanel.tsx | 15 ---- .../nodes/components/sidePanel/ModeToggle.tsx | 43 +++++++++ .../sidePanel/NodeEditorPanelGroup.tsx | 58 ++++++++---- .../components/sidePanel/WorkflowMenu.tsx | 26 ++++++ .../components/sidePanel/WorkflowName.tsx | 37 ++++++++ .../sidePanel/viewMode/WorkflowField.tsx | 53 +++++++++++ .../viewMode/WorkflowInfoTooltipContent.tsx | 68 ++++++++++++++ .../sidePanel/viewMode/WorkflowViewMode.tsx | 39 ++++++++ .../sidePanel/viewMode/WorkflowWarning.tsx | 21 +++++ .../viewMode/WorkflowWarningTooltip.tsx | 20 +++++ .../sidePanel/workflow/WorkflowPanel.tsx | 6 +- .../nodes/hooks/useFieldOriginalValue.ts | 28 ++++++ .../src/features/nodes/hooks/useFieldValue.ts | 23 +++++ .../src/features/nodes/store/nodesSlice.ts | 7 +- .../web/src/features/nodes/store/types.ts | 9 +- .../src/features/nodes/store/workflowSlice.ts | 88 ++++++++++++++++--- .../features/ui/components/tabs/NodesTab.tsx | 25 ++++-- .../components/NewWorkflowButton.tsx | 26 ++++++ .../NewWorkflowConfirmationAlertDialog.tsx | 63 +++++++++++++ .../components/WorkflowLibraryButton.tsx | 4 +- .../NewWorkflowMenuItem.tsx | 64 +++----------- .../WorkflowLibraryMenu.tsx | 2 +- 29 files changed, 649 insertions(+), 134 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowInfoTooltipContent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowViewMode.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarning.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarningTooltip.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldOriginalValue.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts create mode 100644 invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowButton.tsx create mode 100644 invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0fe833db39..f991714c24 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -175,6 +175,7 @@ "statusUpscaling": "Upscaling", "statusUpscalingESRGAN": "Upscaling (ESRGAN)", "template": "Template", + "toResolve": "To resolve", "training": "Training", "trainingDesc1": "A dedicated workflow for training your own embeddings and checkpoints using Textual Inversion and Dreambooth from the web interface.", "trainingDesc2": "InvokeAI already supports training custom embeddourings using Textual Inversion using the main script.", @@ -900,6 +901,7 @@ "doesNotExist": "does not exist", "downloadWorkflow": "Download Workflow JSON", "edge": "Edge", + "editMode": "Edit in Workflow Editor", "enum": "Enum", "enumDescription": "Enums are values that may be one of a number of options.", "executionStateCompleted": "Completed", @@ -995,6 +997,7 @@ "problemReadingMetadata": "Problem reading metadata from image", "problemReadingWorkflow": "Problem reading workflow from image", "problemSettingTitle": "Problem Setting Title", + "resetToDefaultValue": "Reset to default value", "reloadNodeTemplates": "Reload Node Templates", "removeLinearView": "Remove from Linear View", "newWorkflow": "New Workflow", @@ -1067,6 +1070,7 @@ "vaeModelFieldDescription": "TODO", "validateConnections": "Validate Connections and Graph", "validateConnectionsHelp": "Prevent invalid connections from being made, and invalid graphs from being invoked", + "viewMode": "Use in Linear View", "unableToGetWorkflowVersion": "Unable to get workflow schema version", "unrecognizedWorkflowVersion": "Unrecognized workflow schema version {{version}}", "version": "Version", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index 0b37271be7..9307031e6d 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -6,7 +6,6 @@ import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/typ import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; -import { setActiveTab } from 'features/ui/store/uiSlice'; import { t } from 'i18next'; import { z } from 'zod'; import { fromZodError } from 'zod-validation-error'; @@ -53,7 +52,6 @@ export const addWorkflowLoadRequestedListener = () => { }); } - dispatch(setActiveTab('nodes')); requestAnimationFrame(() => { $flow.get()?.fitView(); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx index cb1bcba1fe..ff59e02916 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx @@ -1,6 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useFieldValue } from 'features/nodes/hooks/useFieldValue'; import { selectWorkflowSlice, workflowExposedFieldAdded, @@ -18,7 +19,7 @@ type Props = { const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - + const value = useFieldValue(nodeId, fieldName); const selectIsExposed = useMemo( () => createSelector(selectWorkflowSlice, (workflow) => { @@ -30,8 +31,8 @@ const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => { const isExposed = useAppSelector(selectIsExposed); const handleExposeField = useCallback(() => { - dispatch(workflowExposedFieldAdded({ nodeId, fieldName })); - }, [dispatch, fieldName, nodeId]); + dispatch(workflowExposedFieldAdded({ nodeId, fieldName, value })); + }, [dispatch, fieldName, nodeId, value]); const handleUnexposeField = useCallback(() => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 3ec4ab1b42..b3c563b1d3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -1,12 +1,13 @@ import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; +import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi'; +import { PiArrowCounterClockwiseBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi'; import EditableFieldTitle from './EditableFieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; @@ -19,8 +20,10 @@ type Props = { const LinearViewField = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); + const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); const { t } = useTranslation(); + const handleRemoveField = useCallback(() => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); }, [dispatch, fieldName, nodeId]); @@ -39,6 +42,16 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { + {isValueChanged && ( + } + /> + )} } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx index c87af124bf..a78024074c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx @@ -1,25 +1,23 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton'; import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton'; import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton'; -import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName'; -import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton'; +import { WorkflowName } from 'features/nodes/components/sidePanel/WorkflowName'; import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu'; import { memo } from 'react'; const TopCenterPanel = () => { + const name = useAppSelector((s) => s.workflow.name); return ( - - - - - + + - + {!!name.length && } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx index d356eaa4e1..9fa710bfb5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx @@ -25,6 +25,7 @@ const UpdateNodesButton = () => { icon={} onClick={handleClickUpdateNodes} pointerEvents="auto" + colorScheme="warning" /> ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx deleted file mode 100644 index 527147c67d..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Text } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { memo } from 'react'; - -const TopCenterPanel = () => { - const name = useAppSelector((s) => s.workflow.name); - - return ( - - {name} - - ); -}; - -export default memo(TopCenterPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx deleted file mode 100644 index be939f35bd..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Flex } from '@invoke-ai/ui-library'; -import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton'; -import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu'; -import { memo } from 'react'; - -const TopRightPanel = () => { - return ( - - - - - ); -}; - -export default memo(TopRightPanel); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx new file mode 100644 index 0000000000..555070d673 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx @@ -0,0 +1,43 @@ +import { Flex, IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { workflowModeChanged } from 'features/nodes/store/workflowSlice'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiEyeBold, PiPencilBold } from 'react-icons/pi'; + +export const ModeToggle = () => { + const dispatch = useAppDispatch(); + const mode = useAppSelector((s) => s.workflow.mode); + const { t } = useTranslation(); + + const onClickEdit = useCallback(() => { + dispatch(workflowModeChanged('edit')); + }, [dispatch]); + + const onClickView = useCallback(() => { + dispatch(workflowModeChanged('view')); + }, [dispatch]); + + return ( + + {mode === 'view' && ( + } + colorScheme="invokeBlue" + /> + )} + {mode === 'edit' && ( + } + colorScheme="invokeBlue" + /> + )} + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx index c145003c95..abd3d707d7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx @@ -1,22 +1,37 @@ import 'reactflow/dist/style.css'; import { Flex } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; import QueueControls from 'features/queue/components/QueueControls'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; +import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton'; import type { CSSProperties } from 'react'; import { memo, useCallback, useRef } from 'react'; import type { ImperativePanelGroupHandle } from 'react-resizable-panels'; import { Panel, PanelGroup } from 'react-resizable-panels'; import InspectorPanel from './inspector/InspectorPanel'; +import { WorkflowViewMode } from './viewMode/WorkflowViewMode'; import WorkflowPanel from './workflow/WorkflowPanel'; +import { WorkflowMenu } from './WorkflowMenu'; +import { WorkflowName } from './WorkflowName'; const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' }; +const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => { + return { + mode: workflow.mode, + }; +}); + const NodeEditorPanelGroup = () => { + const { mode } = useAppSelector(selector); const panelGroupRef = useRef(null); const panelStorage = usePanelStorage(); + const handleDoubleClickHandle = useCallback(() => { if (!panelGroupRef.current) { return; @@ -27,22 +42,33 @@ const NodeEditorPanelGroup = () => { return ( - - - - - - - - - + + + + + + + + + {mode === 'view' && } + {mode === 'edit' && ( + + + + + + + + + + )} ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx new file mode 100644 index 0000000000..33dd5dc83f --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx @@ -0,0 +1,26 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; +import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; +import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton'; + +import { ModeToggle } from './ModeToggle'; + +const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => { + return { + mode: workflow.mode, + }; +}); + +export const WorkflowMenu = () => { + const { mode } = useAppSelector(selector); + + return ( + + {mode === 'edit' && } + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx new file mode 100644 index 0000000000..14852945ab --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx @@ -0,0 +1,37 @@ +import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useTranslation } from 'react-i18next'; +import { PiDotOutlineFill } from 'react-icons/pi'; + +import WorkflowInfoTooltipContent from './viewMode/WorkflowInfoTooltipContent'; +import { WorkflowWarning } from './viewMode/WorkflowWarning'; + +export const WorkflowName = () => { + const { name, isTouched, mode } = useAppSelector((s) => s.workflow); + const { t } = useTranslation(); + + return ( + + {name.length ? ( + } placement="top"> + + {name} + + + ) : ( + + {t('workflows.unnamedWorkflow')} + + )} + + {isTouched && mode === 'edit' && ( + + + + + + )} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx new file mode 100644 index 0000000000..0e5857933a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx @@ -0,0 +1,53 @@ +import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; +import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent'; +import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer'; +import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel'; +import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; +import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle'; +import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; +import { t } from 'i18next'; +import { memo } from 'react'; +import { PiArrowCounterClockwiseBold, PiInfoBold } from 'react-icons/pi'; + +type Props = { + nodeId: string; + fieldName: string; +}; + +const WorkflowField = ({ nodeId, fieldName }: Props) => { + const label = useFieldLabel(nodeId, fieldName); + const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'input'); + const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName); + + return ( + + + {label || fieldTemplateTitle} + + + {isValueChanged && ( + } + /> + )} + } + openDelay={HANDLE_TOOLTIP_OPEN_DELAY} + placement="top" + > + + + + + + + + ); +}; + +export default memo(WorkflowField); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowInfoTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowInfoTooltipContent.tsx new file mode 100644 index 0000000000..1145af032e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowInfoTooltipContent.tsx @@ -0,0 +1,68 @@ +import { Box, Flex, Text } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => { + return { + name: workflow.name, + description: workflow.description, + notes: workflow.notes, + author: workflow.author, + tags: workflow.tags, + }; +}); + +const WorkflowInfoTooltipContent = () => { + const { name, description, notes, author, tags } = useAppSelector(selector); + const { t } = useTranslation(); + + return ( + + {!!name.length && ( + + {t('nodes.workflowName')} + + {name} + + + )} + {!!author.length && ( + + {t('nodes.workflowAuthor')} + + {author} + + + )} + {!!tags.length && ( + + {t('nodes.workflowTags')} + + {tags} + + + )} + {!!description.length && ( + + {t('nodes.workflowDescription')} + + {description} + + + )} + {!!notes.length && ( + + {t('nodes.workflowNotes')} + + {notes} + + + )} + + ); +}; + +export default memo(WorkflowInfoTooltipContent); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowViewMode.tsx new file mode 100644 index 0000000000..7bfe256702 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowViewMode.tsx @@ -0,0 +1,39 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; +import { t } from 'i18next'; +import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; + +import WorkflowField from './WorkflowField'; + +const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => { + return { + fields: workflow.exposedFields, + name: workflow.name, + }; +}); + +export const WorkflowViewMode = () => { + const { isLoading } = useGetOpenAPISchemaQuery(); + const { fields } = useAppSelector(selector); + return ( + + + + {isLoading ? ( + + ) : fields.length ? ( + fields.map(({ nodeId, fieldName }) => ( + + )) + ) : ( + + )} + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarning.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarning.tsx new file mode 100644 index 0000000000..d5182ddd62 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarning.tsx @@ -0,0 +1,21 @@ +import { Flex, Icon, Tooltip } from '@invoke-ai/ui-library'; +import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate'; +import { PiWarningBold } from 'react-icons/pi'; + +import { WorkflowWarningTooltip } from './WorkflowWarningTooltip'; + +export const WorkflowWarning = () => { + const nodesNeedUpdate = useGetNodesNeedUpdate(); + + if (!nodesNeedUpdate) { + return <>; + } + + return ( + }> + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarningTooltip.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarningTooltip.tsx new file mode 100644 index 0000000000..7f66d60622 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowWarningTooltip.tsx @@ -0,0 +1,20 @@ +import { Flex, Text } from '@invoke-ai/ui-library'; +import { useTranslation } from 'react-i18next'; + +export const WorkflowWarningTooltip = () => { + const { t } = useTranslation(); + + return ( + + + {t('toast.loadedWithWarnings')} + + {t('common.toResolve')}: + + {t('nodes.editMode')} >> {t('nodes.updateAllNodes')} >> {t('common.save')} + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx index bdc384d40d..cb4a110a89 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowPanel.tsx @@ -12,17 +12,17 @@ const WorkflowPanel = () => { - {t('common.linear')} {t('common.details')} + {t('common.linear')} JSON - + - + diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOriginalValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOriginalValue.ts new file mode 100644 index 0000000000..a9ebc991e2 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOriginalValue.ts @@ -0,0 +1,28 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useFieldValue } from 'features/nodes/hooks/useFieldValue'; +import { fieldValueReset } from 'features/nodes/store/nodesSlice'; +import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; +import { isEqual } from 'lodash-es'; +import { useCallback, useMemo } from 'react'; + +export const useFieldOriginalValue = (nodeId: string, fieldName: string) => { + const dispatch = useAppDispatch(); + const selectOriginalExposedFieldValues = useMemo( + () => + createSelector( + selectWorkflowSlice, + (workflow) => + workflow.originalExposedFieldValues.find((v) => v.nodeId === nodeId && v.fieldName === fieldName)?.value + ), + [nodeId, fieldName] + ); + const originalValue = useAppSelector(selectOriginalExposedFieldValues); + const value = useFieldValue(nodeId, fieldName); + const isValueChanged = useMemo(() => !isEqual(value, originalValue), [value, originalValue]); + const onReset = useCallback(() => { + dispatch(fieldValueReset({ nodeId, fieldName, value: originalValue })); + }, [dispatch, fieldName, nodeId, originalValue]); + + return { originalValue, isValueChanged, onReset }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts new file mode 100644 index 0000000000..5b58a7a345 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts @@ -0,0 +1,23 @@ +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { isInvocationNode } from 'features/nodes/types/invocation'; +import { useMemo } from 'react'; + +export const useFieldValue = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createMemoizedSelector(selectNodesSlice, (nodes) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data.inputs[fieldName]?.value; + }), + [fieldName, nodeId] + ); + + const value = useAppSelector(selector); + + return value; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 761b273f54..aee01b381b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -18,6 +18,7 @@ import type { MainModelFieldValue, SchedulerFieldValue, SDXLRefinerModelFieldValue, + StatefulFieldValue, StringFieldValue, T2IAdapterModelFieldValue, VAEModelFieldValue, @@ -36,6 +37,7 @@ import { zMainModelFieldValue, zSchedulerFieldValue, zSDXLRefinerModelFieldValue, + zStatefulFieldValue, zStringFieldValue, zT2IAdapterModelFieldValue, zVAEModelFieldValue, @@ -478,6 +480,9 @@ export const nodesSlice = createSlice({ selectedEdgesChanged: (state, action: PayloadAction) => { state.selectedEdges = action.payload; }, + fieldValueReset: (state, action: FieldValueAction) => { + fieldValueReducer(state, action, zStatefulFieldValue); + }, fieldStringValueChanged: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zStringFieldValue); }, @@ -760,6 +765,7 @@ export const { edgesChanged, edgesDeleted, edgeUpdated, + fieldValueReset, fieldBoardValueChanged, fieldBooleanValueChanged, fieldColorValueChanged, @@ -834,7 +840,6 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( nodeIsOpenChanged, nodeLabelChanged, nodeNotesChanged, - nodesChanged, nodesDeleted, nodeUseCacheChanged, notesNodeValueChanged, diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index c4a50647b5..8b0de447e4 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -1,4 +1,4 @@ -import type { FieldType } from 'features/nodes/types/field'; +import type { FieldIdentifier, FieldType, StatefulFieldValue } from 'features/nodes/types/field'; import type { AnyNode, InvocationNodeEdge, @@ -33,9 +33,16 @@ export type NodesState = { selectionMode: SelectionMode; }; +export type WorkflowMode = 'edit' | 'view'; +export type FieldIdentifierWithValue = FieldIdentifier & { + value: StatefulFieldValue; +}; + export type WorkflowsState = Omit & { _version: 1; isTouched: boolean; + mode: WorkflowMode; + originalExposedFieldValues: FieldIdentifierWithValue[]; }; export type NodeTemplatesState = { diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 73802da54e..807025032d 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -2,11 +2,16 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { workflowLoaded } from 'features/nodes/store/actions'; -import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice'; -import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types'; +import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice'; +import type { + FieldIdentifierWithValue, + WorkflowMode, + WorkflowsState as WorkflowState, +} from 'features/nodes/store/types'; import type { FieldIdentifier } from 'features/nodes/types/field'; +import { isInvocationNode } from 'features/nodes/types/invocation'; import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow'; -import { cloneDeep, isEqual, uniqBy } from 'lodash-es'; +import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es'; export const blankWorkflow: Omit = { name: '', @@ -23,7 +28,9 @@ export const blankWorkflow: Omit = { export const initialWorkflowState: WorkflowState = { _version: 1, - isTouched: true, + isTouched: false, + mode: 'view', + originalExposedFieldValues: [], ...blankWorkflow, }; @@ -31,15 +38,25 @@ export const workflowSlice = createSlice({ name: 'workflow', initialState: initialWorkflowState, reducers: { - workflowExposedFieldAdded: (state, action: PayloadAction) => { + workflowModeChanged: (state, action: PayloadAction) => { + state.mode = action.payload; + }, + workflowExposedFieldAdded: (state, action: PayloadAction) => { state.exposedFields = uniqBy( - state.exposedFields.concat(action.payload), + state.exposedFields.concat(omit(action.payload, 'value')), + (field) => `${field.nodeId}-${field.fieldName}` + ); + state.originalExposedFieldValues = uniqBy( + state.originalExposedFieldValues.concat(action.payload), (field) => `${field.nodeId}-${field.fieldName}` ); state.isTouched = true; }, workflowExposedFieldRemoved: (state, action: PayloadAction) => { state.exposedFields = state.exposedFields.filter((field) => !isEqual(field, action.payload)); + state.originalExposedFieldValues = state.originalExposedFieldValues.filter( + (field) => !isEqual(omit(field, 'value'), action.payload) + ); state.isTouched = true; }, workflowNameChanged: (state, action: PayloadAction) => { @@ -78,15 +95,43 @@ export const workflowSlice = createSlice({ workflowIDChanged: (state, action: PayloadAction) => { state.id = action.payload; }, - workflowReset: () => cloneDeep(initialWorkflowState), workflowSaved: (state) => { state.isTouched = false; }, }, extraReducers: (builder) => { builder.addCase(workflowLoaded, (state, action) => { - const { nodes: _nodes, edges: _edges, ...workflowExtra } = action.payload; - return { ...initialWorkflowState, ...cloneDeep(workflowExtra) }; + const { nodes, edges: _edges, ...workflowExtra } = action.payload; + + const originalExposedFieldValues: FieldIdentifierWithValue[] = []; + + workflowExtra.exposedFields.forEach((field) => { + const node = nodes.find((n) => n.id === field.nodeId); + + if (!isInvocationNode(node)) { + return; + } + + const input = node.data.inputs[field.fieldName]; + + if (!input) { + return; + } + + const originalExposedFieldValue = { + nodeId: field.nodeId, + fieldName: field.fieldName, + value: input.value, + }; + originalExposedFieldValues.push(originalExposedFieldValue); + }); + + return { + ...cloneDeep(initialWorkflowState), + ...cloneDeep(workflowExtra), + originalExposedFieldValues, + mode: state.mode, + }; }); builder.addCase(nodesDeleted, (state, action) => { @@ -97,6 +142,29 @@ export const workflowSlice = createSlice({ builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState)); + builder.addCase(nodesChanged, (state, action) => { + // Not all changes to nodes should result in the workflow being marked touched + const filteredChanges = action.payload.filter((change) => { + // We always want to mark the workflow as touched if a node is added, removed, or reset + if (['add', 'remove', 'reset'].includes(change.type)) { + return true; + } + + // Position changes can change the position and the dragging status of the node - ignore if the change doesn't + // affect the position + if (change.type === 'position' && (change.position || change.positionAbsolute)) { + return true; + } + + // This change isn't relevant + return false; + }); + + if (filteredChanges.length > 0) { + state.isTouched = true; + } + }); + builder.addMatcher(isAnyNodeOrEdgeMutation, (state) => { state.isTouched = true; }); @@ -104,6 +172,7 @@ export const workflowSlice = createSlice({ }); export const { + workflowModeChanged, workflowExposedFieldAdded, workflowExposedFieldRemoved, workflowNameChanged, @@ -115,7 +184,6 @@ export const { workflowVersionChanged, workflowContactChanged, workflowIDChanged, - workflowReset, workflowSaved, } = workflowSlice.actions; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx index 2e2b07f24b..a707327d5d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/NodesTab.tsx @@ -1,13 +1,28 @@ +import { Box, Flex } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import CurrentImageDisplay from 'features/gallery/components/CurrentImage/CurrentImageDisplay'; import NodeEditor from 'features/nodes/components/NodeEditor'; import { memo } from 'react'; import { ReactFlowProvider } from 'reactflow'; const NodesTab = () => { - return ( - - - - ); + const mode = useAppSelector((s) => s.workflow.mode); + + if (mode === 'edit') { + return ( + + + + ); + } else { + return ( + + + + + + ); + } }; export default memo(NodesTab); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowButton.tsx new file mode 100644 index 0000000000..c1211f9e2b --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowButton.tsx @@ -0,0 +1,26 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiFilePlusBold } from 'react-icons/pi'; + +export const NewWorkflowButton = memo(() => { + const { t } = useTranslation(); + + const renderButton = useCallback( + (onClick: () => void) => ( + } + onClick={onClick} + pointerEvents="auto" + /> + ), + [t] + ); + + return ; +}); + +NewWorkflowButton.displayName = 'NewWorkflowButton'; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx new file mode 100644 index 0000000000..b01d259da7 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx @@ -0,0 +1,63 @@ +import { ConfirmationAlertDialog, Flex, Text, useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +import { workflowModeChanged } from 'features/nodes/store/workflowSlice'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = { + renderButton: (onClick: () => void) => JSX.Element; +}; + +export const NewWorkflowConfirmationAlertDialog = memo((props: Props) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const isTouched = useAppSelector((s) => s.workflow.isTouched); + + const handleNewWorkflow = useCallback(() => { + dispatch(nodeEditorReset()); + dispatch(workflowModeChanged('edit')); + + dispatch( + addToast( + makeToast({ + title: t('workflows.newWorkflowCreated'), + status: 'success', + }) + ) + ); + + onClose(); + }, [dispatch, onClose, t]); + + const onClick = useCallback(() => { + if (!isTouched) { + handleNewWorkflow(); + return; + } + onOpen(); + }, [handleNewWorkflow, isTouched, onOpen]); + + return ( + <> + {props.renderButton(onClick)} + + + + {t('nodes.newWorkflowDesc')} + {t('nodes.newWorkflowDesc2')} + + + + ); +}); + +NewWorkflowConfirmationAlertDialog.displayName = 'NewWorkflowConfirmationAlertDialog'; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx index 33c3cee2bb..09a484f1e5 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx @@ -2,7 +2,7 @@ import { IconButton, useDisclosure } from '@invoke-ai/ui-library'; import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiBooksBold } from 'react-icons/pi'; +import { PiFolderOpenBold } from 'react-icons/pi'; import WorkflowLibraryModal from './WorkflowLibraryModal'; @@ -15,7 +15,7 @@ const WorkflowLibraryButton = () => { } + icon={} onClick={disclosure.onOpen} pointerEvents="auto" /> diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem.tsx index 8a41efc1b8..6c5baa584f 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem.tsx @@ -1,60 +1,22 @@ -import { ConfirmationAlertDialog, Flex, MenuItem, Text, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; -import { addToast } from 'features/system/store/systemSlice'; -import { makeToast } from 'features/system/util/makeToast'; +import { MenuItem } from '@invoke-ai/ui-library'; +import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiFlowArrowBold } from 'react-icons/pi'; +import { PiFilePlusBold } from 'react-icons/pi'; -const NewWorkflowMenuItem = () => { +export const NewWorkflowMenuItem = memo(() => { const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const { isOpen, onOpen, onClose } = useDisclosure(); - const isTouched = useAppSelector((s) => s.workflow.isTouched); - const handleNewWorkflow = useCallback(() => { - dispatch(nodeEditorReset()); - - dispatch( - addToast( - makeToast({ - title: t('workflows.newWorkflowCreated'), - status: 'success', - }) - ) - ); - - onClose(); - }, [dispatch, onClose, t]); - - const onClick = useCallback(() => { - if (!isTouched) { - handleNewWorkflow(); - return; - } - onOpen(); - }, [handleNewWorkflow, isTouched, onOpen]); - - return ( - <> - } onClick={onClick}> + const renderButton = useCallback( + (onClick: () => void) => ( + } onClick={onClick}> {t('nodes.newWorkflow')} - - - - {t('nodes.newWorkflowDesc')} - {t('nodes.newWorkflowDesc2')} - - - + ), + [t] ); -}; -export default memo(NewWorkflowMenuItem); + return ; +}); + +NewWorkflowMenuItem.displayName = 'NewWorkflowMenuItem'; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx index 73d0249d3d..55d8ac2626 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx @@ -8,7 +8,7 @@ import { useGlobalMenuClose, } from '@invoke-ai/ui-library'; import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem'; -import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem'; +import { NewWorkflowMenuItem } from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem'; import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem'; import SaveWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem'; import SettingsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem'; From 34cc26a4ed26665820292933bdbbec4e29820074 Mon Sep 17 00:00:00 2001 From: Mary Hipp Rogers Date: Wed, 14 Feb 2024 10:04:12 -0500 Subject: [PATCH 007/411] revert to using fetch, add token if needed (#5720) Co-authored-by: Mary Hipp --- .../web/src/common/hooks/useDownloadImage.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts index 3195426da3..26a17e1d0c 100644 --- a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts +++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts @@ -1,22 +1,28 @@ +import { useStore } from '@nanostores/react'; import { useAppToaster } from 'app/components/Toaster'; +import { $authToken } from 'app/store/nanostores/authToken'; import { useAppDispatch } from 'app/store/storeHooks'; import { imageDownloaded } from 'features/gallery/store/actions'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useImageUrlToBlob } from './useImageUrlToBlob'; - export const useDownloadImage = () => { const toaster = useAppToaster(); const { t } = useTranslation(); - const imageUrlToBlob = useImageUrlToBlob(); const dispatch = useAppDispatch(); + const authToken = useStore($authToken); const downloadImage = useCallback( async (image_url: string, image_name: string) => { try { - const blob = await imageUrlToBlob(image_url); - + const requestOpts = authToken + ? { + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + : {}; + const blob = await fetch(image_url, requestOpts).then((resp) => resp.blob()); if (!blob) { throw new Error('Unable to create Blob'); } @@ -40,7 +46,7 @@ export const useDownloadImage = () => { }); } }, - [t, toaster, imageUrlToBlob, dispatch] + [t, toaster, dispatch, authToken] ); return { downloadImage }; From b77f6bd0ad5e8eb65e7854a063d6c2d540ae42ac Mon Sep 17 00:00:00 2001 From: Wubbbi Date: Wed, 14 Feb 2024 09:04:55 +0100 Subject: [PATCH 008/411] Update accelerate 0.26.1 -> 0.27.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a9a2157d8b..61bb9e7fa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ ] dependencies = [ # Core generation dependencies, pinned for reproducible builds. - "accelerate==0.26.1", + "accelerate==0.27.0", "clip_anytorch==2.5.2", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", "compel==2.0.2", "controlnet-aux==0.0.7", From 5ed2f6e6c19ff14eba0142a5e3eee2d8dd141458 Mon Sep 17 00:00:00 2001 From: Wubbbi Date: Wed, 14 Feb 2024 10:10:32 +0100 Subject: [PATCH 009/411] bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 61bb9e7fa2..7f4b0d77f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ ] dependencies = [ # Core generation dependencies, pinned for reproducible builds. - "accelerate==0.27.0", + "accelerate==0.27.2", "clip_anytorch==2.5.2", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", "compel==2.0.2", "controlnet-aux==0.0.7", From 2071972a8cc988200c5aacb38bc04bc60ed9c6c5 Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Wed, 14 Feb 2024 14:20:08 -0500 Subject: [PATCH 010/411] refactored to just use a new dnd context, got reordering working and fixed flicker --- .../src/common/components/IAISortableItem.tsx | 45 ++++++++++++ .../Invocation/fields/LinearViewField.tsx | 3 +- .../sidePanel/workflow/WorkflowLinearTab.tsx | 68 +++++++++++-------- .../src/features/nodes/store/workflowSlice.ts | 8 +-- 4 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/IAISortableItem.tsx diff --git a/invokeai/frontend/web/src/common/components/IAISortableItem.tsx b/invokeai/frontend/web/src/common/components/IAISortableItem.tsx new file mode 100644 index 0000000000..fd390b6f1d --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAISortableItem.tsx @@ -0,0 +1,45 @@ +import { Box } from '@invoke-ai/ui-library'; +import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks'; +import type { TypesafeDroppableData } from 'features/dnd/types'; +import { isValidDrop } from 'features/dnd/util/isValidDrop'; +import { AnimatePresence } from 'framer-motion'; +import type { ReactNode } from 'react'; +import { memo, useRef } from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import IAIDropOverlay from './IAIDropOverlay'; + +type IAISortableItemProps = { + dropLabel?: ReactNode; + disabled?: boolean; + data?: TypesafeDroppableData; +}; + +const IAISortableItem = (props: IAISortableItemProps) => { + const { dropLabel, data, disabled } = props; + const dndId = useRef(uuidv4()); + + const { isOver, setNodeRef, active } = useDroppableTypesafe({ + id: dndId.current, + disabled, + data, + }); + + return ( + + + {isValidDrop(data, active) && } + + + ); +}; + +export default memo(IAISortableItem); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 49a676008a..c4fbbb2cd7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -23,11 +23,12 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); const { t } = useTranslation(); + const handleRemoveField = useCallback(() => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); }, [dispatch, fieldName, nodeId]); - const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: nodeId }); + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: `${nodeId}.${fieldName}` }); const style = { transform: CSS.Transform.toString(transform), diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx index fa6bfff41d..3c7dae463c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx @@ -1,15 +1,15 @@ - -import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { DndContext } from '@dnd-kit/core'; +import { arrayMove, SortableContext } from '@dnd-kit/sortable'; import { Box, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import IAIDroppable from 'common/components/IAIDroppable'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import type { TypesafeDroppableData } from 'features/dnd/types'; +import type { DragEndEvent } from 'features/dnd/types'; import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField'; -import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; -import { memo, useMemo } from 'react'; +import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice'; +import { FieldIdentifier } from 'features/nodes/types/field'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; @@ -19,32 +19,46 @@ const WorkflowLinearTab = () => { const fields = useAppSelector(selector); const { isLoading } = useGetOpenAPISchemaQuery(); const { t } = useTranslation(); + const dispatch = useAppDispatch(); - const droppableData = useMemo( - () => ({ - id: 'current-image', - actionType: 'SET_CURRENT_IMAGE', - }), - [] + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + console.log({ active, over }); + const fieldsStrings = fields.map((field) => `${field.nodeId}.${field.fieldName}`); + + if (over && active.id !== over.id) { + const oldIndex = fieldsStrings.indexOf(active.id as string); + const newIndex = fieldsStrings.indexOf(over.id as string); + + const newFields = arrayMove(fieldsStrings, oldIndex, newIndex) + .map((field) => fields.find((obj) => `${obj.nodeId}.${obj.fieldName}` === field)) + .filter((field) => field) as FieldIdentifier[]; + + dispatch(workflowExposedFieldsReordered(newFields)); + } + }, + [dispatch, fields] ); return ( - - field.nodeId)} strategy={verticalListSortingStrategy}> - - {isLoading ? ( - - ) : fields.length ? ( - fields.map(({ nodeId, fieldName }) => ( - - )) - ) : ( - - )} - - + + `${field.nodeId}.${field.fieldName}`)}> + + {isLoading ? ( + + ) : fields.length ? ( + fields.map(({ nodeId, fieldName }) => ( + + )) + ) : ( + + )} + + + ); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 2418f65ceb..f4dad3f668 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -42,11 +42,8 @@ export const workflowSlice = createSlice({ state.exposedFields = state.exposedFields.filter((field) => !isEqual(field, action.payload)); state.isTouched = true; }, - workflowExposedFieldsReordered: (state, action: PayloadAction) => { - state.exposedFields = action.payload.split(',').map((id) => { - const [nodeId, fieldName] = id.split('.'); - return { nodeId, fieldName }; - }); + workflowExposedFieldsReordered: (state, action: PayloadAction) => { + state.exposedFields = action.payload; state.isTouched = true; }, workflowNameChanged: (state, action: PayloadAction) => { @@ -113,6 +110,7 @@ export const workflowSlice = createSlice({ export const { workflowExposedFieldAdded, workflowExposedFieldRemoved, + workflowExposedFieldsReordered, workflowNameChanged, workflowCategoryChanged, workflowDescriptionChanged, From a948bd131075c555fa2ab0abecff6ae92111de9e Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Wed, 14 Feb 2024 14:47:28 -0500 Subject: [PATCH 011/411] refactored dndsortable to be its own component --- .../features/dnd/components/AppDndContext.tsx | 1 - .../features/dnd/components/DndSortable.tsx | 32 +++++++++++++++++ .../Invocation/fields/LinearViewField.tsx | 2 +- .../sidePanel/workflow/WorkflowLinearTab.tsx | 35 +++++++++---------- 4 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx diff --git a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx index a28b865366..799fa7135c 100644 --- a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx +++ b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx @@ -32,7 +32,6 @@ const AppDndContext = (props: PropsWithChildren) => { const handleDragEnd = useCallback( (event: DragEndEvent) => { - console.log('handling drag end', event.active.data.current); log.trace({ dragData: parseify(event.active.data.current) }, 'Drag ended'); const overData = event.over?.data.current; if (!activeDragData || !overData) { diff --git a/invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx b/invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx new file mode 100644 index 0000000000..0e2f5d537a --- /dev/null +++ b/invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx @@ -0,0 +1,32 @@ +import type { DragEndEvent } from '@dnd-kit/core'; +import { MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { SortableContext } from '@dnd-kit/sortable'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +import { DndContextTypesafe } from './DndContextTypesafe'; + +type Props = PropsWithChildren & { + items: string[]; + onDragEnd(event: DragEndEvent): void; +}; + +const DndSortable = (props: Props) => { + const mouseSensor = useSensor(MouseSensor, { + activationConstraint: { distance: 10 }, + }); + + const touchSensor = useSensor(TouchSensor, { + activationConstraint: { distance: 10 }, + }); + + const sensors = useSensors(mouseSensor, touchSensor); + + return ( + + {props.children} + + ); +}; + +export default memo(DndSortable); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index c4fbbb2cd7..2aad446ddf 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -31,7 +31,7 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: `${nodeId}.${fieldName}` }); const style = { - transform: CSS.Transform.toString(transform), + transform: CSS.Translate.toString(transform), transition, }; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx index 3c7dae463c..fa1767138e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx @@ -1,14 +1,14 @@ -import { DndContext } from '@dnd-kit/core'; -import { arrayMove, SortableContext } from '@dnd-kit/sortable'; +import { arrayMove } from '@dnd-kit/sortable'; import { Box, Flex } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import DndSortable from 'features/dnd/components/DndSortable'; import type { DragEndEvent } from 'features/dnd/types'; import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField'; import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice'; -import { FieldIdentifier } from 'features/nodes/types/field'; +import type { FieldIdentifier } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; @@ -24,7 +24,6 @@ const WorkflowLinearTab = () => { const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; - console.log({ active, over }); const fieldsStrings = fields.map((field) => `${field.nodeId}.${field.fieldName}`); if (over && active.id !== over.id) { @@ -44,21 +43,19 @@ const WorkflowLinearTab = () => { return ( - - `${field.nodeId}.${field.fieldName}`)}> - - {isLoading ? ( - - ) : fields.length ? ( - fields.map(({ nodeId, fieldName }) => ( - - )) - ) : ( - - )} - - - + `${field.nodeId}.${field.fieldName}`)}> + + {isLoading ? ( + + ) : fields.length ? ( + fields.map(({ nodeId, fieldName }) => ( + + )) + ) : ( + + )} + + ); From 21ba3c63de6384e8be88956ef4c56b7217e19b60 Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Wed, 14 Feb 2024 14:52:48 -0500 Subject: [PATCH 012/411] cleanup --- .../src/common/components/IAISortableItem.tsx | 45 ------------------- .../features/dnd/components/AppDndContext.tsx | 1 - .../features/dnd/components/DndSortable.tsx | 2 +- .../web/src/features/dnd/types/index.ts | 9 ---- .../Invocation/fields/LinearViewField.tsx | 2 +- 5 files changed, 2 insertions(+), 57 deletions(-) delete mode 100644 invokeai/frontend/web/src/common/components/IAISortableItem.tsx diff --git a/invokeai/frontend/web/src/common/components/IAISortableItem.tsx b/invokeai/frontend/web/src/common/components/IAISortableItem.tsx deleted file mode 100644 index fd390b6f1d..0000000000 --- a/invokeai/frontend/web/src/common/components/IAISortableItem.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Box } from '@invoke-ai/ui-library'; -import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks'; -import type { TypesafeDroppableData } from 'features/dnd/types'; -import { isValidDrop } from 'features/dnd/util/isValidDrop'; -import { AnimatePresence } from 'framer-motion'; -import type { ReactNode } from 'react'; -import { memo, useRef } from 'react'; -import { v4 as uuidv4 } from 'uuid'; - -import IAIDropOverlay from './IAIDropOverlay'; - -type IAISortableItemProps = { - dropLabel?: ReactNode; - disabled?: boolean; - data?: TypesafeDroppableData; -}; - -const IAISortableItem = (props: IAISortableItemProps) => { - const { dropLabel, data, disabled } = props; - const dndId = useRef(uuidv4()); - - const { isOver, setNodeRef, active } = useDroppableTypesafe({ - id: dndId.current, - disabled, - data, - }); - - return ( - - - {isValidDrop(data, active) && } - - - ); -}; - -export default memo(IAISortableItem); diff --git a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx index 799fa7135c..f800e5c869 100644 --- a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx +++ b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx @@ -19,7 +19,6 @@ const AppDndContext = (props: PropsWithChildren) => { const handleDragStart = useCallback( (event: DragStartEvent) => { - console.log('handling drag start', event.active.data.current); log.trace({ dragData: parseify(event.active.data.current) }, 'Drag started'); const activeData = event.active.data.current; if (!activeData) { diff --git a/invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx b/invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx index 0e2f5d537a..de82af796f 100644 --- a/invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx +++ b/invokeai/frontend/web/src/features/dnd/components/DndSortable.tsx @@ -23,7 +23,7 @@ const DndSortable = (props: Props) => { const sensors = useSensors(mouseSensor, touchSensor); return ( - + {props.children} ); diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index d4dc59de98..a13ac821fe 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -80,14 +80,6 @@ export type NodeFieldDraggableData = BaseDragData & { }; }; -export type LinearViewFieldDraggableData = BaseDragData & { - payloadType: 'LINEAR_VIEW_FIELD'; - payload: { - nodeId: string; - fieldName: string; - }; -}; - export type ImageDraggableData = BaseDragData & { payloadType: 'IMAGE_DTO'; payload: { imageDTO: ImageDTO }; @@ -100,7 +92,6 @@ export type GallerySelectionDraggableData = BaseDragData & { export type TypesafeDraggableData = | NodeFieldDraggableData - | LinearViewFieldDraggableData | ImageDraggableData | GallerySelectionDraggableData; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 2aad446ddf..106e307889 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -23,7 +23,7 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); const { t } = useTranslation(); - + const handleRemoveField = useCallback(() => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); }, [dispatch, fieldName, nodeId]); From de832f68621a6c9aea4449ed0bc029e6b334877b Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Wed, 14 Feb 2024 15:00:18 -0500 Subject: [PATCH 013/411] formatting --- invokeai/frontend/web/src/features/dnd/types/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index a13ac821fe..6e680b4ba9 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -90,10 +90,7 @@ export type GallerySelectionDraggableData = BaseDragData & { payload: { boardId: BoardId }; }; -export type TypesafeDraggableData = - | NodeFieldDraggableData - | ImageDraggableData - | GallerySelectionDraggableData; +export type TypesafeDraggableData = NodeFieldDraggableData | ImageDraggableData | GallerySelectionDraggableData; export interface UseDroppableTypesafeArguments extends Omit { data?: TypesafeDroppableData; From 30dae0f5aac4ed9cdf8b36c038ead15dd8f8093a Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Tue, 13 Feb 2024 21:49:42 -0500 Subject: [PATCH 014/411] adding back skipped layer --- invokeai/backend/model_management/seamless.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/backend/model_management/seamless.py b/invokeai/backend/model_management/seamless.py index e145c6f481..eb04770a06 100644 --- a/invokeai/backend/model_management/seamless.py +++ b/invokeai/backend/model_management/seamless.py @@ -30,6 +30,8 @@ def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axe # Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = [] try: + # Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence + skipped_layers = 1 for m_name, m in model.named_modules(): if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): continue @@ -40,8 +42,7 @@ def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axe block_num = int(block_num) resnet_num = int(resnet_num) - # Could be configurable to allow skipping arbitrary numbers of down blocks - if block_num >= len(model.down_blocks): + if block_num >= len(model.down_blocks) - skipped_layers: continue # Skip the second resnet (could be configurable) From 17d5f7bebd9b2effd8367dca66e4462da7bba380 Mon Sep 17 00:00:00 2001 From: Kent Keirsey <31807370+hipsterusername@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:11:48 -0500 Subject: [PATCH 015/411] Critical Space Removal --- invokeai/backend/model_management/seamless.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/backend/model_management/seamless.py b/invokeai/backend/model_management/seamless.py index eb04770a06..1fe7d54089 100644 --- a/invokeai/backend/model_management/seamless.py +++ b/invokeai/backend/model_management/seamless.py @@ -42,7 +42,7 @@ def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axe block_num = int(block_num) resnet_num = int(resnet_num) - if block_num >= len(model.down_blocks) - skipped_layers: + if block_num >= len(model.down_blocks) - skipped_layers: continue # Skip the second resnet (could be configurable) From ff9bd040cc334f8ffcba13c159a5b3099403217a Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 14 Feb 2024 23:01:49 +0530 Subject: [PATCH 016/411] possible fix: Seamless not working with Custom VAE's --- invokeai/backend/model_management/seamless.py | 2 +- .../nodes/util/graph/addHrfToGraph.ts | 11 +++++----- .../util/graph/addSeamlessToLinearGraph.ts | 15 +++++++++++-- .../nodes/util/graph/addVAEToGraph.ts | 22 ++++++++++--------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/invokeai/backend/model_management/seamless.py b/invokeai/backend/model_management/seamless.py index 1fe7d54089..fb9112b56d 100644 --- a/invokeai/backend/model_management/seamless.py +++ b/invokeai/backend/model_management/seamless.py @@ -30,7 +30,7 @@ def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axe # Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = [] try: - # Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence + # Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence skipped_layers = 1 for m_name, m in model.named_modules(): if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts index daa214364d..3020934e3b 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts @@ -23,6 +23,7 @@ import { NOISE, NOISE_HRF, RESIZE_HRF, + SEAMLESS, VAE_LOADER, } from './constants'; import { setMetadataReceivingNode, upsertMetadata } from './metadata'; @@ -30,7 +31,6 @@ import { setMetadataReceivingNode, upsertMetadata } from './metadata'; // Copy certain connections from previous DENOISE_LATENTS to new DENOISE_LATENTS_HRF. function copyConnectionsToDenoiseLatentsHrf(graph: NonNullableGraph): void { const destinationFields = [ - 'vae', 'control', 'ip_adapter', 'metadata', @@ -107,9 +107,10 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = } const log = logger('txt2img'); - const { vae } = state.generation; + const { vae, seamlessXAxis, seamlessYAxis } = state.generation; const { hrfStrength, hrfEnabled, hrfMethod } = state.hrf; const isAutoVae = !vae; + const isSeamlessEnabled = seamlessXAxis || seamlessYAxis; const width = state.generation.width; const height = state.generation.height; const optimalDimension = selectOptimalDimension(state); @@ -158,7 +159,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = }, { source: { - node_id: isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, + node_id: isAutoVae ? MAIN_MODEL_LOADER : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -259,7 +260,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = graph.edges.push( { source: { - node_id: isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, + node_id: isAutoVae ? MAIN_MODEL_LOADER : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -322,7 +323,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = graph.edges.push( { source: { - node_id: isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, + node_id: isAutoVae ? MAIN_MODEL_LOADER : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts index 850b2a65bd..803437e968 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSeamlessToLinearGraph.ts @@ -14,6 +14,7 @@ import { SDXL_IMAGE_TO_IMAGE_GRAPH, SDXL_TEXT_TO_IMAGE_GRAPH, SEAMLESS, + VAE_LOADER, } from './constants'; import { upsertMetadata } from './metadata'; @@ -23,7 +24,8 @@ export const addSeamlessToLinearGraph = ( modelLoaderNodeId: string ): void => { // Remove Existing UNet Connections - const { seamlessXAxis, seamlessYAxis } = state.generation; + const { seamlessXAxis, seamlessYAxis, vae } = state.generation; + const isAutoVae = !vae; graph.nodes[SEAMLESS] = { id: SEAMLESS, @@ -32,6 +34,15 @@ export const addSeamlessToLinearGraph = ( seamless_y: seamlessYAxis, } as SeamlessModeInvocation; + if (!isAutoVae) { + graph.nodes[VAE_LOADER] = { + type: 'vae_loader', + id: VAE_LOADER, + is_intermediate: true, + vae_model: vae, + }; + } + if (seamlessXAxis) { upsertMetadata(graph, { seamless_x: seamlessXAxis, @@ -75,7 +86,7 @@ export const addSeamlessToLinearGraph = ( }, { source: { - node_id: modelLoaderNodeId, + node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts index b886c9b7d5..40d943b796 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts @@ -21,6 +21,7 @@ import { SDXL_IMAGE_TO_IMAGE_GRAPH, SDXL_REFINER_INPAINT_CREATE_MASK, SDXL_TEXT_TO_IMAGE_GRAPH, + SEAMLESS, TEXT_TO_IMAGE_GRAPH, VAE_LOADER, } from './constants'; @@ -31,15 +32,16 @@ export const addVAEToGraph = ( graph: NonNullableGraph, modelLoaderNodeId: string = MAIN_MODEL_LOADER ): void => { - const { vae, canvasCoherenceMode } = state.generation; + const { vae, canvasCoherenceMode, seamlessXAxis, seamlessYAxis } = state.generation; const { boundingBoxScaleMethod } = state.canvas; const { refinerModel } = state.sdxl; const isUsingScaledDimensions = ['auto', 'manual'].includes(boundingBoxScaleMethod); const isAutoVae = !vae; + const isSeamlessEnabled = seamlessXAxis || seamlessYAxis; - if (!isAutoVae) { + if (!isAutoVae && !isSeamlessEnabled) { graph.nodes[VAE_LOADER] = { type: 'vae_loader', id: VAE_LOADER, @@ -56,7 +58,7 @@ export const addVAEToGraph = ( ) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -74,7 +76,7 @@ export const addVAEToGraph = ( ) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -92,7 +94,7 @@ export const addVAEToGraph = ( ) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -111,7 +113,7 @@ export const addVAEToGraph = ( graph.edges.push( { source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -121,7 +123,7 @@ export const addVAEToGraph = ( }, { source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -131,7 +133,7 @@ export const addVAEToGraph = ( }, { source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -145,7 +147,7 @@ export const addVAEToGraph = ( if (canvasCoherenceMode !== 'unmasked') { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { @@ -160,7 +162,7 @@ export const addVAEToGraph = ( if (graph.id === SDXL_CANVAS_INPAINT_GRAPH || graph.id === SDXL_CANVAS_OUTPAINT_GRAPH) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : VAE_LOADER, + node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, field: 'vae', }, destination: { From d7f6af1f070764ece52fc936b72ec625d465a3cc Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Thu, 15 Feb 2024 02:15:55 +0530 Subject: [PATCH 017/411] possible fix: seamless not being seamless with baked --- .../features/nodes/util/graph/addHrfToGraph.ts | 6 +++--- .../features/nodes/util/graph/addVAEToGraph.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts index 3020934e3b..7413302fa5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts @@ -159,7 +159,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = }, { source: { - node_id: isAutoVae ? MAIN_MODEL_LOADER : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, field: 'vae', }, destination: { @@ -260,7 +260,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = graph.edges.push( { source: { - node_id: isAutoVae ? MAIN_MODEL_LOADER : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, field: 'vae', }, destination: { @@ -323,7 +323,7 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = graph.edges.push( { source: { - node_id: isAutoVae ? MAIN_MODEL_LOADER : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? MAIN_MODEL_LOADER : VAE_LOADER, field: 'vae', }, destination: { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts index 40d943b796..95781d08e7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addVAEToGraph.ts @@ -58,7 +58,7 @@ export const addVAEToGraph = ( ) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { @@ -76,7 +76,7 @@ export const addVAEToGraph = ( ) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { @@ -94,7 +94,7 @@ export const addVAEToGraph = ( ) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { @@ -113,7 +113,7 @@ export const addVAEToGraph = ( graph.edges.push( { source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { @@ -123,7 +123,7 @@ export const addVAEToGraph = ( }, { source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { @@ -133,7 +133,7 @@ export const addVAEToGraph = ( }, { source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { @@ -147,7 +147,7 @@ export const addVAEToGraph = ( if (canvasCoherenceMode !== 'unmasked') { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { @@ -162,7 +162,7 @@ export const addVAEToGraph = ( if (graph.id === SDXL_CANVAS_INPAINT_GRAPH || graph.id === SDXL_CANVAS_OUTPAINT_GRAPH) { graph.edges.push({ source: { - node_id: isAutoVae ? modelLoaderNodeId : isSeamlessEnabled ? SEAMLESS : VAE_LOADER, + node_id: isSeamlessEnabled ? SEAMLESS : isAutoVae ? modelLoaderNodeId : VAE_LOADER, field: 'vae', }, destination: { From fc278c5cb17c6ad98824b6a67a1c5e71aa9f0bfc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:52:48 +1100 Subject: [PATCH 018/411] fix(images_default): correct get_metadata error message The error was misleading, indicating an issue with getting the image DTO, when it was actually an issue with getting metadata. --- invokeai/app/services/images/images_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index 74aeeccca5..ff21731a50 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -154,7 +154,7 @@ class ImageService(ImageServiceABC): self.__invoker.services.logger.error("Image record not found") raise except Exception as e: - self.__invoker.services.logger.error("Problem getting image DTO") + self.__invoker.services.logger.error("Problem getting image metadata") raise e def get_workflow(self, image_name: str) -> Optional[WorkflowWithoutID]: From 792131be0129edc06c8bc9697542703bdfb5fb43 Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Wed, 14 Feb 2024 17:27:21 -0500 Subject: [PATCH 019/411] added drag icon, added vertical strategy for smoother scrolling --- .../assets/images/popoverImages/image-2.png | Bin 0 -> 664217 bytes .../assets/images/popoverImages/image.png | Bin 0 -> 591843 bytes invokeai/frontend/web/public/locales/en.json | 1 + .../features/dnd/components/DndSortable.tsx | 6 +- .../Invocation/fields/LinearViewField.tsx | 64 ++++++++++-------- 5 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 invokeai/frontend/web/public/assets/images/popoverImages/image-2.png create mode 100644 invokeai/frontend/web/public/assets/images/popoverImages/image.png diff --git a/invokeai/frontend/web/public/assets/images/popoverImages/image-2.png b/invokeai/frontend/web/public/assets/images/popoverImages/image-2.png new file mode 100644 index 0000000000000000000000000000000000000000..8db14cde13ce82356266a136b609d56165e1328b GIT binary patch literal 664217 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

tlu!{++ODkz*jUXX*j@0=roBF^ z1CdQR+`SheIS>ZU$!6CBCK*O`kj3fCFTPoQ+3DBc+<%(VE3VixJupZJys1-7AnAH9 zTDBKO5E_^j2#52UNw-`lWLi``tJu`l2kp;e^>y&uy%F>(OJJK9xILqRY2^5GFc2!M zqSpe?2;cg6hV=G5Z6K0TwC;Ze7#elXna2SXk!nGJfX8%ryvZ zeDsM+T~+#}tr?E)u|B~)d{gcSrjuQ&X9yo_otAw7Wkg&VQXZC{Xm3yhd?{cJ8r<&9HgZvOROq6cuWNTLOckVV5{=Q;q5 z>%glY=(KxCaS8ib_}a`C9jorybl3tjebc_@t8?h1HrKiBt4kjn-Q&3)Ews46V->_p z6QDdBo`S9q;lsEkh{t({23%LCabM`w0$x%53C5Y5Ku!=I9+k#Mz^;pf?-PYxLu19C z$*p{StAmV3if0OCkI^#*x8Uc2+QP%DZx&3JqfhuTLUo zUYZA!;m9U>XS+OX?c7hnxf1Q#JIeMnnnGjhH0h)@hq7asC58nu~t?P42^TZE*h zewAT#Jp+Zk2M%+z4j#mN-h^l2U`5wQm(gwW0Z*{M$eBI+yb{g(bB%*A#b>1`W~tse z%32)mDQt(y5W->XrJB_I5h}x^{Xv9q?Gvbl5!zGnx^aD&S7t&C$sggG0hggrVizIy z`@v%Gi}~#wm4k$`^)nr8TR#!F`L&bsDctd>5IDvtc#}5Sn2+)pZ{anYdc)x;kI6Pi z`{z1?Dmf#V@CUdRD)!sFLg#L7w})vvr74AikZkVPmNQmlkb4e)J;ysp@naY{Olc-} zC5Em3-GBXJvex~(-+VD1tY5u7O$t%zBcvKAiNdk<$g!2QpRnfKp z06+jqL_t*apdQB9@P1gx+&+c7zO3GM=2o;t2^mjbZEhw6?>3(-o=@|B>^Cm=GP;@% zyt@?3e}DV#>fitVP9u(P*H=HjzO(u~!S`SP%~!*>K0N<)^*{ge2jONpu46xXu*Oo@ z)@mQ#-D_mq*A$G6kv%&T@*Y0Pc(aBY?`R9oaltt*CYZ)6*R}1Qc_Iu;*jdWSo6?c< zcDzhDr$F(}+FwYJfAn&%^(UUo3)^$rILO0>C%r9U09x5A$;$49OYOZc!=ObdJ|)y} zJ#X9A+3i9xE7u;an*W|yBWAQ~ZZ@)w zHF?_HSk^CV^7G_%`^Laa`5BqEHqe(*l*Hm}^XCX}Zq98APYqUb^!fIxy?fgjbX~NC zy_wci%CkMlvVmSK3*15UcDZx$D9dL``~P)3RTLY~wQ)&DZwar?I_ywwG7a8lomlut zv>E=^pFfSzV{`=}q_ltM)2tqp2mMZNd*N++Po9t4Qx*~e z(xu$HWe{Wp(FH_949V3awcu$362fgGX?Q!=6AU;3;>Z0srB4^ zasey>Ge5Q47!@kCnFr|O)n^~yh&dx7;dNOMm22==BTxBWf1Thw0^76)MwKGQK3CtI zRWguU1R|`Zr6#v_tH+hfvO3$i?)ei1WrV5-lD7YVGl%l$&VSOZ46 zs@`g!&jh@szAEl27#5*B90ntzAGiV&YQQunHfy=E74l%O`U-#j%r~&>nj-Foi<9Es z;l}gYo_>0`@5|?A{I)%CJJIpP6K7$L;3^**ZcO2(VHiddQFR?&^{a)raj^PpTg%>M z(QyqUW$|<8?ZozWKl&760VL%|Ndij!Jn65@gJP8lvr`bI*A`r`#Es(6!$bSp?H)mZ zf`z`0yEe}c{xH0rn?)oc&H#gJ#sJKK2?n?{&v=F5X=umwrUjvz5VQDI9M<=6_0@Ne zVt85I%;l_6(zB|{g0{m#-==FJ3^LZ;xOuGsWob%jZy~?ael^yg4?ny$)}>L<8kafA zJ$MNof5b_!j#pR5-~#N;?{|d-)ZJ;5;bN5LaN&ZIa66AQH-l=v+bCu9pw?ybWvGci zE!Qb3W2GG{6y8|9^#^}aR~Q~V^H6|gnKR}N95qJG$zu`^z61+n-_6r4#1LuxHa6gL z8PC}y-Ln*^#rU%HN4++1t81z7m^T@pu4&J|>EFrs`hmu0AU&pkl|%0c_;`kO=kNbD zhsM?=Fv34UU$`|MHyI+2L8L4RHwjzOQ(3rLSJ3oUf=8%cjqMRZ3lVAL#aZF(#0XFEAi6^a#PLo$ZZ-__>oB#WoV4QFi{hb_cO+Yxx1GCkd zWPZ)wlG+opah3qIeR-lF&R^KiliC?T?NhZs3vW5fJpATe>7L^yb)jo{F=ma<>(hN6 zgOA=XKmg=TGNwnx**KWhNHXA{@@k#%8~aWSJ>gMwh_1AbnQXb{M(y$bHjOJE`r^AH zWx(+)m>d$RqY^vu=Dn;K)R0Pe=`AfE6l98**Y?!Wu) z?kFY~>eo(Q#!!$UKQBYd+gplVdmUKt(HcRM@%ci1d)Kv>DYUJTwW7;{`qxKHW3mlfi8GfO%5Q1oD^7iOm@HvuNDDpmXh;dC@sI zFZ;Zg2gw|fIK;U?4O#QOJ_hGRdGy>tcw_jXY;4DOv<1JXgH>M}rNUJu25Ak=IJ8q3 zoO7ZK*s1VujUk^<_V%Oks5Y8wjSL?bqm7-65?jeI@ESLr^@6Nw4%=5tdC|%wXlSgg z4-{i-&g;C+Yp2@p-k6?gt_e9NXHcRntqdz*J(X*fBEB7L4Ol;v6`r+pi4;1r*>?N< zJo7l&>hkrH9Du)i&@VaTQhEjj>6@=Q$F!q;7$(hYTGt|bWMGNElEIY6w-~JOmGwE7 zAe>-2Yim2Byp6EF zq*}Bg@!IvQX$UtL|MLd+i}oAsNdcXO1A)=f);jeZBDnO7WFXBVfw52o_9HwY3=f}5 zT(Eep&ejQ=%3h=eW6U;S|P*#}o+=DY)uQA}ls z3oaro%#B9twDMU^tCL^@LK&(o-WpvLKG*uLOn(Ee|NUZ?;2#0Lx^XiE%o-CFR!@`f zMc%S-aIZMaFTTEC5`-tK`!TDhVxbXBbuRJQ+5@Kv&-C^XqhIPe`Lmem!^!ul!!vytSf;kQFl}po8Mmd3a)Ung zPqFY<8v{(&Ff(7_bU^P5hBccvD}^3MQR({Np+ZA5)iwRDAFsp3?ME*rI~0NGbQ?S4 zUEPoQQHWZ&SW28ZBD>xqcvF&^HO(IkPutqjCotJ7W)mz1<@*$9NJn5khM9sx_~V8* z+2g%^uMTY-!`YSUr5putb*qPv4|bOj%nT)r80Y5YjKkcEo*-usM^Re7v+#ORP|?hS zIk3Vd0n6Af^vNC+L`nxoeGTR}yc;ok2L``vpWIs6D$$hTCn!E^F02=`FWvv8-ScCK zK2MNrEMF;E}VO~8rmUho&1g580N_!wkQ;*juyta({`%_ONF#TLYe~iJxQ&fV6 zPww*|;DV|l0IAau8xt4M9Mbu|yzolMGgoxi*v}qd4SBqret+0te!cG{k z!ljM9ygg`Q<~uw^pl<-@i4XDepxu!t;1&gs_8O7@GBDub6%t z@;|tCX>}tDt5CpKtqscRBF~V$Z$J z_MdcjW(1|`O_q3u3h=@exky|HgXm%X-V2B16X)zua0s|?e#Uz4nHikQM-L9PwF#bK z*Sx))oZ_4|=M`F;>n^aS{paz)gOtWI0mGYNZxfH)?)i(W-OG6oqDM*-B~4pAZP&7r z3n@P;1dovooIE!TL_Ml(wEyO?y%f>beq+ZN>V2kg5A3BB2mzGgZ6^h#NQtnw+&)!2z&g%2Kc2&SuiyB;Xd;=&crefLex=kOzT$Dsi;%bQ)W)M}8!xdB zkDSU_a8NsFB~XXIZV&%Wxrs-giB8OMf;t{E;}8zchp+AaWS5N_bR13DfP?RC2QRN@ zGTL+xU9I}}=98G|T86qZx^udj^> zyy8*t?)|u$p}&!P$eQ8l!>YI6-ySjpKgRE#9h&XkbCJi?hu%E{sM%_foP(FSBoy+n zd5g#yt(-e1+%VD)rrM9z57m$N7UPM}kgc9PewJZ0PyCao)m3Kd4?iCL0vwD$FWYx` zJ_igX(_+tq44=gT}rmeJKBMQI~I6~@X14=^F_pN-R8XtIuM&WstaW z>t?V!-8f|{MTs(C!MU6}QQA(6VwYwD8WwGs=`3sr%)&IOpOr(@3io-&w14lKU_=O` zO!yw5V7?E;15>~+&nfBoUJ!56M~gGCH#^_p7adgg*(bNcO$%DYK$jkYqIN9eCZq$e zXu$(_M0I3wi`ilTBiO{u2}OZ2Z7W7DEbWpNPr0}*hU5{(y{f}Qs&^T;TU8e5*2_6^4jSOrEH>ifXpeM&BqedyqE^l$u@x0aTV!v68K6x*p zDh@nu*%;GgLyJjoG|!HW`_nhpm0{S&eWM6Xf(CRs_=!REnIH-;7|cXwXpK)#*st&|nv|m4q0RsGw_lF8O)AsP z!i`wEHsfXQ7;q9ia8`Kat+v3=$?V~law0@_JHg>>;R4#>!M1PdY3GDpOwc%!pbWRe z?4cky@Qm>Q44p108yX7k!C7URW&`_lEZni0P9Ixt@JTdJFdQqGdYeOtXJ>us0a+6J*E}oKUTG+pu&IeFG8T%Ifftv7XD>5^%0(gTk`(zXX$s+ z_{l+s_wpp3D`Z>tIME76CQ|d$nQsJ%Bl|mwNVszKdZAA;#w{TgjB(D$(GFo+I0sr1 zR(Evxcoc4P@T_cR(e9i|AJL1HxSltYm;Ob`hA5oRvfOVMP7cq)vz(_va6=0w%H}a_ zR8yADgHBc#_G0bSCv>hHA#;P}HX#yB=pK0xIN!WEySg&;xU6~FGRCs1^>BiGUI{!t zT&=ZE7;Cs>=;U=Vhk0KwlrNm^O2U!wrN_w_hvC57dzJy?ZPzJ9&ogkyEQNkI<7p*)S{+22 z_Q{A4Y4}&0J;Osvn6;P69*>>ye}-~eTa;Lq@l>Dj1jvHLaBE(gFr9`GhFxRG#divQ z%PKGY?RE3V{F{)jisQMnXS4E8t(iixx<?SFG|tw zL2Jw$BNA+q4;_Lf9E<#Mb`%lY!RH6bOo3e4;Ek8#=`8i|jGbnC$AoNZHzDV4^IahUb@I@com%0S4wG0CtOJVen0$)Omc?R zgr{}wX^NLQ!DwI`2hO3sWe-X7uD&0lt5NEYWuf}x8QJx`Q^2)3G#wmxr(^&>E(>G? zKj-ei8wH&aj7Nx6@J?r`+w^hEn@nMe%7ak`>Z|f|Z^uMf zcCHo2j9?xY61QI#H<@)N-mZ>e+;bKYhS(pM<(Zg#2D7qaTuO<`1&j~^Q0{e%g!NV( zHitowSVh{dFc$7No1|`hbbYKQHBs4IU%^I9c-bUqR*r>RIN$X)Jh8^TI^s?&%UqhH zFtH2U5oFJ1itnF^-K4U}8`DN(gqOY<7#kg_3Wl1nYw#>;BPjHFgf*5B&sM>__wQuz z)#>TwxxV#l&@m55A#Ut$t^WK^K3o0h!)s0W?lt%_mYi6OkefPk&t)AHUa=4dP1Yj% zwEb5mAD6p0NiIOM*$_)Uz3p#XR$o04l&t;$vbUa|MHvrr64^l zfkjCXESxa_k2wRzv(4>U7%4*BuJh9Q69jngfl4;71V#GrB0J4)tZcEAOmr3*{nCJ9K)TipY4@Nft${kO0-2MZ@P_Jg+;C3sfiAN@CHhi3ZBqQ*O;p!u=TQMfuDTN|ARci^uWniz#FT%Wj`Eri;INAoA+ z+Vn}kEXO*3s&>G%C{xB7ZB!h@my7J%U$aFTFh1M4*0XK>>sV$FF4Xd?zmudt=mxF%Cu7s|1fj|B77v{8j8bNcaA0Al z>|Sc0DU2MvPAE*_=P9u8I+I06CcfpE1d5Nb^~|%Z>yIA3oY1z53HWHy{*=u;O0qa@ z7k+`~T8s9+{Cf34o5s;TV}z_tc!{-?Wt36Me8ddkS*Q-o-oJCNki08-X^y6yy%zgR zB~`eP@^LM%4JD|ciY&xg&ihffJ`?VY@9V>|+tmlgu9@%6RYE!;RpbPraN3P-W+Zzy zkcT&$xAp*|z2&db@0xZ#t)GA1YRPjz;Ir0Gl8?dpf(Hgk{Amm;;qPVbJ}ZI2tCAFK zR{q9?_FCixW0~Ix4*Phoiu+GMHV@vFoC1mQ<{p-LjdFJDYMbtZN!U@dJl>JwR9Qw` z*PAsZ0%)H>a*T1?jLvrwmM#^t#wg0TCySi${7_I2s=w>PzIQXAkoCYLQ4>5GEh)pF zr)(s)B|LWsW1i~$=Jd>uu1&ZRdJBd2?aY3L`dbst|H5#Rd4$|Lf&`9dy7iBq^DHgw z&FRKEd>rL113w%yc2cOMKu&mA!d0>DNQx zWEZ3;s&n>_Bqve$9+ZXdO^W7Lbbzn%K0VF*G~UMA!IBEEFDeTqr=c|OUUVX4#V~Cdl!<23$qP2cnsR1CEl8|9m4!xxPO+1 z>P&LrZed`PWepDd86xX`zz**n*(6yKQCZK()y4>op}pbpCiFZK{WP}ZMaFk{n7zT) zs@7|(gn5pxBZQ#|Jdxbq1FiL~)#hbzh+^@~7&n!z4Kn)@;$|#rqd)bLi>!AOUC@}$ zniPO|5O0rJ*6rbY_$8y$X8WS*ZS9l*;{L;oJ1Ma#IT!NGy?pVc=F%JLOVfMSN@oJi z*-4EDrQO^kmz>Y5{o|j0vid``_GmAUOkj+5I%{_tOb|6h^zqGp(s-q}>c~ zG0)HkQ9Eg$K|dChYk4tPf*xmyx>LOAclYik0K%0R&%#vNqjUHw0X;x4ipXlLG8o&1 zi0$fZHcgAJ4q-2^n}G7&pY9ZEnKy)7$-I;8D&k9vPYEKdiw!(@ncx_6oOr@&o(+g0 z46?c64{{ryK2=CwXD>ja*Y(|qF+!syBZ`d%WE74tj+OI1g{xuILRcV=rFvmhi*VpM z12`7oDed+`EK}+7ufd+0f~+>wk5Cgt3BVM=t1Vjp>>vHIEO6JxI;EU+iv-F(4zctv z<`==_ehnFm6}Wq^En?yPZkcbM=HlIMqCac#Bv@i}29PxiK@DMr$AO{ih)e$~5KJv< zxhD3*=d(iK?sxFqAKHttS7x;0RMOu~=#dPY&Wb!Rg?p`zM4w z7PXqw@K}}Mqpu^79mA?Es0154y^CJBY-gf$&F#4fAL{XSetNGC)f;yxR>$1ydfgv9 z+IOLl{+=jL$|~s>&gNP88u|%_`Z@5J@D-Zz^U0!iOg}?2Ewt#(=Fi84e{$u^yt3Zj zj9I*@SiCv7r$4o&J+9ev78r|3ZJs7fHT2HOYPf>)xQD@qkr>nQnjG`Jfh}HErTxqw;7a@LV@9>8cLnTwhm2w&5-KE*n+EC?b|`t?w$xi zT8CGQ^qD%K9&L1G=BhDYXdxkhK<`;_cAZePFxiFCvW_LMho4%5_Zs>;H@j!F8*O(_ zJNgbb?R$5f@-Z}28;jK{JhqsFUHt#^myhzqJzV|OuYbF`))_`W{^7@~>%|YpUgAIY z2ZTxEQDc-6N+i5qzJ4=4C3Gl@V~gb31l!>cfE-#0|Mhz4x;pf0Xcb-7wmQ)`<0`1K5T{@||q%-b8Ql3kxKs(p2UXH#J`UyUbfi$s~Gt#|(y}cPJJfjRpJ!_rAVNP7_ ze7NU%KL}(FZJZ6zUFXp;RyW?eIYJaT_fn{Kf3*H?&Qg57J*ew#MBk?Xq)KZC z80V5As>UGN5-eem)Y zuBQMIh$*{=joZ%IYk6Ng11jb4d~Iz~+(MMP@U^!I-a=6+mNFL0Vtlm(C;#Fve>U_; zp_3Udz*j$MYr!Y)Ur(kgtTiY_4e8TPMtjCMS;%Bo8$+c&Bu_nVuaW~z(bC5^ZVo=T zQ+%$MnT${^QCNdF>o;D2KRZPA=j~lPpBI*A2d>QvL+alsjG?;`Y)HaZzZ4T57K1Ki z_C?B@?5Jm2YfeRTW=NF`fAPQ)%3KZ2Jv^6J$GT`vS_@|Vt4^bef7vrWat=6_W`>uK zmrU(FH<=adOHYr-TcRy5vVc%A>s}LL_5^W0q|HuQu%T!CQa!a6Z`m3JR2ZPJ#G{nF zG1Ns{`YXCb9b|870u zk4=$d@h{0$4qnA8o4c1T>_#U%Als0Jha_r(y&8du)?n9h!T%`p(0#%!jN5yzyyT1>9kV_CyktFD)R zn&$=EI~PNcJ!DTEA#O9GVRhJw30+AGHrbq+upWcSAKmx0)r}5h{k(IDCLFFsX>S{U7@i%(2*&tWEzKOTg%42(fpLXuZBSJ}Q31}ZlM>^l5aW(2?r!3U*8xE{EF z@zeKL|M)L{S)zu7{h2xoL!Sp>9x+s>#YQ5AQ&RpR@Y+Dk_B!0@puR`NIC8tov44HH z@QJ!|vT>Gzir^@a-i@n?G{R8;T6}qrUZuG{${TloudyoL923Q4gm48wy+N!op87sQ zZ3HIYzpQ#Q$Ew(gWy|4YjNqvJ0)hZH5Q+Dn}biU6*sm|bIef^^% zHF#0iqO7QYpc_0lemIW{Dp~%`f3Tb^&c^oS?znxMY+zfg;uca*P6Z?2xiE@R*TyqA z_Z#yQSgTA(K_XDeI-YiN{NB(D+P>CAZEM>owrDW)44t4ApkgKs&crFpf4`C7bvC9e z6QDkW6;m>1lq3_BFmP%ZlFz}ECla_L7zc}tfkUIZ8ExXbHiJ)D=;=b(n>pe^ycl&7 zg4DAx@T@vqI`fLQdj>O)MMr-z(C7j!5dxue@UE;u4Mq$IRPNP+bs9}}ZM;EMk5(-51ixht1Yd1i`1%wc!t20Y9X&W6p6aWL;hDyF zgx~5>FFN$y4|<+vYtu{0W{ZZgdXsQ7gAy*jKd+z;9QSZlg8Pz#AJ2tq^a|5UTe|p&gb|Sjuo3Ir6^n zq}xbte~<%TS=^V*M|q&SzS|z8IVHY1;MuJ_$1=B3yjYUAx3AA0qKlVP#G)ZoWlgr9 zQ$K}N+N1Y!LPFvx)`8lzx5Qq!3HeM&wtvOU*3iSsp9@b% z5yXDN`0VFfFY8n?hq-;Ny*tiEfoDlD#u%8_`Te|t+gUak1%AM&Qu(cv(#_TZ$1gY! z=2V)JP@kRcjWYgqErC03&>H-9b+sffcnYOzyD|FtM>l5Ou!igV<%D>f^?9Xkwa?=W zt&6hdK-=cHqXod-$JLq6VtQQm-JRqo3L066!0vGNqhQ_&#tR_<7g#se{ox79IaJi% zZ|Rg1>xOR}M3*lPYNvZwQ?>}}xX5;E^O@u-JZX**Hco-H_qcG)?g@!|STZHa4TP0T zYH>cC@TTLgJHg7ho(R-I#EnSC1AWk(ATdzgFE#kZyu$VeX`lCa zVR_~xo)CYn-M1OsX9nT}3H^1p7UQMWOIE^LtvTBD*?gT@-TezZ8qH~BX{?7wfw=Nj z6P&ymdR2Wrf7V*<>EM~HbG-|dowdJlT#SrC*I;O4j1%yXB5fTbcUS`<>P#L{rk#;V zj9*V!ch_5otY_w%ISi5N_v-Q-55dbm*&?+aTo-nhLw0#$-81j-G>T^qx7Dd=MLA#bvYkMSQo)SzI9lq0C1^0oNT*^YQkaU+<3F@^nC`$pf&zu<&6R` z^dYYZX~?3%Tm&m5*BvrI1i0u{2tOii@N8t4dFbb5f&0rp{aG6u37g!=ghAQEYA?&p z5S@pEEzUWvb8tvcpIKA+6=TQbfv{f=OW<&Zn6Lpz&CrMJafCW`vyJ6O&4i z)j5i#7;lQ|!@XCXn7wSK<(=4!=xr?ITHlJuFvrb=Ewdmrj+`K6F%5sFPS02{;cR)2 z;C<<}C`e8GaOToX1-JS$X%~FpCW8{&63R*{A%PK=aVl(Wh|R@4S`?ro_T5 zlpEpC6E&*sxd*R!0LG$iFa`Va-sG%8V>k?97)EeJdz!%jF@e}fblfJ-JZKf22`oGU z&3fI;>v*HZ%A&<%VAA^ohrK_GupAQ&20b#ijaD1$6KI-a7zCX0B8=dQAr36jVhl9m z9V@o1Rg^i)(pt(91d>PX#~EfDgP=I;C&8d!$XIYuony@(e1RiK;IsP`uP!|%csJNh zS_;d+A8oR{m?P?|_$&n4J~=Z$U%ke7fG(mX<>2*nmQ+?C&A~ZF&Nu>IS%21R91XZ$ z$>NU}c<>FLn6Z4dp!&h&mRMrrSRFkT!LeumF)zP`75@ZjN`X8%FqdBQ1}L-cQ~_u+RVOXzo{2fvafawldy{WgyM4V5Rn zkH-U5Y_P_#54K-r*ld#%5+U@fjo4nE9_vAHDB0wF48=dh1&FEj(8>N96T|&^)&ii@P z`8U_EU!Qfxz6bqxrp1&FuSae0b?tPCILvDyL=L;OcyGiXLq!I@b6GD_JmSNi+^Fnk z7JuQ-yipXFyB!xn0FY>5H$gh6Mo}>~_{!M?WyY?twq~f>*|{*@n{}D!z+Zc_HzWQb zoO4hg96Ux_W#d8@EcYd80m6F?W6ZUTm6NqMbHPFSph7S#A zY#zYYLJtc{(Keo9aj2Iu2sjw!s5(x_3fskd(c#c&Wf%t3>bXVWtB}!XUU->Dvst`` z2&eWyDyQAyTTQWLEm(Lp*k|K-0_Yl#PW75U6OLM+W_*K_7ouCegj%Ud*nQ!&r7YIF zjgQ_R-aq`>+;6O|2Fu6Su9v~=Rr{5eQ2a}^gE#2HZVJHlPcAjS2~c=ne~ciE6t4=& z+C0~qo5As;IZ)Q2rwQPl;Q25A?5E)**(4qy!2-NFTP5Z&=kt8rfw~Nl)(_!=x{^nCyxuo%$xFQ`?BA=iif}LP}7w6aB?nM zCflq<6Q{xfj}}9SbCV?Qc+vZA(LJp2Xc+EpUTyDO^rrr0rt^Lrt)3Qkbh$pPb(rpr z4$9qYuN>YWVZ>j2_Je2{JtPm+N8w%nENXi8i+0_^^*EH3yW+(wm|1(KkM{Fl<2cMdxvwzz|H1>@bbl=PsVAq zLg*MU(XGL&{CJ&u2F~;!YQb?jx$jLp2A?C#3RhfT&r?^Ow#m%e9SwQCu3zYP7D|0%?I*Swy_i@bOX_YxZ^M2{+q!4^k1t#L7&NaBx;ZNdHF>UTfhp$e^TAf9l z7n?9M5cTkUlj&Rxd?)M5xjKk}+n|JLTXdaEfr0EcIiBRUMohF=dtfFjMo6?F*ygnd zDPPx$t9+8hO-$^o;CYsCPn#2SdieVJqQE_C(`(+C9U2E>jPPDo_L~-<&p!Dy3s4qf z)}gM;=HpzCD{X$AbBH1UeZU;`0TF>O%9z1-2ctE3Cb&u4-TIM06Cpb}_`T~FSO4TM z|3RDK3KeY9{rV42Cd80+#U@*!W}X@CH)b@8M9;o!@+=`*30dJu=AMH&V77fbS)|v` zonO5ydBvkg_nX8Yrj$Nuu~-DxDjT9OMud4@he@eeJK%lRey+VXwmwb?64x3eKVa7o1V$FoS_Rd@NSKK~Fd^(ZX0TEI(cMxoa#9_O|$J z0YsEueeK?I9g{E#eHXuK{4L(2R4K3dd8&UN_iS$nIx?+oP%W=1*S}@r`&P;0XG{aX)U)!D5S*r1dGmn?=}$YnFQ!-XDE%qu6d|ZeZeh5~>p|YiwW| zpo6e4te^08j`ibg8-Fbp!DJ!AK$mBFHlg1_#kIU-7Q=X>;X-IeQEJR&_6sq6H_kDu z=FnR2V+pM;bi=DNLTqEO^cUlMS1QbhKncGWti$7I}CW8mk^-GN*#)5#9Uk z?M|Y;xB9`SAI1YDSU_I2*8-9Z9IVZa1cOO`8(`AtPIjT{r0_U6u-UZw_S<{W$A00z zyURH+HjGuzrNV&;SI^>E1dVlPI0VZQYFml=$16zLe(?11YU|<$@t?+_r@fRl_f9OE zv)7=$y~z8%fls#hOqmqr84q(aerfDz^WL2eVF6$4t}UwJR~r^JI7C|~n}?T%&D)-< zoM9gt?!(Yh7rh6CO6JrT z85ol)!#$y75wy;35+u52E+v@lXS90VSuscPo(R3q410N?5_<6seDYGlr#9atI0@$? zyt=17hq;3F(StS)r=a1#gp`Xtvz<|pu=To-a?i%Xs-JbYa*Z!M;4eWRo|cS*+QE^g zr@?J4SC*iN4#s;NoFm}rct|k+z!KOAf38J8=43xJPJK2Wi~`07jCddN&8jg%BQL}l zT+}nq^?Va`6sd+#fBRO|eHl*N7*rQ!0enPIypP9u_@wa+no*AMqkdfJk)d^ijrOl} zok2Aq!p+qQ)&Etvu$~3*z@}}_vgosn>yG}ueRs73;WHYCTV4VOz8}77BX*zHsvNCs z6h^jqXxii0TH-mLi{!qMY4ERlFi{}(;*7@&V&JBi{Z5~J@P2%&y={%_bgP=}>)n3u z)@nDI!Jclsc0BsVDS6~qKl~7uht?*dK!0mHgs#q|AW-ZssIxXU6IcnU6B-p=mP)`$v@ zz+V}mY!1`Kw`9P@OU~MoQT?L6yx@z>qMd-9wka?U)`i12WpbgEP4>ecF;|@RWb&NM zd!hOcE4DXlCI=#I{@9P@`rxKE?W>A|49W@XEIh@n|b`YGo8p% z=FY_wa>mLtEYg*;)#7mpj~-@(v{&$>kKbP%ae$!lvIC7@<=GYbda0xq`n;KA zVX|S~DVCmlb}9-1JQzZz#P!KZ{cO?`PJWFwo5Wp(CK3%cx04~zG;K%a?;Ih;;>uh6_(#rRfun9b`f9|sQ} zH&CV1%!9?9YNBW#QV`$WJ9k#Me);oaQ!O58dLd@FOeK3AzWS<({cX>)YVQ|z$698v zWip{J;waF0BHYjDEL^6+Tn0!n3>1S*IOk{sJ$vh%#%e8*XjExhM~cv!~`%$*G7mQ1`zH{)(TrFFhYV{zcWGL zkdTXs%wk)e@JNZ%9+oGLmWR;ToGgM}4&GyiqM!+1+k}b5t734BTMc=4ulHf{UDLCr zE^vUZsn=pVOvSgV>|yhC=6+;2xQ(Gl?~sX=^CnCi@X{Zv zi)AwKvOw*x?l&o~H2JPwD6MRaXkw0gZW094HVEI^G^_odrPP3D+{xqSfiHd1=N7)` zWGQSB=vvQF$j7S{ek(S@v5sNn=%OOwfW?S%<#Xk`cDBJ8S`+4xll zhWhqxcu=ka7DJ0UVQTTRR#_c>^R_vaHQTcw=4H}OfRqTNIg_40(lmJHxcjIq z(GaoQVUgDglY5eJ=QqFmA_K;iveR5!buQlE{9Qb1JS&Y13#T}Fp(FU(+xm7T*Lu~B z;LOs_qi%DpaMhDcI1Z6M*Xiek1B##-j)p0BytwxA@CsZgd>Eem)TxHO`P1B;Jw~1h zPXtk7vI7(^#B&LvW9&e)A&jtNTo<9dE++U9N`=xa0$|h_UcHRnqPQcIu2%=abD6in z;5Pz61M{UmYh%Wzq2#)_OQG&tATYcUl`}{vOYp+LuJZyt~v! zY~v|`fqE${N1KacVg7EG{mVWe2MABbs@7%W?KR5z2=J|ckDRxa6}kI_57uPg4JU=` ze4&5)(K{iDko7uam}Cn=-UGcQ{v6-H(>ndHY%{OE zhS#BkXr+F^r8NgtA)eu_Jc?agZ>NmztLQM zKySRMGI>!v(Ce%_2wQ*RTj*IkNOI;8Iu2TbD*mXAx4i1TE))voP}`%7spxn-ec<&a@cZhZ{W4kqfhq_Uz!%Kkyv)QvVRbyV|~S@kg~=pW*dn%zO9ztYO-$ zcrm0svy91!QZjr)y<~*uBB2-k5B|-k`a<56SVWuF!X;Fg0gVA7xb&;~n_lypbxQjX z-22d6ZMctKbapM8^m#4AjQ7FrQFQcf!Y~97fha~s7$CGIO1_|L!KLt z7@AHtA7yUANHkk+f#hI{D)JcRt2*%vp{XLegc0iqnUdXfn+Gd^?FF3dx$%RMhw29& zJcg>+$+>9R9-#fU&yWil@9&o7b1g;N@dfy)bC>RP?$&M|)$Q}=;u9GHZd@Nl9?u{n z@=#uF&)s(3QQ1MAw`HI9`AgtJ(ygzQ3IlGeX}mJp&;Xr)ZF8MXNe6`pvDR{fu|#l% zLY7Hl5XS0S9r4lx@M4i;Fv`ziSrx!&#Y98&Y|?80rB-~DhQ=M{&?+=`P~LW0+CdZG ze1bX#2(kC`MlC1hp3Pg;Y4h#?a?s{`6O$t?u0Z zYW3ZBk5_jJX?t2a)U`I$e$xP5-YJXKg{?B+gu^Tv2;@@f6DfKG$+|sw9Lrjh1asM| zj%UI5yhV)Hjd1-qOZM&W?ys&E;&7uuZ<^H0z(ah&L`X)oygUG4K%c)dqa1Wz6U#cw zj*tu!rmp3REatbKVcps?ZMo!~{bUi?ELG#-yE4jP4n1o@_@f{J7w}WLhH{A6#0yJ9 zaNsd_mjwy)Sjr309A=Dkc9^54=xb zT<`g=Oc`4*SG_iT*Zt}LAhPR&m--F&lP12!>19IwqXa24 zK*k|fNQ!{-93FPCokf0E3d=V3!`W_Dz>6`MM}?m4HP4+7wwEOa^W2OHC8~$S+CZC( zGc6h@|7F>LzPYlo}f-~O&^QMq{$KAa{lMLrMcm7dut3`ESm z^s`@n^=(;X96ES;78}YP8ZfSC)B@Dxo$+Z9dM``S<&G4Hj}T^OaY;xUy6L8c+=9Fa z9cW{r5rT^>VQ{Vz&O|t-^l68p-NR!YCpf*!6KKI8q!Jb;ba3hlqxDnB%#52B;$`M^ zmuCLUMp$iiA@~#YwKIFr!pGcb+~8&e2j%?Dd|2j66*m`xE#U#Ywk*s77!L8H<6eR0 zd+}mnonFA9PxTvCeKTJM7kUZj$DX^2xZC}(Z{CGJp*s3*h&A4xVO4k< zMPw2w%!E$B?u2;!5gllK5gx27jBX&Po$g;QlPcb8ZxKZs81^EK0jV#z(Lv$ptm%u; zQEjzH8A6JTiv*d5aMmTykDvXR002M$NklqHXU0Q`2 zhbOn9HaF3Y{*!q|spvWL$b2zaXX@a1&j|Cugjbvh*+UYci*a`582;S6o`LP3J-#-9 zTNf@SLue1IA+GZEESxAK{rXIM#Qd}Q8E~PcKYCJ%7bHXd5vB8&h z5kC9DuO(DK%K_4TN=gEIV>Kbw13)l!-`uqh%Cv@#-pu~F*tL0uuX?_9VDLWhqCrNw z#@g8H4?OfNyudZZMW?|c>~WB_bb9D->5ug{9!!p_eYiwPv-)kn;TTFQ>(Fv_kMY1WeI6yUd8>w5+p0*E$T<;9DLf9xRbIXD zOD9&-@l){}o}ROf&k_oV7Nhm%@U&UIykY=-*1q=VZ=4fU0yJ|yetWTX z`bGMP%&t#5Kk)8%--VO4&cJ-N`s~sVnF)U+m387HP~B+0KNia{E}s zwN5(A2=sCBs=K+aq^BgLAk241FG{e`ISQTB{iMyC>ezE|Vnpdw=Q%05cY||Ocz_VL z4U-{Y2?9F}*oO&A*R!OePedq`z@c7XG$Eg4rM1A&LZyPFUB3t}gJWV5ME3XFuo%N2 zuu!1vYp?)lipRv{8j6`74mY1cS{TtXiOw~cKmF+sR)6}ZKUsbC#qHJSzyEgi+pq4g z{-y&-O~8vOa&v}J1M<@!|F8wi2EnY036SS*e2{gy#pHDsu!spGmoAe9T*}acaC2V> zPi{c*yN^;GfZ7O#@)iGwHtfrSucbR zVjFr-aBLDR%>Sf~LImpH(VLQGM2lRX4dL<}rfE{Qh^im+3cRkFsA}#tZTP-?9TpYv zbAPTWeSA}KpX);aHM9HPgTAUKmNNLVmkI(x+tQ{-`h%0+rZ4GA(OLUL66_teHZ)q_ zmvGI(>{18_CM>ijCkv9a%VSwsCzLxu>g+kvoq%Vze_r_b1&@*sKsxSlNE=YQ} zkoJ+yae2&t^JfVeS+f5nbm^ppuRn^O&QsEL{qwJGum1MSd#m@}yRrJ)-~M6dJVoND zv0BfwDZcj9?T0g`EW$V1GxcGaj(+|5*Q+~)7(I=Fy-aWttNi26%=+*C?8mEv3@Xo? zhX;8nDODDL!{+VPLU!z>`|9gQjd_gu;CZ~@)#~1(GCdWN^!z{nF0ZA%cJYfsdY;4t z53>;c^A6%9d0pJp-0m%bMCW`!huC}QDR?5N5B32_H3U0S*iJj5p0Nmhx0J? zTu4&kayB0e6S0XBZf$b4xpvkQi(51&`N&4fdZvYGcf4XOD)r;U+d1^oHDiwU@JXY@ z+I+i>&eAq-1vlYjvR`KH5~e!}P;=Shtp$BHej7;_7DidW#$(a_SK-fM=hSiY$$k*L z=S51W`5)B|eN7l>?U*yHloTq|!Wv8HI9Wu8p3U!2JJ*m$^-hMZ-cKxw5n(-NFtGH0 zKVAcS7&A%hQ9$_*_bA%e1l+tij9n9pj`q9XY(Ktz{HcDX{Q%&#hb$t?=&&fhLN#6- zl=i#vJ{zwXo;~YRbq9;EZZ8{%DS$yVLLB874HD$eosV}E60(_4W8E7x7<5LmJW}94 zd#3#$@gdO_5;aVE?Fyc9xOsWFgaqv5v zvwn1cx>r36ui6!+NLhU~@=J!yaAW_0=Y)l>P9;1y#zJTCP5rl*Ssy2?tQsd1T62uz z>4hfXcqpuYivbc}#%*f5_P`2ue1Y*{&U;V zrb!|Rj6sYTo&d-jBU}xgEXJ5{U!Ty)aGLrzh6h!_^ecLwFvr%f z;f+1OV|l(XRq_B>$Su=Ov~6ywXcVu;Nk}*P#kpNy=qmm=YaH)_@##ld>rCT%wq#WL z272%67|*V8oAjPj)!bk9`z*?Wak6g>?sXl!6v7EBrby{)F%*Rvxc)Kv_Y*xl%c2y zNsoHdKc*)=D)giX1tqOS27?rhxQ3dUogLf08{SvJ6pX%~yj8vXC-eoX>i4_%++*@& z=1F(*gk#;OH1;YmRQdEcI_zFQGhQ-6j6_?*r>lEpq57tHRSZt)vr8GZ2&45(LI_XX zhwCoyuQ(Y`(N6f}{Y`5Me4mij-im*?!0=GC2Pa0U2#dZ@s+T zp1d=KH^VcbsJv4Q1NPXHn@*M3%@~XmFgan2>|oJ7avyBAw&Rtdz_M|=Ol0D7h4}Ca z5JI_Xze>}(e*0F)MNxtXx$X;Dp|EiMPD;qViF?`tW71ECoUbKBJ4kgqt?OZ7Zj>77 zA9qVddM1~>JwbOmE%s>ygLW`1$PXb*z=bTp4nN!O`Smh-9Icla1;&VYrI0+CW+_pE z1!cTVZJJswSj`hA(_q& z5it)v4lq-g>IaVzh2}?jfj<23oix$g%isL$*C}9+GN^SPfc zXy3re<;s-{%UjTEi#h2GQ&A7>79;80Zta`MJ!iHJweaG8JOmjeU4jTX2-XK-H3XNa2hQ#Qbl@7=$4T$2uCrWqn<< z<*k+qC!zDOOh0z6JqKmY9)%Y$3jmLGlZYKmJ{pZ2>PY(K;O zXqUHjC&38Ad)Nl-&%e60{M(;=(%9ROin$T610Eq@=UEIXhO@JCy9Fr&N_1ZLT71`( zo~KT4_WsS~*C}4V`SN;oubC@Zw<+i|#$Y>Ec$R56(}>j1Yy5ooaQpJ?!Rt(zc*C=d z2vV7|NFT@|#8S=jv>obG;I z3;xg>*hOlRpO~wC%YPWq&6StU9*{oIo~7__e;!_Bc(|L8%!0}4FaG&PSe?Z? zf{&2i*>lnSf%Bbvca|%;`-O_sVDq~!wge{&y|GKnKUVGT!Er*E)4TCap~x;4Pq-51 zR!xKMv-5`ruN8pXp7%327cN-@=RL$Q^ttlTncwF5n{vVXS?*)pXaoJidpvHPmsL=j za*c1!Z$cE{uYaQWzd!TE<8z_6vA1*x+nPyMbvfhU?CjwZ^G-1 z>(?eb73F9Z8VRb_lh(Fybt5lN6FJ@%{Na>+9*kqXyQaO3$0W?M>`w;B>em;n4UG`q z<`C81F;#fJv()>Xo}Wkqn4_e0gX~NV+)RqCU9nqDbcDhG6xvW zu#*GC=10BpnnsV`bRG>Wvaq)&&sO|%{?Qm2TaS4m80U-^>?lrPj`yGoEF282F5pcl z8)30=>7&qhaGuD9@Epu$&T7wGX96^N&84JLvRS314z3s877g1|2R>jy$gDf9&9zn7 zxTp}JiceIM>G!q^-MnXnoLo^yf1i(^u=0{!cn+WujMt+M48K zr8)Se0ZN6n6MVeAPj|d~Ct82ap?}7z9e;%@&4r1orZ37Rkm~fdunlY%CmwRm@Klj>e!yji4Q+48b!tW@Pqr_%bfkOwwL4fuT9A*=L z{iG;bcMhk_^k_a?OQic=E&4svcJn}4GoKU){qf$})}L<8yn(aJ_*DkaD1GM8;hqP3 zpLgxBu*>FC@!7u<+*^#No{>4UlW~%fci(Fwi)UPOkty8Ys9PY0Iqq8o-YK!kKj(u4t!f7A-rI;k5`(WgIK#y z_~nH%NA@RAC@0yYHIdQXoH33aVn`&vFvfIG!WcLr4gG{#S|6+9SnW26V=zBlXgJT% zz1CF&&7ZzLX9SDKF*H0VA)Nb#qRo7rD3?Jr8bDg$2$a&u9%(?&QWU=Y z@~h>u>$gVfIbN93jT?8tbrxLNE-WMtl(f(sZIZXD>`JMz&&E{75*ES`93ZgFJBVE> z=!Xp$12}!Mu#)cC_ym~eLRJF&l?dWhXASL?f|8JkvD_&pI&@qf9o(Mt102vMjO94Z zHehd+6HZr#DOa~mgoJql`Tdlj=5B}y1CS>AM8b!xD?-~gvUEUn$g(0-a8R&sh-I{7 z*j*VRgVK!u_{TqNU%~l2I%Q4Iz4OWEG1tv9inW)i344%d3bEWid}q0oa`&wd zzO{V#gZDf0s5Gl-tWVpwB!51FwlMMpefG&$9U%66`TWk#^4lA?mZQPs+Kt=wq4@F` zBMtg_Rv%t6smq1yHL}9|9PA~dzidUNCj|QlZuVuM6^P#uj&)d-m~#ZiQqEF%v{0)N zAwrN7F}}@9XY2p7ah1}}5%MS-7;lyAd!kf}xYS!0)*qky0%ocL z#OxUc&)RGPM!0MKCXB0c1dDaUs6AwzPK{y4KzbD3tPAJNU3Cm{SI;O3>J4zCY&Bke zaZP{s`ZnJ++OtuBeeOR7uRKo|9B}O~QUu8<^!Mx2m%wu}{CzJkii^PfRr`Z3vbth^mIe+G2?0`^@@C4CwOYo73Eat( zX(Rjj7^uBEJL47Z)bPS9zKn;lwA-UtdYFs~d0`8^J6Y(jbIinSAE@8xhwvhc&C3h| zuMV`2Fy?BHn1mXhtn69)l+4HA_8?dj0+C`$U^+%L^ysHW z=AC!m8HU{~8`g69%Im`ep4103!!Tlj7iMv?&HKk%Fb-v`v1u6ZB$!A2ea~>xJ3=sd zm}fxmFCM@$7vt+_(p$Nm`Ut07qfpIX^ijLjVSZgw zlzRW0av7rw`~YZ zfg^mA{gk|UvMmbg6sBvv#upew!M-IWUeG?i1bJhCzi1VXfg?FkC~Zv+E}7rDI{l>x zHoSReeh1pssUGvz8`D-_r@Z+<3p1vk*Sp$E2FNheqpDtUap2N-+d0tJM3B`{(PR=p ziRv&|Yr1(yo3Vw~oMiq7_u?hirBZDtR~%{V$5VdDmd;@dSmAe`=9HjI9dV(rWN?S* zI(|SH>t-~2x;;JCxL2&`l>tB7YVPMSIiE4~aEclk;a_a}M0D2+dVeI#7`)Pv3gZgTt@@@=6INse-;_ajijJGDCAaY+G1Uxed!6bg5 zl?sRBVUpzyz>F)*3nAJoP*{x^Y6prT82u;M5C9aRX}srqwoy^G@hfz*0^QV z(|-hyKo5@K(p<#Ao+eOL-IQUCWJDo=8b1rciPJ4|gVx;i_E{;@hrU>HFpmk(2{!KS z3q(U~mGGT_t8L*MD-kw^`ZJ88ceL-COlf^FK5ZEj&dogy{?e`ZeuprrzsSpY%6IFpaDo7-m&_7%stuaz5;%{tyfZq-)1otfrmn z!eEAwEMPISN=(+a%AQXM8pRf~>SrgMjoOxrg+e_I_tmy_y3oU#x6tuqa_Jh&XJ1~+ zFz|T!pioMu)0@+^-;FT^XXd~n+|{fGtJ;q)v^PR+Gzs4=MwBKDD@R9tjPQMK9(U`{ z^UB!^1_DyXHs~=}n1bHUs!QR{BaJDXeNQ=N9$dDxRpWWNpV#W#x#KA-!K9-dx7z#nq`2V! z@QaU^FWcL+-CiTG`7{ft1!%I#gp+R;*7vA`26wlkzsIY!PW(Ea@#N{9)w&w`P|A3; zcH(7Q-+42<)3*dCiaGSE4?_}t^uZ!Df>?Y3Z^|w;JO}}P;AI4Nd}QULN8|St z8goF=F?U{zc-JltfC-JLZ|#%MTEg?B9iH#u)Ed@k{R;Q$jd>!NPgq;HAXC$mXp(RY z*PUymt7BN8ln;KjCm}(^>Foqk{7C%-qGLi;^rM%AIpI&62`Kn8+K}83%pi<@&)8LBJy@Q5c=OWeOP*C|dT3cxU99 z#-k$R>UZ$YkfU5ypZosHN@z^>fT-fxRcl>u?95*ySDjA`>I@W(v-Q3&9)%E_W9BdY@ zh_6rBr22#?H6IdG2(g^=TcW#{8P^y#kL5`ok3vlmG{M}S&Z-C&;Y<99L1e3qIT3_#7A=3b(A!tHM-|q6czX zkH#qE16k+rDBiW&q^NJ-h+zFTXM@x6$(`^U{Pc@=$~^6_=VTu5Pncx&kJqDd_%F-u z8v5DS+PB~QWU>>WQ&0v@m9qvo6X-}zf|13#_o6Y4N3_R*Xu`{c^like2j1O>OK8y^ zP+_vxMsVes)h|3@Gx-1>lPUZR?$v*5zUSz7!a2<$0M!?t=3eW>&_Hcaguqd~TC4+tTl3DBPKVN%oaGVZC;EQ&v!}*TewLJ=-oCGC)zAG z>%fgW?Gfr_ds>dCSzU~{-YKQ<$q3ZOW2tVZCn***!_6#OvZ%4(+6!|vYx$RXI53MR zgJ>=0wJ+uM>3%j2Z;g`lq>f!EOPqZ;&S0>~{6WmhUZ5LUfA1H1AcV}a)Uf+kM04+t zHp(I%5>Pe>WhO@YUBpEABvAA0ARq$38WLwR(PnfR-gx6uKNnMtQ*=U72S0&}jBG6C z&r&p5428;^*lHpRB|6?IyLa!l*}MZ8ue@=l6uTv0xVN*spBL)g%V%2n8h7&{OlXAR z`pI*~v$p=QRPo4#6@&i4r^P1j+4rU>Gm?{=ALlMSTwjDE9o)dUBBJWH(GOERm^A?7 zAcTT)*fhi{6l}8pR5#`^SC6NNU)=&ri-frt3)tRYg;*%jC!~dYBYXvH%JmRPW2>`$ zUkR0fVUAs_M2|aUOMd$^Dd$h07GjCn)n2o<_NO`S=F|Z$YnV}A>ubDcC77Df_fg`i zS|42cJ3^FleU@kgGYui;IiVA5W@CE_u-jmRnbg}(z3z9RYuvrghk0h>UN}77hVv)K zt;-X{`gXLxh|+K|ulL@2yLpMpB)}UDu}Jna@%!L3=aTHr#q_CujYXzsz)z_OYwKHr zTp*a_64i%E?Zj9R0Htu2zvh}?Ap;}^WE!6*jJ#~T&mMQ0cLLNLyhnJdcF-Zff#tL6 zJend!`FoaNBrH=3^wV2;d7D_q2^QFbdyw(r zWpg!)PW?u&#%Yg^6#e?TUH^v3=FyQ^P8_lLU`i4?dz#R9C#(07EZfc$yZtH)RK|u= zS>3l1D&XMF_AFh$y=?%!pK{mX&@Vb`!vS#zI%nqyynu)GZQn}j)bqf(NQ4JBZVKm3 zVY8R$=!fsr&kQEbo4y;XGmp$+jSM~>L2uy?mp(xPt_eqSRt#Rmknp%Uadw-7E`@Jc zAX&}{8hF#u<2BH{fQbdj=5JX8g(na?dabzW2V@80toCw)Q1M zj1sGFeYai;4M$^Oe>fgK0(XM~tE15u!^P3+Ce%fa@o)+lJfQ%s__EP^yD!wyfGW!< zK{>TXDTl@m#~Y%a6<~E=@y~AII zG79wI(ZDB9QV|lAi(`d?k!_EhsGd4Rel|BxpGD8fZZ94-zdc{anUb&GS*xe`jN7=Z zBOqDVmILrPxS1oX``}b-tY>6<_@X}aj4lRuD#v)wa4zu>&*kurwt&?8Pvwb!uq$`hOoaDTl0;5_;7F?V|Ta#j~PP8Gu}9(p+gfh|DuV%kc(8uUJ8S2 zQf#(=Fq@q1U&)0;z^Q`{+I;to*O&J?EO4t(F=vQyN3zUwPy5RX^P;^w7YZTyQt8X){zTx=v@cNN_tCgV~BP zA4|i=P+lg8LT+03s<4DGjmLz`^g_VHF#KbA6*e$}XGmvphma;7qaUF+X0UGw3IKzg zZ@hk?{XpL>$39nHZn~tSQ%%-`gelfwVSp@R-~O=7afLiRNO64U%`3(9m&P=${jIB4 zrjJ!O?!7P}D|Q~SCSfv+^sYnpP8KS7Cq?s@pS71FME|27yu1A2AN*)}aQnt`wsU}X zE^Oz0E4H;vBif6m8h<0}XN3L6G#TQ8g{(q8WB3E`%F6z=A?0?wVFYBZuANB9uhIG^ zw9i1gz#r0kmsn!o!`_3n?%Hx_t%W7@nz=Z!d1`s@>f5Gj-B=%v^2!OHK&+T@VCi$j zTb-DS`gzb+hN(|I>%0DAW-rQYrbq7ScXTvFC4{DL9;66ctQ(2VrQ{m#rW{H zzRki~Pj+}L53!^KZ@zgxoIJYx`nO+IhfUcnGQEEwI&whZc$LgUjO_`A8IH>yXv^9; zNlz>s1j)vXft~8GNoVvNd6l81tWmEBJ1G^%`fC+%;`+fBVmNH^-rY%=59frU5~|KN zjyEoLI(#q|!o#w3`V6no*$Dg)2C5;QzqKJPH&hvAg{h_lBo!Qnvy07J-!@4VSw zs*DmXjD!?@WVv2bUx(kdh4(JLcu!zq<>#}EiW9TDq#TY;u)GAne(a54K^wxXb@ojz2Bkxmwde#)pQ;f}#3abdD zjy>MuPB^0ZQ8HAvcU4QTt-MD62w1MM8po@63Xis_|7&5Yy0kq!w)V}1wePTfK-Gii z_HgfhpY^(Wl~3=OHRD+8YmSEDAA|@(!x*6|J^ZlO^n+05;KC8Y!Lz!2h9ilN{f&nE zy$WT~NPV@RaJ#T?;b)9;Qh1M{3crdkPgb^+4fI0zTZLKe;b+xWKaa)N$IBE?A*?t| z-C7G)vnM6mdD)t&K7ukKSKFJDB%of_M;2!AmV= zD7)V2LgtN(z(w&uZxoqztqQKy#~>r-m{tA&4+=sFK4_kxo@moNAD)C!vY$?dyxwED zG*>gs{sJC&N;5pNN-x5ZnvDnVET=LCOT#Q8WQ-%hP@mRyvN;P5ghImSMm%Iym@1Qe zF#F5u&+&xRF|fol)){QR2)r5(ZfIC|=04tl@q~-^_u$M45LLdHpXLd_dGpKVt+(DD zA(!PIEFUG$kQ0nuJ;pHMRg5s^z68_yO}aDwCZUI{;?KtqHTm*c5T_{?)_ zq4vP(M4{8X>AZ%-4`avgDN`dv7~0f!C|F5R84I3h3_Al14@q;B0Ju+ z#wMKZScU-WJ6;6uAcJHjJe;~>zIo`-`c7L1^g<}{PW!>p9vJP*=93I2xS&UKfCdjH zpHaS^G{&7|@G)XEhI8%7miz#nfE}ZzF}{qZMqciHyiNGs$``6NSdg7(kLH8+-|PrO zYw^cGcr9dp^Zl#qx0~ETVv4J!h!9>dueUmL;Ro-((|#aXmqHxRvRs_VGGM|VW~IH? zUYIjkm8Gn99>=4bcVmzp(rOT6X=q|Y&JW+dJo^V|kg(xm#2=-`Ogh0hp>}IF5uct= zxF_A?8Df1V&_Qkj!Z4Qz$pDQG;JJ+fKAzH~29Vx8fygkT?jzup_*c!`gixvgk$crf z><>S@y1f1N8)c@*DpLVW4S{Y)_z?XFfvftC^}|bM6Y00!x-_YUWwWrqU_3wd?Fh?P z%xz6 ziEJQ{n_xxJW9_D-@>)#DLGYQu29{on5k!o=#&ThANXJnvPdZs0{EQ*s1cQ|zV;{7q z87HVhIK&E0ew%0aM`4HvfqwP8RLJG~-ao7bk@FbL-U8_&)kH`Y@1VtE(!Ezd z>wuVDZ4EQ-^Zr>nYbRnzI;<0wi9iNN;mufJx_!{5+n%>PEw^rU=x{{&!MktHSy&j< z98MR5a1i6+gl-!J*RT)pZaj_A`9U*Y3tcz>^u}`T!ujPwi#7Q1)C@c;3#W$9f>vM~ zLA(27U8&rW1X{MA2T$8y_wcKwoR7=-*UwGjhMhbEk~ch0fp}WmJ9&S8^~p!euRgyu z!m*UiklkLhjf@jp8@tPQPF|iA*N5}6o;sGbrM^AQ67#IRe}C}3Hz)g=)6h@l5jvTo zaiY1wq<0fw4mI98cOEUDd~t2LRXEN4gm(+nKWLxbYh%!;x#n!MiG5Lw?}7Tq`*EVr z=IXsXqUX=GnD_bBQ~k2IzFf{k%d#v|$WP_Ho@|BYg<=zCSIZor#+&xXKmPXeD43mZ z0i>WFYq4~e(XBkVcjHqOKB>A9$KAX3Puz*FUM`;%P6r3!3t?{dN2HkAZ}iL0uH_-W zn=*91{f-%KcyFQu{Bbh$gKJcQqrYZi74YM95$ zCj#rx6*56z20Cj*i+Us!UlpgVzR$~IfJ0az* zPx|mY!E*RSJWXN~$x{fMBX|XGeWFx8&nga&cr6?d|GWcL3D00Yp4DJW84k1~xEsGv zJ_ZjKbYWXEhHh;!O1Ni^Yd@LDerU!pFn=8H<4wS08tIISA|WyXR7Jt;?xe&Okh>WV z95@f!w1+bI9#3g}_C)r-6wGji;)XfQ+c776Zz2Vn6UwV>yOhqWZ@;ths0vSLZpgEh z{DOuk0{Ult7IlJGnJ))b3)zD^t9QG(ir=CSbPHFlDR5*mI#$>53}Em~y=dWieLfcd z8`@}2dNlAl`YMIBqGKqS`p6}*PveKJWD`pnSlFkkz@d1wwLLpbJTB3o+sCAk%d_QoSUKD;@ROtmVr)rp^gf{vQQ z=t)Q)xgT@_#_%5a1@=K;zxRnMXK)`2_EM=U!f#hA?ktU<`-Ev3!ORU*8^93fF_RVCw|5?NhRW^6^TsG6;$_#E(rOLJgm??eGmNqreRu=>n2-z=PytLeEv-vx3Us>L|czJpH=eD!Xx@Rb7KF~fvl9xcYR6_4;}|XEa5Zq-Crzx4`RsLVL#Wp(8r*fNA<22N?}yu zi>s&cvhWOpf_TU*<_oUo)qOBopCf#qtG5?12-jpRA@F9Rd=>&vVu#w&_G*!>w(1$7 zq`za~>t%#b$!iEIZLd>%P!a;)ZvMEpXMrKCH)oep*7e!Xqm<|S9TaOJlX93P$%a_C zBQ%D9OVBe4UM_vMDA(g)iR{nf@Vh(Ox>Ydf%dHL#_%b&Fn zvMfG&Q1X)Amtf#Q3OFTF>T1S~o2ACa;6D58%jLBTrGmf3+|gD9WThYCqnMyPauPACI^A%e-hYb(q_|yduZ*s6M=TeT0;gd5kDoQrJ7g z<#PM8KKkN|_{ihoYnR`8bNM%a(LTJqqQ5QW{mpPFP>i8K`p+xP`SCQS$1UDRf)^a( ziG*WCkj?@kTy!S{K_%ti~!NxKc&Zdw@`f$SIu`=;F@R8z0k&x*Kz6F#Ko{rfM zWe&}LJk&Cdz8MS|E5=&Ia|rjkBuL=7IT^m-K`-JL4#ED>`Tqplr=ONElV{|`nqpIxEi^GGU{Mr^J12aeD=sSj|>=i;pr0A=#vZn&Yp~L zebX8`Ajh48uTZ_}KbTefctW!X7&JHnX>tMjKij@U8Sy;3pAkh;iK$;d^7hYurr^Y| zYkXu9dx++6==eTpKI?rvmMlOKk=3pa&S8<@$G~wk;~7-EdF{&)$Qe~B3i$H!GMOP* zK$qRek7k^b0$BUa>qZ7!us@ZTkf%dm4=1awf+SB?No4>ueA2gdCYtqlGsE|K##6AY z1H7T+fCtGxCr;Te(INwGy)_9_8uzUGl{r>jqDpLou%?f(b`QlF3|9lt>U|aBJYiIM zn|Pe?la2NfS?Bb}d=aR@b7ZJ!&fe~-ZXNIJ%dnJE&qza9Cks_Y5iv2Iml#o3|p=4B!dtn*#aSt)Zfi3-e9<0aF=n=x8Y zn5vNsyx*R;T$DldwR*~;r6MXeQpD{8a`q+~QI_&v4b#6-HkxlT&%t007Z@VoFono^ z%qxeMJQMli9xvY>0LQ~UFsqJ}B{!0|V1Jy95i!^4>fnw^>i@b&(zxqSFohf7#E7l1 zIiC6(ll{hcgEbor$kBLy*zyN-^2?yp$zbnyqS=YnS0sIg8Lw$ z>=fh=D3pn-2hT45@Q;5#K`$jEgo{75IA-CD2(|XK(6PsD0={{>u$2h!e48*2B+z~L z!*^pyM`Ii<&`s9;V){>%ZnHRzbks6Gw8)-|uxW`q4P>T2>|pibtvV(Zb@O*Th2dUx z9159FM&$AI@&3$YLCz+vZ8k#$CT85N0q0bfU=uR2vW*j>&B8xO+;IE5m4)PDp+fJJ zwL*;HMhJlrKl|*9<*j$#P8c{{s_YvB+;bPsF=><&eLG?-WvH0@1c#l3GYG_+H5-@f zvk(*$!ehjxXH%xkZ%?zxvnZWDccn?pGPC=j{ZluW8+jFe^rIh@ZuN`cSGYq;OYmkbu_TVUGP#NAE*$o(u*kYE37ybv7@OFjyflMz1mQiFd&+v!BIl@;kIS`DvqU%CLURY%F zznSp7UKngps@%)Hwnk9N+Ht?Ix}W{pxjyZ&%e~Eeh5_H{e4d-zAJ;z&rBJWDG&Wmq zCICnRAav_lI8low{kXgQoB#0hHuk49h7a2*4i+5$&V`#i6i3e_oJ9ZUFTWnFOL?AB zM4{QrTI3x#aJ;=bHda0--8Vkzg?SS$w&(3sR?|QEYoCW*y!0Fd zopD6SgAJ&hanDLO3y<-x7d>MYAN&qC^^aHg!Tr+R#)B+TEWQi`Tk(~b36kgEyEOQE z^Y*>0pe(4-XnfU1W9KbdunC&y66nYW4yhCVM0wq=@2&|ksr}82Q1(~Y^R@5QI+n2F z%$R4*<*~BxeN!&NiFw)=_`&LHC1rTXo)S`r33#(`l0dq^I#xLAF}~q5>4ZquhrO~& zvh+$6Qd|J4O<;LT7)iJu2rYE7Dc6T(}kC|qM5PUx8so=U1|s;VA1CY0*L;nuq# zYQ5Y@(Vs%WgW?S|j2>Ef5{(eRcnzEhGVmb)>UV#=D1?mQX3d)YQ}ODCx*7%htDO4K zvj`IF&3K*q+w4UnS}F=*EQ$$?-RJdrQ6)#)L-F#(k!9<(&CBO&x@q-I z6vL}zrTDW1C;Bp$>;}K$X?)~F{mVFKPS0OTuBboH>kr_7$))yH&!+V1@b54W2tlyO zl96$&0CgE|_!0J<)48!X^Tt^{Lb>qmNtV-EH%9?9*RKwT3jx+VTK2)--mlnPbJ9=a zWz4hx@ytesz4pxPJ>rAggmUu=M-Rs5m6I&O8ajM5nC}K(p_7c` zqG0A+rS9VqLM<6z8R^CY-u)1IXp|?y9GK%VCe)t(qmMDd^%-6OYCx60;xnm%Grzj3 zL-w$aQiqT#GXi-ysjl$`fmQXHK6p5nfy{qnb5USa=lL2<;)zh+uMHqgvUyfy_?`0W zOF+`N!|mppoT|MszSNH+!vhsw<)$$@G%mO`dlsu?JWBl)Qb-n`y{^%Qy;6o1nAh^v zPyV#p>+8rfjjP&Lg}?i&Cz?E*^8~W#@j+CjY%2|ia zU(octXYhx7w9-}cG<0n4T*-^ci0Ye6$c$O&K0^wc_RhwM7SLH3s&oXxfN?epipBZr zo0n(NB-q(sa-_WkH2q<&b>?Y;{OtsgGiT0qpI0IGaEs~vd-s>im&JM>=sXn%9wpR- zj3?XB{L`QRGU2-naGfXQdK6Hh(Y-9HZ*|U&eINs+>L&mh3NO>_ISMgYd4xG_B|y!B z(jq<^xD#G5Ss89jCZ)%BOrB7=9-z8f@M-N3hoxQ!*`>l;-g@Io!b%5t!k3;+ioyVU z^_{E3h>jlJoLu`WlN86Fa2(@%Tx=uZ;!%I^rQp5(`Ww~L#^>Fc@9U%2pEa7g=x@G?QwXZtrWme=y+ zQ4WPCLPX;k1uxi1zZ_ZxE8%B-?>T~p{{$gc=f^1p#&IC)_c56`8oIdG9#zIXH1B=4 z_k?@+^5DJl79L$GH>3!Pr9fF!ygg2W!efJsCMU)um(~?1i#UbaO64H2Wq;D9# z6QV=%KEvsXjqpfOqjZAX5O~5ZOX-9$G;ZZFIFmSn$kfxf%InedVE_O?07*naR3FNm z&+~b-eJo&$VImBIojF*&LM<4cfmNfDyg({+%(@N@%zo#!?^OSbvb0&GX)sV3FUf_K+o%ELDCM!@+fa-Y$JTnOKML_3WNm{lOPD(FVrbA$+*xV zqFLs-iC?DBeDJ;Zm*4%p@AYn0x)h4@h0ozDfYvO}+`J0ce)Y4TEuVh;RW#W+YU7Jr zcT(|HV{#fycnJ&(qPPdqPF?L_npA>f2PYXnRik;naB*y_W^n^-Voep(10qGlzUocxnuCF z#a|K;c-4%oxEQm&l!A9I!_1l$aan%-#r5T9zxizNs45yyJZ9!F97^cOF0}l#gIw?B z%{m>uOYE==MkpI$Dtgf|ugh&_sQ#Ui5l8C8mj< zP4&VF&c`bpK8?Pm{N|xL!H|>Sa<9;5p*(~=X~vyz2KRY7dF&3Am_@x5og;ax&>bUz zlidkm_w%qzDq&N*n0Yi0_oVv9bG|7T{^J_H6R9$0P*TCkWzE79{Kg&dF#K+;*`5Sv z3!;oqRkA+wdp_^I)mOB+S5kBKX|H`6m>bg-c(M20P51Xo^On^8%B}ZxozO~17$v6XhvJ`lG~?eJ z8yrUGBTV3@RRTAyW1=14<|?E#K5c{DUbFf=&$@#@GuF*RZ+Kq zmHMaK=ytp}DOQ1|;${&KMRQoLT7Z@S`8j-&LcU( zi^>V>cLtJvt#I>xxD%Z4ct#xx*DQ05O$hFIvw{&gn8z_BR?m3#t?%B?NF+RG`rM)f zt)!?{o?K#l`sq57p>7m`;LI3YoZ-k!yBW)>v^A);yS|tQiUUhF%Rl(=G#+aXyD|7> zW4d)r7CvwWm(4>WcjE8C29ERWGI(v)&J4Tz+8LZ}p7e3rZ5$)O_8q?)kG^pPSFqbo z<{4vO;}qGl(%dY-^~GGGLvT%)ewy*LHh7E<78-1=V}!9s)wm{Vru8Vaiw4&9f_$fb z&=9F~JP;vSzjmUj~=0I_-SmA>YGxDG9 z9HnE;0|nz{33Pa?tk(`=)mMjmPV$lZW?Tmjt!cP<>|Lt?Ie8(N>5s58w9Syk!-h=A z65vXh@n__rJ-q4LdN06%*g6Mmao>z@WSd@;#LB&Sp}%9KHr_5+I}q2$KEwC@a7i;e zc-%8{;eCBIAQ39gqQf#7@*TBsqNpt`!=Zui)YTe<4tkedFJS@&Mr4hUeBB^xlF>9V z2K$Vc!$WFEpWL&KIY-Z4EQ-HAdIo;x2(OGxRs&(YQ^lxe?&~@p);$U;qxB3d_*P=> zueBL(_}1%aPO_twfA7I6KFi*MG3D$@yQ(0BGvOOy4DFALOvhNt0Zr=MCkb$OvSyx) zF^q;7b9v+HTO-(Q=O*4Pig~3X*?rBo1qWy32M#&Vr_VWVdl5Q#5M{+ zIS^Ak0+B+LLkDfL&b{5FQphaeyk5=s2n&R>H!fdZE_L?N5wW8UmgV!a&#rgo&$}rk zHeRjP7vO!&GnJAht*yM{h*S8BJm@lL32|WsIF+&o>33f`W!piig=Q6ENa2K`a-Itn z6a#tg!o^^hFcO?!#kgg}liK>zuf9xKY(5(M=>*DKzx;Ul@ZEQola2XM-ZP8Vnc{M} z&>0g(QEmQMD`$)~1W~k2!D3;j(@u=inU^}h!P^Iv?t+}r-?7!bfssN>-l2ig2y#Y}(w@%2d? zZ$YQ<-VH}Dq__)H5bk%bgEm=NURrD;dJF2#_Ps)%Zp}Wr`zZ-8gOy|%kAdwxc(&cZrFXr)btuU}B;o|*=_nIgZlo2NHto<)P`PyfnJEvI3uo z@39ykO_Bf3+pjHG+s9-*l%oFhxeJr&P6%P*)6Ch64sAYn>df+2Kl|14@A9zi#7imP zG6+%PWt%*eP{OEjENin&S1|9=rPn4jNAeQHWFaFwnd94K-*h$u*rI9SH3YaS9K#9v z_uAQ#Vo-EWQFzc^Wi8Iqv z_~-BZfP=p2puTC>GnYTnkM=EgtD$K%R4Owqq?AZ53rZ>Z>HvVn|$CcIMdK5esNwij|!MRscP@0eC(_SoNL3h%OgD#$^49|(s zUX7kQ8$3Mk=Lp3&ek~c%-S&CD`S#U>_~tv<3wuA-oF6z|Kl<;TxdyJ$X7C^y!9)A{KeVzAXPyBFa0$0u=eYhfo>A6y*w_uq)n_u)KA)METEo!_C4HP1 zmC+_xdOojLjW%X}DG=%%xb#V#(Zc>*U{C1XwQu0^uEZ24GH~jr*Nv&7b-bV27wSgI zH#eS-M>qZuX^8Q#$-52JQ$X~El$ojE=I+!#f@`EL+|Bhx4ITi<3d z}IMtN6mj_Ffne#?W_GB5}B98WbenN$_$2+VL_ou>fO?SK4ng zhf0}{db?Xl()$U!*Yg~0B+MNydy+-RMnRdU2(+it)>-dYpcN8QJGM`l1cKbtHqWsD zBuj@3ZLVswruI!DWFh!tR3^z}VUWV99Q^uvo+tfvsygV|8}sk~@@Hc?ee3m3a8Dos zoHc|YAeqSeM+g{X58fN~!%5AQotG_c7=f%JCWxzBW;hBEA(^6bxJ{eUbAormXZ;k{ zEX=72tF3UHNB5Wa-?*FwnX=Jdth1%$&i<{w<-moqU$O9HE$TX3@`kPYNKqktAHf9r zx4&V~z%hmw{e^rl6A&@MxYr;J?VG~W>mhqyu^1zUIYKUFq5BkHa1~Ka|Qy(+S~G* zpIz%P+>$3`U6;X6CbF9;MdHw(wW;@P-hqct4lMuruYQ$hED)wmoM}&sumytulV|s) z{Rur~1w`~Qb(W^nTW9koZq1%B?b*EVaNl)NTk-BH z$s&0CHKO>9gq9}zEbr18A5+`0dxHeW7p zmcIUAMg}~UX!0r}ibN0?a!8+&n3uMPQoS;RJ&7l6rNG{}lNC5?>b<_dney<5@4t~J zCtg&a9%KQPIORn1DWvdZ_;X_G$nsjsd>tA3XQ{mXY4hIJs0YFR+RZyDa5t8_?a|?3 zl7)?6J85B?yR9~G&zUYM#>V7yZ3`AOmMQB|)=B~?<;Nb0!On2uYdFa8t$;9qzW>+Xq+B!n?_^NgY?JRglvQ1N*5AvJ2%58s6^ zGMTMZ?(R3^B3LxYp;tZH<3f!m>fejWM=LI49GdodZmi>2Kf_l_Npx-Ps3!OK1+-Pz z_TEYtXrl@Tui%<>6pnd!LMipDGT{Do=l1K~pchemFhVBqibewiK&-1|EUroqfe);hpz6m|9Wj=m~CKZAYhwywd^wGC^! z21j7(wN*I}?(PllR!)8TtgimyF|JXTRiqF7HRt#n&|6oOowh2mDnWl&w(7goqmR*j zJbTKolQ?4+RzHV%f>rfTUxT+-2Y%Cc^QR| z5hl2|&S~%T2IF{lCu65}{CC;LF!b3|-UGt%gdz44;nuv8=~fRQrAO#}Kgk(2us092 zk^E+@Ko|PT5N$up(eQP=aP@N{Dtd10gnkO~WGVB8P8wR{*l3MARe14cusoTvN=lGX zY`k9Z91fe$UK#^Obx)EHect1f@NB~G;LqUi3J+^O*h#<5*e3yi%x`!H`So!9iqQkj zjBnr$2YPN6R@nL7*n8h!>k7UCUh3{oCA-KVd+#x34vqA8Wrr&*)bT0HfDd1_;%u{- zyPo;(M;qk z^-dq>T6weim`f51S0^maAaq|H_b(gEjn4OnGcN{**ZO)&#ESThxz)2$RL!EGvoj zA1BCRWDfr14wR~sP)TUN)u!c#SswK3VwTk7Z4S3Uu++H64SFdh?s$ker%cy*P7Vsz zozC(3{&(L?usf21lu+9^OqK~En9#tG-u&+mZ5W^@ttRa@!a2P~%TLfL^{>r*PqPMD zNc8=}YZqd&31K0Eb4Xrg<-U2R{X{IX>S;2cJr7Y5=pGe@b-gg51GyR1XVUHYIhRnm zn}jnPrE}8?ml4jgT|aHzpb$JxxTA>j0@?3z{no8P#y|e??=PQ!{`vAZdE9vAE+j;M zzkOQ=AahLQFaG-H%jbm~33dF=d+&}QdA=kH5cl?-EYD^CJ5*oq)VI@l%50uSX^v6G1-t6#R2bd8YrHHnH^k)4R&LwqbfDUTSe#)88toLUNA=`>r z&FwHr1XX)03lGf0_)IQ5ZxQej=FE38j63;mmEE$89$+&{ww_%k}%K#l8BRf{cDxhEBF$W4rk| zQsRI!B`P>lQh~7uHr_@w?ce(H%Zv;UmPgIm>ELs?@yY)7pzL{QK-lV$1bH-ZbmQeD z5RkZIC&kPBKD&OS`zb>``|!QDhAF~(hsUupS_tb__!?~sRcP^&4*ShQXoXpxjF0@) z&wjgHdF!n?K>7dpSO3@Y-~W?8TmJ1&ewi2R_2u!s8#9*geCw^{H^2RKInx3ufyquD zq~oP`{`B)}%XdF`Yx&79KU=>2{@WA7``3T*|1H0|?$Fc|rL+$1WXMSBw}!uJK{^wB zZr|##)F*|Lrrg=j`1V_`En8BLKMl5};B{@*_d8ktFxQO|49I{via~b0hlLmtD7J42 z)6HYF^L+X2#r@@j_bxB*zH?>yDtbh>FN)9qMadQ3c&+_8g_1tZ%>1xDSf>kJy?n7n zH9USCjl33&|LOnye=biG7#;=}9&M|{cB#mDBj6OJ>r8y*VhLhiQDlT*v;fVi*8!RE zHCD6eZTLVqm+C(Ja;#h_bqAXRSw1%_YldXW5x!++CK{mi5626;smY>htg@Z`OykO^f_mW-una8xGYN#?YW% zC27w6rMMAJ@C3Z^eu_5_vj_n^&H%ly{+O84&m%(j?OZdkCRC)BUhQtT&zB3w8=3LsuuLeH4 zTOEHl&%riaCj8Y*^{zq|_!E>I9<0Ck9|K%(&)iJC`Ua-eKNsFU{i~nN&srBqucH7n z<0UujCseV{=+}&=zcbwGB-xEOK-JY7_Ku8kEIM`#{Hxc0UJUgR20_bB^EC*mNJbv8 zmQ4)Y8CeE<6bUYMVEW)4Q#ZrWhyG8$z`c6ROCPJs9oc+94~#uBbgn5{AQLs%s+TPMLyyW=gZ9dp5`P06AFNJ0g^Ktbl=ft|a3|5C zVXIa9tsg#o4Ijp&*3Evv2`@tL_ShV)-{a}+p5qauJJ)wg8yF0{>w`WUw+4(GJdK6W z50|yAEKn-6;)Xtec`{26)-u5a|3UrUQQy3`)@gFB3m_DO>EX)r_uE#tcELy6Qq?O8 zr&oSd$NI0H*@M+Q?Qx7Ifv0b3n6k#?KiZoC)sLZL{ZVNTu?A(vNYH)l3~%V2Q3TOj z{h0Pz*Npq`(->!s1PLivw9~Q}WBJbbEPk;W4<-_;iEzX>qlSQZr z8zgVhj!Ehlq?$_BzD;LMiU~d(2F|4DIRID2rD5qHc{d(+o)FK;v#jWIplHGqODu-q zP{bEmUZ1D9G?WyTti^SBgyxu;%tc(4EWd}d^wOwLn1n8F+gEM)gpBqzoh$xcmM+N= zPUiVxeb65knS37&?NfP}plkEC zb5EWhe3l@8W?=TLFs4)C$h*9Qg=#&iO^X}GhYAQ^G<^vq9;ECZ44%;JTy1U@g22Kq z#pP!6ZT=@NyFSaJGDI5_;Cd%Cvm}lC970!bs>`%t{`%6rCcX<^i=#+1>`$M2Ru}rI zuI|6&nT%l$^TS}9Csnde9cV5w1-N=FK~gxukvx^+zAq*yKga_bH%f37>X2a5oF1#* zZHjgZ|Kr9zXAv|=i*U7F2A~QW0cHig|Y4CMLF2!dt=ZpIz=;EDNfg}-z^!( zqvgGf4d+vy@1#`T%X=Z5?DAIaRxf5Q;edn(&XplJ3!i#?T$n-q%M$rKf%;YS`lRtb zPx#oauY_O88xn`}iX1F#Gdin|GtLsr0Ps$V-Gy*;r}~a`?@xd4_lBlExO#Pr31<&q zSZ)S`Nfb~m58AU7gQyLmx25q8S1tvoPdj()Y&gXubvxYs`Nv-_AGEjUIbPFx+e|J` zyB~Qh-+KN0^4@FVKxw^Co(d;ZsH7u5UB<`L2Tvx{wO=nhI~3m81Nc0CBkR}i{_gjd zFYf*I@+uFK7<`i)&m62EbWR%Z-25l<28Dnr*eRxksh(hM(FjfG zdG0k2v)ET0xw~e;T|e|0ok5RYG~&G>6O^YeyrjlFovc&Q1}z6{z!m()y4+_k&p%Q~ z#XR@@Af=@GSAnJXdOpu)O`T@c9##(PTIZ$ma7K3~x|W&j`YUIZ93S5gDg{`qGEl8=x!$R)6;e zsdcW6zk4x4mg2#MDFecU{3Mb3v?@R9Fn(k2w_((ceXm51=33$7i=NhgJP<$8ZWVaB zS677`-Hs5vcW(uacWcMEE3Z%9nd;HJd!F}iLMXLl3{?;JCoOXC;^Xt~Uaww?A_*VY z5mfigW%M3RuZE4roRXw%LT(-QsqZP2$!-L5G76luA7Skn}XVtk+cg6ws6WRu3m6SAt8~`rBI56ll*^|K$n8qO3 zldxyxO>5YGk^^m+b9-|KUX{TBMi%Jh9!Q?_+x>fjLQBC~YQXE^2gR2$4Dp~GGS=6A}66Vs;vwZ662 zb|8{9P=`h#sh-&n6~4GP5hSXwUSa#%4S|9;Tw%}%2I_=rF5_-qdt;vMf7kaZ*K?%g zem9`$;4oNC2m8A}COV8VG85{X`5QjbJLY6HsDN>}6VJhqHpH<0>Nmfw#QQD&7V0W! z!9G;ypO?`J;=A@Ye`#zqToYAIbu(l|XmzYch1~!cph&+hQxbs7Kt$8lsc~_|#7BYZ zJrl!=!D{w2LHKdPl0yt98%^y$D6C03N}JgbpSpPi5Yy|pe0ZL*8CqW;g)z%kG%;b!P zqdr&!c_iilw=^=p(l3(4aw@;qhs zWo@#?MHphXd6Iz?qkM-GHrcXJ3d)g$V5@9wTM43kW^ea3TMP*yrz>=40 z^3>M`TxZQ0O0a;osO%<~3SIlw>2oPB?I8<}H?Jk!m$}YCp^rLHnAiV#_3;ec>o82` zZ9T3FaOLHzEJi}Z3hjE8r|?XP2tIiC2SZyA@(TUaKmQlYrIgb*JJeGG00}#8<^8*r zN9Utof4Y28DA~RC;XS5Y(Mb5mwY{0(V*%T4fnqTd z|Ln{N0``j-zhn)M8yC3`s9F{S&blf8!NdE@Z_2**##@({J1NK~QxcBkfe~-Ll@f8L z@Yv^%A1;^AM)SRUEKlAWSI!38SIh5z@12zT7-?!|)|~ z{^Cw)xU-fukIwvg|Mk<$r4DDk5KSLVcs#w8SFnzq*~*Ahs4PW@Q36kT?aYOFc1OzX z7N*B}jJ~*aufv`1q;x-Ae){njg*lcK;?%Lqw)d}jAFDg&NXV+cr<){f{f*0)3URv; zFFU$CiMg^cEr+(F)#L5q`aF1^Uq1ch)86aw*z+9@cl*Y20WYnN8}W}9X>V`{&F^G2 zJQjbAWQVs3YZDe~)96l#8V+?BpDa}HpD}1cH7a``CFoIrcona4rj$h*I=_l1ZAf?# zEDuY2P?w!?w71 zWuLuD84ET|V7O4ZgqUy2WnAlm=nuhe@9O@laOk!*hd>pOn(g`HJrv*n=OL+ko(k#y zTEJKztGFk73-zpj=BhibJ?1mfvG?(R-Ww4DJtP1qvsbZ|5^vh?i?Zf&{#8aXtw>rE z+BbbGFB>7K8%$=?KRqD`T6=qmfJPWK->!8<)_$ecTLCZi^;wJojtFhHk4VN?2g~;63d5;m?H9N(+JLP~sMp5uVBhz~ zU-bmNxuz{+n8*EUUX)v(&KmKmwOOA=3FxWyTmJ~v_C)$R@aZ#|0=z~@)rbBI-5r9d zpnefLg^aJ)eoK2_dqJ1hYwgR5C(5t*vyLq;q&do za_+#H?ipYIzrN?$%w^BObyc+MbN|+4SMgU$W9^CbnXoL%CLz4Gr|v4$9{JjM$jiQu z7b<)Qt9{zQBP8tN2?Kv^PIsnD^$~$~fBM*3xK3AnG||RxrXidsQ!YTQAJ@6-%lb|w zSC9{E^@ozo@Fv02%!hufuNNFp%F0Ae;p9*$xR`SW+mWZbYb=9@lx*)M*YA`xiXNej z*@9EKSwi$v_lB^{Yd>ISj__9Z`@y?s{wr>8I2m+()(v&RjXJs3v*JHE%zkCK)>=Fs z%+^BpC3lk0>QIL0jq?ea(!o9~JnZPknW|eC+39SEH2Y3Q%-aK86Sz)RLR*&2A+`Wv zvJCD-EfGfkYX&+U7*9}({G)_t17dMxVQie!5>njC>Q5+})Su<& zpL$!MSEk)Qh{E7(B;3wZ;LMSS9iT}v5Sry6OKq^gI^o)RM>c&!N8^FGckk^??7j0s z#EZfOF^w-m0A3B*WCC5l4PZ^;^Slq*cFo?WfIgleN>oS;VOS=>#zB+>ma(p%JvZb3 z?MMGEB}2Mx8}jlh)F+wnc%mMMSkHz0lxc3WF>SVQK;8FJBp;~2aCz*&G!_H(oaNFs8&D-E`*>fo;La5-#fspTb!twE)vJqar(s&;&?-zDNcx1I!29I*c?x`#*r^_~o z5y+;+vmu+{k>>SM$pP%MI8nIXR{Mfv44UwO-XBYPV?nedh+yJ<=1!l5J`V>)2tEp7 zLTQA)M%4DI<5)_F27USDGBPp1t^IwHg>pC+(#Ajr&x-x7B2mYM5u0`8Yze3b$e zt|l-ee8~hPICwHjhJNypMn9*cf(V^Q@Jfmuyu7=U(w>*$d@y_S;_1AUXWE2+xefOz zg9#aj!Yzx=)6%GKHqYDJ50>Bj^k;KO>D|U+AIOUYA$yH>QevJo#{1C*oV_JnGNsO* zv}Xy_KX~Wz;HxpKTm1aF1o7u^y>UvX?To88Qvm+t_kXZ_^x2)|7r*>;`ICR}z2*P?$*(5!;Ro-%QC782m-pZQR?7VSS$yrY zxpDJm&(Fr#68gi9LkDCFY@Z;-eHMp+Y{4SH#e2c~%_|=)A6!0{64^eL`f@MaqX?Yo zM_4QCf0;q%$(%QJuQR#6=!~+4uk4mk5w*87F zLka%d?X999K1eyIhzrw{agVY{$rYY^ygiU7OO8S?c&n7RPn*l5WwE-DS8`3+yYew% zYx+MaufvaKqdM%-$}5@yM>z3?i?8+k<#^Y61G65`S;KhOIacO3jDEI6FnUOk4ZQ@nga zU!yB$2cZ>SRRYEw(i}|el|)|@yl={75p1cpa;}Sa-*9X2u{in$-+QQ3^EpNP)Z*&K zTzdA?&S}b~L9k4x@Q@D3#?}7FS zAQi(m{oez_+GyYQzwcGK7SKfacp{-~?V$d6#jp9$-wOGxKee;o_qWTfdERF)`rhAl z%>CXg=H3+Bf7S!HW=4!{zORMXclPIMt(T>^;?veB78ROu0ZU{GMb19o@s>nS1FxwL zj?|y{kqV|C)idq&t~%G>){qpjXvF;J8;GyHSpN>5ieu}tCe0`C*qgU`(68Q}Dm`DJ z?$Zj96*lv@U+ew(t$XorKOxVvd)?@V+(ght7Zd`c=;}Mq zxy-fZ39oUj2{6b%prda0duOlj@JgFW0`6ALI&a<`WheSocCP-&o{2W}-E+fY)U>y2 zEw@hYcwQR=Z;rlH)KpA=hCchcTSiwGIc)aCRtWv;he#oCo0qgzyHKg&cdcGga8Q1r zT}QmfFg{m()E=62zmfO5PwW4_ukC2Tn=797H+X>U+W)l<^1$HF)Hk)SzYK~1rlGuu zlnk|vmsdlz;g24`h4Q#Jg{>jYzd5$n1fnvc%4l2&e!PIU#fZR|VrUp}$lMM6^><(C zRI|EA*)b8i(cG)wy@5-A?QI{$zOv*)?^JZ}UUm4=xbZO|hhPDhTzC|HgEGajvf^N< zTzebm@(7*F!YY2$NCqhf`4V75Gz!Jn7KK{DLw%P9SARt>k0uDI>y#0!3?!i{;G8M7 ztBuhJhO3IFM#vo3rPRM?^Hf-<0nr!;S`b)&ls5qn^PpJhcS96|5AtRDcx$yRwCKak zi&ri+;WTZbY$-SOTg?5WgiT_dxM34keP4e0$?})~{;vvGy14xChd&4?jkWe{M&@aF zRAPg}DHP8AxqkiTSjCSFYCKQPlxf;7chIR z>|yru988ef4lX<}JB39Vd$MX7>TV@qb>e)2*G}ypKf2lz31+f`jTJbY7Do5Rm2(Nc z7nk?my}Dc}nS@2d*(~?+Vttu6xG*7d_QD8{`(8=_FU|8p znpm@C^?I66j<=poz&nxk@$I)?FAe#{!u`Hg1}}^!uU_zbzx%0k!Q{a5{(EmOKQCnS zqZErb+JAC3!R$)mbVWUdkI}1nY?5~<(({3W8rf4AD~J5goHib?)?qQYkj;J9=!kVg{kN8(ZVG1 zESxGqz<2WGy;A~|BQeA8e0XZodkar|xp92?y>GwMo~P%_mAsi-rFnkR{-=lc*K=rY z!-w$3zPXQ5P{e9K>idzH)vXkdN733=iUlR(*rB|Jd0w_rM_CrmHr{W&^ID2Zhk@qN zxp4l%9JqWs@5arW*T;jj{UjI!hd1lnfs76J+Y|K9|K(pU_v^#sLRWb_&8P5Fhc`PT zLsEy$5bMToZY-A%t~BtVeG-&*Nl17fFO-CXclT6#5?MBn$0sO3lh*n$#c1Vul+)*@ z&Mz+sgyGMF8;|Q_hfQbna00y5Zzq1>Y${B6yU%~w{;B`$kAA1~)`C$A&e@dZm-XEu zKuKj#d734SvUx0JRe$29v#{K~U-FBs)5{l~`F1QCxtM3GoD;)0j%ee_uxM@_2oYHLPR7f853^=0kr)A3TaTm^WctjC|nH zk~~Ey-#&QLy$O4oDyn0z-r39>T*upxAZ)$U_}Yg$JngM*?~g;BMrA!4A+N&Tr_78| zt~y45YmV)Mf+Og8_7%3CnWJ@|ORuP_xn>a5)S7IDQEU~ydOSQ8UJ_1ZV0F&hw9~!m z>-_d>#H~JbQ4FhZ1jO~u{2pOz;GsBS1=g()THn^9TDuw-Ug){HX1wz*_<+qkQF5>R zytVgz{XCZ9JtcG=^ow9QgEHUEp*2G!2RMx2Ss8dC!h}H#n9U&B2CqL^X3p71Q#CVB zd-9QRd25g{|J^9SVO4^RVea=~`UOu`q^v(ZT!%Uz*Zammz|+nOgMpDo{dlL&y9dt) zul3zq)`WSxGJRlxQ=aU@C?jOfK2B?){!KVxynV961q;SG^-ieT)Yr4-d)jXl)1TU4 z{hvKJ!BkJiNS%x%iHE`RLW6g$uivZJeUUBd8e>Xh5mr6eU`#&s*AN>>-`3%}zdz0u z9-gR+g0p@$M$h)?S{2%ZVe_xgt5>23Cqh?cy}7UOp&RY4xYih|4{uiwFD=@k&@yxy zJGmDwNW!s3Dlz&GbU)r$@25{!G&czZC$*=HHhMEw}QXo{m9TQS>Gz{WU&k5IWu_EWo42U;55p~7UQ z=yL@?Z&a2sf`i1n;IQt$3)HnaS>woA&C?ig+ymQq-oOm4^c-)ql|?eM@vL|6WNY}~ zX>}h;uAVtdrrN;dX^y~l0I<91-m|s4b#hAh&L6Ox?s z!Agzc{o!3#_bl~(wpbcU0Qzo+2_k=)h$z(kUJJ2isq^6 ze(#M7%R7ZMfnC~F@A7)Y>=NdF{&VdgU+xsf_P4+Jd@^ydUT-J-LpU+8_X@*#Qt}2V zyji}_IUzFkOR95X$o>Q8e-*k|YLOfc&y4E=<1ijwB zTDHFXS@^eV7qi?-ar$8M?fO+hfdC;5=584bKmEKAumoK5g^6CsyYPeWf2Y&EBfj7< z6Rwk82nn$~N?`mS|EK?D`S9ECEgy7X?&ZrDQbh6ywx`S)5FfWE#romE-?vK=a3aAN zZc^m$mZ0I!|Mx#{5y#DH4(`75c?h*qrt%_e^zOZ{N*3|I{qyCY{KtQo zp&_?%L`UF!kYf48^&5Hlt_9yhB*Wt;d+DZ)=|BI|KUj|B4GPi;Ur#C@ZZ@8~*BWyT zp3Y;%>c(TRlk#;(f{enxBK=@jLWM#cg)luz+1qZuAJ$(>*Qo^EOL@9plyTr+{Xaih z{^nPol#07F$|;B#KCgrn+cG9%=9?)~H%e)Ku2kP=5`cePNF=v*2$m-!g%Yi15jv37 z?quGLqj@ngQ^w9084?aoy5!k!nx3cDS|Jl! z6;QgXs&$R%?cJHR;qH<{Bl(49B!2>b7(cF!Ey)>jxI5EoTa`uu1(84knTZrgEh7b= z=k*A5%|ECMWWDsCeaL&#A!L zIlw**+V6R6T4PkM*mvE`wMI8$B8y;ufZJmi{nXz{TTxJ4L>c0Kq7wugrhN1)ScXVj znJ|3ZX@i399(8YE9lks_ix&V33GGqQxaz2y0IzH%cxbR z5O^&Qz|ZLSAVg+_@%Fk6R&RnJtG3T}3Xm=Kae(_5_Z4d2`@c3qjMeAO2YD5oh=*EG zt0)U?mhI^M7mu5ue5V=mYVkLg>1$?xntVc<10FHxrDNY5?!s!%MT z^|?2x&q>&{X(#i2_Owt-);Znap%(=l)8zaf%1-%0LK~a#51<6LPH7@z-xoZnXyLwf zrJrjV^^xhCtun6wrcjXd z6-62AjKGF2$(O0!I>euSX_??DeN5w>JLM-0m3|J*bP)wM`P1eKcWZqxU+$fhxumle z{hz4078YKmjlQRHl@|GCeR8OTk7NC*o;{KWf~o(K6?ph353W#d`L;=+s17q|lCfF$ zA5stNqO{lE4((Hb&6ENtjn z3>!@-_;R9j48xsMWa$L`{ERzak!{+7ME3+6H z;nECN87v$dM}#!qg1KEQUwr)yb%PTIAEcSS8pnd7yV_5G^g#q6`(6!;%)l#{@E6O= z<=NxKavx#mls>;hUz&PKb931k5jA$C{z(5U|zSz+4nL-pSsx&CL8z0 zE$B%%K|_}xqM;W6-4?gN!#EKOUlBdb{a|5Qz}Gl!9)^A<*%t#9UxmU<=+yet1ZmOWe`9TCC8AYfY$euzNaOgn#I|5fo?j0%XtF(W1Hm-}WP!yO)>&x)a zBnlFRPqeo%BQJGP>>z|ZbA{(ZeEQi}C?kj<$DW{<%e&>RJD0G6f>Y{?(7}A>M6P?G zaQ8AlG2`Xn+AZpF9#MhQjk{cwAP93E2!jgzx&iK!esX3KJc3)so_(`Ul)uOG3+&0_ z%qHmV2z=hWJc+X7pzP4!j?3@AGZpKjo0e5F#btoL8Ds95z+(2+?Ku_`TxMzGF5{d@ znEighb#TspzCVI*@I!RHU6}79gj>hoD-XtnhKG9wq2;bJ4lHBz$DaH27`wA5AqMFy zh4cn{%6ggCx)8+o=$B9a_$2};f-8cRT3s3E<`Nr5xiX1h#(5b!mT4wITxfC*8T0^B zK&`*3gSABlAoIFF=Y_SBCIXL`H?bdN4AbCG?k&f@P*|6t zYm*SXs95GiU~64r1ALLaXQ#MJ8n{R1A%b+2d!7r{&skFL0jn-Nu3!Bq@1#3}4k;wn z0~DJuH``I0UWHz2uCN6V6Ii#G%ujBb_X6iTVXdAbEvD{uD)AvM_($sIopqbn5!SqH zB$p|zCx6Yl>K+K*k*4x6rnSU+wV1bxs;g0FAsNEs~Geg!QrdaCw4N@Gn?!c>_9k61@t=NyZfm8erX?4=D%Mp2qb|ME6D!h<sFpg|W z^|%DRY~p?fqEw9bcH6KPUBNMZBOKx*9yom4$ofS5*S-rj_|V)5$1xK1LzB~@c;V_V!9o2yky3wmnU9_3N z6fo3QQB~YZo}raqTztvGpmGo(`1Aac8m14%i?X`#)$f7Pa?;g0G7h8F%Ye60Rf}GI zR&7&ja2+BL#9{$+uB*=^k(8V~LLg1zkwENhe@Y-~<`r~~jc!G2)NCoN8Kd0}alko& zm?@JhXJ&e%B6{f+%mGzNE{X-sGOrZ!`#gQ&}wXUmoA7b~ejJ8Jet zo*D=RJr|s6apE~RbD@-&>LE|!5&S?DIq4Z3T%v6rz9|EI#-w$0Oh|FsvWJlIbvG6y z7|u@!K=6Zi-ykRg`vXXO217nXR6T_N_de{Q#os`)KTLlevDaZ2W|*a~T))WP8#+}a zWy30fo^~1xp{rcJI0hontn=;wf_`-WMS1${Wm)^x7ijg}z*;SF?z@8Y5nyo9!gaC1 z0NjMZqM2Anz%~DjY-W&#D+p0T2nLhG<7^sVEcYHfjaIyees@GV&2{|whq&Pp9-9s1 zhKzo(U}P|?dJxgn3@%LIYH*7FrlZyoD1QCR-@<5ghhdjd-a-4VC2AkHGyC!kTc`V= zI-%?VO#Vi4dpQ5BeE7ll%BTbntv>krewpWNA%zMzZ$4RCEz98O1~_kHu{%I`afANy z(i-k}FL50hB^@*fbGc4^^9x^h4&VBoi*Q4d4iJ@#NrXoP&zSitUHyNV^_d)cD`k%#eftor|))j>v(wKwCefh!d2 zd4zut5I(}%v~PI`&qk0l3Sb*`U6%B_PSd0%L%~m-nBRr%zVO zy=Tj1pE+!TzWNAjoC~+}@gB^NK``bBywSy;pw9M@@_c8Z?9hjvUvU8a+*`kwaXvsG z5zkm!UR+{KpO=>_o4^PhxZa&{2AbCTFdI;#E>nH&xXm%=Iln6GNV_st&L^4k7>Ck* zJOr3C6mGvOS7l}a75Hds=6Yds!DEW4e~b}s>q2ZI7i0T`r!JIvWF{{0;QC^QjMo%W z-}x|1BJX9IYiM7}W8C@Mf*}7a%rfJ-M4+&NOPLsdn$EA5(Bxbgjj3xT|FS;1U^mMO zJjqo*qL8GZnzptg7VQ?vee3eKPjc~9)7EFT9G{6`lkowrOr1H58`tfWQ42~1 zV$6SH*_6RxvkL37Kl8jh8em_E$W>$)iu{=vHC1Au~B>=(yw zRg@|$1TUmZBt_w1tjJvEP-$>%vz&jm5PLj`e3fqFo~fgU1{Dwh2&E+nSKz58SOc{l z>2h>{2Z}B~9)|2b+*eyGT>>3fh%9&S#@H`wbH&H?BOtG9@wVV<$Fwmfzzj<;=H%5HC^^|@T|JG@htRF>D`sX z^5P0ken_4S;u;?b9}*6?lpbuLS8 zs;^}`-gL1hafOBF=9foRC<=d`a?-mg-#Q~0fuG>INh3DYJOZd9!FDA=FQqGY=<@Bn zX#d6V0={gsLYG$zB=oCoG3>dDec@J!juD8Wg*TBZT`cSzCm=Nw?esR~!N|(+x0Pow z%rZPjXsvCKZc|!#9Gt6Y{+~QvfG7Y8A;E#ItBD$jLA0Ki*t_E)rPI@s(KzWk*N+>G z9pEI~a=Y^^ph*2NmV0Q2je4fWMx=SDSFcPj>Eo9PL5dk0;JHOg@lI||UP_OVQXxGV z6A_p|2@rx>=OBbm(!J)vqj?U2e1aAkcQy8O0K|SEku-ArxZlezO>}bC0 zVM6i%OD8MknKc;9GlD&!_{L3WwcNZi=lF1h0s~ADf{{_(ma!920Z&%Ncsns%T@P+!1!I5 z94@!1U%D{*nTJpvZoP^bd~IXBEEADS?U-6D1sMB6CQsKdg|p@7OJxF=!hYJV1l|C# zkDo5bO0~tN_E(#n@{H9=!ga5X=hE0#UB8T=WdI8;5)K3uXy6P>fpx*0FL1WY?|%Pj z`HPP~V84?K-bU$%gElPuHWrjanBLD|E)^uD-$ZiZTT2~7AVSO-?htPfykP_r_vF-A znQ>vFuX{MJ$B0NPt7~PO1xf~Sg0rTi^VN-ow9|kbYwV-)z`=g3SqI=R79Xq@o4~&G z5^EL=|0-v8d5EX}vbMuOAcv$GLc^?Yw|jV=Ei9L5+yalW0PGPBZU}3M!3mZ&VPg2F zLLo#=eg)uxK{a}4{jn&v)4xXFb1z3Gbu2q-(HuKQS3AYR*T)z&1b8PJ$8Dni?PJv{ zCv1@Kn*#SNrnGSf_dSR(LgWSk2DWh-T*3OGf~4`gdlBLl(iBP)K@D6uv8-(!WX#j` z2p(7iwdf&?u43i6e38YS(^Yl(+M&*M_W3BBDL`uZTw@G6R@|Gza4rpe$_|V`2x0oq zG8I%FEv!Ln8|59&B052UJRlO{=;%QC^4`PJML&Fg|7rQz_uq{r0y4* zmGTwV9~BEn;JFW%({bEW2WKY|1Y@B4y!_z3JK&hek>J&y+t89q`=4 zub}-t`p_U1s|3H;hW_cXa{0zwxxzlVJ~X#ll~oj-08c;l^VykIYdy8pW`9ylEyMDJs8*Wx);Bz^&z&-bTJIItC~>0zU?1C6(Qvb{D+CH{`rcB> zZ`;JT>AGmIII+I!F(ZtOQ!1zeB+M9;@i3!FW^x>Xj6-^$v?%Fnq zviJmVfPU7225{+S*TcM}i<8TfVltx%XC@^(ded@9fiUM6e9OQtFlc+P*BXv@x43+O%dTE2DWUor(YGqk}_lRRZh7@;4& zg+92jK2qejDTSk%JLLgH>Zg3KKrY@=sA)~@1ujS%iwdyYVoL=hweHL{WJ*3tNkNoN zPMLd)(SqD+zX?)?APEy?r>)-muld_|fj_^cTjmF)jfI1k$PBfmltw6}S(ybvZ?ZM=D*}#OtCcox- z@B3i7)Q-rw+Z<^;i!=nIxl{%~>0OOS$0-Af#%G`+P@KXHM9O2K5V10WT3}?H)xyU; zz_tOKf}9h+5qb8BTGo!s-%o$~5w2YX4ge2(2w|PWOh>SpyhU=Ic6~If*S)1I6>%xE9Y`To5YXWKP?Du=B3wi2sDa+WET)aXgV0yV8Bo48{Hw#)S%Pv3Y7+^(4yABZBK?(I%}i9wD{=r>!3K#;J$zy6CKmMz`muzrqX5gP_)hHy4Oh8jR&9KZ_Jfcbs3 z*?{TAMUQm3x! zu?^mwB=pNQoOE~3S-FE7-6}yFzW&W;xCg$^VyQbH^-!L4;#kNO!J;X>YE5jDF2Ua! z!lVa|t|JI6B4|62yuupgp0xLje1TQQP9Xh>m|}30Vz7#rkHcIauQ?$x;#S|`0J0~jhpBaRv{<-1(ZSS+dDbKYlJbC2^#I3 zLNBNwACdQe0A>A4}SIsLdSvS)+f5Gs-$ zee*O*fw!*q?Y0A0hyK*_#N=MbbL|UiQfbmaE4ZrClpM^06hgoQNCLX@c$k-;cM;PEHOha;- zm-&Z0K9hkWQIk=tbul?5vrG(AW7Z67wQkom;c5rhzLPGiwv#%nGoR##kFb8dA|e>@ zL$@rMO52#U(6F$$(5oDDO;XsAE-hKbTIyg;G*1`=*Gdirjo?*lRALcu`Evf z=aurjp$wUdRy+Kz?-(oKu9p3$I;1n(;`7?ZTAoGKJD+l;A@#nlrS-jSsO*obpVlQV zcrRl7<4V7z4D(SJk4^7d;$=T4ef^cf)v~uSH_{vVT>&A13oT`c)0As9KC!MQ5L(Mk z?;|}ZgeP>sy@~A$*HoymCZa5vdB=$XY{DSYw=%1e&IlIU&=MWqMI=|(Lj|K=`q`+p zOl)EJE?mTYHf;t6v`tVcHs|I9Ptcnc9l*3mSx!sH zl@g?H9|j$%B&jLSw7%oA{Cs;`abv%lNtjb+GhK2pPMbfI*FnYbv6%cdKX^tPGvJ!_ zSfqcny&}Xf+Dp1>)wGW)ZVvIB+2d#u`DulPKxl&?8rx?%(n;#1Ut8ZPkuvJos{qM| zY5hxB{^XQ)>zprm%p57#1+of1_BUgIf6`3A%{Tlv^R0EIy)?`BwU3yCOeh};d}j!& zj!loG878oSbDmfeovuydIf2ihsSV@`@oa!A4LRnoV+#S=V@?3lV}Mjzbo(2HPZY7Z zjmnFKKWCgc_jI~f#6pZX7Ly$nS$F{D+l6JC&A6UJBe}*b3W4eZqeen0aR#@o@40A` zB5k598Bm2BwfGM1^egWbYE0w)6A4h)91-HVH4YjD7`2VMWb`1k08HpkHzGBeV$&1p zWk?$^jc!&p1Z0?c5a^yCM~VYiQ<{Tf7pCL|5wSk|^vm+IfBk1=7Up;GCL8$s7KxBH z7dMi97M=g^zxX4>KHD5DP^`gbu-~dzI_F`TKscQcffSmgD!&NI` zM;yz1&`1xc-N&s)4UK1&^l(zW8@FYMR&WD5Mmy{~H@%+%^Cu6Uu&?JTdmNU)cV%!! zM~LXeg!neXgbb^To?0Bp2imCvW&l_k1c^|qGyu*-o6OtfIIeBrZ5>x1+aqD$f3&~^ zjOH0UD}A?fqP^jGo!C66)o{TEhcNtd|3F)rHbNx`xhPZKKw*2{@{Sa;RVEN*U&p6nVGlXDpW#%EQ;7D`1n&v&U z_dC#{TFWk?iPh7Ibp=8Q=^=6+^%(u^DgxaAu6{*bl}2pOXLCf2+iElEcM zkLB6g3jF|+O1s*iG2H_u5ax7=lQFbTEj@#5*zd*V$}%1sCK`9SZJ7AAr%TB*0v+PH zRF;UKr%WredEwqXA%~65hbvLrLKd(gEu>9rffM1|@4i{i54Ra`FeNNZ3gR;2qX=bh z(U+T)cY-yj2dmjAOtD(Pt3*O`OnC;!F2cZzmz(E-9{gqdYD_sl**cUWIUlAz|`r4(wzJ3HZ1Q`Zj`OWWt1)~gu1s+vU zD8RX2<~c&!0d(VHZ$HU+8`;mX=cJ-gc4cZL=X+^6bZ^rbLWF@OW|=44gX7`3&J9;@ z-6RU&MK;+(#dLcY7B)@slUF$32OJJe(Emh4{P2Ufcn*`zd}A~_vbq|@jbj@4JVR=5 zXdH_cV<%{O)^JbzdSSl&_aFa&`g+P1^rY3ay|WMc93haz z9+ofmkI|p~BOVn%*y>mGK|hu{=8`M8#yW4Elm38yQ$ux)J$5Uv*4b>_RYuOxQh_h` z;uwa#!8uQz3NsumJUey)D=dQM_7RpO0#P8;UzsxOJ3^`RDe(Q@|NJW~e8co1f*+Qm zsS5--K+qY$N_UIBYa3WrJ>1oZcg`za>>WCUZae_mpb)MGYRL`}@PGS=WdPoNLodt|bNB22!OQ=V(9;FvGW zrZ5ACzv*~z%K$%9AXWK?vI88qm{1##H5#z(zwFOtjm!R9%Vlm>bkJqdc2|vUWqPdM za#~tl``vZ-tSQvWjOev%)=_X!UzZP!eQOgCx5)NJ+IqsZ9LCE z+etkwOQ_AtC3;N9kI*CaWKTHhrBN^2Yl7;gmJAsSK9|m=qYAGH>lHyeCCi%GS0=7s zDhJXo`-uE`ZZ_)6xG))QiV)y^lcr3P4o)>dZQfkA-~O%_UGch1=QClq-To7q8Hc&& z3Gao)+`*+qm=-)(h-tk|TU%+G?^2_%*+H$piZWoYZ3N}bOCT(Z=N9dU-}NKUbL+Dd zS?jfw>=mN?j7`gs&eUu)%aWRgSohrvn?M;YMEH$xEj|RJwuN%Ue_-*UIJYkUn)qj} z5xiuNa85FTOPY!k@zI*YA}IMTbx1cTT$5IMH?LW%GAX}&5Rw4TFs{8E2(F^dNYswY zDC+H7((PbK&>%91;(wo`I<$W4`C{EF5so=aP~}l*%)XW$S-%W$A|3nNZ~@gw98koN9d9?MydPzAKocH{_%TZ0(8SO z9JWG-(xlqI&8-dgwRlh?^#S-9jCVU>e$`ex1{`3XS!A>m_fJR;{R0RV%7w#8+Q{Z)gr&L5mxBKT@UjO( z)lRf9 z1;mK8LQSEe20f70?pVW`B9l17?`K~=&`edXaM0%=d7mL#QHYP9-EP=_t1PVmM^}#&$&4eEi}_|fAoA64~Eor=#2iepJkW_I3#l% zU+H07*515+wcH_yK|{tLccTp~H6GrnVEV;Z-{9u>7^~DV2fz|pj=nh}YFEOxLobkH z>47<%XP?{MtH*5W{|0wLtUAzDI|BPCE=@hUYe7d2oPGM<$Y`3w71fP|XXxX$HW+vM zUKa!>d>Kw*P>Z*PaAj}{L(>0&{XiGShuI&+=2aFYS;htnpc7aZ+8Oudv?1^rc;f4o z1$@4V1h-7IKFakxDi1EZba{@j$OD{lb5OpXUo893gYI*#B?l1k413%hCzmb};fr>3 zVRa;>|lb3kb9)%A8~Vf4}-a zW?n!nOR$b@_V>9M-0mJgu$V0W{;z&Wq`DP^kxfF*6WSVgku?Hz%uHc)2sOZE6ZkKb(6fks?`||!{pgSw?vCnT3H%g}JvWarx?%iu~Rciz1fB4;31)B8s>l!osiM7y8Oh@UQUY7W)5dB8Q%EF$E$=~fIqIlqR<;$AJB zUI0$>&KW1v+09;}UHVDu`*-E40aSz=h=AL5!*fYolfpy+e~UqKp?4z9+U6R|Pumy4 z$o6M4=e>-P>GB;mgl+||q-k9#3i?xEF`8xL$TQT4Iz~Ui`X;UOg!C3Bj_(L-l;NSq zzH)(9V|&Ja&rd(OpW_PNd;eXOXC597L&N;<+82h53LTSi3lV0NIsu1guNl!WGj%?< zUA9MoRv7#(!KiSmu3+ScPDDb4D4w^NAplL@wBHGVbTSzhkq>cWnf&=Q3=epZhMl&U zj{>qb2G^vqD)Xic%gJRSR^U_XBp=`YtElrC`~#V5-!)U#E zPiwp-g%47X?`nTqckY{Iq^^uXYp9nDv6tyyKNR;ATE}%AtCpR1rXl_kcJbM&!(T#6 zMggKcNky9Hlb;enttYRktO`!ex+ypK;JZmKq$Xv+e?8mSfnmG6x7BKjlw#?T&KHwEp8o#k2YSZF?w>axxzH>3b1VVJ8jc`rD+gFY&OV z9Qg|c>n;?ip1Gv=m3&s_Gs@WB!fhOWT2F;RJZ}`Xx>4M@9VSNx$j}341T1i(ydl1` zO(Tjt+Cx2=pZMl~#+1r+_r&WO+o>hPCN#QW?rE?Z$Fnop3^xk`6imJE#kG|wr8)V^ zP-HV9FYVXf_~C1Tso`nE3<0p2mAY6L@|{9>rrVB8suP?YXI;{ui$g`05*q+ANczz3 zx}mW}tV+O6cy_akHwjbr8LOb~Z+4RKspYrv?)O0{17mVGlX4HtrQyIcrYJvAI4En5 zu(D1rnM6RmjHC>&(y7e$Tko;=fI;t>DOKxHtuto~T3EVtW;7)r%3uk0u6jf3SV9T%ste2l_`;-*6X81LPK{p>SC zIUDYH2ZrdIXDe~@dhg9^<^KG9xk*0TF0l+ypDsh(mN~2i_Tz)6Yz9a0(*3SiMn@AU zg2)jRx`V~U5YU4#VYlb5Ou@Xf znG@zpEqEt{(I9RA5C>1fq`t&0?ZPPgh^}8K583PC#(88hEMn|&kl>jVX=9uHB>N3QCL`>e;Y!d)SstR^^F0}Ua3Ma_w8agDaX@FYkA~3LFp>K(>@xnyo>|OB z`g8&1Y!xgL0_Z_q{kT}#=W61|2pK-i?;-Y^^F!%Kl zS!~D6fGe?v$iVBa=UBaZt#bEUNq37%qq2HgbY{N|N!|czLr!do7xJ|C% z4mZO7J|m7f#=F4jH3Z$=VJy7tcT+(CQLs;ip@&7JF0xvxR7ikp#-!`GlkKyoFUrHO z9}02xIgWuzBdO z5!-sv-Wz$zh>!=zTRB*C>E1RBM0@#P{`O~O3>o1$RxTqPYLV_hA@TwjLMQ88tVFkO zUxU6m_iKc+mT1Efa5=fpvY)QaF%RC1Xs8rEMU=jA&WAe0wfD(01V3nQ1Oa>-Yt}vj z+oy|P5KWPg+eC6)Ca8gt7UvjyJ6IRKVc(c?jfV|u&Ufyi$p~E>sLbK8eOeIR$HQEs zV0%m-Hn2eTvwz5N+*){a^)=F_)?zJj?E_;l?YOcsA^h(1dyqPcpm%O3)b<|M>S=`9 zx^Kk!Q|)ih3k;{?A|xulpD~xV@|;&i~(&9PX$e6{ouEZj0?Za zTo^i@F#%>h@SOozWFw+!2Ao5`c7 z^dDbg6U1HMsW$*!C^QNDg-xgr`%{TRn-==9@wypW%^%t0_&zi~Tz1Q_6U-HztUYBiJ ziNo^!^dtHC9j1-nK7clyfsL%OyoiEH z6zKgTpJYG9y)petSoQ+TL(9Rsy2 zzhr(}?w|@083VQ9z-T{NgoqG_e${80SA#E}Sy>Z4lhoh2vHB)`h}RHieetc&&;kji zqmYaBlG<9W*Ycd_>hJtYx%nN~1kz;ikNlSZYNgGKz|ES=_-j2+HnY?Qrj5aIp4B>9 zb@QlsOYUzMU?I{*9ycrDS%s7q&@VPQAC4vZlK**Lg$)byfq82=Hq168uUUC#+B&GM zu8zMZ*rcmv$g9?5rlky-eoK|6EGHP{mCwx6tiwFs2Mpl#*2}L5G0g|kj##xxx=HK3 zV;@CujSQspWZ-A7;*Fvr3 zM;MOa!Pn9T$Z6{xa2qiX|OgCa`EmCu+JDTSjM6#s?4S;fTFg;0Vq!sgw#zxcPXa^XP zVP$TD_uIV~8R#IQgA)MNI@*a22s^uW28hEd;Jxl;yD%dAxK+udDo_|x+%g=T3R-Hn zv@YmU5?3EI>@r8Ah6ct;F`(e%L`EJFBT_|95Cu})qv4_@BWnNz)AaW@7khxx4a%+& z9w#jqRXdMd(i=>wcz`Lyt;KUVRyiE2YRN!8cfsC_rlCMzW?z{%tan^D$Q z!PyKO{~ymU5@_Kfk<-}xO^9kYecN_TZfGF0>!0RcwnBV7tX4ORpgJxsFuXd$N9&m%Laxz~2neON5jNaJ$IKgoOt zVMU^M|HLlzx~oM5AF`Q#m|X-5rwb4;@a;cy?7d$QIE z{oT_|UDM-OY7okCPtu*S7dNsSoLBUS$Y788gD_!WPRhei?$Ie&aS+IKIXk5NQmf2w zCl(}p68Yu&qwstR({}~NwXiQ`7I!uJylpRI&rsTK8aL>Ohd+AYC?x7Y$v(6*+}v{T z9%UFTLEP;ld~HLc-C7QzDe3%}=SGw3V@!Bn*Q13OBap) zy;z5I=RsBvgKZsu_`~OE^Y=geu#Ai?gZC-Uz~O8Zz?rx($M|Fq8~c)U&(xLB@YH+9 z?Fg*PW#*EHxenmkMY)IPWr@90I|wFYV`CXtYb)yrO{1y*=Jm^*1%O{X0^i{1B$kju zkd`U>6N?k`hhvZt%b8Au{l-=78i6tH-nq*8Te!Z`KKsmbn|}S&`pUxBXc{J zmqTdl7FOA1f+g%=salxdh;_wq{@VzkPY{B>nK$4`Pq{E+2yPA&#vL1ykG|Cv6 zn^-LuX`7ZjgIMTV>GBp9g}QZ>6jv_pTJ~M;zAKkI5^mqP2K@sgp$=T0a9@vx=G~KV@yVmW?YEb0sDf{^ z+`PAMc$viCD&A*4=C661`xYQRYbiKkJFJa#;0xrp!lI<^0 z7JkJ_1i|Aj+j&#VqoBNtt6!@?vspgh4xO~!WX*facb)S}_F8*QU`I=bmX$Ju$+QZN z_4|}DX8AP(U(jZL0khxcOYQtfpCn0zC+~a??k(AzX?@aiPaP4a`7Iu6ddlOTNBs7G zK63B4XbHSY<15mN=fK!(hxOHYMN6@D2)1QfcKx$>apbe~U$ZXT5O*+eW0^KRs3xsr z%1f&A?sfn8BlHFRLgS_ft+i}^q<ts8LcNa$**-1~~{r2>{vn$uq$gDN2`eM|LUvF6H3*0%Pkr zFfXupxPQgOT89RW%!CWjFpIs6o=A4Xy+mWQBctHhP#Kf#bD$2RJ!SBMNKXQ<^@3oD ziF{5JZfcjAyPBs8O~IXTFv_%4G9yn=K3gmfTW&8i{&(oUW&JbO;~=4Wx0dHIqv zgIrO->wqE6D0m-h#Q{Qtr|>$AoKnnx1S9SHbIuVtLj`!BP1L%d$Y5fhDmQWMi>4j; z^#ea)ZwnMtZnI%JQB%&2%H_!$fX}dmxt77U?#3Ke8JL?51QsXj378(EkLgb3+@N5p zY(Ic$rK^J|eBuK^ODl~6&G^_*m~p2xUDmcar1BZ=OvlrGFp6y4-(inaj-^2=KkCFa zX&pFaO3z_5*$~QNu#OAU0R0AN<(tRghfvG9fNicWmH+8){xaI@GlUnnkt(o~+&n13JsT43NlzGziR2P*o`3-CjqqVv`&k z@+zCpsRb?jQMrwE>f*)m@*n^H*X0;|X&saH6{e(F_sZ;1j)I)@4m#Mkgr$W393-fQ zR*)O){hGj)u@^-^CIZIBF1VJtws#lM=ub{f!KC2g$pmo#li3S%Ge7@4dw)FC)V8UV zfJK0bKSuD!GAY^60;5gkJSXT9pkhhgCt}}l$EDKi*dZ)+SNm?ch$Z3}L9o-n4{a>6 z6GXSe>SS;TUCt&5SH6QAoPM@ZG1L9-D`C_yK@nXDhFbsJL1@|oL`PxziO?Q_d=~*^ zntf){sqS~uo7(D`-Z2D8gbpH`4Gm9}=NuHf3y!tA4loa>dC`2m$iBCiD~m*{TH(AO z0((%-_y6q2lzmD_?it#|INIZYU92i2gfah+eR@umjyFKSeX+Z|g~dX*Foli@go6_% zL!+YU&e#jwJ8MK88lh3JRiqK!VCgnp;kw{~%EB7)o)!c#9W41pIL)@_oEmZ!SZTkwRVIz!%-h1 zs3~ZBC?8;QU@%eJwC2%&M_ACDXSdipWO!Wp7&Tj6^rtSw#|SM)(A+BTH+XL#6lu|b z5rchj7UZC@%^b=GRNB~wLP7Ui^k;>tOS z!TcS$q!SeyneWAo>jiBM^8_xP=RKS{0;%K3i@sv`WX!gh4Vem=hcGz2a~;SB&n(mV zI{)dnXdwCNLaq?xn5FeNz^W$B$}MBNMMrs zCtQrbX4+WCP~r)xdk=(P;Vt+jPm2NZw>V5$+y`IzbZ!JBg+edNj^#{R7E19K5Xr+M z%MlRa_oLN63a$BF-_$fFGHp7``)0eF`BO&ozGfFH9`FY?(r5XZgcjfGEAPBa%N1Ps z%ir~l{il#A(>KI^VxL>8Yh`efdS&`5ljz%c)i9Rb2kl(!cnaQL!w=I*z8bi|<^YEa zA>~$xoO?-=x~QM|!*!wslf@)8AK?r5mPe|29VuQw(!#lkg_C|$fHK&EkHUoe&7TRf zg^9Z)2glN#6d}I(Oc~NgeNQ?%(r(Nv%lAc6`mjRdlh;=g@Z5YUq*-^fjMSV0Yrn~Z z>BW&T6ewwv^{{YOr9>*^84oEiwL*6cF=!DiIFM4dnS`gN4~D$;nTdVF{E_^8CzaXF zZyFxhznKjsUY;{TYF=KZ3d&IcFtgxTZ0MD5=OzpBy?e+^(|VUZT0o$nA}l@^7Vlj% z!@pWsE((Sg<%twP5~iKnHC*`5KDJ#7fvzp`_6k$-=bfyt zDu5MK^j`5iFVsazD{yW>h>%A=rVW6ik;FPx5R8>A8s}d6fMrMh4Se zE(-;9##}9m0DzqNEhrA;D~WJbMm@Sh-XxL_#Q>1L+&OBmfS9>k?2``8Ig z;#0y@u%lEuQa3pT5KU}LXmY9^aFPh)3wIBWNtz%5XU+K=w%p^FqXjYf@mCl1%?G&nx3Im|ML&t zz_n_D!%Z)9s3QW8);s#h4ea!F`6&S{#KnUr%Lo(@9DS3?*1-YOPG>@3ni%af`?8d$ zuMP;_u!>gs2x5c@0=`p#8bO#c$itRV&B)hDNJBS~cfs^Yya^jlIybrRDQFX_`=qgj zAl4uxHh~JDt26c)Z6g^SQtqoA4i2SF?lXDHfwE%+ml$P{SLhyu_Ov!0(}sPR&QCvk zSYF|BcZG;!AHM&V!z1+PM6%DG7~{pS;ojp0^Tn5IxVF)EwAFoU?P%e2=TiYNJ?Cb3=^fy=#PMm?0lef(l0XX7kmO@R}KA;x+Gj_To`)mnBy zzv{Hw2dyM(3;E8W5#8%#l(gP`$(cLfV@&>^oY(XA8@J2q{RidJg=t)`9+n@z|91KM z%SYvBKl!kH@(13-v~OVXYSU^1-Aa?fJPp(9Lf3%+I%8x&CY^3v5}nisn4A>cHWA9& zF+B}}GefX@U|*~PFg4@2Z~g6m_c!z%d%h^?A{IX_wg=1!PRND<$Na=_VVr4VvCLHk1%f5G)m7xe>?CJcjc1Nf!|om%$Bz~e6$6X7CwgG(IYAYU z2tF`|;HS{CgXL#+@kRORfBV-2*I;j><2;irO~X>geHaT7emctQ7Pm6L|wz+*ff(zibllL7~_MgrBru3!&OS3I$eoo|q0) zB#$BQkJ(_q&wy$}A>bKLgIMvp8V8|SwPFglYS^-82HY(ey1R=*Re_;{U<&7y*VWZu zzIwdG*axQwy5qPLdT6eK-yn2j!!^YU zU5{JO0x#M9j&;NYzO*3zfH?02tG?oS%J41t05`2N0=w^cCZ24A5c*!)H-}~W`}O6$ zX~`q|nDRtC&lIFuLXGJoAemOCin-R}nuyQXuj?@H?Hk@vy(N;2-{kY#`gz13AKZJJ z%Nxr}jy!38n6F-br;aAft#+hb^QAD$eVz67jW}tg&qDyTy(tqWFYowK>7zm+UD0vT zmaDx!qwhOm%vx=5v%CL7KUC{HXgRLU&WlE&%bqc70yuG!E2yj9G4eXCJ`@<+VxP4x z$BFBJm$+=vQPPNq`kyxyEPC36_2Qj*0xzABG}cgyOPbbw3pe?0_D#)7{lOh&HH)IN zt(LzEyT3_P%NHIBO%-JD{K-Rjsv*wFQd^cc;BAL!!Iol}^6-h^Axyku#LNDXfSTAy z-982uK5O~hNw>kR&ndSm224_)3zwH^(m&06;m9+1!P<7)nbBg3yt9@n&{#AxP|CM{ zp7UuP`Qmwm9e%r3dAtgQ6Mi~-@l2Gm!56nG@AZsPS*Q1q^MWqDC#=8nt_Bm!S%mOA z_P6|=I5u4r_UR8w;#qZzXO(44>dB-qt#v!DY?Nu+ge`M!^FEd!`+`o2qD1ig;h*@V zt+p*#MzU79ch0wKw0wcVT&!CmE!#H)h2ItTj3sDRVafSJmr&O`1sr*J_Z+)Fh#av> zDL|!Nlwn!oC-ejCdTYp^Sf0v7VhM2ITHhE9?G~EPzPK3$$RhxU36fSe*&`wnbdOUA z@!q{EGIu#pm1ndNO|W5+J*R^G6bL?hdu)0}C{QAmf!5TcdCWrXgenN>&+0xTe#-=r ze+MS-!NVuGKy{WkZd?hoWwaA|`)0%HtK!Q{v&ZxJeZt-Sfx4QV^)yR^eJ{_ul0 zt-V~oG6Tbozc+C2?-A(>oE?B*zzH$MWIz=}Z|-`{4x4-xy|%>&Qk%2v)2x_fU z773U-Dlmrr02Ud3prlY9B$nFIFL=*3DoAaAWz#&PC4f;mjnX#L>snlrCTH3;E*dQhAIJ zX*hEYu$`#k4xp=ktXO@}>@0mZgfKZqi0mP(S-RIL{Er}j_VT^6{xZ=BU#zW%nLkDy zZ}5X%EPjXJ)+lTTFsO#!mbo>8pBjlx_M9k$9OBw^hP&II0uuE**4ml8=$J%a>j55J z9s7EQXctzmcJ>Zo^-y?ir{9LPPJrtj4%b~Dk%)Ed zX_@liUFI6k>oAP8nkFMJ9^tm9+lc#z+8I-ADo$X?a}EGyyAP)WUtcY*3OsK1@58NC z%aPjY>}LT8%QMi4p<6xpwyTc=mEA8x`$t$Rz;&nq06+jqL_t(cfBqLgE(;Ilv1+ZC z`4hi`JIAtAuATDs_T5cI`EomD6~A9)7Y2K3+oh6Yht z+r>?j03A54Asl0eq&?Ld+!NQ!pW~IHm%@(dZOZnv8JU` zZL!+D6K*`*czx>#YaAQ&p}AoM!Q<1z?C;ye?NPzoXjS6soal3#C?Prs8{R*HRgOMZ zVd3ZxGspOJQ8_swkPBnnv#UA@rEW(V9LF=BbWv41sKQ3t`>tHV9p;j`%RaZyTvMvz ztqCP-lzfD1k5j0NBB;rH$Antbo1Z`bw`snHu9&*Wqu8BH$@ecdZ_B8;@L8DaqYF8G zTaT|Ee?Du;5W5cRvpWO4SJ3M@xv!g=?AInNF7iH^xU35T@FAa4f(v|vIa*c01{i4e6_K-Xn3~aG-n2s#MeJ%s!xCi@fAWufe9tpFK+l@)B!%mR@ z49@sz{?sS_{oQpa<>muXq#najN8sh1->p7R zzl$IR6L8qVH&s?#2qb@^Q5aRSqzyhu8|^96RCp+WgcXlT=L_-)YwHqwt>4LO8m{CL zXRqsQ-dmN=L=AANz>eY}`DzKx+l;r22bpairYJ^kLT~d!-m?>)z@SPnbl^|zDswXT zD!NX!BJk}Y=%nNoj5O7XGwTpiupk%#$H!J;KL|;TMtMk@W-^K1s{&VI(Ic29sbsOPF2gikom`YhvLe4 zqwY_vPZNBK{BGgU6Q}lz!j0p*D)H=YX~}ic^)^Zu$G>2#b@pO{b#H%Q?zz9-&>07$kD+)#z~CGhS?wxRf?r zyA__~t1Q=h$FznPd3_bEx`TqbD7qu~kXAhUTsJw#0r-JLXn==$t;3k>LS|$kr9MMs z>sM!3P^~tb9D3Z$ECkqzgB%LO?1L3C;Ir*lL=CJKpwQkA6r?WHBcXVIAW zp2^@edueJ~)3^wV3|-xHr;@SFWJnPqJ_B}^8| z55NBb8^WK#%$D+>{^1w6-&|ln&1ktybhB9wlT`v1XCnL<%Yp6;XE0dq#Tw&Od-w2Y zDp$(&egtpf&z z_=rDo{`GS#Y)HuwUTM38uGIrlMC*)Tf#%RDO1FvCWt(Vs1GIOYJvl~jdxhJZ zgQV-!y$3M%2z2_XAEI&CBAoU-dlzot=YB${VNbE|CKRz}p}5KZ6q;x-NKVk88w5aM z@RYmC7B1D+)gh&+P8hk8$9DcmALmrPzSa4f!j$sUS+ zqR~BpQ8>)Lk9I5@x-;3nF6tU3%(7?Js7>F-HLS017-8g9_QS;d3Qfq^y9kfN=#T13 z1!JrukXl+b=7TVR8F2JrFHFfa0`1Amm9p@MPub4{t+Lmon+SP^bwr|KL>kikoJexj z>Y&9%?#Q)K?fo(PaT010!S|zgZY5$$H)F@KwSyqlAk7i9;$)^PsC%LYhqgJiRO_K9 zm~*ZWjMe_WV@rm?vEh8>20&2mV5feqQAbFthFso7!7#vB&xU#SMrqFXfY)8dNJkfE z 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 literal 0 HcmV?d00001 diff --git a/invokeai/frontend/web/public/assets/images/popoverImages/image.png b/invokeai/frontend/web/public/assets/images/popoverImages/image.png new file mode 100644 index 0000000000000000000000000000000000000000..3c882aec48435eb5facc11a9ba57cae855eadd63 GIT binary patch literal 591843 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 +*-QJm%Bd9utji?*>)Jr^bqTlgjz}JGKTS6 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≦r62D2N;b@9QLsx>t&-eRp_Ub4z#x{see z#ia5Y$_RU-phv-cozwr^GtQPj z#hk&J%9{_qn*6)p{72dpmvLjRY58hGxQ|j@7o2}Y*{f3?{Z?V~0`n`Rtg%RuXH;F4^I#B}s+VNDr_&iQHzhke=z$(8q2A@EfP0WVdoQ5vko(!Z1e}#h zlygt&M4hB=I3xzsRK#Flj>sdYSfMP5UnSZa@CaoY`WbCTrK^s}uQsHaqv^k;d>pJ* zv|Jt2jKXUOw1-!5v(2P0+6JjX`?TgM+vHy}THC%eqFY<*}zJ)P;?;rG_l%qpV zOGRnZ1m=|c>NG9Y{8gn=<;~N>TwS)F``t32jeze!ZF!iEZCHo>BRs(m(yFJqTiY}J z@CJwp&*)&hhi_nmkl(3Nr5_LRiU;8iTqTvz_8l;T1?QhQ>LGPt>Ek4fUeXPCE{;pEyT~0H0^ypM)1EBRCrG_p~O)w z3!Rj=aN*th`Hz5JuAZ!&-Bob&x1Dr|0KPQ}Kc;V5 zF4MBV5PP7GQu6k!z9du^jN3mlZaSh8=t&KJLKVyq_l<*|`w=zOm2(f33na>vo90JCv zk6r~c58h+Om)BUmXfkhH<$>ZMm>Ob5qO#%m?j(+MX*%z!kQ0U*jMq+9D>E)LMk%wb zqTtCi9D`g|-MYO;{TUXgS5k=NQpwT#as}OpH)iO_ZbpO2DI}lwn z#yRGD&#Nm}WhUu4#wbdTjmQfg0^+@8Dof$T7{itFy0-JX(-}brwrn-vdZLZT>~Wb{4+8CXkY&N z16ZE^-9FP!yW4oS+vy`Vp8SY|u;!k^wD6?Y;Aa9`l!i9#W{hL(PCV(KBIwSF?k_&J z1)u(|uYcjU!HzY(Pqzm}kQ9Q;cFu+l3fg&Z56m6TNjyo1M> zAaJjmGXqA5@P&WU*@~GsgJh@UKMlZ!a%gV(l=d`GPpAF0FW22UP9{u zcHEfpvES-H|EK?uJh_5JNlhNHV)IGcbp+@I6iXGD*UUzFAjK_AEZq;~QS+9>qbE<` z^eUMgG(mn4w=+wnBBu!)HIZ2&PY*nY@mMdmg(!e1Co+i~UcpS=0m_UffST6bKtXwn z`Jz>G8^zH>Ijl3y3!ifs$T1Ais}fGwYo{H#W*fJ;Z^oN7pZxSPW(uaIGL7by6+>6a z9K2MFyu{PH*BmUK{Pb+{l$EZ1X4@{q#8pBzu`~Ce5t-^!?u%35{PEEAXZ*~qj;RNmyrdWFGN@Gdd?=m6ma zfXbDM<(5ht;q%m)a2w>sJNZ2Je`Sd~YAAd0s?feBPxh5FHow1T5b++IeI_croZJ57 zd!N-~R~PMiePSmoZnVAhUHd78u-+h5_bFn1MEM zk^cmlZ|X~c5*8ZYGfJafzNOpf;itkUP58kOMvCSQnD9*-D{)i`^DTbyDcrP&&!RK9 zE4~O!WBk-)ll01GLqy>uzEAM_HbcOO*S{~%FD-!-;53ru$XEBZ{8PU1t$-A>bT49s@vfd0o0nd_{pxEv6)IJP=7XQ|Z1H^!$l~YM;L{vk_DNXc z2jllPW>^|N3Y$Sc1R z?j?D^-|D8P6+9-^=qpF16&eO-rD=atmWbj7z%gT&{3t}O zh^ZXxVVNXutudT*m5;l!v<~s4 zYw@fxo(dzK9>c%PxTWH@#wyw30xM%oK+i$nJPOt2ODcAZo8(Jbier*fE*_xb)&p1E z(6+#Z1qrKLDnjnTKV!Rsd+RI(Eq*qc$X0CP36PZAE9QFW^57gNwOpJ03gR&lFZt__m0IybB zHR0-^F_bIybBYH;c-&6j`gjT@waB>7dZ2X#jPPAKwpX4b%PG4#(d`)%xn3sYL0V<% zSX_RMpXt}sCGadIqszBHTz64b^q>D?@Bpg@#vui zJ$#KE-I-Cll&3;s2eQLkz;?o%Fa~s0s4G~dgjqiR`5041BE0b~&Nj>96eyBG;yBxp zDh9q7X~j#L!htZe9-}l8Mmm{gqVf7Ipo~su@YrL)>CBAPp}_3Es(m_@N2{tlsEFOz zT4UwJ`>E=VHkY8q;VPTVMrL`y%#t%OcAm^6!30!9P18=FWqlZu9bU!GGLtdc={GQ0 zRA4sG?=k5+W~IWzNL~_ z=I-RoiypzZ!yy=6ge>zUyBw|09xoIrl>miqSIfM%;D?7lhMC;FzJV$n(OB$l;ey%-ny}s z^3`tq#miULRxXT2GtI1+cK9;-EjCxbK`D5Q!YNa7pWH6@l$o9;lB>*~sZd90CfvKt zJmtwtygkNW#6t%3D`9h2Sxa@~A|MF2AKU|KZ=3ul-z)J_)7}nCPTY z={R9iJWq1dUPt9Cem}oFzq|yh*!-O>#Ie#*FciFx70{d&M^7OJ&vH{C)^o<{1mTL% zQRx&;>#e)QQ<&moIu{HC?WiWvC9XSzvlgrj5Z?+OwpZJcQ7QzQ7PlR)Vdb^mM5 zf=Mpp?5=PjVSSQEQ>mxF6V&|rtH+;(EQ-@_#L~;mg6PQFJ1>4JMmff$4`~V*JeA^5 z5BMop$@dk$J&?+A^4@LC+2Q;0xJ(AZ@9c-MqyUwD*h4$sv**=)P;P0FW#sTTh*1%7 zlJPy(Et>a{L~t{|7#O*m3d0V{h)4K)GNfAskVc|tIF_hHx#!=N9>*)kAyhEN09in$ zzYoS&WdIa1(~aD~SmG+uqAPTS$^IOhl#NzgVIsZAPRJ=MC~4+O8BE1Z{91}=ZL`X^ zRzXf&W&)lv*R;|SR!(v}R}P#9_9`of?x(wq5;w>Awag^Bcf`6XrGj^YlIs3GGw*g5 z6*Cpr%P4-1vBG+PtND@EE8JuIfpSLN3ms0bYWbo}K`lkTxJ;R>ZXiRT+#Vv6oMF}D zDUix`D#9O0?>#bs?OU19Ed`$b2-V1wOwc+WQg?1K(xOK5Ru7V~?mQhkbqLS4+?2mv zMbu0*G8l2(S7)8XqJ=zXe6@5f7gF)J-udXBy@57}7S>+Xc4z%&aEyXx{GqX@*+Ilr z{buy;317*Xh>~Wona1rdKzzJ z*59WqZY5aIsKDtXoXViz!NK=HWWwxpbPDL`v9}g-hR~y|3e?^%m+PrM3{y5&dfZs5f(Lp!5O|?KvnH02 z#eu&a*}Xq1bl!)h(w2iC=uobl=iX2xs}WuS?nI94=&L}7f!-aE|?Ec*iler?hZS+$arxUbGMw=tVM<`k-9 zR;S#Db)6MD6@#ZgJw+KfjRLWbva@`ZQg@)>zzb*iTy=ZOel$uy%4+T&gTX(zcQ1pA zD?^rtCapPcmwjE^l%c0YYHljye8bAz**oj@1ph5o-j1n%86>3-!}e~oP2kP{bId)L zQS7XD%|BJd4>>$V^HAHIw0kF>8@1gytm3x944w+3*9K%Y3)(kPvYx$Uj~i_zg9R&Y znj<<8y6?<>649APnPLs_yJFJkoM~!x_!fpFmWpH=Ec2IN%JCg|RNrS*JVA=;1 zMj5uOHQ)8Z@LV!azbM7WDw3zrSktOAj?ky+GmhXqohMk1d3ae4wm%0c)1nwEh{W|5 zr~TSL!_x1CwLi8nOd`!7(LQ#T@i{8ePef&Q43=b>tIQ;U-v+_6SQrp^tx8 zw0tKWBKi(LO*s^J;*NxpFVPg+b*-q({VIDUIt#IbM7V?|;AoTg_x76C98im?6H z^2uM1R|>B5$^R-$dT0mq1Jg)e8X-lSWu4So{q^r8^5y53mOxe2zYwd+*+9UmokSYZ zhcNzE&HLRS>!Z8tZQQBX)Mcn&y(76Nen*h~V2q~I)?=Bx z_!4KDc!HA-krQ7Md!$Bv;^65Rq!OlVqv`v5`10M`*Csc&8DHfeAd=_E+m#1YX5`sg z<2X6@PPZ(J^c_EJDz*!IFy7l*0}3cB?@{7b^n}7 zkn|`MII+6ODwi=!zG4!V@kYxRSK}N%G}kjPSDg@rkGQ|zv0K@}Fej`%AzLdI>GTj1 z4-WBw73Cz)1|VXc@Mu24F~`KJEs~5NK;GcbTZMrA5(s=lr3O2 za)A8j)|4`Zbeji|t&yYiX^N-&MxIn%L)HFY-->QnpS`S6)y&fnQ>^ zZS&@}$uW69BE4)AAg`V+AbqX`#*z%2#^S)ShxrzcYq{JO1Ksh-UEh z$i7=tqT~^78m9>9-+>Jc1uNx4*x=Ow2DoV-N)}0A2~7)wOWbj( z^)hJq1pqMRkU05CtP7>x{OTp{Brv8jo-<(ektBP%v*82FkGDwe10b@%Dvk2&S&{qM z5p6#{fkU9E8Y)-|MWM)<_!5|8a3z2kr&pp0R63-kpaIenRc)*Wnm}TNA@o@TXQ_ox zpy`YR8atCu8ZaH+-#_FIk@Xw~VvRY2WM^JPaZz}d zaW12%xZ<&cVkTpBf8N7KPs2o=d0SuIqzqZ5L20?f%#zB(8Hbf9C_3Bqnv)c5qC1{0 z2#hOld#u(Su(D>I38NCHspK}t+Iy;@=82zB(w;qk6?2L+?6_6h_EDawG5%n1?EkuV z?*VtU?LylTN(|{h?>mmv^}=!QmQ#4V>`}fbR|_aCuB077hvuWte%i^&k?yZsrfglc zT*h?bClpBQ&SP%3xPbZv2dun%&)sSqrEfgji1)&I-`WEAtt??%e|2MP@*~RM5hp{+ zXtkez$%^0>CX>!es`R}IUKD+3yaK~jk<$j=wyuJyndTlVcCNO$;`Iy@Q3ru7R{q@h zZM$~{P4mG8%EuL|3>GM*QQl$1o@V78ZZ_j?RwzAohcc=4LTlSGNxO7%MwEN3J= zfzv*xDTMpUylSB8n?p|+938n_LGz9rS8_e&Q=!0(+8ZdAwgt=072cZEAWzhlZO;ML zw;RQs2(i!2(as#473ihk2@_3WoD|Viw&*TXj@FSwySyUZ+Wy}9t9gol-Z=Rz9$?F} zEQ&|uO+N`=frhMd5$;nsbb!gi?(Y_+pQ%hAzQx3TgdHn~!e!h;bN3dDE83 zhqJr(UqS*STq!@LOd7Z6RX(F)6~D?HmKLTnMkuA z0jOw=pcbVkGDg+iGmwc~7<#xRG-NcM)3zB-(&_Ao{N`__oEjbFSAdz1!K1(nNlU2; zCaE^0z_nOY#65_R@TQhVMK+MW*6=3?FIY;+EXX8_Y;U-MvZhsp6R%l$ga2mRgsVa1 z)&8o*OC3n23g#p-nDLWWDHk}oc$8H#R_z$0J`hj7J;xZU`TZ`dZf`wU1OD_*1&uh4 zf8x?2NCgtE!apZyNuN1#SN}W|%)-&is8;6TC@MYhBgY0y%1@OQRm62Qo zjSPjDhMZOV#JvAgIc6PM#T7N>3*$wOVsa6(QeVcAo{vo8I>GEE-|tY^-eZF30b(B* zkB?A@oG6!D@39|GdiJocQmJ(tjEbA)o=#-G+^NZ0uS0;uJWl9qVyTjp1Ng|Jhl`~| z;e~R1$71)Odpy5ZDvONYDxPiuQCV}Jzxi--*1dF&*L^#~G4vjy+*v0o+l~z?vC66E zj&t~#S;v0M#3t>Sld%O@ia2%Tm{j9%jW=B;PO}q5Whe1B?Pokrc4*;?|BNBao$>qU-sLkz@cHlfS_7XCwmb0O#C-v% zV3fhTNJ=VCY`zs5#7!X%0!@Pp9>aKuhA$6|t%Rb=urM~rq-`Ns8R9Ce zgMXz?d}xdS)M*wD8i)c2Ztx`!Fcvp~y8lcCOl8Ye5*xakL`_%oL07oEfLUgupQ~OT z)L_0Lb)YqhD+)!H^a-a=hLNhsts>m6fm^o>%XlhJ?sfAovq8=Zk#1H#U_9%rcB#Z@ zI^g~)PY+T6HlLQQvXc9bZnGk&^7iNNA2Bm?iWwY=3{3rql^XXDt+5B`+Um97azE5A zNA$azCa(MJV>`hv-92Vs-{%gnAAe+Z?bT%G?G8)_^EeJYQK)zGuXikY$@C@a_PZY+ zf|FS=X!ei{h5XI+%_xu-^Umux`Q8Q3HCDq^a6pj5N(%GL(vlDR%qlCHW9MeSiZe2$ia^KrLt9jcS z+mq)npL0C_o5^ELA@AM2iJvo)pCXO;wMhLyvga49(#X|0{4);h>m+dae?+W2c4WVK&bG!;&%7e^~p_?shrkH zoqO7&E0^w*bk)yZ;K`*|+sbB#{Q*i+H&Kqa$oIUjZmTKn?U>U@1>kJ~^ z@#CtW%CK$w+Byg7U~1_;IopfxBL+A7j>?**CE~Q*ySG9!Pgj+VUk7EdH;ypVQ`)f2 z=8yO)s`e!Z%oGr1CU3Cs_D5yI$29!2{p87a>(6=~1J$-f0GN!mf zMo3E<#saV9=_9S3wvfsXxVo1hd_k{NLM>qh1^F0Ncrk7;M zy&WQ@*V0uB;u)gMkTdfUnE2$U;wAEhAA_fG1`0d)>cQ9aJ0uO?a`X}WfQT3{1g&*k z{aHo9xq?!4#EFHDTT2|K(I?KYz&G_1kIIHN3kv-5C(Y-)zf{G>J!9TACLfZcL;-`h zsFXR0=c$ig0`1*wNAdtHHkP8 ztrFuwCWpvfDr*@77>mSBcYDO9HTnFrs`2L!hI14++x%Gn6XOUK+J z@U1hD$!~pku%Ew&QZiO`R ztc;>lnBi%-^WulCK{XKfS>O`D1Q!7<9CQxU(ZYk13Z}3#keh&=op)oeDVUyh>!b@# zhAZd=EjaUSyoSrh-36@_Uhv{-6`RHcHPYvwl$CI0J}Xy(3g1nyT#{DURVi9Tp(0w8 zESJX*Q6Ba=!d6AV6*^bZDk(Y>aYX41(m5SCrvTxn>6a@@b}kv1LT&7=oe?4p?UP+? zks9;rs)VyExyTsC=}ei-FiZNZLb*D1jXhN|9m~4t=6(_OAFXqAyr%&E^l}$d!&eCD z94EiT-Z+G8JG&j)gE2e^_dS<`?{L_N=8&%Bte^nh1n&XnosUq0Zrr-Z9)cC~qhun~YdWZ?b{P)&0;TK-(>_fGENc}E52LVb6addy8GFuMW?q=>N}_h& znq9nn%_e|k{yPRC%nhHtBo21=9Pw*8I7rnUDU?5}RNkYevT_1mh7IcbQtn*4hT`M_ zD%ZESbN9&sD|=pmnSE#|q90k^JU-+!NM`0dsAGe?eBeYU6(<$_*Kb%^WKi40eA0b_ zt{SR{x!U-F!Ej@9BXxL%cA)v>9{bn4A1A9$?1f9YptPQ$j5?!f+uWjFRD4_&b3^uZ z4r*CvwahY&X^aYyBaL-L9jiP#K-8W92SIG$ZU3|<2S%qWT@ka-IHT%hLz%ahS*#NW zZhBUjp?bjOU2V)r;_smuKvte;Lz!tbJ>uiZe+QqM4HU2atdxY_hl>wL2SLEw;&e$vy~#lER9~0F*vpE> zIQS{0keE>hi=XuP@|(_ULIB^=q<=kyQT*XKpwFAY@H4*6Ptpg*N8V<1;o`>6ke<$v z?*bp?#k7U%^p}uT!j1VL0)f7)=gJSTMr!%?)$!xWXO)kAT7UnNH_}jH_lZEIuk?bq zr=x&pIf*|oljV)_wbRit&VToO{bySyS%FWI8k|&zPSjtdpdx#ye?r@=_(t z?J1PsoN|WcsJz5PCS(2yd+#hiWiU-3mG9izva`oF4C-f>^vHfpCWUc)dwX!iM7@)c zObRnmnS)+XmTSd}l{r=bj~UB+yjCYGV9>C~1h`7B^>jrOQe>Y+4j8jc0|%_*y-)Uu z+m1-XF;On+DyNgL*4I1pY(3J}l^f*0%Cxp#>#MgvdA0eF538_kUc)MfJgAX6(JtP! zc#AM!zU#4hrWN7^0z zE8aB3Fq~?XNr)0tSO~mGL#;pE9jN?7xJn6^Ujz)}L!TW&^DzBn0t&R63)rD$2yQCP ziAqF`$t1$~TTVrKy|Ygz^FA|}o(vQJ17>JkT9-MgFrDDmo}1i9s9r#L)`r|P(h=!& z-ksv?kOGZI*v^Bm1n@Gul!>`&VLIk5<_dh5(By2pi~@m*ytu++LA+%J$l0wG?6W=L z(F+%EUtiCjp%YB`JPpY_tt`QqnY|M487pRs2;jT!D?-^iMH%|`!2?b*;%+h&9&O8I z-kyqC=7*#OvvB2c3+3hsW`g@LyknHfw<cTa(Q^!NodQ?|hFqcCo` zw~&0l=FT+-EBD~J`FDjfu)aL{UYdN*(-z*NxVe8%)6>_K-%aY!c48ZFCN9bic~Nn8 z71#kWN)dg=wkwS)cg{enTzOiT%sl3i%>3=_(S~WKnsL~?avB%)Y2R~DQF(T-r3Xeq zvu(Nm&Q(zNX*w|4Eqj~(2$oi=R$Vj!MlRntNJ7zMUgjx~dsdFt#X%C zSXOxnAX@JXuD>uB?CZMfij$PjzhA)@wYT0Z^V zbJDzrcr)nn?cXWCadIFi5*sF8!(a0gYe&$6^-4K(SUpBh@1qHBXhb2u@Xyt?Z4O0~ z_s*ldY;9l_5t+bsHS56sKh38fqEXTCZHw}bE?0|cZLJgz@p&1wiiH+{DhQvDi*{I@ zI>S^?MZzm_JcvU+uL7iLo-0P~d%nW4`R6Ek*;9`46M2d8$H`~Y^>S)W4wYq`I92&9 zWv=#=iSrUPsvJ3XU18PGi6oUX71CqILX}iko#dBJW;y}xc>bOV-&iMbvUKjFBSFVn z!!u5ko`J2LmvF<{*6rXrk_iQyqwamKyvw&*GSD;&u)T&zW@iR%f$`fwI`a1J z`ObSxa7!Cev>r2Q!P}JWsG7xC9F=()MG0>8mbo~aV(c2cWhM#%HbNDYG(J96Y;1HY zR`Dl>LRys?WYL&oO4V}JrPo`m-gv--CqH_$?N@gn zWN(_b+n!vwjFPj4Qna?hN*wfh;q)qpfH*6pS-}m2XfF%@-9P_6d$cs!d;NNsLd6^t z+^)E|g1E7X8becch(gK*t(N3?P%6H*Y{QiY4Za9?Ib=kcVjew}1Fw)Z@F!eOA`CZ`>e% zWrF#T(<9$~V2>Pj{NMlZ0p|eR6}9w=1lct*&mM2%^xV6z#F1W_h_c;05?JdvJwpoz+VBFnV~3 zec;Z`>(n*v=vjRUWVDP|Y0E0X*1a>2DzIVdluYlf6D3u~RO(f>?U$~sIY`P3mQf%bJnZM5&UFLx4iAaR zff|I1+qhY^qkLQ)whvo&xz>WVVcj|rk;w>XQki#;fr{uyW>i1Xhw{+A!xwBf_8`8i zCsnY&@)iC3XQ=%l`~+I+DgG380D|x;2}pQ-+y4{J+qkx8aS0PQ{^s2WXO#sw3{yz( z-3LGM2N%EPZgKIH*XURH`8J(KW{(9T1NV^{h69OulyTef~GWAX*62KAcZ28SZNDc|Dj>v6 zUlneoY2p#`@v`e{!GnGfrNV?U?>U>}oxNe;k`Advr-`+P zCm4rhlFVc~1FMtXFJA32ajlYd%!Dc9B;%hG`IgVCUG5uYbx{_0h#91-bB>OhVD6&; zzTT^?3$8LAvsX^u=>o&v{uyaFQBS>*{$-Um#`=R(rlEtMGAFMLuPMVExsNTB*C$xx zT1drmhDlgg`y9KKk)^*Z9IAb&JbFF=rZ%(=lw8ujiVScadFuL&YtT}Ys5XLJo=tfK ziHPDN9nfBUsufM!lHtZEo<4?dFxJ61TqZJldc3jkfPL^B978(2^^Z`^I7cyxui!ED z6ST%TUHVVQq$%J9QU&F)zwyhD;nFdsrBdklreBHJewuGy{(vYME`NdQblwJj@%0&^ zj9J5N%&nc-;C2i@~l4lmHYVz(~L=gbU065Lj+LfRFe^Yw37< zSbbNT3V<>HB30X^sn?9LiTj;o;`XC6WN|3mM5#1!o)U2A&^CNvWe&cRE*bG)3%Uma zxMpy4IKu4^nu}>V;HAzo4h21ru2t|fA0B3riyvWlhKb)&~7jSX=HM#V$?P6w0ZYQ#!aShqvjV2Phg|jOl$J zkVVIZF{4~M+oPZ@19HaY7Rtc2&2^a7@#LphnDL?DsTkTA4q1UCRrbCeN13>N>n4IY zj2wl>BkEO9#qFG#hgirYJ#52Oq?fNSOQf8hzk1C~6B~Ge@wCC@89F@pmnD3VwxD{OgnI;ZIz>nJ^j(a zWtG!RH#fbo`6c&$ln!{m$@j~-bepIHE&*_u2)2!q< z5D>0H=Rqc}W(wm&k4c};25S^-^J5>A52(nQ7guJ@tNQ@_j&het3l%#jJJPAsZ_^L? z_H7%J4V51>>A{mhD=x$9QL(TNL$~;Nq&W|tdimFUntw6m;%7}cX=w;-x=~mLKarA3 z`wk-yg#)_wCC@27X`1~JU#Gx)`9HXP6O#Pls5q65c?isi@Wch)!;_-Grtah;^&~$% z9$uzuxkz@x%me?VClh75{l|A`Gy*VXa$5?jL>2uNCRV|UFH3W)FFX_7_G38ZP(HlA zJiojITBY{79V4h3s5U(IkJ|{;Nj+7+?W5|xe+=)>r9dkIg>9uC-VJ5>bpL;WhDa72 zMhv>E?FAp7GokHc>=(ETKZfUzpDAq@fcOll9oS&qcn-y)M@c`p3=pV?3+&V1_4WDR z4%6QS)zCGx6*lxCL1t)am^*QKTf>bm_^MCj3FHoWw{nfVeG9&CK8o)uP4Kj`?=>$T z{oPx}D2TG*mJSpg#tSWEJPsa~9VN_FrbYPlkung3)6cPWnF-4)@;8V$W~+zv3v~(QP{qdkPCY(9D}k=EG4bvpCZ5u)wxlBThRJy? zpbn6E&N%}>Q%%cLI$pCk{fK;!!yH!P77|1|^2xohC~_}fGpP@L+lrHv?#Z|OcUZ;q zP?AGnD7>szdI#MRa!&45qkLRx>l4G4myI(MnUtrKt;&U(H#*K*rslzVs!2P>06xsO zzmC<`ffg{z5?b1$EDP`RalW*Xr@ubd7&!@fO?msQW(0J8v4{h&wPD5sUK*3z} zt8ag80}YRiCO+-^UwgGUw&~vgFQA#mPCyTxdZ)inKOOFp%Ytka>gn_I=(Aou@<2^R zyZBpqEvkc75Bup1{+W+LG4hh4Dys4v1qIe`(K2aKg{z0|48#)3U%v5E7*H^G`P%Rx zGn|HRnBOUa1O$KlTv}G>?1R3u6GCbODrn--P#eZX%*>HJ9E1wTKE(w3TY8H4mTK^b z+tr2=BsshRT2(}(OT~)M%((2aZ_TA~6`xaPO*{l6!ZOT30o40JT#h#lg-$OTmQlGH zVW*L5S2&F0a=SAs3hS1p9n|A%?L;ayr!X1Q_R{4Du;kTFt9gRQ-)lB^69r@mleFZ8 z0`UYt?+S}j$cmo!&aU2hNQbuHF3(>Rh8dmfTicV@tme3K;!*E)=L#nxp_uKlXU&xl zPfL{f9I}7QMkHh2VpYw}w;q1+grn)-?CwXIS!C~9?fW~QY@%R=i9qYaN33|k^i`EXasLm0^Y!FU51(b9-|v3^ z9Y^rLNEq|{txJ;|x9^7TLzy}Bcu(9Zijxq{Tl)56SS)hvPqo>bv>eQ;r zFN2vEeI9UPrNX+3;F_kGv~k;&N|2KW&Z5y1CkvRmu3=^XC8Cl31zE- zs%_N%lX&FA zK_q)Bm|UPIM8R|KU^bsaZ(T9rRRx|b$;1Rgfqmcp;EJAQY(JD&_}HI)!j$py({fNy zDgTg50B^$pNI35BLEpj>SCrxt#;0DU19tLPFe)iEXxcZU3Fv^*E~c}(jT{jwef^X8s#!5xb>^2 zp!^L_CYCfNU15N?4iy4?qVVz^oU|_-@fWc}r4i(%zAYvMte3o-ym2b}gVl_}*1%>}nGi(KK-Y{$(7Epzar z)4_dFvd9kIKIiI-RJHkx_j>255K}VwF;c_QzjP)*BZ%*q)dR3ZT%^-Otz|=dAYSG zJ$_<>msA-eP>fDLAq#O5IERe51;FyzXF}LjGY>*LV8!nct1GvUxJUbhw9SVK)d6vi zj%#vWdCqHaw9xWU5?9!A=P7mIHASD0ALo!Ak2sVh`@t#COO&y7pt5Hj%S$as+o`m; zeZlr*joRKE-(3Xpi>y&MR0nN#p1OBpf0@0B>l@3FGnSd$_i7yvm$Tl+&8*raiCpl- zV|WT)n4dqLhx8C(FZvlao2D=wuJ`H&k$@mE+RvWegYqUytw^RR{tUK!D~GzLbeXvS z{YP7*kmGJ7$Zz^nbPiwT=x}_ct{QG}6xIvyd=|ga9~l1syfhoq9#u#a_wn8MQPCU} zEv#NMzbzVVpJa$^C(v1fg99@Ie*RT7g{}tsc-$n?>|AhU=?9u zU^r7)Tp~@JifTje4M42*FO9SbB`P#3LG@Sr#pYph&fqwk5oOa^7LM%SVGolG$rBal zFbK`&R|FZAE-xisk^>YGbOw|)I=u`}0Y%wHx$g``PrP;}_Z|6`LieBbeMgjJgxuVj zLMK&uk{(y%pf&>MoMr;7AT5D=m1FERck@#0H>?E7Ha6MJ>P6Qct*?^1$6hS)c>%U| z;yWlDDrvv^t9#JCh+_4GeP%nHe97Hs&~$)l&C z{S#r|Jb1w2BY&d&DL2yo^|xPR=I}Oy$7}AB+rpevb4gFJoI}_8;n8FE@o{nYi=XgY z%?h1I?cT(MU{&y3gVlXn8N*SfcR?@&ngyXjAxiW zc%tG@FJEL(v<$oudyhOFu)?`X{6$P3-A8u+?!A7C50|TfhO1e5|6F0q>h%h+mVS9IG*SMB}bGpvdYC_}TA_@?i^E$g( z*DrYegp9F-h#o7mgT!(09+;mAXa~ciBI08=4R0|!)s?k)5?0uV z+Rw@Z@Ku#K&j!!1u z8d7Kh*Eo$S_T;h#Mdf(H+QNrZcwO&t)tfq3fyL(8K0|avTqfbDkCq| zcJhX3$U7&D_pv4-j(315N68XYs-(|JdB+;Z3@4Of)>%nAC!T!8t8z}606u3-R0(tZ zaDq=OCBr>Q(S6y*S++@-(-rLONmX zguh!U$O_{k2cMCzQzJ6odrG-H_Z)sIOLcPJ6-{}shn#3_WtnMjWgWPeFVzdIyw-6R z@_|DxWe=g>Zhi6MZe{TVTCx1Mvc*Nz`ix8e_5c^K{>fKw!vQ5?+KAy}K)=Cz@iDFV zx1b)Sa5T(@U5wgrRs1p54p?t7bRVH~=P&8Dc%7~Wn6iWTDvoMjfh0WmfbUTnh8{dk z03Y)(n|eXFWM<<8-m)wd(*z27$mbBbmsF21dgDyt3j+KQCI0n8t=D5p%T0*EvX^5!`%kP*A>YxAk8yxtMCbr-Zw&DXCcfnQmh}6;w zZBW!oour^#zQ*?#v?cv*)7btK$BelrVy-ZiLOqq#$&EBHSZM9?j-2?c|gWJ%?VI457 zG8dJpGHAd!obj`aKca-04|(M=2Wy-i?XzOU!5genzS-Y}A)QSgK75Qq%uEn;cwosD z?oA`-)XyhO8`m&ZJeb_@elu3aY+E}h)ZXRhNt^TYT-eQukf%smKbx$A{pMHqL+_ta z%9c=c%!jj&yC_B~Z7Qg)7I`t{-~H#mV|ML0@x1@+6Mn9!J>^;e_j-9Ur)Gw>PY=vM z%Es>CzayIzan>2WVHyY|D_z%$c)i)HSr(L7kBBjydZ@a|mbR zWGG&=ZhLjrPZP+MRg_B=GnoGgvw)iQ+NK>?RpM0gJVfU$SKYui@2 zv_EMV&sTVWWoX*hXsfI4t5BLC-^O!rwyhgK{DK0pOf@?oeCsqnvl#fuO7 zW`#B?@@no8v|#aV2z1NmYu~Y7Wzr`b2ZqHK9~yI7D`ITvetzIx-#>RE+aEeUm)9(x?)w1?gqv zC#@`1?p$HCRK14DlRT{wIltqc)>)RhE0{SolE7-Cais1HbMj9`Zhf6AsgV7={s9c> zcL5Tq9nH5-f7jRWYg)5GvtNnYz508I4gTH_!Bb<0t)(YdZO|`^)+eDMYl=+Q^h+qd zdW4pTg83Pgzu}*E+0GcWg2JX zqYqT2N(=-#m^;&R3?sK05ST_gLuJdYsRn1ID^1R5h^fa>;E*oU zRp8E1Wahr^V+xLik!163e}P-V{$XC%uox2Inp>Ke=2@bVZL2*H@z(T z8-_BW+x%93G{JkeP}Ne-<7^i4wLxi&TUqUwkF>_JevG}|NnnSh(~e3K3gX1Nrd9xVMYlVd9ux( zt376i6t+E1|2j&KvuDmey+(jvfQAKD*1QvL?lMXgCq-&rr)lUOCX}ACq_W{k(FTf! z=7K6|#yN;tMIBP&eQ-ON8hpb694E}ax$>jJp&6x@io1XB1*?N=Tw4B;Rk0h_w^5{7 z)kKjyW`)tqy5D`AgQ4!hm^LQ&Z{6mS@IOs%f$so@nv)MvNUor~s?=Iv&$;Tra(?pi zCr*ppM(LZ&W!xS%vVbY+;UT9?I-sy(>B!_gXm6nt7D%0*g%N5_aD2iM{VzE9Un&#@w>0{Fjv_lL<>U)`U);Q$wp#eebY75n18W)_cCO3K-T zL6$LhrCCG~)6U*$HMQr`0pI$!+%J8qle*?9mlI}QFEQwQ@pongNwZeQ(tA)qYegO8 zY1_8jOUyLdSLCCv%(|jxJMz-f58jbSnbri3roc>0xG!1x%YW3pWI z)z68T2HxV32hva8iBY@~UO?XyK_)QBlaF;11xH+Z}kN@a6eMB~Y!HC=^{Uz0@yW6mNcAmFQtYrG5+nz_OplZ15c>ky1$wWQs+|@l#lY z1?F>L@tX$NT#Bk`y9_>1w5Bwm4@!T0j2E{l^57Oe&Ym$prk;rtfCd-$&wfN)o+T0B z?s%KCP?l%%i_`w&GcMtHw4Xo+xS#|VPhlybA$V}~$PG~77Qwphb=K4LZE&KCe2fRT zJjQU9Hz(rcS%>Ufe)HA?%4`qtsPHZ)Vf;fWP)SNGWwMe-G(A*h&@w_Z!$n{nH(=s9 zkj(wfW=?*Po(`?&XfIl6T*A+T!9?kJqVf5BF#tVuGj)wZvGN6*|lhFI{2+ z96yGO$r)Q6wCHdP!Z{~Ano%bgoye4DqSs`P+M)6ScJfVLFQOTlVM(-1AXk4Wj2^ z{3+o+_%-kPzVJIkn|KD`u^VqIuE_u$WCpz9HLQpCqzbO3)B!*@h6}GN+u3R;R-_Rl z&?)rLIr$+H|7a)#=c&q70AUpXL2+s>GZhGWLNbyfqkM{qRXXu8lRQaFhu{7g7-w(9 z6{iFqk5DGFih|9z3|+>3XL}n3Z9g=< zM3MT0B4xd7vtLfRQejaAMl%6T?kvX*6vJKY!95Ch|A12%fj5q;dahEcv}=~Ig>v~< zUq6^!V`XuJl`9VznY+A%((@)tr~9-t30+39xOe+bRUTXmPiQNFs(!6$D})(}vy zc=RyDxWCL5HfQP%nOURQvhVBuz5DE&+kswY?x@qBxGzpcNT%t2!5r&PeQlAa1L)Q^ z-lO=x{q|eRj)?&Bw@Tet_vDbTFtW#D9e0 zKI0N^_d#mbdJpAV{3KCRDWCL*T$#tGOy$>_MQOZ2As@hRMTS*;G;O z<*E{E`!sxo)mBY=OPOI%W!6tcxNPZZ&%k_OkXKo^gP7JCGnCrwJDCxaNxq%kw65%W z;?Q(hLTuxfxhAv=q_a%^Rx#CdubL~uUfH18tL@My{g`;>(?PA2fD%IFEhZ3!EW$^n zr*}Pu*|+P-zSn$1MIz(RTV+T%|D?|+@l3C|jaGla;b(vJNnGFj3my0u|1zxTv_1pj zoA92eGV`J*(xHWo^491Y(_!6&HyT&^q}jxILUWJ9J3<$A?jw!K2k*2g()T-|{6tv; zPJyRP8I5mf#ZMWHa7XDhZQ5OjDNuP{tQa|&I3Qf%c}EdaL-K(FAbFlmtgJ8rA!DXibV zwZUX6))gofIiL(0N>q0&sqqAMfJ=W?v^0T)|G=J+9m>T0oUsYrg(OwZG>wF}XR=4x z&Q&1z3G{f#mt($TX$~@B%!pMI3B;TaIRP#d{^#Lf$~2CrbBr-L7z8|;BK9~==9cMw z$&iuV(oT_?$cKByV?qf%u3}whTruw|Yg<@AZFBI-gPYrvdz`|2o9_TvK&QX!$XHA8 zUdOk+lOry@-p3^IFxD=YEl*4$Kl1m93^?hi$m+3=P7eC-EmlbTjC)R!sjymB2TWeO z&tEgom#>kzn0)@gc;;A4aS?{8B;kjL$QhOoo@bn%=_;g?saimJ9)gP7;em&^Fwv`A zNj_9|<((>^t~R=N-3nJZbJE+1X7{=|xqR-#J$X>Bv)wp`EVDA|wwF71Zcetj5=dG0 z(+On>oFyuJy{7Gl1ZJO^plPGk1!;Nm8pDCN^Op|Ozeh2|kWa~|ry=a0{hjb0#s|(J z3NW_YJihodt>E-$2x<-i2d574R?jxbu=H);esa+rYAyM6%`~#HX=P5YJbv5%uwYS43FPe z3A7Wbs4DP?UnL~;!VRP7QfS!+2g!zecZbw_vZOH`&zJxJKmbWZK~xORO}Z*j;&-MR zF>JELPA}^cQU@lOx87Ko4)_^ z4=?~Wt#VOzmexsGrTO6K04bl9IF6{l#WC$iD03??IA_G9#}g&J(`0jTZL-6W_8-VM zXVIg??PaEG?fxB@?-6*8S^Ya@Ws8GMSQY#Dfol&q9AppWMrG{R-+q<*%Z^^Y%5m_T z2%e${s(@Plwxb)Im{B}xaq}>DPs98*$Nzu8EY%9~P?$qj z=dQpwU0M3TiqjilmpD#;9c5>Yd)oF;FkS6)h0({>9;6U=aXA^Zs6mIPnasw4tFsJbxeEqViyH^t(|tINdrnB zHuPSSJ%@t4>RvGN|A{nJCY@X`Jy#}Gd{pQ(fw3^%Tx?s?G{W|0C$!8Q2vx%En_2zB zZyvZaW(K1K*dpyXl(7n^`_XXbU=a7^X{vH{g|<(6-mhjKvd#Obyk#)CB%Sz003!gD zS12Rs`<>820TXIueKXCP7ef|S%lzC{GQ{Yr&SA0hDKsafBO0AxCP z^*{6KU!@^G=vZ+>Kfp=C^x`&LcaV{WbU>px6>jAbzU5~|6-QVGF3hZ~7sFcTRrnj`T8`C?t zNytkq&az@Ew>e4Q1%46vWrZnZE{*t+4G&~)UsxQiC#_ZKEOZS7NB*| z2`fon!0nze<*k~)Au?mz2^K1D2l(KrkjPc<+5e^G#Yg-+%*)BU9CJqgVs?m9W&ODoo)dAi@_mw@`Quu>u8&<(+xIk`v2S-9L%O6d8RH#yOCa7oEC3y>aue8wY_@!iSzrke)yPar*lAic9Oc4rNCKt7j%Fhf*7zpG zhjA2EGsBEuFBD1Y`SQRKSCT$Lo0mrBBq!6B2EyE{rLDF~AjHDp?07IUn1SV`%&i%o3WWRKEaxQz zUDk0dSQL*Bm=o@yNYq8sH@2^{&*~|tV3sg}t8~nVKRlj%`|bTOV-Jkjz_jr)%GfE4 z&qFItoC$&+?ai+<8zs|LLHo39-CzR^qqZ!Wvx6@8x1CO2>|xT!0T>iX7`R8)FQ7QS zVSm#un{O{)#@ql?IPY@vYJt6-V$!bKWi<#BPmg@x|Myckq$F8H0-MxKdvVyXAe&Wgog1C(Q63iAwXmN36@<0FK zzrxH}!SsGOOc~vm<{c<&n1fy;?~uVI*$*fCF!O)^w|~pZ87q(s9uBsqvB0XSmwJj@ zL0P+V4-RpKOJ#Fwo4e*{JMWp*`|-)6$?opX}n5?GvuDdR>Cb zu4Q4H(`Mbl#aU4&0aVUBbx|e$+9szk(rz38jwl~ZIW$FF!Oq_afp;pZ7}0yU4cW?o zo6}B7%fYm-q{Zx#!A7Od_L_Plmu|#%_HOPndkk<(pZ!C{#5%R_NWV(6@x*h5JL~L| z(qfx)rZlIH+4pR3Hhv!4g|m#c0_XG(RcCu0{z{3mUd0_{%g)=rvzq0gqTzI#~Glt$>#SDvTvK?3n}!l+*pAkVi5tY7o;<@x0$P$ggSsu@t$)_uIn z?RPvnFz@cv-|DmFl`_yndfX{ZKlho2l?yD^Js$;II6KDv?uoainW%<^>LMlt2G3Zy z=?IMI6M%A7ise_{zzn!5;uL)9Dq8rqgZUI_x&82a@WVmcqOAwV$Q`FY{jZhk0McT$ zl%`Re|IACEixC4*u`SsiTi7WC_LzC)eF8?Mj*x{AVb0S`Asg zn=OEMM{5^MCO5EV(E@3MiB%`g-6HbZ{pZLcZyBQ&7+04+Fust6EXavZC&k{e-~E(4 zMNVau(KOWf4meK0daaW8$sQcmX_U$@489Ze$QMQ8_J}5?s1U87b3ImLA?R zPvN+#XaT8!dSSP#blyAqj`nzE&J|52*ikm!RVA6wIB9xJ2;+4ef;Hm*TK zV2di9LQH3b{I2SqA)1h<#< zXw98(k;PGOox@*+HHaRWa54E*z1h~LF!4^PKU2tZ4|ya6sepJb=Im>TZ>$;5%Y*c3#a(Xd=RxFMYu{F;Ylc zJV+*fc0TFhf9^_=k)E-Ih}@@BjJFS@ql8x$(L8xCND7YT!!J6QV*}DOqXxpP6IqeiIw*CwGHlz zd&YjYy~+RkU;Ybay~m(N{xAc*{NXBxe_Z2w1NP6+?%q?s3ozu@Z+0i&eshEC1fF2J z!bRq2_lpZ0+5*G&Fqc;-Z}8WwYHn_BGGoTckkpS&!YdVArLyfhVX#kIREhF5P!AEw zMss@7K36)dvnsg0#)+crM_ZZ27c)zAh6Ctr<{8}r+Ab8m!Dry0=-RFlqyNw)(ih=!7U&ag}c734u?nK3$BM0 zco1}FLlK9pc_p3x;;OMw>;R;gS+L& zxXmj;e40+$W8C=)@_{A+yEo3kKT!Rr0R(B#jc^#&>A=Zn$sJ|8N@C8?V=^<(5Hr5^@ANg|+5er)_S9(ilpnzRJ(OydgE-vpbp9SQc{P+I3U4KD zFFsQZ4}yQc#fri$8m@RPwG|onKat5yXntZf=RL>JPq03573T<<#4QJ&Htg7W#KgAw zc4E@^JA?%%={{m1;u!TF9-lHI@$6M&Z1JuzWeq1x&!I760K<~9QRJvh>a*%aoHb5= z^scq5gm>-g*_vY|q%mO{U|qiqrs(BR5}9K=zvI$_+~_l`8$mt%mJ zd6Ny~0}e+)ez|mxd8n}X?J*&(>N-a~ZjvX@D!`JFyH`)L_slWrGCbMw?}R+8a97&F zJH~p_^~BD5H(eRjV(L9sG7C8iV2cx)l_6bmyvnNPIlOWmWp0<1JIyb>s^_C)A!#We zGrZRL0zuwwdj;#B_oV9t>_^7!E=OBlu5v2>DYJNXz#6h%j9ML9z?TNfPsRsZlFbTS z{fU479^h>`@c-^FFj1s$>NUmDam~B|?Cq}mC%(-x^o-sK5O;_pWo3YdKX`=Y87GNb zUN=0#hyJOb;jM=@$jaapVislLO6qYIG)^KUEhDzbGv%gt{7nzqg{G~QvN%L(FaYEk z5H)`(?S29nKd&IDvMK)yh*~UDrr#($&C?hXSt1fFavBdaC*4F!MJ2@pp#SQ!Xk2_p zXaZRjIwTw8sYr=4LR1K=1pRilJ`A4hd+L#bL(Cu*p6D3G1aPTM@Dw8xZ_SkZmI2se zO+#Tp92ad*Yx+`FnWC0p=I>PvdGP_w*S@=yTV}K6}L8oijs3TNC!+ z6thOk;2LI^-Z|zh((nH8J!XyfqZn?iVloQsfBzqUKl%FpefGSqbJ_Om$zANYzkm1y zrPky8SzTkb(ZR#?)~=!~VIJ|_4^P=EcMoL*lSNkE{`Wuqp4B2xl{zJDt{-4E%6rki zVxQcfe)yi*txuENtZbe!Fe&oNJn!GfyplSxOplNDVkWtUS>q{#p!>%h#IA85i?fhR z)Q6@c&IEcHruAaEZnD?w6^E>BV9wwvjVca@)T!a_Z==#DZ`m(sznVX>54yUA87<13 z^(2j&TB=alJ~bP8iy5!&*%KQbR4!vy;iQD7lr#^NcId#?ei1rct+OxKUtI-uMo_az z+Yvff>t~+uMf>~2#&POd6O2-DoUyF^3GQ|CPPqB2?xAo3ql5(8nom~1C^LFV+Pg}e zhs_j+<=6g6n|9C%KWKf+xblzU1%CDZo&X6HAb**i6av2Ef9e zuyp{6L0TS|Ip|0@LWg+)P+B{lhVIMjFD`*;$+fC!F)4fNBc9Xx>}ai@>0`b0F|_b0 zJfw~R3aH+rI76!Vjl3Jm0!|y4CNe@WLS|%XoFyo+Oy6MJi!t)iqIO=9&zZPESip_2 zpa(+_(b4z8rb&vM$8gj9+!J5p@o(Ddg)ABqIiD3jz73TqjgvHi(fXnV4UYcZ-=Z3T zDwC2rJjwqExD`u92{6)eZ?ZCqY1BQr@+bD1lS%J#ef5$nb3myTjS7SEnC5yOo?#hX zW?a&}fNZ9zrrbz=ueFc(Jn-W(xXYpnc?@|4f>=Rg^~w80k&V{a550_^_jqnGmTqlx z@-Q;RIun$O9NJ+KETH%WhiR}%=H&V%##+a!IrsCEPA1y`_9REgMNeIJQr`CC*x=qX z_l$WW^a=dVd*@a>+=Mca<|WGM7@oMxAtuUd7!_r_J!Eg(DH`7*6Oqa`9~ieB=Wntd z1=6r+Vw(l=-9xWjtkUV0g)~Ld(aNSQQa-Y3cs9AoK0Hs4)K3LgK;=tMj#N2ERK}lg z=u&Bmbqd9x487!Dcj&M^y47f&cJYw~iHsNI(UGb@y`4+rth@CA%B8o-9^QT8-vf<5 zatD9=zUXhB4RUDcxO5c_%|RZAFr0l%DjI;KZ47t~pF$VCdYDk~;QrYo0P)iPr-99P zz&*_~<&m;85`PzPSG*CP5Enqei!W6U8_Ym_p&P~gtaA)CrKv!ZwuQ%|JKu3wM&6|; zu{>cICd_wufbK2~wc@wh6}XGJNjU$E4rnTY56jf0^Lm6YK_&NY&|n4b%t>M!5mdN> zekoL3xF#fd_j;`9RS)fi_nRHO-uZ zDc`s&ySxM_49<6&rC;D%LmH{CnpXjFlBCs>0Khv~57fhsIS7 z9$CR`;|+9j(i0bRAF~no?sb$v%KH^7f_GT8dy5&P^va0tZrz4CvfRIPlX5yk8G}A% z+FT9vZab_XCx8Es|2VmR>pC{z^OHx8fC;^-@B88Ro?8rN&RXU;p(Xh z%p!{H$4e)Z7tdct0ovVt#cUZ0BWZiT+f7!!+%vaBo6rPtA9`Q!$&`HpBwk)u|shedMrE5XT7w0 zRY9A&cMqB?lC}ajTYJp#D&?l~cNs(dt6qUX863H^MSEIC>2YAVLi?94l`k25j_xN9 z^R&@9+JcI>t9f&jn|17oS00t@!0f4&QR18}wYSiw?4PSB$PNIW^d+OSjjGIDy2R=lLK6|4=n-Bd z;`9lfGJc<=Sx%_P(3tQbAH@<}GL7O-9(h@rA%!>LClB=LN^4;8GEY&O36o!R1vkL) zGk=tAG7JTzA;py^5b$kXO1Akky+Oexex+L2GW)Jm9F_}jOCXPc#%*PyUrp2jm)unx z!Ne1VPM|N(FDL;DJddTwU;gV?ZriINK7q8Y;p+A3r|kxx{Ok3S2*E*M+Gk=1mvt=G zz_$36$~1PRQdlaiyN%rc3d;6Xh{rJ9-M|wEc%z2kmO}DVunCG2SN=DjyvnZ-@XKc~ zH1CG35=+A`oc@NsK)2u!QU0QCpAD`)#b-b4PjHwPekr9Eaex>cEyk3fc%*+;%t~$s ze%haUU8Ty1X}!W~nFREZCs)!OH<(Cf%-du#d2f~Ro7JbYbGLq|P=Pp+C4hLqk`w$s zGIqEJ&65rnnS4Vz;6&l@Kj0TwS~@t@tj=v5?7?CnS0-9tce}uG#>^|oC+{#_%=8s2 zO2$1s^Pmp)wXyQ!#I%-3@)1upUPZb1KkU8PdtNzmr}sdC&H}87gMRoK8f|^&?V1f);j8d`hv8b1R0Rqd|Qw{64w2j*YU!E+^U^CxbP zS3cdJH))GY5<)}BYH7x^g-v2G4*#+++GrDFIrHmhU@F~8Uv!f+21iSS!b}{0#$-x5BKSy|{P3!u@}J|=PX@P_Eh=OoSq;Gy5xzdWvfmtPg8vBsf(vBL&| zG#CroLO0WwOnhr4R}X`D1E*gm8+^E%rFZbf$x4VFlb#eN2eSMlj$ry(2pzA5u3tJN z4bcfezIhwLxWZ=(Sy=z+A3UaC@$tsbNsf)LPu9o-Zaaq;Osfm7a3BK_r?4-witYc&Of+d*(}g8lr(4_N10%8t#K zkA8(vgusEi-E}V5wj!KBDM;=>WA5uBuBLL|pt85-fmH-1ck|qJRDIDEvIX+_luM>P zXslt3J&cEJB5|wOw+crTs|!`SI=9a!1`XtSjuZ)TtVCb{_(R{3_!=D|NNgGLBsXr&w?M>5E`XeEw5C?l_!{p1s@|{cpefeFScek<7ppH?gYq>aFUa zTA+M~<;k7wHm;FBh3z|7oqYB7RRnRy&JOj=g)~$~0TdKfKeXLxed{gi zs`^K7o%&E8eGve#w0qb9aB?`2_7wjsla+_^u#5#VZZ=$C?-fwom^2Fi(c@S3(v8DrGJbP}7#-hb{iNoNb>) zL+EJf^CIv1x11-jmS@Uc9v0&Cw;p(EN3Lj)$drk=$+!JTC_%0B1{Ij_!NecqPZ^MY z@(i$C`I5Y*&I6kw!DYS*w+RF8_*K7rr%l)n@bWXS zg3!ZYs^;p`VD%isSN#bZ@CK7MAcEj=MD0&=HUH`pw)!#7aoQF7BRT>b1X(yJjQ*PV z#FL2C=k!f?1FYWo_x{mHN}IQ{3ESeDtK_hfGbH~56gXWCL30{oOmlY+i3r70-&ReB z9o`8xFB5q6>(6K2{d0)XXzl(z+UIXLBx3|+52KXFnP_7g*gG*I?P`*P465@mIgx)ZeFzm2QJSh z*N{;2Dl6%PGw9aoIpg*6!b%pg1#*6~%vgu+1_B!M3f;Wt7znFWt!w?DimSJvFuDDRJ$aSyHM7h^Ny+PQUMQb3UL~>axqiY5*NUi5dhcLxK4=t z?qPx2@zC-sb$!+U%bN7AcdcEhJ;XTAd}yA@-!a16dsIP}ID;l%^a_9z1TT+-EV0mg z&iKB^yiBXli=Jj*E{|Srvztd} zO1mO%`c-k`j1Tf||5Pn?+;x*c%gkOkWZN*_C&p)cVQ*W$D*(TI#3S{j%O~l9p~fT= zjxPbahe$xQAm0=I1#Pcuusy6t_03;<3Em-xcXw12Q@Z--KbgLv9bYA9Xz_yJ;3se4 z1R#^qh_M43V0Zxhj-$T_H2kWcpZb(6^?8li=? zp!!Vo3J^<=j(RYeq{Auc)uqw7s zCb}?I4WPnO0CWoP1WO^e>;(pV7wr5vX`Iop0W>B}VETJpL1e}>iQ^7Tx{kXAOt@|M z?l>r{dAEiW)+MYT9n*1DLlVD-Ii=XiloJ(qFSMRCkDj>Yat@sMPwAANF)}Zz1n|ImuS!CDl4BAx5yum$gT6FsM@r&eB zv$>l3*N;|0zEz<+MDP9vJ9al7{)JsJ%nq+%ikI@;*+Wp&I0VADhnd|0wf@rspveJ6wGg}GIqmK!J8j$z$10zz zqNk|7I2m_G6hzrY@vQ~PsHbXal71JPU$mXMYYtXiM*z51#Z(p1NQ-yr8BZZl0r3#4 zpWCcNY8lhB3+D`ymNiMG-z$_MmSqL^BZs@~T=o?$XlblO)k+)muzOBi`+;rRK2eJ@ zKv3s&DRLJSq(K6@fs-D2M$)zM$PoDRrJ!X$H%wmQKOgagNI&^7+^2qV7@v;_Q6^mf z>zj|TJ#jYNLE9o%nh+G4D{TcMk<{)uF7TCLCPhYc2&LR`CTyTbx_u;|;G{c3u-+`< z$X_i3Lx%@&`+Z@rc$G(dOdGGBu4P;0Lou2k{>h!qdoLI91}0S~rKBtVFfS7;yrw4; zS8$JJ0yIY$rCySzi5u6C_E?t@GP}nw@1I!$mL>Jo+J#@1ZA!L2nvYPz8QjV};XOvc z5^o&0T1P@!n-Gim>s{f&&`=s&ZKv=S*78$$N_0IFEHDupXvaDPw}2MhP?@H2%D0CN zS)v@${)RW8$#kVz3qKXBNbY!ecVsQ4(D74#p8+;fhWL|FDomg9O?C%>=I}MnQl;Js zqs1av-taS?UM@sz*x{eGU=}`!uZA;Ru-&}17R%P}_s$u|*fm0hu3Ho|wiif8r4F2_ zIy8e|2#g2p&ag0h!j9Sn{akbN?o6$)kiF-5et8qZh^mt=%tc+AjN8YfuI=JH2rm$< zq;Zx7X0>x>;3aCqxClB=n(TLFtaNdEo`K2Bz84VEW*BE+C>c`}Mpg5iXX{N1jjBwl z5ftNTEDjsz0)f*w3MyK!bY5}_Emy2*DW7sD(z|Ry%i=L2BMZo@oGn-L zNWQL?mFL7&?Z3uC@6!_}}qiNM^dKnw^ zhluK>9ll}><7C4z`7(|%`;(aQnhfa)Q5Fq)JT?(y9KF!sF^n;|6MhBA2xD4Bn(^4! zCg4Eq(-X^wev=7xcjYzo1Rnnh92HtI#~5%)4{)8VdiMkka0Kp8^B;Qe7@b7OBS<}~>^%~2 zX&gXhpof*EF$hlwbS0@CGf3Lv1Fs|-&k3Mun~uVbar>k%ZsPzSx_Ns5G?QN#$?e-W z=F^kHaDZee0}Ry^&z(fuYBp`zkRew+YR zX;njYhtU$0E-%Gq-;DfmuNM=2^J+(xm3yeSb|*aa%C4O&VeZnv2y!Nlcl8pni*j3}AaPoJZ)g*WFrY;c{Spj>$&;4(xW_s`0&FdTN)@_Y$ zV^aT&c#j_4%OLQ2e>?Z59D!Gr$904<)eCmGkIpXdO5GBS>-L?SqaP3iw=fd21`|BP zvXlb4W#@GT)XHf4)hlqoP|5p41VvZ@g5L4y>GKz(M;K1AJt=soN_Gt)YX!k+86nHM zTVdzVz8{@)VD8<0NEtt-A0YUj?x7yBk-T|-p1WZel^iDX<>w8Y};-glx>WriH z>bV0~&CG`@eh$j+fVp_E%wVcjDchTBDGFrL=kBNNMQc{G3^Z9$rHcr+hbp7ygqM~W z{Akk(p6c1#?p5V=XDmAW(Bf4LT0OP>9-pw=OW$>6)pLDXo?5|5<_zs`AJt081J3-@ z-lcz*#RvM43j>~0h}y5Y$U=&JRKczKrWc3P?1O`1tcQ2{r~zauixa#B6C`}WJ|iy} zBGtD*9Y(|*Pv-%}7l>tH75el?g+|lrJSV&gm4N_q;1NvnJQ^8R@xV1c$$ zHbTlfKkXxRZb;QHe5DyB9ue^HJN^UkzvGxlo7nj;xgGE4*gE() zJc+l2O}U;c;qkKlx4;3c+GMro7UrwNb*JBnGdLPd|L$)i6i!??^^aG3x9=oS^psD6 z`KgQ#bP||we$+4DmTXPo71 z{RwBXGWP;UuO}DeTsU)aT5TJ5d>lhe+}`gvsRoaCjUI9o!7=|3!6QPNX)zuu;CVbl zHM}~Z^KB&{K(ukiWvo)(dpP0scbJZaAzW&3Cd|JVGz5z&beUGwT8 z=1d%8f7{R)t|b!;9cI!`cgQrbBW-@^WfPGGkbq7A2cfAtpw=@kH|WFb^Ku=hHGyZ+ zi8~nM*8}>yr4fSk>;COM(A@pO6um6`7R^P>g0o`cKUYK)-;C%~wszlGL84QeM<>kY&24^F*<_kW zFikJq&SV`%6|04`lM)yg52AH5$BJIGgWXVkK-MKfq#d)#a9j*sJ zcsls-7VYg%F?eBlX)xjdLC>>w3L*D6i?++%YkTaNP$TRr-6CJ4$0d~dZ*6X(FTcu) z-VNwFA3gcu<>(OgJn8am%pOA5F~W~rXWA`$Ci|MzcX!Z%Vw3ZC-#&hZmB=q(bgYJv zXLqk2KD>|FUe1`IxBeD&LJt7HAD&ju)Dy?uAUTcHmN zkb9Wahq<1fu!_fysFUL<2Ls@aXlL%G&CXyThH}(!hpUSvoRx6oVQJf{0I;_)WNdI(~ll@QN9Ll&2k>$5@?b1SjzI;Ggusg_oO{VE+JBmt z?;fEg+P|w!Hyo1*HMsHuTmOz@;#+_rZ0yt(Z-^M;4uF#hU3D8P@Ds$RZ^HMM(1f^o zi6OG%4R+Vgusr;Mxe2@;oAlnFX1sIw{WSBGIwwW3GcLIcr@&%fT@X@JDD$_>*A(nr zNSsBR!<{%(03k^p;pUu1JucZ*W8_SZn88GgV})^Gk-5wgfAhdhepRO=eaJFFOxP=M<16p%kQ1Mb#+s&LZSdZ zc`q6;ud&YQ<=9@M;8<`w>bt}KlQM7P@dTCQc&6LKdwvWh#UXiljWEEDJ?mQrKEN4o{P~%8 z1-9R+*`EL=k`e?-+2N}LIT19^M#`!;;Z8a(-M*&d#Nv?On8WwbCoL}iy)jjqX0#?F z$paJDj4Xc8iEYI0kfm^e6n`d#6*&#lGLUJ6uSk$lMe83=Z>ZwPBq$@7Tgr9*NPiFR zx`W_jM|Q%g0I+!zgA?pfyhByc1Ger&tRcirvr6JbxeP;?222d0L58NX%T!k+(U72&b6}jjCcH ze{mocxf+J&ByDEe!?#)+Jc)4(1rX_TRm_RH#zb<7GQyzupByUmfka(scTC(#*=Dy( z)d`u7D`G30xAL+{nbgW6#znSnkM7;RJ^JqZ$6==LxenljGFn9t@vNQqlBp`H{`frj zworleyxp}anC;uM(Oo9sZUs_1<$)7B+GT6a7`xH^m~;mYVP zhFf;niF8+W|AXZ}$2r1f+DNH0Y9Z=Re|>^*#I6}>xSMM|sotr>YnmxL~|lE(nBiZJWIs$jN9+dM^3`|?*0>0`7-%3T3?kMnjO%=V0) z?Z-B$%B+W~-@ofS(vLvYqEPzef#vqK3?=O3=_+Bco>JIMI-zM2oqgYataWi89_^kLHoJe>u& z6kU3q{HAa^z?o+CtbnJVNJGaN?BKE-QWu2yzY8$NlD{bg`IxM{t;g3-Sg8p~kZc(z zkN6X{9{e(h35Z{nLB;b-p$NQoA-I5b6}u0-g+m%>{l@pPXeeXLBYE%ihe!8B3(&D9 z`7@ej1&+{?Z(6H77lh`&gkfp1ZWOZ3x8-AkmUERuaNsXFQ8lsA9U4m3@lc0P8hnJbwFWefzZ6Pt<})d(40ae}tp@Bo6h@zsV=0 zRI+#%j?X|^nk}r+SU3t-aNsdcz4}f3TV|y{Z9H(5pNiLG8(#|Qx6_am1nC)XH2OJ? zAE<%OA&&r6NDzR;2Om8vKnB?2)jQ?fKm;;RSNwXs{_d#ycYjYLRn8W`OINakqu0sU zPtW4Pj-M_7t9f#UrQ+@F4;*K}014yZHH4c>CLxaR(x=Lus(qHRJzK#^obm_#WP8qY zqA^5(2DGlBG2wL1Gy2k@+4}IN#8ePt@s5?rRTe0{4{nLHmJkkO7|Vq{7wkye(sf?o zyvT&)F{Uq8`OCoHtVz0a_L1>*&*L+s?|HmM#>Q24zAV)p#^xoCqO4OMrIyMe^vVM)vSA-teloQj#c&}p4a;s|^|8&xeWz2a=&J=sosQUNGY zg$?rhDZhg3@JUzrhHr?NbUK!a@P^a)J+$8Tj_xyd1xj!6(@utT20S>1XEOfSowBfj z{UrV>wgH8}LXuu($#=&!rdb%~s7`c*t`!g~tN*2)6C^s>m7VDte@@DrEEv&F*uygc zAbt+Pw~3?~4ul;zxS23Oj9bS}yc(^I*lJXL0w^7Qk|Q&ioCF$X!oETm#Aqy~tM@1n zc|Og_r{Q57(4>Gf!=Go(uI*PoKEb3spW|aEck)(vU@2i#FiK$DCZCz*QbmOjIXGZsAYF&!yRfTi~hy2opmr5Njt?P;&Q7dR$dexN>LDGIa;UJi4QzP^-$G>T#y` z9u+_>EA6tAw1LqMEf}ee`R@Jx=;6J4VF(Hq8>k81LEzG)z6`+~Gx2@|7sbHYFuoDg zrqKuRFs<9|Q-p87c`>?y>HPolU;i`b&qkxKzxjr|ouFQap%o_h3cfoSB$2sWrqiq* zEx^3)pw_5LrK@;PUc4H;L?~O~p0Ra=yWE37b*Yz87?> z4r<$4plxbqUzNsBbFAdO*}=^JEzSmdZ3K7jxZ{O7#5zV&6vAJ=d;`;-irMAO&07%$ zkHD)yZojPE6jl!v%F|{P8cR^PhOlN^RG`%2l>;jZFSKurb(3%cK9&44; zcJ7)EeUG$aM1?^|)syr`+PlIWu`=1wPp06nIBmUo!4;P{Fi?PU%s9E$U{^c zBwgstBfavKMt=!G6=XS7o~rD7B>Q;IFhY;<%(Q(nEK+lyt->jP1=lMT~mw2~^!R(pLSmJ?;n-X^b&W(j(T2RH;;c$FLPjqjrLl^!4!}Wr0C9 zSw~-GWmfWBn22P z6T1NHCE7-+#Ve^PK85HL{{`kxOYnZ@FN=&F{&}2a27C%e)NWGqEEX&VIL~a87KEwp z=veEK8IK`ck}t=TtRRwXG&ID6;HXe|iRO*x=M_@Tmpt_V?Gz21$qyG0rZoM30WF_U zW%QZ@$F>8;xoPrbPjIpLl;aYf*Y|1ykEn#tk&g2&&wi_Bi5a@o!TH58V>oJ#%Pe#= z-}6q??D!FC`co%WD=SN!fg_%pHuKD*9Lt@r@fm|B9(8cRw91lud%@)yzu86IRW_de zJK-FnS4K&c^Kb=G&&=g&9nxGTuQSZmQmA0-?N-dT-}L{aQs2$n*VQKxjM5w;w0b8W z!=Q0UtcRISbN@KB`g4LikROxo7WYT51|xQpz8WRIpLrI9zxJ0bi%PHM%*!^>BaA7= zNBa2-?Lm>BE98vx)xvxYo_;iXj1>?AYhkux@ST+f0V+(Fk6-Jr^1Mk zI31q(SV{}5HU_<&P}c;<2>=r^I)1`L3S4%_cz<$5$AxZslan8XuM`Y;oY;I~d(3e@ zlN91A=)p?tnZOp#o|&2m(U9qIK10K58O*nF5<32|lBB8d^~}kv*f9W?!bq)XYv@CU zQu@nw?7BT3^|WMy^J3^UAG5O(tBwuM-5@7G(@eg#2&wR-F$o!(yK)L&PFPh?b7E*Y zb>?Xk@mur?e=#^g`C7KBEQE=rqf(CG*POq*CufwUyLz!W30!u`I6HI$BOMx)z@$Z( zkS26J-9!-angvbnzC-=-kaB-?<92pzpFMvOQ}~bW z*RIm~(ps4Nqx;;ehT!-PLmsX={r=Z~A1jg$GO8use{eqldO(H0jQKX_`u_CwccYuA zOKNe`%cNJ?Z96~EAPlfrf%M+A52$E@hy%jh%f{-OY82L~W#RpB+X%Mz@7+hp`4F{; zjV11QBmL{-nP>=rSbE&vW8ecX{=P7gvU<~OAaN#13JBnS<60bDN>wL@KOwPx2EI@Z@lZJ`l znLGwUGAn-rQK7S-lWzUA-@+++Ib0Uye76MUS;nk?BuyLkk=C|{HqciFqN+qhUW;n| zVcX6C06+jqL_t(Ih(jwOmc*NN|MBn{e7vp0sGgk2W53S;n0X(fsoK z{1UJR{7d<_pS(md_Vg=H%9{$X&-z$%wu!=0{Vn+TDUf`%@bRkx4dah=IzHW0XFY>k zLMoLOa^+QR(s&hd4C%LV{TpMbFTa~#Pp^m?@YH7yBo_F`U$#3C#Jl^NijOI@K;8-M zAn*^L7-xpe_w6!PqNI}R36^J05gb`CX0i785aT5>TeNfLSzt}I@gslqyn2qN zWW2O=r^pK$EbPpgyc!lZD6b~69s&!Ln+YC4nc{ecm0{gjo|fH>^+gujx#EaL@&y*Y zlU^`8Kezw)R#kP1SIT+d%~s&>RKF2M+G;c@4DlO~$x8w>j_Id~H~*`l6(r%^-*+f# zIJHJk2^mtm&oj?OxFj;Lp>fF}d+PrYdc z2u=7~)^;pd5uKc~J>)^PHMj09Cp%!N9w?w{Xoa;X%LF{axLaeM>MWbZ&p*qBf6j%p zdgO%GiL5I?vR!xG7BMp6gxdFEE~I2n1cvqjx964=vdqUC3{D|S;o=QDaL=A{ZVVyg zkTY0bjx7zUylH|zD@vrHiTjuANPY9&V@&0*a4Gfe;CsP6XWN{CQ)qpUTH^B`eh6L# zaRtgH)CN;N&_Py5cW>W`@Tzd|&6DTsEOJK)t9|a|%wfIoahrGT4huKx_5hnc7z%sgVl$VL#!>X5Le4opGMz)`&8SQ2xaVQQAP@_wWB-3 zRRzmgakNF+QN63i`rbfL&4KN#%rO8m_{g9=Yq^hgsaNmzvM8YHg!jq0AfPbjJ$x=c z{QB=76h`5j|{X~9HB%s`|R}twsG5$Z`CJPS+R?u75Yll3TbPl z_m5RAXj|s4JykkgRB*vSfj@uL|0;urcjh>0pp{mCrH!RkQtVoK(j|6ytYi+^KP zaSQ6xch8L5|KtPt6NxZ3ng;K8rK79d26cWFPZ^{Cd0(M{KzT{+RDo04h4rC9>tBLi z;fN6iN{3XZ^@iBPJILe> z{*zX~8*=^Sc+hRwMEZgxU*#ZAHJKAfcyQ0NZ(u?fX_$Z0>bP{o&qN#Cuq&7Eo?A2Z z%ky(f!1DE>2zjc##F;N$gjm<{G~7BNR{oc7#ct&u9EsSz z>*a3!&_o&yz8Z=MKI7!ZX%^=C7Pqd+FWXhI0~_8o13Xa~F246qYPc z7XeeMRH(d%SxQ!I}3CGsveFO=g18zwVRGXc){51SmF*B$IC}+EbwZaZH4*D9)KLvWA4Iu z#Mv|pL|jY2PN6MhW1Qi9x>p6=WL$sGv4KSvC@&dT*KTmd4hBC)b7=T5C%JTPM@dX8 z7(t7C-E;Tuig~=jMa-I0B4{$#bN5hzkfOn;2y;Z@EfUXhh~;z2(8Yh%G8NX0%i=ou zQ(ZJWh~^Vv)bc*f(HG~-ZM=b=1qDRMAuweQ4W3*dh2JuHUqy@O#*GygwAUE(SD1%L zx2@mD-*Q=(|JKv{Y9NX^r2h&(8LMOZF&?0LJDv#q9KNR`)=9kj_ov(w9s~p$WbyUK zw0fk$b0SW6UV-_Xnx3NX9%0h2y9sN-V<;(V{@t^`C$J4(E$R9me1oTq9y%^Yk!bU<{)?Plpb2w}-L ze1X*(({Uxty!j}|T~ct~4WirK3N08W4OWjLEHH_ebqXUhQpa1m<$5Ow6z)E0!RZnv z>iuR46`q4rZ`$)wGCoU2f%62T800R@XNU7AHf4Q`V5lnG`sO+U&tZ1{6j;5t%X3$%1F8hMs3AhX z>XJrQEl(lf=9KCq?YV_S>&XTc9CiAykcMu4# zA?z)}+*NIenP7;tRchGdyC+Yn(KW1B?s2Y+1KsRwtw6&%!pYWGk48Vdc+REaA0imv zftAQm3|M1^n-yyxN zSqkeH-(!!Ix&HCu$FlaxyDFV8L+O7$_@%$aFFmJ%Q*ng%I?A{4I>K^=o{4mfd?n3l z7(+B2AtT8*TtQpzayEa-ON#Y9_7#UAKk<@9KjD5;g{bz`690Ke@ld;E1wD@^h zwgsQ~@uPi%Abo~Nvb~86S2EqN(C0WKNZ-qB-IGu9XPPc5t7&xJ z@*rQ4lc)z3{;GPUgfKq1D5-F$Fyq}~G3ig3e9t^tZt~rz$1}gth>{uzna|Mv9Cr^H z%jYr9@e!V-8UIxTEblgxwt2?YeHO@9K99*#pEfr$Df65S~RmS;6Cz3&z} zT*&l#AIqfrZN%7KbkgzsADwHRdAxXAXq63PPtx0i3f+o4c->>Gz$o zKs&|-)G5xdyZAcK;{5D2?;%wwwB~tkP&h-j{G5Y@2Sb{R&K_OS)`RJI7SQt3z=(_G zmemDUuBfKa-Ad&29bHlMuWKF#NNp zyjqHRO;TT>SEDu92|UIS<|-WD684#AKK+rYd=}XIkf@Wzmd|kk8mG+(aCHp9X(a@{ z@j%=|(l`cpVil}@_m1J3y(gMw=m+)l>@{wFFwZ8 zu!4U==&uT!=d)s62qtuSc~q-V;POI7G8RG2Nt~-N&T?c#PINCxTg>jfNSzZJcYe<8 z%%l}7N#bbCO9o?y6puR>c52JagSnO|(GusJylBl*gC0vT9Ut|;EwmYR#m~h_ITFu5 z=dhNK7hEdPeZn}&_Rc#5ntL(AA)~cCa``cg_rcvQgoCn-^p?BN_AyAJfTIx+RWcP; z4%nu*Y;CAJT*|%7E}Uw6o=bBlQl>4le8vu(mNwlzJ0tDBQ>hT+{cpx`Fj3f8A+MX9 zt+PnpA{45Y`Hl-D&slbR`)(K2!rhdC7kBR>SbFE%F=x599^M;${}>fdF7n<#I)sTL z*g%>FGxpJEzkyoiCM#@jiSw6lzt0ZbEmTKOkGMel=B?E87g(=!M@-`@ORU%_Uz@jc zblVlQysp6WW#;Yn=GJKU5IFqzcHhUC%mx>L--3B+9qQ%FXQMA4+#Nk-Ck%ccVyL!Dt40oEp`9=ex$y9;XDbuwW6pK{r0slVEOVA!ZTx>|awg)AUdW0WQhnz-7z zie*O;cyQT1w}+6X0KI-=9h?Yz7$0$<)11BojprA%4CcLko~2U=%RtePO!+w2WXFs2v>fRncf)P7WFsrP3AaT7@4xD+PJbkmf8;YOxP-?|{TtRF z-8ALE=&lF6z(AnrSIpdQ+;w5>% z8zBm)p^<>l0kD1*F8#um@GCv?NGEfql?A2X4m%5Ne%eoigUE>wj&b=kdc>NH;V=y& zCN18-JU_bxdXd$;e@#TkOJDy|?akHOh>*IesR2T#JSOk%E0FEm2tEUX9#eSS#FHgn zeOk&y4hCSv6H9jV?NBB@DfEAV2SwU;f+jo((aYcXwy5#S&lol?f8fyt+@X4;YRf~k z=H1EZZ&9SZnr1`lTlz%p)4xrp`cixo(EHX^|3n+YD_0F!Fc;1SRWE{zL=(ZQ7nYy6 z{YyZ;D_9y6M*T%lbKV8qF{baIK6^X*fB*b=^gsXnbo7#OC}VrzNQZavrLdGI| zqS7yq)q6;b59c##A)T^#9HA?e$^&Xa$by>15ZfvXiW=8aDA7onBzr%bqniswBp zcE&>Ml;^|_Tidl_sA*d}ejw`rLAsFYp9e1|*_8Ositan>}*C?Ni zc~Ho@e9qJ@A$U1@89Otk~rf(n3?3r}$OpB}X#F%8F@=H1h( z^u$n)xa+9|J6un`=nT2RHGmt5kK`u^$~DH+UYwK*NqF zzCEDDnDiLvP8hL-&0p~!TZ-R75#VtS#^@57x~YLPXBf2=F?K zAk3tR#%>49Sq57Mf8>$LoZH9D1bLkBSQ())On}A&4i`W)RbC>Dkiv=E&R>&-zB2E2 zbPN~RYYH-rKa&KI6@JsG36Cp9DI94NX%$o6^=;vSi=aW6#4V^*!m3NQ=3!hi2(;};lZ4oH^qiiFMvolo zqOQPO{8FM&r1eJ+=(@W_Ec!z2pw%|IyN|Bsl;CT5v7MRncLM2oD|Z@}m{4ajO}*Ha zyyFD^$X5S6){_>pv$GGAd4$@cj82t8+J9?DI9_0 z(Y?FbAw*V58+L_GgDJBMT=-4NnAYQGFJRp4hH>uBdQ#}qN~06hHE?N brn`~zx~ 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 z%0A{`DjT*)4_!wgYv^P>s+UVzX=vGPzTxej_oY!=&LF=gaf)dw2dg#e;3Z_3_3F`1 zR+>#9JaK}~A+uPUB#}gZtp9RTg%(Cm48kaXCQ6LN`vLlOHwCHXLZ;IY)MyJug_<%L zx8&vPU+V*M^IqMm$*De^_<*Y75YM#I!nj1H2)r|u&)kJJdA9Dr!@6^`C|!N|Q^~Xx z=}*O-bPhmQe@zT%t%sL5<#2Ri8jZim3t*c^G%p*Qjn2`KjFQ+I1uD%=NJI%>1=c%W z^N#<#S722baKy@_26CgNEOQvS#883-ko2j!AZtEWI2Y-&whVv#+0ZF<>_zPwy zQ!6G(Ee9p7xH1J6G7YRfhp@yCBl6Nzg{yFVfT8)aak##~6YrnV2p_Z7Yn9O<1)M}K zYgxFYBfd%zIg2`|O6J!%3DoDh3eQ%a7C(hHX&8Koi%Ke5lS9L?F`=-hMc=A z?y`Z(7#)u@X7>PTK$gEBJm~J+y&W1_-!k!K4zu1z$P8mtc-D$Wr=v+vo;{13=Q=Z3 zgM?|6{`#Yj=$za^(oq)#E?XflEiGoY;O6aH-Sg+qp%drHk+;0Ei*dGwKm8(W@m^vm zQo%XX#qD|#8%_x^N{2y1?={2-W9mrWQ%c!0@NeBmaUoQt%Cc)PZ?gf)8iI0{5jx8j z*9{WbTs?PWoO*&>jOm$(E4)qz(CV9Pip|Zh;>e@cWdsuUKC8pYb2ek-S0u6Y@J%#SN^ zD{|xwut2B3yq7;400I)|Gxz+|-9>_=GrINlT6r|0+=vw8)_jC%yvTm=F2$P?6XvJr z(eQZt`)w$okrW<4Ze&Y)=-Nh;tq(s@l;GEqSs)T3z>F^tsW7@>Gefnp( z@```2$O3+?{o-k1P^bT!y)*08EKAPvKJ%D!WmeYI)q|}bJb)3FEsy{q5C$QzWbqTY zM*IL@ATHtG_yWW|AY<8$hwiR+4X&!J%*yd(=9%Zi^Stlg-#JD`blvAZ=lj0B*AOdK zM68GvYub#OWuGwN>INB2$)?IDu%*zR05XPU}D+R~Imx#VwZJHiwXLmNiH zDJy`{xI9y223Cv$CuNHN$YqKFX8w3+U5HO9z!2B`H)N`#IH(I|c#u+YHhj8#&ZFUp zEbg{*^+eq*!ZNORgpH)&^h~E|^U_V_E0x=Z}V;V0*V%&GXwo`f&KQ_vsrDX9wN)Yw3Kt zPs)MBWA>Ii&?~L%M_8_xeV8nVgf_mO0%SOV!ZkkJ$|EU0(pniM2t?6Ygxgp0@q*_g zz#Ri?AJ{Lk-3Qz+vTx0SLiw~zo!$F>z<{(4vIB?mZi9EZy>p+_FX6*SKmUXQ{jDq^ zRYz){@@_Q;&>6(^rQzCTOSK+Bm*3cf(SAu_`Dq$yXxoszwrBfB?q7v?;H>R}hxDOb ziE^KJ*}nhYT^!hp;SqcF?W?(`(Y@;)f~M2k6Du9`9Jd-U`>E|yD%Jn;)w2b}NBpIo zc)E<&U-gSPE4%8h>L=-I{{)#(Zm6UEG!l?7;v4!)clqI^9XVs*Kzp3FN!dE#=L{f( zHp&(CO&gLP)K@;+x1iGDp)^qj)DxB`{^oH&TNom0dU>wi2oVhr1RtwMV$0CMW3J0G zEx??>joro}bVY*~L2_jV*Jo2l*W|9ng@5AhstNII@qrP3Qm((|^|DCQh@18CIprtL zx{fL7%Vwd4m6!N|D|5&ZqN!v^E1_-d#>?CQ9AOyV=PFwT+5*aWnQeR%`seBuQBM8E3LnaR3ruCX& z7}n=G8tDOh*LLEtxp{M!(HGM>1E-RTcOANkGc6xsDHW?!La@M2b%_pz4M~Sm_V=<( z1>wrYgUQoS2sUS{RN5}0R9A6ae5Z&LSZna2K{eP23$)@zlSvQ>=1*N>kK-~I4CUJ}X6xnI7( znlNL-x_U=a_HkTb!m!6XNA9uj&0}2WSyAb7Olbm9l28;?v{G(KI+M~*tk88o# zSc>`8*I%<1`|j}hgRh2n-+5;^$GB)vKKk^phVQADX+--d*mLLgnF%|k7xQF`**Zs) z=-g-Ih|YfK04=4_36AX&qk=mgOagKDtvfPthEaIy_GXrnxO|bSMnU@iGGEM%nBm)D zkciVUmoHJ=HaZ%nr&j5(Z6PbtZgJTqo>llB@wy?s@zx@1GBFo20HE-c=lmjDlN3yG zD%NQ^j;DbdZ@YVN{ZRo#MlOM<%-a?vkNLxX*GD;Dv4ABYdy4XA22s6BJ!D z8AaQbdM~shS@0sST0u+;rUorD5(LXXXH%_TD{48t*C1aFv|H&Fe8`ZauuV(G7_8oo z*Z~|GtE9b@3h@^Qf5$}T9K#b)6oB2d%@a6*!WVgks4kvh`b_S~M)L7w{A3i2Ydw-Z zY1PHT@{UTo~05W8O@g^Nv>AH ztj*r=O8N+|kfexEnr{LGE!$4X0VwfE-n1L;Afli2k!;I(md@3G>cH3r^_nP;>DAjg z-kb8AdDMQ*!N=5#IQOvGp_XC^19|PC)wT==I5V504X{(pZwFmnQiiT&HA#4k-PQT} zqd)$O;g`St!SJg;_->Y$zw`FZ;j=Hl#(p`-h7PA%U>`WniddG1d*}!a7i*LOZ{fF> z%MD!?>AO|c?e1&4$g*8mJb3iK{R@`cVb@MqpeOnpbszmGaZ@kwuX9T}9DSEb4rhHw z>GtdA(Bz!iwga{id@%gxFW+ZY4|kNlh7QgW_}a(PQIezTvo@mQ->PQy(F zzs_0Tu}G&s&&o%gdF8}`fi;}-58r(Y$8kO9E9|oB$o8qz?rIff%WW|Z9(T5{t5n)= z1*m1p=+FfmptrcVv~@&?Nafl@hS@NrTIDBrTUY832i3Y*5kMygX>$wnGO$XVPNmCM z<)w#Pxg1g*=5u6^9MM716FO5+3;b+Tp_{l+gfxhg#vp|4D)NnUc@|w|9?Q6D^V;5{ zdk9yaglAc^i+kgve~b?-=}(;rmvh!X@j=77PW9#2c)v#&wohqk`wu!v=xRq;^3VY7g)!7Cgm(R!;%()Su`~h|lt3*&oZBdcZ(#(I zL!@#*2+vVwq&7-u%{x9*mT*L9Vt6`^JZDsan5k2y(9#Z*KRHG`ycZ^DT0j^Iw0PhO zmRFoK^3P?I3*~2d#HDQVCS8!HT!1)}<~rm@yd?YKlb0NB;Ybw?3F-00#GdLjU8%v0 znN=p%Cs$xv^Q)I}Qi9)Tc<(Q-#-=I>45WOSP5J?y-m(-t;y@1@jeFbXz{zlzI2<4C zqx4vZy>Ww?r9)^!gGb?A@PbSmn4>e!lIgVEboPt)uU$|t6|o1JoHKItgr$b>bI`{< z-fecoCebC_`z@9!-r+qkj&9wc1AW45&5@@Cf{SH7W9Dj=B?4}=Mcgq6oY6#iuW)`3 z`5Xh+6RTEPa-hQAVHsh1{y1PRlej@yH*YRe#$AMU;QrR|#iM5^zXeV|y2n1TeU=@v zlyPro*y4V3g;~Ru4Q2|rF)$pz3r{Z?W!ins%PRq(^KpnncD!@Sez%9i9bV-5l;xf` zuCbI8A-_glZ$Ptqw>GjQ!xI_GOL_8i@CwlK9_{5)N9-;^H)0nm;9d?ZMpE35gzlz_*0^><-GCU+d93uR7B#h z^w^fAfir*$i_ElYKyb2GafqGa@&FQ*ylLaO+Q4yql}0-X%9L`G6U^uoU|zO8KoW$iRtwLWWdY$Ly%r!9F<$4#wC~)#$_mL$b^oYqlwC<4x%H`mfdy*bu3dnsig$VzKURJ=fXHE zRGm>C_?^_DOXx_xDtBm2TN7tfIWCwyBsWKvnAtx0i@t&O))y*&WX>EBhQ{}B(iIb`Qv}W9yU$}(j%Ht@%V`vP7wBXb9Eo;I-T3EBx1t7wg zFaBAMb!ytubws)4z4`uyD4@hhw@3~*kxyE%gB_8TbsO>e2t7J4PTU?0A?=`I%0lu)Pl2k zPoL_DIGR@wsGJCbbQZ?HxO>W7I%njMoViSafA`Bck}VFYBV{LV=vCZ|^Uq5ew(ntu zEK6FAF|&FYJykw!sZSI)#z1TDsvkypz#+hl`Sn)V;vC&Q#V48iS)JClA-!=A$VlA+ zLnljKjJyOv!>ymT$165~{0@4VE`wY;C8V`HJKN5F_@^VTz1N{UXYkaeg4xrCZgy|i zHum}t|My>TO5|G{>3(y#&ntQy08^(rxUFLo^nvA;f%yeywXl^AYU;Fj%AbRJBnC4b z6m%;ce$&K3RK~0eaBvBd$GT^L6!}yZ^V%5d<^f~b_5htCXV!st#-4IQ;Tin|2PpQi z(NBs0pX`@^$|=_W@Na*AxV?Z~0#}#wm!X~E+GneQGVBae_HkNwbG5ZB|G<0yOYEFQ>d+R0# zn!O%A|MC%OaBiV-=mW|Nz;H{4ud~rkWd=369=U+{{;5ahZ4dPd!uf)>^ zKBsb_Q{(KKvc8Nn_~D1|WYF|6FG<%XxMbRG8_uq-uOSbVV_#*3Bm8x|U7ex~dw5NB ziDgAbYwMLeX_f&=!o|)I(|Vo#K1FXhAThIF+) zzRbLIwjVX>qLBt9PF>@Oqsx+aojX0$uZPOXZy^=rWn1?G)8TWaArE*7Jz8c4GoK|%@ z_nEbXj)Bf|k|lZbr0(>~9Q0ij!Jc#kEE<&p9VLQr3Z7&s#IPVFGzdl=LL8ZBr!Pjv zJj7=tiTh+RmsxB)QTi&P8vZCX;-b(H5Cus=5%ItyZ#p)>GL7)k!Q@5(sZt~`n6_+L zSVobTJQ6OmAtcL`zjYM8ok{ARwdx_3a7yefg=ECyIZGfP;jBI81=;%<`GU|kAU;+q zOdyw+xd94H2!o)sZk8lQ6NV<5E~)FM-^oAT8#>WW$llvNH1% zWdRrhXX5rzW-6X1EIHg{uh>nD!Ok}CpGw_S^U0&Ym`v{X6g62R}Q} z19-n0?%ce|>=#GRZh8m?Yt}<&sF^mQA#xVe5jW}st|~C~ntMuI&SZlY-YyR0vllp! zET2_gHG0a14Q+!Ny3Hs|H&a(`HEPE$-_W*4KEm-A7YiGDtDEqMhH0|xjA}y zM;;g%C@2T-Y%eMlN1xO|K0CAe-h1!za@CvJ19-?up4w)Ot|F7=394Aq+y06JF)CW> znk7^OM&9^VCKfUL#+{ubYdCDq_yUgbQ7##kwCpH9Z5HJ?LTH`*Q^9&AoN`s%&QhNF z;#2Bzz%1$x_QB=1YtYRR%kYJcgnQh4Ik&GHaR25iCrK_8!GQ zwoRbNfYFXLpc1c26mZBxUS7fxWRX(O_}8oD)F7FNWGUa9@|2mQ4zLu5ucG9p@I3{T)#L4!Xdt@qiIuF6Cv#}?sP_#CCHqQ?Dk&$e2!gF zf5^s(ciOXZ%dkuS7$(1UCM<`4xTpN{4F6=H_qH`-XG{4KPM8^tNqTOyDa6^1=jFeV z+(*xh`a*bVD@G|RNn$x7Y@SgMQKnlc3=bTXpmH2LFK%2@-spxYT?@5XF7lF8T8Y!; zB}|?ZX1Grg-qL%lmj#5jq)?`3AD`ui@!UHDnO6p>tAb?H&vX~*;-ryI$KX_;se=)1 zLxD}W_UD4x#MCp+fOQpq>!H4#`YNWeeqA1^?z1y(a_kQ_&?S)SI-PtBd|;j{3&8#P z$6pSge*V>P<5zv9&nBlgukmhD-ofSU27_wOe9bX%D*;>ua>fjr`@)ZpIEvmqSLkbT zF}=S!e4cvfY6w?2u&@WZQxA0OB7ga=ZDzz!{J~3_78_^%;X*6v}!{fsB~a)f1iWLuwUu3Q9@3G1Q{J< z2Nx?(@By49b~^6JL6+>P-y^4$39=Ipxd=PNS$oa8$X)Es{4!1|^+hX!8w1Pqf83s7 zJ5&GO;B`cI?_6go=gYi{cacHR@Q`wHXcPTg5AceO!X|kdrza>X6PBz@*f&*_99Z@@ zcETtdx_F}p43qb%69mj`8_((i<>mgncZTcq!#@A~0rrlg@o(@_Z_2T6w9YbgO|@f{Z}?!i zUlh`VanJ{p7pE{V3M-B%{C1h=2|CR!7TH6|M2d2sWs~G{LclVWCF`s_POqK_U=35~ zszVPR)I~W`r)3$XdWIzOrw)L1#u`G{5*5#H9ifW@WR&78oPW=5yrbfPG!QB>O3y@FyJM?~G1I zIwTtSSPp;!dBjfzkPFlz@WfhnDKkZmX(o(BhI>;@m-n-0q5~pfBW<`hR|Ve=d3k1M z%TpYWQG`evjuuDN4naN zN8$U9v@MoQZr;9$QfKy!b@torRl9!O*}#1|{-g!hV>;_@#-y$0P?J*{>CHR$auVYz zd+R*?P{nRpj=ZgM&Y z)OaX^X|&u+E4f_>=#vbW=aB=xrUo9xXk2pm+dJFJD2ZgEyCI{+LFnKc0~dL9ll02) z>%Z~?49_OBZYm3)fQQOHBY_w`@$*u-Sf>4ewt2(_da8^a9nv7!ndu2s9u7hs`G{7; z8W@3%d}u7x4H>-!rnCwAlFB+!W)!XlnzTAw>DT~Yr~ly4_vdkX*X}WNKEz?iL1zEM z*Uxu{hkU-?;)V7casBKC2heQob70Lu1~U%fhqCWX=NT^=RsLHy^%+#7+zjRrXiW8t zDdZh^@e$8-9LIR+Bju-#gLP@Av9RTf^h#dK8}lnW7Bb58oY!dj(!^)U5yMhj8LLdC+MS&F)z`{K~VC9gMlfu zH`h0&faDLu~( zqhyW66>iGONLHV(;FUIKe$v?4vD_Ic1x{L@g>59?Nn?mt@Z>kmYy6ddwrIoRh^do= z62RuVypj^SPfoS%!i@PzxNydd7Ab7>-vrlu)vxwlqzgEuzEXgWCxcx_*kxzKjPuJs z+o7d61LmNlwRHB&*|+Y4BvwUDO%B*2{)?o!>(kP$SOWm(7ot+mz-KUpfYr`C*jI9uF>afBf zt;+`O)O*sk+a(@w;MjlpFvMD=^PP8s}{ z$uYSAxREU%Up;;f zKYys}z&F2rhJ3Vn<-PIpz=ZyYkT}WPwohxo6Or0j`@TsF-_yTCCsbaX!CV-A z_@i%=oSDm>dL(I`MnIVzGQFb#i}t+Hunltw{3vQCUJEQft-bnc(KZ;hjJA3omV zT_?`2=@dy+r|R4BlO6={lNZiZN1rYvicjN(cyj2-V2_&0uqFBAmIMbeV2 zFrNrL!VuHuvoz&by7*$uc?{YZ(Hp5TJzl^?BBb~x^0i(-{s(neUz+aEuc)CFxS}^Nsq8~ zUA!YsE=9EdzH1Ajr{=zBQODAkcz^!j>)|`!{x+`?*y1pdSFEvL1UAR)^Y@Y^5Z5tC zt}{2|CeGMPoJOoi6yd#l_t@w5lo3Q z_@SJ%N0g;X*)-1dYUXK`j20^Q^TYl3?&D~$4S(|EA7}Qsod@8eSiE=dz^Qg0n%R+y z{8=8)%A4|>b=kfOA48Mgyv$&O2YgQU2&;>nN^pJam>zIwOj~%BupPdrq6QgW#+jnl2-r zlo=XQl68t+kWvy-f*Kwi%PZxFrp@B&vN|NoK239 z_4AtXIeCP~$Dd^JOPV!&3^r5RRDNt*X5mYn7v1DDe=!o`Mnazrt>NG{A;6&^V}k#P zQ{wD6aCPa7cO5y`;7|N9^v(56C{Qj)^|AxXhrj4h+KsaX8MLtN+pbj?vKqcvi<1$4~Jj*{&$D>@84zL_d{gMK}{VJ2hjMnb8febd$%pPl3`_q zcf6v*)xX-kQ%<=P2X*W|gI}(8(RNBJ?QHgkBOBhkq*MAGGgvGB>OdW~dHMzFi#Si@ zt0Q6A49Wp}o<7J4``8ZmvH#2#{p91X7*IYLe&@HoH(aOAyZf%#kl=OJbUa<5Z-$**)4epZKqWTPQci?b#M{%0z(;N8Z@-_CD{AG*{01MAy6K3FHL2lPR#-Zzx>^A4Ih8P zanRVl%`V$oq?c)=pO^jn$bM*~lp3gW+*vqk7Fjles*Io7P@c%^HU#KFr6!pd#8lVl z0V9Mn*4-Qb^cR#%LqoBI>bE0NjK1S#g(*^9-Ygz8dtxV!BQkV#Eo(_Az@Y2X0|c{t3XNM z05y!iFpPVh2GbA}H2lug-6g9NzKZ!I#4-d9LB`WHgHgzQCGnjie3p z#nv-szV?}Yd^vpj;320fo#8aG2aeerc_FTQ7y$e7`*g5hu=LP(rs=5B6Cv&OC<`4R z`SaiZ@DHQdoz;BuYKu`XN6s#W7dVl77_SFkJwW-K@~*XOdF8+b25yO^2+vrSAQhb5 zyU7c|Z{4^(mapT$YXoriq}G~7j>e&*t>fe<<_bsd+o^vW=cP+ZMa>awWkCm|2eY^o zB1hwcld_^AbC00uEL(%6eCXgmcmEnfWT&GF5NAS#iL;<{=%7T57Vzb*byp_&Nt=lf zjdC8@gw`qumxMT|5Qzha$O2*FsfNs-niRR$Xp5Vjf*3iHWUV%3Xdqo4^QS-kDWin! z6JRN)qk{TYo)YFV0`uw+2`6oX{I(^H3qaF0GvWu*B+qgZJDK9->7zQOp62Kkh3$=T zz>}taq^USJia7DM%?jT?U`t!;9z2vg;JWluYmreo7UG}KX>l|74jaB-v6RN;vCsLu zp#45&_U*|QGjALe^pxMN*S^sFm_Y)T|KNPv@vt0c-Rv}_u9PnhGNd#ujzrFI6)*F7 zm4Bw?xj_jtb>dVgL=>iBv{%CXOyP+e)2IF;Y=Q!EGFU!0pZw^R8Glk;$thGd2;t1Pz=xNc<`+?EuxD@f7}7 zE+>7|IMaGHKXH^b+0n?N(ZbW(Ph-3!}J`zLc;t?JTv9;etJ~U`S?G|yn~&AF@`DCX^+V}CYll_%Iw;9S3UQ2-BG`Kf=}V z(YbP&pm6Ls`w@>FR+s$Q&pwUaf17u+Ihds|O?%aB!;?<+a0;KR7hM_Cj=D<&>o}_0 z)z9>g(7BfHrM>j1_|yfSXx}_(QaxRLLy=^1d7dgn`=7(kOoL-2OkXeKAgFt)WtWa4 z-@|8L{nhZ_{^+ye^|_;g>@#zK_duG+du^H>{0{EJx!A7SCG}WkFZ2ax(<`^-KZ7DV z3CQvh?dbsfeSj^g{m~9_5DPZtLVFJ%*61VXtli`+r$6<=dJF^z33{UYkgue0jeKd!?^eD-v#IdRlKDqKEi3Tr1V3jZJHyG$@?v z$=FAb_SZ6`Pw)Hd^2*6xDPI9R;$Zpem!ui<1t}x=2&Z~Zd|H>_Fy$Y?iK*$M@3;%* zfJoiLPwVcda8g(5EIy&^s2Aj&mvx`f&vJu~#H-$O5F!LLxde~!1?hTa zsE`X-i0aa_on6_FmA@^FV}*fKwmw zjUe-DgSJE~V@c94^zg=zkv2YEy_y!n1nZ={8a=z09%%ynhA&f$`0~k*zm(&>#fF|~ zfP|n|moWAoqrSO;v%`keuO2=eRybrs*zVziDRJPw_1?Ex=l+rxN8jc!j%&P2ZF6|S zQphX`gr2V%S#sm^b@qJOh@7!`!3JlSMY=}!R2st%KN^}>98BU#e!jEo1YSAwG_aXi z=jW}P*HaT;G~Jh9pAUcf(Wk>(yyW|ky>nkbeVS208mgVzJh;jrPz zpx`4f)W<74lW%sIRR9fCLc^Pm*4SCM4ZlSH!3pmLssLINvxDq+E|}F1JpYCx(F4k#@j2MABC8@ z*^%|dtG?!8>6oRrv{$wx4!bO?Sy|*AG%E}!u&&?ty}2L517MtmTUury0zCOI{e{Ps z*(4e1J%5G<8{*TZ35iZh`6^KoNR(zt8rz|1rrhcs(U-l9kN7N~5HTi!#% zlmWEq*Zk9`IFW}-J*A-t(xgd8VTJ_>0(VxJ0V&74mC54hz2$mE{w&!4)T1+m8AVU0 zGs)h3{lXL|U6l=6O?b+ePtx7xaA)i_lE2EQytIAUfm8R+a|n@nB46BiFLQh*8c{6F zjO;4kz?tOL_xwcXkS_7ai`T^K@Lkw}D8FsO;HE4`PuXtOMZzK8aOKU5fXEQHN|A zFu6A@0YWgGFFz*e5<}v}-+X+$jFy%S=&xm)Pd=tbi6>6vMw9}V_?CeqK$`|o=%XI= z*%KMD)}f2Iu?8lzf?_ZJa`^Q(7kB26X;d==%9?1a#f5oOD^$uRgRtgX&|k$akVR1Vvf$$ zR=Ujp930NbC$4U#a5heyGoa6}Gfhl{<#NDN8=?JaM;%#bue&D&|NUS4_An35nMI@i&MxYlILjuv{ZgJRZ)8r`dqUX@&@!?LJo^*!BwHbJ}T&b(Mm09<2Ie6{C9q!k^eS_^43=Ti!$b8#^uK;r8O-|XSzB;hZfOuKgW#}@M zX?j;7`K#>vsT_a`VR68yxtC=t4{w{7#NWMCuBs3kFqu0Q9gqB~!V;A8?f33eC;7<4wXMOuT>JMmd_45V5@I#!qw>esd zDVtgpxSQcwzFVF$Fq2m^n^CyZQ~OPys5BsIgh4Lup)c?j#ZA2}!r4qp#u$Epm2twV zVzg0&p%5+i_)O#)(}^I3P`D(rzh?i6GQwCZB0Q0`125pT2lD+@d3WcE-C= zc;VuQ-+G4@!lqi5C+;%)cAe?>oKgtLl8-Z9mc5G;$Vrkci=5}>j$0gLBDt^eGHs>+QOF0pV&E0)%x?l`KS%ek zF5hJtz_!|o85s@I5l7*^J#4V~do!D>H6SkeaLqovRPh3{n{W|sXy}VIT}tOt&lqgN zoW^%)={EcDUh(cV_pcoQ--fR-)H%@zEwkj(rKKuyjg>|!3WH)SDvHQ5pbsJ$?pC3X zvYyTnC_%3UmMxu8VK?5Uk3)X_geV(%Pa2-Wi`l3gA*i9u$ex_A)c2XrI|q-v`#wu6 zaF8|9j^?81t?L5LjCIU#uQ_m(C!>Xi%3pU$jf2a{&s%_@xsH_Q8j|@XRptY4p_vQHm&1nsVr>0n~xC^JIA} z{K&|x?|NI?XS5MRy9b^wkzB)p^MI0Nm%@Ozau8fBS|JBUPvtT|`2BdFXlY`({MlZ; zvmN;4eefgGO!*R4{Zw64IHNuTwld(QffJr^$N%u;>a!8bk+LG5Udds3K8$hR2Nuu0 zDjySEakV_J;+Cv|t6W)8jgI#@tOV8n0EVopz`|q(q z4Wa&ZZXzM&31vvr4DvBpZ67se1A=AGBN&Ru0cl)L$=hRQgMi@~czX=~D)qb0tlAA6 zHn(a#;lMNHGqYFpak8XTVS&Z&Id!#={F5JfRfBQ4TAp}P>=?q0|273dC`4Rs!=$5b z?dUV}M832+(_`n%w9jg(KmXYm!*@S;hXd8_u$^QN`nnQ`fpz*#H^Eyxb3!<;7ZO(o z$2~v5gY(ws-LH?lrqq5Zx3SNW+q9N3!k^-6jV2it@mafRMCxKYNNs@ATJg+{9J*>h zY-Kv5J=wPR_KoD5v+EM@J}^NO}XNBBjcyQnMnoA)>5+?|)HYdHI}< zp72KHQwL8a*E9mP!XoRH6Lk2^X;kDHQ>ET1C_1cVr=k$nRN}yY8m(n%sARTj44;Z? zXT%X7@;UWSoS8#B6l|9soYSd0U+(3wlwKWK zup`bhpG!LNkU-N(_{PufeI751nBpa;PHZ~P<8P9M;e}~>udtIk>j0s?uwPwnrf5uE zNBRM*Pv1kB5frlTalps!{?2fC=T;i+9fYn$0G zc*uKcw%O12>4Qhigsf2x`v`F?*V&YOg0VQoDPBHjmJI|J*ra>{jyvpmbENPFBZu<% z`ST}?#x1dQ0f!J?`hJuR@_)@~lN%`CL!3R=$8R7fn;7{`I_mBRkhhD3In#65Nz!Sd z^A2@2LdxhmCs3;RoSik@5p~?xVM9h<;k0exq*_NEM+7Z0u3Ro-$W|$58m*Ks%7^l( zY{>K6!lNhz0_26AhOEz~ffAFE%Tv4(!)6@=652AAN&mbYVaifDc&Ng%fXIk(&{Czw z6jyLx-Q++T6!^g*Cv8$EQ!e1VtkRpv-vn1A0k2&8vfVkoML@frTp4u+dY8N|CsFA; z!{$;E_nXCmA#F5XU;vq4L;^g)jLOe}leO%U9&1j#w2?4GXaFVB8T3z=&NeB?w0nUY zUgazBICF%mjE|5FPsKV!E?1AJH?wN1IHfL;T|IDbA$u9=l-PNZSN4Ttl&ObhrY*;y z5+Rj4D9EgeH=a7*U^k|`R(42`+5eDzr11rCv1=|r24`{;1^A46$+;{(W) zZ?11l0eB<7wS7>M53(PkBJ1dq$&PeMWbHvasUFYn@;6(JAk2hLmhZ@z&Ol^A`0_{o zdFxj?7w9qW?FeP0Bk`3J zvm^^BXY3hj-$q)xUr9JPKBnVdU9--Lkk}#Pfak!NC*-;eG4EJacjIUcryPNgE%@~F zN5kVEZ4LKsU&qnYjx)=`YXOdrJ4mHY_jM=^R-K)&PnbRDM@QJ$W8P(or=#8KKo;{* z=u9=O4xP& zvI1veo~4nQ3Jufz%k4#T8ZSgW#P`-0VTg}!a4>N{EytT zazX;5ZbsD$#*3I@2D4H3A7 zflNGFu*p)SU@9z$fGPlaVOmxyZuwT2^x`1wAO}1`tlQW~h@pHNXVU@0d`-+ZLPOnx z1L5vr^7Ob}I`OY?D%=a@p&O5!S!3VY*PJx>ntd^jI02A+T;e4}tLDYCg^G9X`}Nnp zjhKu=^5iQ7@MhEKI8+8M;C}J*>qgAhs=+LJH3pdg`>j#Npiq zKlvcqnKH(w5K+tE??H|dbZ*RJm|%`D?%VL|CHvT3?i_PM=`s5b4{7_m?0efsUU)?W^0mz#H+kw| zJTC3+(o!J!k5G!2@S~%|K^ft?T)I}=5;(@0Cg~DI?%=DQlrC`Icrutgoz&zbvFUR! zw8Aj2d2*RVSmZNlFYk~E!Zk2g=~Fxc2_9GrFXg0gEZclu)xm@lscc%Nr+KyLU^~*l z`#0A&qCj|B8k$e;j}pK`@01POmLtfqizW}x;0GNT`BTw5Pz#$g#NVr7js&!w85eEP}o8K-q$utG<@J~`)CNklK)0UqW~y)d3DpCJg*JTor_Rv+B#bzty{ccx zBb_WSANw(*Zi1k~s+>qa^NewUGnQ|fF|BZe(-bCb(~1W&bLn)71QicV@BMJ8kn)%u zGu+g92_F?!mT!W~R95uq#Ab0!of==jWG6g}3WTpBNaX-*rz-24VTxgqzu-dyuwc{M zZ~(xXhb()LVWK#V{7EEA4I{lJN8)9)9deP^W3*A5&$`=rvynME=HU+JOXq_d!^JUI z;8hvQ%)~jm1fnV)6+l2*Cj{Knuv9cQ3LC5)Oi#1h=VYX990ogV&tC4+X{%$P!V1AG z7i5JZGX9Ez-Q>RGhB)H$zLwxbTxcei1aO~gj7wNU8axY6^?@76Q3%$%4Pk^q4wFlh z{PL~J79*W0`QVb)sR7z8ju4VZGz@n%#LM}VBbNzsKto?L%$^@kRNUF$AR(K48I|$H z)*1~*9NjCn4^8%1D~~5e7$dwWxaXXVxJxH@jkE{O#^D4VL*v7PU1)ibR|V7vAb8;5 zGD461ca-daO~ltY#{UH`%ycudPS_!{J-3+|6K}6&4m)wCW*?=aqq)e)kf%E#oT5MyObd9i+g80Mag&bs4PeoJYpP!2mp}{mG=F2-yQzx zzy232BV8LF{N?AYd*8`Pix#RLaU}4Jk#A)s$}hqO4kK=96WsgDS7Z_Dr9O_XZjjH7 z^U`$nnB|8!Ns&PX8Cu6At3Ys*@?4H2f`&I&aUmUZ_sb9M;*}5Yyu>9lCXpeZsL#l6;fi;SU4h5ZHWy%& zwMm~$Z6W}riGP$^IT9{qL6dZbt^9ahl{IHylRokXFTh25$r)#8Y%}sHG-nhfWdyW3 zwfj!g52jbwcxcU=>zh)byh<)*7+S)#av|h|C-R;4%$Rfd6}>_nEJ)|&3_YR_j4rb6 z^Eb203MiL+CphgX&&m`0KSvKlcc?V98M2(Th9zgSnKf77@MKw$LFCmzAaWLd>OfNS zaqnN?8$?=)h<$QDkp*Pn2=uF~s2}0cC!>C78#f&^5uW*kDvi81%<{bjz75@QpS_Y? zWVn=dJBY^FVuX_!1oWr6roFSAwnNY%A(U<03<%Ew z0RWJ({dnn++qrZn(0;q<71O4%P+X>O;%wz9|n?pIobe?TSN9fwM4eT1TmjHLK zn6^^9UEXhbI%(F~nMg$|a&8QaPiK6Rg~);Ywa(ziPi z)iUKUeslpwQ4;b{2f=wQI!4kw)aat z0I}OQfzCR2nUTX@1%LVs_HLz@c*!$YccpCTXjsS{JW6lrE>6**sU9_;ZAKSZ5!0f) zg15LQ4LpK3wbv%GjHGNl_tkv?-u$%R?j1H|`&OZylmn;~vJSx|d{03lmoURYMM5Wx z{-kV3wlqu&Y=_N?D*UZPI*<~Yq){8<8pS~h@2RXJWy72t7@aPeW(PQHv!hf{Kxn>p zOp0tq5v7-ZqQd2Sy_|y1G72?i!cEaU-pH6JGAm>Nzb;8|y|TxZrqc@ZPgoOu#FW1q zR(=22AxG5jGVAk-&h|DZ=IOM(=3~d(kqDK54Nk@gx=aIp3pa#+pQTPJPZak&n<&ee zNYbV(1atUn+RJB?`{c)*(!DbYrB4}ag61wBGBU((KJOz~2(+qRp4p%PI^zTP!Hh5W zU(KVep772SH>EC|Ee)@R9mqSEchBWyID~wK(WDKQNxJlK702k5wbSz`;srXfci5M9 z@2xw-pMUg8l$g%c%U7>60;LmfM{a?RzfRw0k00{#Xp|!k(=JN%f{oES8GAT|i|`_j z-U{y%VQJ*MZ{J6;>~p}!9h8voI&sOJFV0?J9sb+Qe6X($Cr@K>or6^NaPaQmdz(X1 zUJs`_YaBr0te^(w2;MFzXUO0Z^>(R-2cc{-LgvO{mvQ*fVfA=^FAa$@Ae}##U?^AVR1+7+MW=+!ZaTN_Y2=84ouzs9Xf9naYIyzD z@Ed>k?+iDub8w6&pJKFp-2!e_=o$G1h`h6&j_&3}Fsh%XCrZ?-xGQs_Xssa+baW)u zPNVw)rM1qFzb;!5p0k*?`N$^qNZTR*smp)t#6YJ-kN$_w#ig*VSB(VE)GY-DN9cl# zL4w&w-4%z0@)$d&F18i$vW@Vo4il;)jPlsQgy6`G@d}-VMLwPysw3y#LzhZk?sL-^ zcNK!8(wI|K9$?XvKuxDWK2i>$e2S{d_)OkPDDc)Gm`2TEU1D&|m-NPqyKs#+U-@DF zfEErZq*9H88I4pb_@j;)#Dd21r+~TF7^K`Wz2&sfl;!fLlxY~5{F{w{nh$t(u@WZY zNoU$O*Egd;2^Th!v7Cp#89lG;+v0MNS2}~~zE>9DfhXAc-XXcs&Q4`pX698M$^!q9 zBHI#tjtoUk<)7^U+lCCMa|oPZpc$*U z1e{6E2_EllFSHcpnGE)?-5(v3MVK~I{gEZN=(DNqa#Q^H5knt=&c|}CGtsC8+hOEC zsalPhyxUsElN6?Mzr1ZXA;KuhQl_D^@Ua}ASwMiMO!G@BK6%b<+oTaw8h>+{So&Hh zms!2ismCr{(9c<9pwO)yr=-pPFB~I~%_~=IbGF|L7Kpf4=^V$*N<%RQ-`2BC&;HdB z1DmKYY#M`T+FA_26JAfWi@v{!!su8nnDflKPt;qFm$ z`@q6H_L{KcQwG}9o#=ISr@GL=Yi*}?dYUE7iWRDx6%@#iuPoF#wtW_N0J#4)7*I9G+&-S7t;II9P3v6q2 zt=!-lw7Dp6l39Mm&Hk_bBI{|L;uIhYfJqznIdnmW+oqvK@{k}6h&&N=@>8$&d+85I zQ}FL>sPtbRo?2&dpuOsRYs0dPm3w7VN67SI73=xMn94S&sVdC`A7No#3hSe6s|OR>I_ z*TD^OS8I|ZjyGhZywco8&WsdSdN9GGut$CX3$;s5>0G#`wmdIs?)_wgqtggqV7YL% zmsoD_BCWcad#}hFx9*>Gz{vhv=_Z~8`e0us`jAJ`2-{-IXK@fQ;f)`D%!>->jk?6B z5YmDzNT4H95?w+GR7b*l%N0pKHdylr!^?(}4ptu_Ovn;Et0C=BNQ~yUGuQG8!4aP_ zVgz7mL=}pNAbfNJqj^aptc<7xzY4JO*~GfzOdF1z zO9btp_9#^wmlxmE$(q_oW|d3a7%vgzubt5;K{J;=L|wu%&*e2HOMqa~aLASAZV=e# zTqF#gtv1j^$-MH+Li`DTh)|77Uvuh+x3kIvmtk{JaCF8|=sHj;tfhm+9F6bVbUURP zu>*|9^d7NfZhvb_0&}w9^6>ca)8Y2*J1il^IYME&5%~ng>zej`-kEYE$Lr$&%`FdK zK7Jer*%Kg7p|6;(s`PR2u3uwzjOqK^H}6mnmV;29&a}%lJj`Q<<&th#zRQV^F%CFS zUw-wFmqz}O#){IXvpt6l=qP%Ci5-bUoJ!ch=-A#kGr}flWW?E%4UEq<_M*jE!-I2% zMVXbZbF?hy?Cf+LA7B8$K?NPfByVj@bS_jFE|qnJN{4?J8QQ_PW&a3@OIq4~5dHAT z)?-_-aib&Sw4I=!G{`EJ@X;?WWyiG1Z-DufW%1*$?W4c;c9Lu4y(#)W6^y6(kkvx9 z%{qFzj?=I(e8ecF?~Gf4Zm)QSg2yEIOG9+p$rSEqhXy9vom;u|)nzEZrPx zpLZQP_bMh0;_^}zoQ|p^w8zKfBR@H;QjD>6P zPqXJh1!0Jb&%jdgFJMUftimn68AP!TW{l2J>4Q5s=j1SuQzi~@0{7T|;aL+ zj}M7y_?z!WM|6giALW@;@Yq2d`OZURp|4yRv-_jG$rirS7m$hB z3)NW=LB|HWPXsyB`3`SKVTmlsdwD9H@ZB443FTH9TL69>56Wv;G`b;>wN82;WYITgTM`+vxNANYLbnqe+yr%jD+GU=k0#Elyzmr@Uvw*|Gc7yy>~CR z9uE(BHO&W`w@JgEc;Y>=I?xW$I+SPK9hkmx z^IGtG!K;dNG&VQbUSaz~uIi|9UD_4pKrGdIy| z&^2vV!An|x_)T9GV72F@g*HGMf4Mb3VamMeqaSHQi#R^+?bn1Mo&gp*H#dE#mMy-P zm%OPTFo?5VnO+*CjFbZ`9;3IA6XQ*r>RO0!{4xh|=?8G10d#|fpRy`P+1DEiCNOvA z6@=JX7e9sCz{0-_Q}1$0Bmp2sok6g|9mh!o6-MF=mkwa;Ll88R!e}M^vmo=?NpQp^ zvjZb+VOZJBR=Dg*e0(OCjmr2jFgkpywB?)Dc)e5^V#hQ$)|BTGI$X*q1)=G%Iu+k# zc66?JX|xB6yv9NEeI7b)Iwx+>bVd!(DaX!U_iBif1hG6jvlH=4lNeSClAyVV4DU`Q??~IuzTW+y8P5eEGgfx^ zbtava_0W+WWXgKPS&IWGNy~3%#L_0Ho1>(Tj=F>*Bb>Ai8>qZU`vM<%9^QMh8+9dn zc&Pkklm@&Vl{S6kvyu?r_-t8M#cKgLuF3a>lH2Un+S&VT`1wyi8s1{B;X2DARgkxC z->P8*KmP8i-=UA40Uc@SWO-GrQQCk?!xb}On}+-733Y|UYbM}} zFEoU#p`CQH(UZhyFGpSv!C{HYA2aIfQrx{GmdsMO985wdW8Im!WjjmO&CyNyL47bF zq9gx;o486K3W!1Au6(vULkS0;>GD}+tOhZ(7@4+r6RyG@gF*{s%B8(EaD@h#T+!Xq z8=9#cRGXGFEfJG6Vky9kTw0VykBF&U%aV_!t#wo1x1$n7tm2#N8&aSQ8aX@TxY8Rr znD|8-%WM-oPB`)EBiJ$O5C_{_A<%(R5=XBnSMp6sQUUxp0`bTFRXsRhPG9Q(xlc$+`ULQK6~F=;=!qHR$1hq{24*EjnF8?*Zzm_ zQL49NWf3wh6X;j%^0Vw2hSij;+*o##3yAL zP8qgx;f?t$ExO5sCHiIg;!Ykriz$OT*{c(zKwAYIbL#}WoHU5@VMNl$ma)9hI1UMs zpRPi1&~aYphe1YppwqMH5|ROto@<8_LImsz$fYuC|oX3W7iEVNEZ(r`ctoql!XGV(XAIB?JP`Z`N1!NKL2ttB&t z^u^-LP8#!tj*du|L*it@r-j8eK1(?Q|E;%fq&+=)^b{O@|Ll7D z6^Ha0mv~>Rw%>tdWijuOBj3_8@PK>9t78ZbO|svdvLlP|#r~LA>O!JADiNQ2Im0Z^ z#6L11o)E(O)QRx4H;H3qy!;K^o>FapRrrU{r8CFY;C#D(|??}QR&n~i-VkALMy>tmTz zF@r}Lzyw70-;YjZ`y7QK@W8h1QI2^n)3&Jo$ci?2uiYhtOw@-2;vPEM_NX7NkPt3$ zBdq<1!U=}r$tO75Mgze3X7h$v;=?q>Qp}pbAOG@eyiX388^*^V`Um1?TQfd7Ls-&@ zFoR1kK?2+~Ey{qvGR%pM&KQOR9XYWe$yowbbaSsoR$b7EJ?8H*@) zh~liz5&Kd+!hIX%v&WK0kDA|OzuF$1YfsHNMA>AC8vcrmm^y`R0KfviII3iH3gT2s zZKzmWM*4t%6}dFv6WSp%;Sxby(x4Z=67T_c1QQKF0}LV~N1EZ1%l-?;hgkl8O23 z**fH?eqTCiN6y)sEoQjvY-{*8InaXsEvG@Q4_|-zNK24q2`e~Zb_%yRhP1D4}OZ*QVV89mXGZ;-}i8n>9)lK`8nH-8PR$6tI|0mkt@J>#@X zg@3Hy0&?e`I+c@-LpmqmEgdx$jz;OQ(w?ag&Z09dd}2(9Gq1)e3WUaEoUeIsR@P;- z?p=GuI`Ul(U6C&uKYvvewmFaAUt#04ClcBm!Xt0^L{^pA@h_3&o-UIq5B@9P1#Mfw zfuSNEA_A1kzr~u6PZcZoY3vwA1VpJOahDG?sS?m7<>0r$BQ5sB-2q4AW!h!JV|DBVhW-q%gou z+QPZe?2NsU=d{c0v4MAI@O_TgH>k|5vw0)A{03Lb_0=5d+>Bw?JDowEU5*glA~AWc zlJ}+S!nD;BPa2hQ2Z2n6u@hf)8TDKAkR4_uoPl>~|PF7j=U$KVHT&Gk(wP<~2P3Fya_ z2XuzR--)P|moi@)YBIu*GsEPa9nsmqD8b>BGDUmIz=$D$i>_(!Fg(tF3i+x2k?-cQ z-B5lzB*oAc;#~$B<-K~1JY5z^JGRJFLAkj6B+RdZ;m(eZ?aZ(gl#^!dKPba(e-<>> zS+FV3U)xCwl4AKc5a6b<9puSgkCjcKMazijL-3FWw#5ug6kN;TzHKy`r7+27s|yaM zy>eNk)iOnjH!U~CWsIX+8%yVYAiB=_gT^4%xzr_)-{GR#zYc?xmh%mhFtyEH5}*>3iSeBxv;V zYv`Q)xM*F|`w+8*>8H1*X&zME}ssY@TP@tPu6 z@7ND{&Y^Sm_0~3dT|sQ7x=&cPJ!iS;7Z7frz%Zj^b96Q#SyD>*kpn$Ocm)EatK*r~ zS&^AZRz|%JJ`oerMaKfza?{o#vlb_?$~iZ^2;0F4caOOJM{gS?{c;(w_LS+NnDM z;-jpCAms!A_mt% zL?~^NTW#AZWpd=gh$w*l7pA%lIO4CKNFILGt>Gi#JSL5>eGs42YbwJqOU0D5tg_+mRkp!@FWydFB<`s^JnQlyz7xfj^cYyvIm;nf@o9&=%yZvT>MX9nRoSQrE(izJ z5eKtMGo+r{_eGoiHAou8OEyDrH=}js@ya#UCMg0@T&QC@z&iK7#Hf#e2 zY)8X^klWZTXqo+KuXnaluD;`g4lV1~uj80K`ugec0%z)wJ!S5Jvkkf|^{X$xVAg6q zOH!q|IQe4A9hW~cW46G{uaB9PGyDemUhc4T4+Xx;`?8b+l{jC+vnS7nJGXdYH~h71 zNBUIynQcH_MR8eg>b3|oJTzj%(LxN1YrLI3^Z0s?k(VzTkPDQ)BT*fpp@J;KkPj+p z9RSztd&r10iFwZ)OD`i|7?2!-0!duL>vBG4Cgq3QomViL+0d;F(KcKH6Mh6g?~*iM zDueQ?xQEQdR3)WkrbJCo{i3uii= zwNx29&XhVT>&k#Q0x#G4=X1$JF)q%5PV%zN? zNugQxsH?K@}VmwCPrqKw*%XHSqTR!r9c5hwzi$g2BsawFBfKTq5 ziRaQ@8Io?HlZ|t{47Z~nzD8Ed@6xS?Ef@%wJQFTc{x`qhgaRXjW^|Mv&|76L)52AB zg*`8XU)@nw5rVIITU6RkG=o$iX*49OqlyUAeb4GLq2>_Y=)FW++>P)h(@`xm>EXX@YTcia= zHfQn~IaRL!NBPXNK$C^A@m_fapSC6=n^w5OqKW}w{)9m%S9BD&;wmtU$(3ZyTSv-! zbj4U-6N|_59bP8>uE`OXu70n{Xe*SbNHQq{F}qS`MuvAe*BY9 zhu{B?|9N=!>;>$0)*ll{pJ#0iy^1|k_wBJ|;X1ZB4xCWM(SbOn_+$MX{?%umI9N?^d{pbn! z^LiHsfn8OT8AS4S6^v~JzJuv9{Tp9(wF=@ ziU4VSHr`q*o7J!VZQ9C3+m<%aGHlRJ{&SCNQcl4}_gPun7l-bR>atVtvI-KKWl zeUYrtCKZXwppMoDK4qx$XHWv;Qi##Msf$zXDaEv;5g#K|DAvzCGsiIBb4^JUzcHp()vR?(n-HMYy66!*OX+uq*JGQ`s?FTL^Up@L7`q_zK-8)W>qhvaC&a%iaN5Sm4>d5#Gvz(Mg=}S~(jiYIu zu5j6*Yw@o!)8|fW6_PIu^@Z3fpk?Zz?$2nS#)Z$cbV~=pkEdMueiCQcblQBk$~y04 zbHvd7Zx`-k(9n3-55INMXt< zk{TBw%FxK0jRXPEK+Eem#1@JIIl`enX0L%xvGni&niK189$;9m%FujXDssidk088D z+5AS~Lo*1^h^~$*qme0>U&1`h$Lc#;*?L#TrIoTy{{k32=p?ctO&xHsh7e_Hhw7@< z)i%o@0|6eaBR<8hv=ATRiDB@Tu>5CKjj1=$H)t|Op*QtSJGW{8%rYO~E5GhLyr6#g zWYmp)dWUnvKKt7?aNagndBp`Xw(dwXan9V@d9pw$S7XDI1mLnRpqwj9LV|1SC_cVk z!}3#4Ql*_6ZbK$qm}7MUW8#1KMVabI^Y{xUR-0#e>NQengl~EBcV?`{@Tj_GOrF5D z(CH#wk+>2z#l89c|4?9RHBA-@N^tG30zUBw-uNFIZBC+Z!)6qs+ z{wzz%NE*JnemHmPCY1}xyp8R2T@l`gI0ID8xkj~@%UB17@Aye8(+k6}|R@My{5AfR zK`QO6+eKXY;^7vntSr!`E@1bP7oDxHHl3#^x~fOp9i2m6w6i*Sr|5pg$yGRW^S*Gq zPML$|=JjMrXWpcW?~`-(&3q0VuQF3-U5^fUkF52lkL60A6J`ycJbp5K_`$b^dF=Wj z^-I$M%ESkIdiFcDIj$|az!_YjKjD(cJ9jv^3>*HCR~}g3I&0L8dkd_*0*k!T%KAAV zojvaeUAHyp)X|?&FLPCnN0%Wm+Gb2M$)xfS`Bo;VaAXZZZ+Vp$b+ON>h?U`4SsNvf z-$+#GW`B2&!Q&p>Aw4}r3;7_QeTpZ~UJmcvyUlxK*BNwQNxw~4PHfoUk(Q~mP{SnT z$CCIGhuDP>RDArH$Y$1lNHev~Nd@cXfVuUwZhYG$xe`mi$Pm+rA9a#Hp^tjWB8W3Z z$~G-Ygu=5B(tnlFnpXuf@Fe)GV9IyfrwS@Io7^ZJsp_@)!(HC6GB06bImk%^hI4)P|1>%^Rb{gVC zRsW1qnM-W+L4>sx0BI_hRB}KCOUYrj1;9rOi<2;Q62yqGI6vM`7kM-C06Yc88kmop zH1;TO%h$AYG6fJ-F)rUqDp*J6VQSSi>OG40T0dYioQq7|gn5#y6Okbz&Y_l8eIfnQmA_3E?V zNy(Q@T{`q*M~hfs_v*dS6tA2HW|5XHfbkN~3b&&d@F#*=&N}s-)qAA1Zsv0a&FN&N z(uN{7&a~+ev-&*>O1wN}$~U6rh+gDlj-`I`BF+yDE}MH9E$KcfPG=k)Ces|Xmx05@ z(f=sPDh3>FoIH(>`ekQz@14jPwzva+=x^ z*}`6BrHWDNBL^W+`D)XG$)K%~m1lgwFZV``yi5J*5T&z^f>I9B;7Pl_x`BbZ9S3A< z>qTC4E=*4v+uMExT~_njgpEx`7%`Futjo`m5}PJvcxm{(7xz*d*@8YW9;qRrag=L5 zE9HI+OgJ>bVS^8g45D~4mCjWp9lSM!&frBq@Eq9%zRt09(qW3DFv1oA+b7_w(7jiN zC%Op3KjMoEbeDGIH!PQNbZ2r6##X0$Q`)i=h9gQhR`#ta6MaT5(fxn3Hv_lac6kGR^B_^DXh)wD=Xdp*a!ofq?caoUZ|npiu<)Z9^NlPhdtp}W1^M8+ zB67|wyi23p`Mb5qCt>Ab`W2R>w{uFp=cuQ%2}xxAK*{jp zR$X_BF5W~>zT!pAZ{J*HYr)#^*=K*rJ3v>5fBDb;!SDxv@Lz}j@rOUA-qI0!i(NIp zGfp~94oEMf$E#oL&oJXc8~61)tO>|Kqo+l_|K8itOZG7`h=-Hqz^Kc>qcI4d?93Sk zI;TYfcZogj)?tYy2HMfXLzZT$AJB)#^skONXe1{V0!19ux*p`Eb9Tf&U@!X=+WKQ( zqr$-KDmK|!Lif5KG6;IaD}zocQ^#)ERWPJghng<*+~>Wp!T?wMj~?~^lve`X;?(HV z*St#>n;|c>RgU%9dT<4^MdTcLanN~<)2;8k^;WjceD#2VWwfL#ZhW_zxCS;rvOf)Y zQpNYs$_H(R&&%v<_r~(RL%;v>(G65I99NmpH9F z_TD%?9TcvvkjIA2aF8zOK`NX2r>_Af^2GA8#2VfwU+CPnBBrn|c|$?U4?aAU=BZWd zU|mURYqgd<0$cc$8M$s)2Cbx*3CshY>blKeM!wz-LH zkZZ&RxVikrVG9!_7LPe~#P7 z+7UZ<*`$O8zdA$Swo!4H2BQ-gXH0^M<7E8ECR5^I`b&H&!u{20QqdNcM3zaC!juV% zgquDAV|edp-((OcKjO%Lh#?*m>@j-)M|2G!UV~zK>nMvw-A|SUP%q(`z`q!p)PW%C zsliLfj0_6#Ifg&(ldwV)4q-rXZjjb}f_u!it?^Q98@SGw8{RK2_A(0R(uw(9_xrI< z9Y;n3;)s-^Xx74$qEv_)C-G3(*!Pr1KA5bBTRCq(j znY_EeJHuhO5y+#|)4J23y>~fc4kJNPxzxcIbCy3ifE>8tsF^2+?z{Px@^l6l$*U1j ziL3bXz8YlB{cS44J(P3?1o+*d!|u$Bw4Iij_=*5qiL9aTjzh~cw2J&l&yg?T4LJMP zOD@Yt_(4OESMtMX@BLRmy%C&3PLPPoDt}%p8(=PG(T;hNE_|Hbln=fKCNTM$ z;5aCw{P|qvtkY*(w0u9_8{g+i7fkp`m5GAhd!-#DuTmGnn>m z4NCQz`3<%6YaVr!21cFZtf2?m93JE3B8NGNlqHgz>%1rd2hKfjIXMi5rhXJ?*#eAA zh`@LipFUgX7$xFr2)t3u#kF$YJt!u#6I=NoE0pJe1z!cnvUGssbXz7k%!&>jT5fSH ze!)jw#x<=v$V2Ek@x^?lZ{yy4{~{EyEy**l%WvqIFlnxoRX$5!Qccm48+)d+qKq(H zVE`Z7mXTu{Buiz-j0Brzv_tBm97}CG69#QdNG6LcDIZs!6BFFzd)uitTiay2;G439 z RKp!BLpyQ;W5zQ7w0-6Rf;vv#G5 zVd@~lEi3=liyJW%;rD*`pAUcXCx3zS zF%A0JuW@i42>++Od&{k(2m&zdSc!3h07XIq6vPTZ?0*9wu?Rr$V2PK2LhRVY3IA6$ z$9ZJ|jD2RNZ`IXR)zw{noASSD5b>k*{NsyOz|>~WzNYl;d9!SjMQ!IFee&k(7hk3a zHh*oTewO|bBWc{=)y8f+U3;94bK0X>Hhzz-7%>2+oya`{-SNsmu6AYGk97LOVB4ix zpNlD*rJNUiUIxqb9WKwz_GJLJl1Ce*-FLdP@2)X6FPaV02Ae5nP@Q$~2s&C(wfeGC zuK#?Q@%N&U(aZK|`(Sh=P4?_LA~*lUQyuxo8Chl*pEt8cXI^yxhZ(4kZIQ4PvQ-j{ zNu6`Fa8f27IwhOIh_}ipUwV`-)(#ARF&ICKGqN;JI+=)RznG5w<+0CXAUNYL5xGkM zN_)s+a0|>$_7*0PC7yKq(tttk+Xk`c`?U2tw3}}-diBqg@l`9O^p$~Kg zeuJy$0Pf!Vv@u7y#{YNG1hE6&2L;Ia{Kp%Bf z-su5)qj*EJ4~PyhxCz`n*rjPe3l8-d$|t>7nFmmQaDSk$81Z!mwBYA2*nTc~?N!E8 z7BRXeeA7M9DnIzrvZJfOnb?b)bPurcaNGx9@gMyoCKw;|(WV(07%Ob?fC7?1qhte1 z8QSKfQ=j68j8In*RViohNyo#UI7+Uq*XI@&jH?&rhT*P~R-uu&|n=PmwN`IsMEKiA|t z3annsA${Nl5gsViu zhekgtmoY^0AkCAMlx%Km6z@|0=Qni#cEUH*7%gXloV|Tp46FtT0Y7dYGXjJvdYm##>H26<|>j#FpzUG{lJn~8Nt}P z)JqAaI?1;N+U(l_L)mJahe*f*4>eX#>-z?GVziL1~nf32Z z@}%UQ{5ZHy{RoHh(@4WZzWB`n|Mi(DQ>KxpsY}aahnLu_Z9oOz0AR>z_@-e;ADbyF z_}1H~ukMPd&~hl<$foifzbT^#=oizbZb~&o-6t^zV(`wPZs>r`>^Qh39lxP@ypKNo z)Qd-qwh@M4DNh}pfgLdZZ!_AiqaQi4q}IlK8{irDbdY_MX5GwMo(8=Jn`Hf@Q>JW7 z#DyLHU~d`V_o3cPyYc~Pa<_GS_;9$v;}5=2axC4_rq|$=I=2pLZyfXRviZ#$zru$$ z1LsIr^_3@()OEzvLobB%SjdLt+HHFy51_ z`f-wriIqN>Fo9-e)Pd@EI`(nK)Ew$0%afcR^09U61XD76R|i+NR5|TI3MW5VpTgCz z#c&iEomBl6iD8gf9(94C5E9`=re`%koi0Y(c&Z}+b9Rj$?0vktg+<`&I4=^#g-;PG zJ8UN6q;y+YC%G-O;2_v0>>eK$w0<)f?vWz%~v--rHybnpnb_lz+# zig&}Kf)1h7+!;fd*fBW6pH{X15_Vce-qq7ZqGM0=% z(xul8zvw{O~fZhzW+pXkXbA~)KGmz}cA;Pc6zSLYqj6^c6>L*JXZf};UO zdS>zcrvW|7=-)Q_=(7TK@+LonnvWVool=xmzw6@do~Wofuj#G0tN$5Fw++O?*{Y5w ztM9c`3=5n>K8c*j$KznC~4PM>+CQkEUFC@twM%yzvSAfvMcQ`ePo^12c$Sn>u~PaKGkC7hn+O5OPDcW4F2v#M$uV5SsA=<*^jW`K@cDIrN@!lbGKbE z&DENdZ4~!?5^Z__rQ_gN?Qlr4(zhOB_MC&Z { return ( - {props.children} + + {props.children} + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index 106e307889..b6187aa532 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -8,7 +8,7 @@ import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice' import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi'; +import { PiInfoBold, PiTrashSimpleBold, PiDotsSixVerticalBold } from 'react-icons/pi'; import EditableFieldTitle from './EditableFieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; @@ -23,7 +23,7 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); const { t } = useTranslation(); - + const handleRemoveField = useCallback(() => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); }, [dispatch, fieldName, nodeId]); @@ -40,39 +40,49 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { onMouseEnter={handleMouseOver} onMouseLeave={handleMouseOut} layerStyle="second" + alignItems="center" position="relative" borderRadius="base" w="full" p={4} - flexDir="column" + paddingLeft={0} ref={setNodeRef} style={style} - {...attributes} - {...listeners} > - - - - } - openDelay={HANDLE_TOOLTIP_OPEN_DELAY} - placement="top" - > - - - - - } - /> + } + {...listeners} + {...attributes} + mx={2} + height="full" + /> + + + + + } + openDelay={HANDLE_TOOLTIP_OPEN_DELAY} + placement="top" + > + + + + + } + /> + + + - - ); }; From e0f2404c0005ef8a9da4949eedda883e6fafc8be Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Wed, 14 Feb 2024 18:07:15 -0500 Subject: [PATCH 020/411] added reset to default back in, removed unneeded activation constraints --- .../assets/images/popoverImages/image-2.png | Bin 664217 -> 0 bytes .../assets/images/popoverImages/image.png | Bin 591843 -> 0 bytes .../features/dnd/components/DndSortable.tsx | 13 +------------ .../Invocation/fields/LinearViewField.tsx | 14 +++++++++++++- 4 files changed, 14 insertions(+), 13 deletions(-) delete mode 100644 invokeai/frontend/web/public/assets/images/popoverImages/image-2.png delete mode 100644 invokeai/frontend/web/public/assets/images/popoverImages/image.png diff --git a/invokeai/frontend/web/public/assets/images/popoverImages/image-2.png b/invokeai/frontend/web/public/assets/images/popoverImages/image-2.png deleted file mode 100644 index 8db14cde13ce82356266a136b609d56165e1328b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 664217 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

T9IxZ{Ar4C8RW67>9 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 a%$U|fT^f2E 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 o`rXJYN{< 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

tlu!{++ODkz*jUXX*j@0=roBF^ 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 z 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 diff --git a/invokeai/frontend/web/public/assets/images/popoverImages/image.png b/invokeai/frontend/web/public/assets/images/popoverImages/image.png deleted file mode 100644 index 3c882aec48435eb5facc11a9ba57cae855eadd63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 591843 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 +*-QJm%Bd9utji?*>)Jr^bqTlgjz}JGKTS6 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≦r62D2N;b@9QLsx>t&-eRp_Ub4z#x{see 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 brn`~zx~ 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 z%0A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 z RKp!BLpyQ;W5zQ7w0-6Rf;vv#G5 zVd@~lEi3=liyJW%;rD*`pAUcXCx3zS zF%A0JuW@i42>++Od&{k(2m&zdSc!3h07XIq6vPTZ?0*9wu?Rr$V2PK2LhRVY3IA6$ z$9ZJ|jD2RNZ`IXR)zw{noASSD5b>k*{NsyOz|>~WzNYl;d9!SjMQ!IFee&k(7hk3a zHh*oTewO|bBWc{=)y8f+U3;94bK0X>Hhzz-7%>2+oya`{-SNsmu6AYGk97LOVB4ix zpNlD*rJNUiUIxqb9WKwz_GJLJl1Ce*-FLdP@2)X6FPaV02Ae5nP@Q$~2s&C(wfeGC zuK#?Q@%N&U(aZK|`(Sh=P4?_LA~*lUQyuxo8Chl*pEt8cXI^yxhZ(4kZIQ4PvQ-j{ zNu6`Fa8f27IwhOIh_}ipUwV`-)(#ARF&ICKGqN;JI+=)RznG5w<+0CXAUNYL5xGkM zN_)s+a0|>$_7*0PC7yKq(tttk+Xk`c`?U2tw3}}-diBqg@l`9O^p$~Kg zeuJy$0Pf!Vv@u7y#{YNG1hE6&2L;Ia{Kp%Bf z-su5)qj*EJ4~PyhxCz`n*rjPe3l8-d$|t>7nFmmQaDSk$81Z!mwBYA2*nTc~?N!E8 z7BRXeeA7M9DnIzrvZJfOnb?b)bPurcaNGx9@gMyoCKw;|(WV(07%Ob?fC7?1qhte1 z8QSKfQ=j68j8In*RViohNyo#UI7+Uq*XI@&jH?&rhT*P~R-uu&|n=PmwN`IsMEKiA|t z3annsA${Nl5gsViu zhekgtmoY^0AkCAMlx%Km6z@|0=Qni#cEUH*7%gXloV|Tp46FtT0Y7dYGXjJvdYm##>H26<|>j#FpzUG{lJn~8Nt}P z)JqAaI?1;N+U(l_L)mJahe*f*4>eX#>-z?GVziL1~nf32Z z@}%UQ{5ZHy{RoHh(@4WZzWB`n|Mi(DQ>KxpsY}aahnLu_Z9oOz0AR>z_@-e;ADbyF z_}1H~ukMPd&~hl<$foifzbT^#=oizbZb~&o-6t^zV(`wPZs>r`>^Qh39lxP@ypKNo z)Qd-qwh@M4DNh}pfgLdZZ!_AiqaQi4q}IlK8{irDbdY_MX5GwMo(8=Jn`Hf@Q>JW7 z#DyLHU~d`V_o3cPyYc~Pa<_GS_;9$v;}5=2axC4_rq|$=I=2pLZyfXRviZ#$zru$$ z1LsIr^_3@()OEzvLobB%SjdLt+HHFy51_ z`f-wriIqN>Fo9-e)Pd@EI`(nK)Ew$0%afcR^09U61XD76R|i+NR5|TI3MW5VpTgCz z#c&iEomBl6iD8gf9(94C5E9`=re`%koi0Y(c&Z}+b9Rj$?0vktg+<`&I4=^#g-;PG zJ8UN6q;y+YC%G-O;2_v0>>eK$w0<)f?vWz%~v--rHybnpnb_lz+# zig&}Kf)1h7+!;fd*fBW6pH{X15_Vce-qq7ZqGM0=% z(xul8zvw{O~fZhzW+pXkXbA~)KGmz}cA;Pc6zSLYqj6^c6>L*JXZf};UO zdS>zcrvW|7=-)Q_=(7TK@+LonnvWVool=xmzw6@do~Wofuj#G0tN$5Fw++O?*{Y5w ztM9c`3=5n>K8c*j$KznC~4PM>+CQkEUFC@twM%yzvSAfvMcQ`ePo^12c$Sn>u~PaKGkC7hn+O5OPDcW4F2v#M$uV5SsA=<*^jW`K@cDIrN@!lbGKbE z&DENdZ4~!?5^Z__rQ_gN?Qlr4(zhOB_MC&Z { - const mouseSensor = useSensor(MouseSensor, { - activationConstraint: { distance: 10 }, - }); - - const touchSensor = useSensor(TouchSensor, { - activationConstraint: { distance: 10 }, - }); - - const sensors = useSensors(mouseSensor, touchSensor); - return ( - + {props.children} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index afd64ca0bd..d0a30ecc3c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -3,12 +3,13 @@ import { CSS } from '@dnd-kit/utilities'; import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; +import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiDotsSixVerticalBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi'; +import { PiArrowCounterClockwiseBold, PiDotsSixVerticalBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi'; import EditableFieldTitle from './EditableFieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; @@ -21,6 +22,7 @@ type Props = { const LinearViewField = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); + const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName); const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId); const { t } = useTranslation(); @@ -62,6 +64,16 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { + {isValueChanged && ( + } + /> + )} } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} From c5aeb36230f9af7a1e842d4c45007720fe958be1 Mon Sep 17 00:00:00 2001 From: Copper Phosphate Date: Tue, 13 Feb 2024 22:01:30 +0100 Subject: [PATCH 021/411] fix: repair Dockerfile for ROCm With these changes, the Docker image can be built and executed successfully on hosts with AMD devices with ROCm acceleration. Previously, a ROCm-enabled version of torch would be installed, but later removed during installation of InvokeAI itself. This was caused by InvokeAI needing a newer torch version than was previously installed. The fix consists of multiple components: * Update the hardcoded versions of torch and torchvision to the versions currently used in pyproject.toml, so that a new version need not be installed during installation of InvokeAI. * Specify --extra-index-url on installation of InvokeAI so that even if a verison mismatch occurs, the correct torch version should still be installed. This also necessitates changing --index-url to --extra-index-url for the Torch repo. Otherwise non-torch dependencies would not be found. * In run.sh, build the image for the selected service. --- docker/Dockerfile | 8 ++++---- docker/run.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c89a5773f7..2de4d0ffce 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -18,8 +18,8 @@ ENV INVOKEAI_SRC=/opt/invokeai ENV VIRTUAL_ENV=/opt/venv/invokeai ENV PATH="$VIRTUAL_ENV/bin:$PATH" -ARG TORCH_VERSION=2.1.0 -ARG TORCHVISION_VERSION=0.16 +ARG TORCH_VERSION=2.1.2 +ARG TORCHVISION_VERSION=0.16.2 ARG GPU_DRIVER=cuda ARG TARGETPLATFORM="linux/amd64" # unused but available @@ -35,7 +35,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \ if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then \ extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cpu"; \ elif [ "$GPU_DRIVER" = "rocm" ]; then \ - extra_index_url_arg="--index-url https://download.pytorch.org/whl/rocm5.6"; \ + extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/rocm5.6"; \ else \ extra_index_url_arg="--extra-index-url https://download.pytorch.org/whl/cu121"; \ fi &&\ @@ -54,7 +54,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \ if [ "$GPU_DRIVER" = "cuda" ] && [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ pip install -e ".[xformers]"; \ else \ - pip install -e "."; \ + pip install $extra_index_url_arg -e "."; \ fi # #### Build the Web UI ------------------------------------ diff --git a/docker/run.sh b/docker/run.sh index 409df508dd..d413e53453 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -21,7 +21,7 @@ run() { printf "%s\n" "$build_args" fi - docker compose build $build_args + docker compose build $build_args $service_name unset build_args printf "%s\n" "starting service $service_name" From 163b22a7b30de0eb88b238738418990caae55ed7 Mon Sep 17 00:00:00 2001 From: Millun Atluri Date: Thu, 15 Feb 2024 07:34:31 -0700 Subject: [PATCH 022/411] {release} 3.7.0 --- invokeai/version/invokeai_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py index 6a45bf596d..46f67e7f8d 100644 --- a/invokeai/version/invokeai_version.py +++ b/invokeai/version/invokeai_version.py @@ -1 +1 @@ -__version__ = "3.6.3" +__version__ = "3.7.0" From f36b5990ed3bedd352cdb966b2196c46305e5c03 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:57:30 +1100 Subject: [PATCH 023/411] fix(ui): do not provide auth headers for openapi.json --- .../frontend/web/src/services/api/index.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 8cb9aa8618..6a342bb72d 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -52,10 +52,22 @@ const dynamicBaseQuery: BaseQueryFn { + }; + + // When fetching the openapi.json, we need to remove circular references from the JSON. + if (isOpenAPIRequest) { + fetchBaseQueryArgs.jsonReplacer = getCircularReplacer(); + } + + // openapi.json isn't protected by authorization, but all other requests need to include the auth token and project id. + if (!isOpenAPIRequest) { + fetchBaseQueryArgs.prepareHeaders = (headers) => { if (authToken) { headers.set('Authorization', `Bearer ${authToken}`); } @@ -64,15 +76,7 @@ const dynamicBaseQuery: BaseQueryFn Date: Sat, 17 Feb 2024 13:02:07 +0100 Subject: [PATCH 024/411] translationBot(ui): update translation (German) Currently translated at 80.2% (1143 of 1424 strings) Co-authored-by: B N Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 91 ++++++++++++-------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 3dca1267dc..44ae981b02 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -81,7 +81,7 @@ "outputs": "Ausgabe", "data": "Daten", "safetensors": "Safe-Tensors", - "outpaint": "Ausmalen", + "outpaint": "Outpaint (Außen ausmalen)", "details": "Details", "format": "Format", "unknown": "Unbekannt", @@ -110,17 +110,17 @@ "nextPage": "Nächste Seite", "unknownError": "Unbekannter Fehler", "unsaved": "Nicht gespeichert", - "aboutDesc": "Verwenden Sie Invoke für die Arbeit? Dann siehe hier:", + "aboutDesc": "Verwenden Sie Invoke für die Arbeit? Siehe hier:", "localSystem": "Lokales System", "orderBy": "Ordnen nach", - "saveAs": "Speicher als", + "saveAs": "Speichern als", "updated": "Aktualisiert", "copy": "Kopieren", "aboutHeading": "Nutzen Sie Ihre kreative Energie" }, "gallery": { "generations": "Erzeugungen", - "showGenerations": "Zeige Erzeugnisse", + "showGenerations": "Zeige Ergebnisse", "uploads": "Uploads", "showUploads": "Zeige Uploads", "galleryImageSize": "Bildgröße", @@ -150,9 +150,9 @@ "problemDeletingImagesDesc": "Ein oder mehrere Bilder konnten nicht gelöscht werden", "starImage": "Bild markieren", "assets": "Ressourcen", - "unstarImage": "Markierung Entfernen", + "unstarImage": "Markierung entfernen", "image": "Bild", - "deleteSelection": "Lösche markierte", + "deleteSelection": "Lösche Auswahl", "dropToUpload": "$t(gallery.drop) zum hochladen", "dropOrUpload": "$t(gallery.drop) oder hochladen", "drop": "Ablegen", @@ -590,7 +590,15 @@ "general": "Allgemein", "hiresStrength": "High Res Stärke", "hidePreview": "Verstecke Vorschau", - "showPreview": "Zeige Vorschau" + "showPreview": "Zeige Vorschau", + "aspect": "Seitenverhältnis", + "aspectRatio": "Seitenverhältnis", + "scheduler": "Planer", + "aspectRatioFree": "Frei", + "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (kann zu groß sein)", + "lockAspectRatio": "Seitenverhältnis sperren", + "swapDimensions": "Seitenverhältnis umkehren", + "setToOptimalSize": "Optimiere Größe für Modell" }, "settings": { "displayInProgress": "Bilder in Bearbeitung anzeigen", @@ -606,7 +614,9 @@ "useSlidersForAll": "Schieberegler für alle Optionen verwenden", "showAdvancedOptions": "Erweiterte Optionen anzeigen", "alternateCanvasLayout": "Alternatives Leinwand-Layout", - "clearIntermediatesDesc1": "Das Löschen der Zwischenprodukte setzt Leinwand und ControlNet zurück." + "clearIntermediatesDesc1": "Das Löschen der Zwischenprodukte setzt Leinwand und ControlNet zurück.", + "favoriteSchedulers": "Lieblings-Planer", + "favoriteSchedulersPlaceholder": "Keine Planer favorisiert" }, "toast": { "tempFoldersEmptied": "Temp-Ordner geleert", @@ -733,23 +743,23 @@ "accessibility": { "modelSelect": "Modell-Auswahl", "uploadImage": "Bild hochladen", - "previousImage": "Voriges Bild", + "previousImage": "Vorheriges Bild", "useThisParameter": "Benutze diesen Parameter", - "copyMetadataJson": "Kopiere Metadaten JSON", + "copyMetadataJson": "Kopiere JSON-Metadaten", "zoomIn": "Vergrößern", "rotateClockwise": "Im Uhrzeigersinn drehen", "flipHorizontally": "Horizontal drehen", "flipVertically": "Vertikal drehen", "modifyConfig": "Optionen einstellen", "toggleAutoscroll": "Auroscroll ein/ausschalten", - "toggleLogViewer": "Log Betrachter ein/ausschalten", + "toggleLogViewer": "Log-Betrachter ein/ausschalten", "showOptionsPanel": "Seitenpanel anzeigen", "reset": "Zurücksetzten", "nextImage": "Nächstes Bild", "zoomOut": "Verkleinern", "rotateCounterClockwise": "Gegen den Uhrzeigersinn drehen", - "showGalleryPanel": "Galeriefenster anzeigen", - "exitViewer": "Betrachten beenden", + "showGalleryPanel": "Galerie-Panel anzeigen", + "exitViewer": "Betrachter beenden", "menu": "Menü", "loadMore": "Mehr laden", "invokeProgressBar": "Invoke Fortschrittsanzeige", @@ -759,7 +769,7 @@ "about": "Über" }, "boards": { - "autoAddBoard": "Automatisches Hinzufügen zum Ordner", + "autoAddBoard": "Automatisches Hinzufügen zum Board", "topMessage": "Dieser Ordner enthält Bilder die in den folgenden Funktionen verwendet werden:", "move": "Bewegen", "menuItemAutoAdd": "Auto-Hinzufügen zu diesem Ordner", @@ -768,13 +778,13 @@ "noMatching": "Keine passenden Ordner", "selectBoard": "Ordner aussuchen", "cancel": "Abbrechen", - "addBoard": "Ordner hinzufügen", + "addBoard": "Board hinzufügen", "uncategorized": "Ohne Kategorie", "downloadBoard": "Ordner runterladen", "changeBoard": "Ordner wechseln", "loading": "Laden...", "clearSearch": "Suche leeren", - "bottomMessage": "Durch das Löschen dieses Ordners und seiner Bilder werden alle Funktionen zurückgesetzt, die sie derzeit verwenden.", + "bottomMessage": "Löschen des Boards und seiner Bilder setzt alle Funktionen zurück, die sie gerade verwenden.", "deleteBoardOnly": "Nur Ordner löschen", "deleteBoard": "Löschen Ordner", "deleteBoardAndImages": "Löschen Ordner und Bilder", @@ -865,11 +875,11 @@ "maxFaces": "Maximale Anzahl Gesichter", "resizeSimple": "Größe ändern (einfach)", "large": "Groß", - "modelSize": "Modell Größe", + "modelSize": "Modellgröße", "small": "Klein", "base": "Basis", - "depthAnything": "Depth Anything / \"Tiefe irgendwas\"", - "depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth Anything-Technik" + "depthAnything": "Depth Anything", + "depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth-Anything-Technik" }, "queue": { "status": "Status", @@ -904,7 +914,7 @@ "batchValues": "Stapel Werte", "queueCountPrediction": "{{promptsCount}} Prompts × {{iterations}} Iterationen -> {{count}} Generationen", "queuedCount": "{{pending}} wartenden Elemente", - "clearQueueAlertDialog": "Die Warteschlange leeren, stoppt den aktuellen Prozess und leert die Warteschlange komplett.", + "clearQueueAlertDialog": "\"Die Warteschlange leeren\" stoppt den aktuellen Prozess und leert die Warteschlange komplett.", "completedIn": "Fertig in", "cancelBatchSucceeded": "Stapel abgebrochen", "cancelBatch": "Stapel stoppen", @@ -913,20 +923,20 @@ "cancelBatchFailed": "Problem beim Abbruch vom Stapel", "clearQueueAlertDialog2": "Warteschlange wirklich leeren?", "pruneSucceeded": "{{item_count}} abgeschlossene Elemente aus der Warteschlange entfernt", - "pauseSucceeded": "Prozessor angehalten", + "pauseSucceeded": "Prozess angehalten", "cancelFailed": "Problem beim Stornieren des Auftrags", - "pauseFailed": "Problem beim Anhalten des Prozessors", + "pauseFailed": "Problem beim Anhalten des Prozesses", "front": "Vorne", "pruneTooltip": "Bereinigen Sie {{item_count}} abgeschlossene Aufträge", - "resumeFailed": "Problem beim wieder aufnehmen von Prozessor", + "resumeFailed": "Problem beim Fortsetzen des Prozesses", "pruneFailed": "Problem beim leeren der Warteschlange", - "pauseTooltip": "Pause von Prozessor", + "pauseTooltip": "Prozess anhalten", "back": "Hinten", - "resumeSucceeded": "Prozessor wieder aufgenommen", - "resumeTooltip": "Prozessor wieder aufnehmen", + "resumeSucceeded": "Prozess wird fortgesetzt", + "resumeTooltip": "Prozess wieder aufnehmen", "time": "Zeit", - "batchQueuedDesc_one": "{{count}} Eintrag ans {{direction}} der Wartschlange hinzugefügt", - "batchQueuedDesc_other": "{{count}} Einträge ans {{direction}} der Wartschlange hinzugefügt", + "batchQueuedDesc_one": "{{count}} Eintrag an {{direction}} der Wartschlange hinzugefügt", + "batchQueuedDesc_other": "{{count}} Einträge an {{direction}} der Wartschlange hinzugefügt", "openQueue": "Warteschlange öffnen", "batchFailedToQueue": "Fehler beim Einreihen in die Stapelverarbeitung", "batchFieldValues": "Stapelverarbeitungswerte", @@ -965,7 +975,7 @@ }, "popovers": { "noiseUseCPU": { - "heading": "Nutze Prozessor rauschen", + "heading": "Nutze CPU-Rauschen", "paragraphs": [ "Entscheidet, ob auf der CPU oder GPU Rauschen erzeugt wird.", "Mit aktiviertem CPU-Rauschen wird ein bestimmter Seedwert das gleiche Bild auf jeder Maschine erzeugen.", @@ -1084,12 +1094,18 @@ "Wie stark wird das ControlNet das generierte Bild beeinflussen wird." ], "heading": "Einfluss" + }, + "paramScheduler": { + "paragraphs": [ + "\"Planer\" definiert, wie iterativ Rauschen zu einem Bild hinzugefügt wird, oder wie ein Sample bei der Ausgabe eines Modells aktualisiert wird." + ], + "heading": "Planer" } }, "ui": { "lockRatio": "Verhältnis sperren", - "hideProgressImages": "Verstecke Prozess Bild", - "showProgressImages": "Zeige Prozess Bild", + "hideProgressImages": "Fortschrittsbilder verbergen", + "showProgressImages": "Fortschrittsbilder anzeigen", "swapSizes": "Tausche Größen" }, "invocationCache": { @@ -1336,12 +1352,12 @@ }, "control": { "title": "Kontrolle", - "controlAdaptersTab": "Kontroll Adapter", - "ipTab": "Bild Beschreibung" + "controlAdaptersTab": "Kontroll-Adapter", + "ipTab": "Bild-Prompts" }, "compositing": { "coherenceTab": "Kohärenzpass", - "infillTab": "Füllung", + "infillTab": "Füllung / Infill", "title": "Compositing" } }, @@ -1379,5 +1395,12 @@ }, "app": { "storeNotInitialized": "App-Store ist nicht initialisiert" + }, + "sdxl": { + "concatPromptStyle": "Verknüpfen von Prompt & Stil", + "scheduler": "Planer" + }, + "dynamicPrompts": { + "showDynamicPrompts": "Dynamische Prompts anzeigen" } } From 716b584f03952636eee49469296f9fd493eb4a6b Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Sun, 18 Feb 2024 15:02:01 +0100 Subject: [PATCH 025/411] translationBot(ui): update translation (Italian) Currently translated at 97.1% (1384 of 1424 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 590e9ee28f..8a427482dc 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -1138,7 +1138,11 @@ "unsupportedAnyOfLength": "unione di troppi elementi ({{count}})", "clearWorkflowDesc": "Cancellare questo flusso di lavoro e avviarne uno nuovo?", "clearWorkflow": "Cancella il flusso di lavoro", - "clearWorkflowDesc2": "Il tuo flusso di lavoro attuale presenta modifiche non salvate." + "clearWorkflowDesc2": "Il tuo flusso di lavoro attuale presenta modifiche non salvate.", + "viewMode": "Utilizzare nella vista lineare", + "reorderLinearView": "Riordina la vista lineare", + "editMode": "Modifica nell'editor del flusso di lavoro", + "resetToDefaultValue": "Ripristina il valore predefinito" }, "boards": { "autoAddBoard": "Aggiungi automaticamente bacheca", @@ -1241,7 +1245,11 @@ "large": "Grande", "small": "Piccolo", "depthAnythingDescription": "Generazione di mappe di profondità utilizzando la tecnica Depth Anything", - "modelSize": "Dimensioni del modello" + "modelSize": "Dimensioni del modello", + "dwOpenposeDescription": "Stima della posa umana utilizzando DW Openpose", + "face": "Viso", + "body": "Corpo", + "hands": "Mani" }, "queue": { "queueFront": "Aggiungi all'inizio della coda", From 3e48edda6f7bb7e64fa2e7e1ca77a22a44f82c97 Mon Sep 17 00:00:00 2001 From: gogurtenjoyer <36354352+gogurtenjoyer@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:53:35 -0500 Subject: [PATCH 026/411] add latent-upscale to communityNodes.md (#5728) Adds the 'latent upscale' community node --- docs/nodes/communityNodes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/nodes/communityNodes.md b/docs/nodes/communityNodes.md index 906111f45b..33dddad162 100644 --- a/docs/nodes/communityNodes.md +++ b/docs/nodes/communityNodes.md @@ -32,6 +32,7 @@ To use a community workflow, download the the `.json` node graph file and load i + [Image to Character Art Image Nodes](#image-to-character-art-image-nodes) + [Image Picker](#image-picker) + [Image Resize Plus](#image-resize-plus) + + [Latent Upscale](#latent-upscale) + [Load Video Frame](#load-video-frame) + [Make 3D](#make-3d) + [Mask Operations](#mask-operations) @@ -290,6 +291,13 @@ View:
+-------------------------------- +### Latent Upscale + +**Description:** This node uses a small (~2.4mb) model to upscale the latents used in a Stable Diffusion 1.5 or Stable Diffusion XL image generation, rather than the typical interpolation method, avoiding the traditional downsides of the latent upscale technique. + +**Node Link:** [https://github.com/gogurtenjoyer/latent-upscale](https://github.com/gogurtenjoyer/latent-upscale) + -------------------------------- ### Load Video Frame From f0d4c7196082de4bf65fa5ef486e176171e700f9 Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Mon, 19 Feb 2024 12:50:11 -0500 Subject: [PATCH 027/411] updated tooltip popovers --- .../InformationalPopover/constants.ts | 105 +++++++++++++++++- .../ParamControlAdapterBeginEnd.tsx | 11 +- .../ParamControlAdapterProcessorSelect.tsx | 5 +- .../hrf/components/ParamHrfMethod.tsx | 5 +- .../hrf/components/ParamHrfStrength.tsx | 5 +- .../hrf/components/ParamHrfToggle.tsx | 5 +- .../src/features/lora/components/LoRACard.tsx | 49 ++++---- .../features/lora/components/LoRASelect.tsx | 5 +- .../ParamInfillPatchmatchDownscaleSize.tsx | 5 +- .../components/Core/ParamHeight.tsx | 5 +- .../parameters/components/Core/ParamWidth.tsx | 5 +- .../ImageSize/AspectRatioSelect.tsx | 5 +- .../ImageToImage/ImageToImageFit.tsx | 5 +- .../MainModel/ParamMainModelSelect.tsx | 29 +++-- .../Seamless/ParamSeamlessXAxis.tsx | 5 +- .../Seamless/ParamSeamlessYAxis.tsx | 5 +- .../SDXLRefiner/ParamSDXLRefinerCFGScale.tsx | 5 +- .../ParamSDXLRefinerModelSelect.tsx | 5 +- ...ParamSDXLRefinerNegativeAestheticScore.tsx | 5 +- ...ParamSDXLRefinerPositiveAestheticScore.tsx | 5 +- .../SDXLRefiner/ParamSDXLRefinerScheduler.tsx | 5 +- .../SDXLRefiner/ParamSDXLRefinerStart.tsx | 5 +- .../SDXLRefiner/ParamSDXLRefinerSteps.tsx | 5 +- 23 files changed, 224 insertions(+), 65 deletions(-) diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts index bdcd1ca9a4..2e88c28df6 100644 --- a/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/constants.ts @@ -13,28 +13,46 @@ export type Feature = | 'compositingCoherenceSteps' | 'compositingStrength' | 'compositingMaskAdjustments' + | 'controlNet' | 'controlNetBeginEnd' | 'controlNetControlMode' + | 'controlNetProcessor' | 'controlNetResizeMode' - | 'controlNet' | 'controlNetWeight' | 'dynamicPrompts' | 'dynamicPromptsMaxPrompts' | 'dynamicPromptsSeedBehaviour' + | 'imageFit' | 'infillMethod' | 'lora' + | 'loraWeight' | 'noiseUseCPU' + | 'paramAspect' | 'paramCFGScale' | 'paramCFGRescaleMultiplier' | 'paramDenoisingStrength' + | 'paramHeight' + | 'paramHrf' | 'paramIterations' | 'paramModel' | 'paramRatio' | 'paramSeed' | 'paramSteps' + | 'paramUpscaleMethod' | 'paramVAE' | 'paramVAEPrecision' - | 'scaleBeforeProcessing'; + | 'paramWidth' + | 'patchmatchDownScaleSize' + | 'refinerModel' + | 'refinerNegativeAestheticScore' + | 'refinerPositiveAestheticScore' + | 'refinerScheduler' + | 'refinerStart' + | 'refinerSteps' + | 'refinerCfgScale' + | 'scaleBeforeProcessing' + | 'seamlessTilingXAxis' + | 'seamlessTilingYAxis'; export type PopoverData = PopoverProps & { image?: string; @@ -46,21 +64,57 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { paramNegativeConditioning: { placement: 'right', }, + clipSkip: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, controlNet: { href: 'https://support.invoke.ai/support/solutions/articles/151000105880', }, + controlNetBeginEnd: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', + }, + controlNetWeight: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', + }, lora: { href: 'https://support.invoke.ai/support/solutions/articles/151000159072', }, + loraWeight: { + href: 'https://support.invoke.ai/support/solutions/articles/151000159072-concepts-low-rank-adaptations-loras-', + }, + compositingBlur: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, + compositingBlurMethod: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, compositingCoherenceMode: { - href: 'https://support.invoke.ai/support/solutions/articles/151000158838', + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, + compositingCoherenceSteps: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', + }, + compositingStrength: { + href: 'https://support.invoke.ai/support/solutions/articles/151000158838-compositing-settings', }, infillMethod: { - href: 'https://support.invoke.ai/support/solutions/articles/151000158841', + href: 'https://support.invoke.ai/support/solutions/articles/151000158841-infill-and-scaling', }, scaleBeforeProcessing: { href: 'https://support.invoke.ai/support/solutions/articles/151000158841', }, + paramCFGScale: { + href: 'https://www.youtube.com/watch?v=1OeHEJrsTpI', + }, + paramCFGRescaleMultiplier: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + paramDenoisingStrength: { + href: 'https://support.invoke.ai/support/solutions/articles/151000094998-image-to-image', + }, + paramHrf: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096700-how-can-i-get-larger-images-what-does-upscaling-do-', + }, paramIterations: { href: 'https://support.invoke.ai/support/solutions/articles/151000159073', }, @@ -70,7 +124,10 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { }, paramScheduler: { placement: 'right', - href: 'https://support.invoke.ai/support/solutions/articles/151000159073', + href: 'https://www.youtube.com/watch?v=1OeHEJrsTpI', + }, + paramSeed: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096684-what-is-a-seed-how-do-i-use-it-to-recreate-the-same-image-', }, paramModel: { placement: 'right', @@ -81,15 +138,53 @@ export const POPOVER_DATA: { [key in Feature]?: PopoverData } = { }, controlNetControlMode: { placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', + }, + controlNetProcessor: { + placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000105880-using-controlnet', }, controlNetResizeMode: { placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178148', }, paramVAE: { placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', }, paramVAEPrecision: { placement: 'right', + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + paramUpscaleMethod: { + href: 'https://support.invoke.ai/support/solutions/articles/151000096700-how-can-i-get-larger-images-what-does-upscaling-do-', + }, + refinerModel: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerNegativeAestheticScore: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerPositiveAestheticScore: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerScheduler: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerStart: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerSteps: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + refinerCfgScale: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178333-using-the-refiner', + }, + seamlessTilingXAxis: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', + }, + seamlessTilingYAxis: { + href: 'https://support.invoke.ai/support/solutions/articles/151000178161-advanced-settings', }, } as const; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx index a62f7e7d8a..245c182b9f 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterBeginEnd.tsx @@ -1,5 +1,6 @@ import { CompositeRangeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useControlAdapterBeginEndStepPct } from 'features/controlAdapters/hooks/useControlAdapterBeginEndStepPct'; import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; import { @@ -61,12 +62,10 @@ export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => { } return ( - - {t('controlnet.beginEndStepPercent')} + + + {t('controlnet.beginEndStepPercent')} + { } return ( - {t('controlnet.processor')} + + {t('controlnet.processor')} + ); diff --git a/invokeai/frontend/web/src/features/hrf/components/ParamHrfMethod.tsx b/invokeai/frontend/web/src/features/hrf/components/ParamHrfMethod.tsx index 8a94544233..65f65240fc 100644 --- a/invokeai/frontend/web/src/features/hrf/components/ParamHrfMethod.tsx +++ b/invokeai/frontend/web/src/features/hrf/components/ParamHrfMethod.tsx @@ -1,6 +1,7 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setHrfMethod } from 'features/hrf/store/hrfSlice'; import { isParameterHRFMethod } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; @@ -30,7 +31,9 @@ const ParamHrfMethodSelect = () => { return ( - {t('hrf.upscaleMethod')} + + {t('hrf.upscaleMethod')} + ); diff --git a/invokeai/frontend/web/src/features/hrf/components/ParamHrfStrength.tsx b/invokeai/frontend/web/src/features/hrf/components/ParamHrfStrength.tsx index c663989b08..3cb9f7e528 100644 --- a/invokeai/frontend/web/src/features/hrf/components/ParamHrfStrength.tsx +++ b/invokeai/frontend/web/src/features/hrf/components/ParamHrfStrength.tsx @@ -1,5 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setHrfStrength } from 'features/hrf/store/hrfSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,7 +26,9 @@ const ParamHrfStrength = () => { return ( - {t('parameters.denoisingStrength')} + + {`${t('parameters.denoisingStrength')}`} + { return ( - {t('hrf.enableHrf')} + + {t('hrf.enableHrf')} + ); diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index caedde875a..28bd8afe95 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -10,6 +10,7 @@ import { Text, } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import type { LoRA } from 'features/lora/store/loraSlice'; import { loraIsEnabledChanged, loraRemoved, loraWeightChanged } from 'features/lora/store/loraSlice'; import { memo, useCallback } from 'react'; @@ -57,29 +58,31 @@ export const LoRACard = memo((props: LoRACardProps) => {
- - - - + + + + + + ); }); diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index 30ef99d2f7..ed70a4d44a 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -2,6 +2,7 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { loraAdded, selectLoraSlice } from 'features/lora/store/loraSlice'; import { memo, useCallback, useMemo } from 'react'; @@ -57,7 +58,9 @@ const LoRASelect = () => { return ( - {t('models.lora')} + + {t('models.lora')} + { return ( - {t('parameters.patchmatchDownScaleSize')} + + {t('parameters.patchmatchDownScaleSize')} + { return ( - {t('parameters.height')} + + {t('parameters.height')} + { return ( - {t('parameters.width')} + + {t('parameters.width')} + { return ( - {t('parameters.aspect')} + + {t('parameters.aspect')} + ); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx index 9c41b2d56c..a772daa177 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageFit.tsx @@ -1,6 +1,7 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import type { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setShouldFitToWidthHeight } from 'features/parameters/store/generationSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; @@ -22,7 +23,9 @@ const ImageToImageFit = () => { return ( - {t('parameters.imageFit')} + + {t('parameters.imageFit')} + ); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx index 13e03962b4..c6c77b5fe9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx @@ -1,6 +1,7 @@ -import { Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library'; +import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { modelSelected } from 'features/parameters/store/actions'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; @@ -41,18 +42,22 @@ const ParamMainModelSelect = () => { }); return ( - - + + {t('modelManager.model')} - - - + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx index b66293580a..739cf7d83f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx @@ -1,5 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setSeamlessXAxis } from 'features/parameters/store/generationSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; @@ -20,7 +21,9 @@ const ParamSeamlessXAxis = () => { return ( - {t('parameters.seamlessXAxis')} + + {t('parameters.seamlessXAxis')} + ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx index e983565476..455e50b90f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx @@ -1,5 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setSeamlessYAxis } from 'features/parameters/store/generationSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; @@ -18,7 +19,9 @@ const ParamSeamlessYAxis = () => { return ( - {t('parameters.seamlessYAxis')} + + {t('parameters.seamlessYAxis')} + ); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx index 9f3bf33848..a4409955cc 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx @@ -1,5 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setRefinerCFGScale } from 'features/sdxl/store/sdxlSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,7 +22,9 @@ const ParamSDXLRefinerCFGScale = () => { return ( - {t('sdxl.cfgScale')} + + {t('sdxl.cfgScale')} + { }); return ( - {t('sdxl.refinermodel')} + + {t('sdxl.refinermodel')} + { return ( - {t('sdxl.negAestheticScore')} + + {t('sdxl.negAestheticScore')} + { return ( - {t('sdxl.posAestheticScore')} + + {t('sdxl.posAestheticScore')} + { return ( - {t('sdxl.scheduler')} + + {t('sdxl.scheduler')} + ); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx index c2eea5d925..fd7b1f89cf 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx @@ -1,5 +1,6 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { setRefinerStart } from 'features/sdxl/store/sdxlSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,7 +13,9 @@ const ParamSDXLRefinerStart = () => { return ( - {t('sdxl.refinerStart')} + + {t('sdxl.refinerStart')} + { return ( - {t('sdxl.steps')} + + {t('sdxl.steps')} + Date: Mon, 19 Feb 2024 12:50:35 -0500 Subject: [PATCH 028/411] updated copy --- invokeai/frontend/web/public/locales/en.json | 179 +++++++++++++++---- 1 file changed, 143 insertions(+), 36 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d4f00a5970..32c707f908 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1424,9 +1424,8 @@ "clipSkip": { "heading": "CLIP Skip", "paragraphs": [ - "Choose how many layers of the CLIP model to skip.", - "Some models work better with certain CLIP Skip settings.", - "A higher value typically results in a less detailed image." + "How many layers of the CLIP model to skip.", + "Certain models are better suited to be used with CLIP Skip." ] }, "paramNegativeConditioning": { @@ -1446,7 +1445,8 @@ "paramScheduler": { "heading": "Scheduler", "paragraphs": [ - "Scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output." + "Scheduler used during the generation process.", + "Each scheduler defines how to iteratively add noise to an image or how to update a sample based on a model's output." ] }, "compositingBlur": { @@ -1463,47 +1463,52 @@ }, "compositingCoherenceMode": { "heading": "Mode", - "paragraphs": ["The mode of the Coherence Pass."] + "paragraphs": ["Method used to create a coherent image with the newly generated masked area."] }, "compositingCoherenceSteps": { "heading": "Steps", - "paragraphs": ["Number of denoising steps used in the Coherence Pass.", "Same as the main Steps parameter."] + "paragraphs": ["Number of steps in the Coherence Pass.", "Similar to Generation Steps."] }, "compositingStrength": { "heading": "Strength", - "paragraphs": [ - "Denoising strength for the Coherence Pass.", - "Same as the Image to Image Denoising Strength parameter." - ] + "paragraphs": ["Amount of noise added for the Coherence Pass.", "Similar to Denoising Strength."] }, "compositingMaskAdjustments": { "heading": "Mask Adjustments", "paragraphs": ["Adjust the mask."] }, - "controlNetBeginEnd": { - "heading": "Begin / End Step Percentage", - "paragraphs": [ - "Which steps of the denoising process will have the ControlNet applied.", - "ControlNets applied at the beginning of the process guide composition, and ControlNets applied at the end guide details." - ] - }, - "controlNetControlMode": { - "heading": "Control Mode", - "paragraphs": ["Lends more weight to either the prompt or ControlNet."] - }, - "controlNetResizeMode": { - "heading": "Resize Mode", - "paragraphs": ["How the ControlNet image will be fit to the image output size."] - }, "controlNet": { "heading": "ControlNet", "paragraphs": [ "ControlNets provide guidance to the generation process, helping create images with controlled composition, structure, or style, depending on the model selected." ] }, + "controlNetBeginEnd": { + "heading": "Begin / End Step Percentage", + "paragraphs": [ + "The part of the of the denoising process that will have the Control Adapter applied.", + "Generally, Control Adapters applied at the start of the process guide composition, and Control Adapters applied at the end guide details." + ] + }, + "controlNetControlMode": { + "heading": "Control Mode", + "paragraphs": ["Lend more weight to either the prompt or ControlNet."] + }, + "controlNetProcessor": { + "heading": "Processor", + "paragraphs": [ + "Method of processing the input image to guide the generation process. Different processors will providedifferent effects or styles in your generated images." + ] + }, + "controlNetResizeMode": { + "heading": "Resize Mode", + "paragraphs": ["Method to fit Control Adapter's input image size to the output generation size."] + }, "controlNetWeight": { "heading": "Weight", - "paragraphs": ["How strongly the ControlNet will impact the generated image."] + "paragraphs": [ + "Weight of the Control Adapter. Higher weight will lead to larger impacts on the final image." + ] }, "dynamicPrompts": { "heading": "Dynamic Prompts", @@ -1526,13 +1531,23 @@ "Per Image will use a unique seed for each image. This provides more variation." ] }, + "imageFit": { + "heading": "Fit Initial Image to Output Size", + "paragraphs": [ + "Resizes the initial image to the width and height of the output image. Recommended to enable." + ] + }, "infillMethod": { "heading": "Infill Method", - "paragraphs": ["Method to infill the selected area."] + "paragraphs": ["Method of infilling during the Outpainting or Inpainting process."] }, "lora": { - "heading": "LoRA Weight", - "paragraphs": ["Higher LoRA weight will lead to larger impacts on the final image."] + "heading": "LoRA", + "paragraphs": ["Lightweight models that are used in conjunction with base models."] + }, + "loraWeight": { + "heading": "Weight", + "paragraphs": ["Weight of the LoRA. Higher weight will lead to larger impacts on the final image."] }, "noiseUseCPU": { "heading": "Use CPU Noise", @@ -1542,14 +1557,25 @@ "There is no performance impact to enabling CPU Noise." ] }, + "paramAspect": { + "heading": "Aspect", + "paragraphs": [ + "Aspect ratio of the generated image. Changing the ratio will update the Width and Height accordingly.", + "“Optimize” will set the Width and Height to optimal dimensions for the chosen model." + ] + }, "paramCFGScale": { "heading": "CFG Scale", - "paragraphs": ["Controls how much your prompt influences the generation process."] + "paragraphs": [ + "Controls how much the prompt influences the generation process.", + "High CFG Scale values can result in over-saturation and distorted generation results. " + ] }, "paramCFGRescaleMultiplier": { "heading": "CFG Rescale Multiplier", "paragraphs": [ - "Rescale multiplier for CFG guidance, used for models trained using zero-terminal SNR (ztsnr). Suggested value 0.7." + "Rescale multiplier for CFG guidance, used for models trained using zero-terminal SNR (ztsnr).", + "Suggested value of 0.7 for these models." ] }, "paramDenoisingStrength": { @@ -1559,6 +1585,16 @@ "0 will result in an identical image, while 1 will result in a completely new image." ] }, + "paramHeight": { + "heading": "Height", + "paragraphs": ["Height of the generated image. Must be a multiple of 8."] + }, + "paramHrf": { + "heading": "Enable High Resolution Fix", + "paragraphs": [ + "Generate high quality images at a larger resolution than optimal for the model. Generally used to prevent duplication in the generated image." + ] + }, "paramIterations": { "heading": "Iterations", "paragraphs": [ @@ -1569,8 +1605,7 @@ "paramModel": { "heading": "Model", "paragraphs": [ - "Model used for the denoising steps.", - "Different models are typically trained to specialize in producing particular aesthetic results and content." + "Model used for generation. Different models are trained to specialize in producing different aesthetic results and content." ] }, "paramRatio": { @@ -1584,7 +1619,7 @@ "heading": "Seed", "paragraphs": [ "Controls the starting noise used for generation.", - "Disable “Random Seed” to produce identical results with the same generation settings." + "Disable the “Random” option to produce identical results with the same generation settings." ] }, "paramSteps": { @@ -1594,6 +1629,10 @@ "Higher step counts will typically create better images but will require more generation time." ] }, + "paramUpscaleMethod": { + "heading": "Upscale Method", + "paragraphs": ["Method used to upscale the image for High Resolution Fix."] + }, "paramVAE": { "heading": "VAE", "paragraphs": ["Model used for translating AI output into the final image."] @@ -1601,14 +1640,82 @@ "paramVAEPrecision": { "heading": "VAE Precision", "paragraphs": [ - "The precision used during VAE encoding and decoding. FP16/half precision is more efficient, at the expense of minor image variations." + "The precision used during VAE encoding and decoding.", + "Fp16/Half precision is more efficient, at the expense of minor image variations." + ] + }, + "paramWidth": { + "heading": "Width", + "paragraphs": ["Width of the generated image. Must be a multiple of 8."] + }, + "patchmatchDownScaleSize": { + "heading": "Downscale", + "paragraphs": [ + "How much downscaling occurs before infilling.", + "Higher downscaling will improve performance and reduce quality." + ] + }, + "refinerModel": { + "heading": "Refiner Model", + "paragraphs": [ + "Model used during the refiner portion of the generation process.", + "Similar to the Generation Model." + ] + }, + "refinerPositiveAestheticScore": { + "heading": "Positive Aesthetic Score", + "paragraphs": [ + "Weight generations to be more similar to images with a high aesthetic score, based on the training data." + ] + }, + "refinerNegativeAestheticScore": { + "heading": "Negative Aesthetic Score", + "paragraphs": [ + "Weight generations to be more similar to images with a low aesthetic score, based on the training data." + ] + }, + "refinerScheduler": { + "heading": "Scheduler", + "paragraphs": [ + "Scheduler used during the refiner portion of the generation process.", + "Similar to the Generation Scheduler." + ] + }, + "refinerStart": { + "heading": "Refiner Start", + "paragraphs": [ + "Where in the generation process the refiner will start to be used.", + "0 means the refiner will be used for the entire generation process, 0.8 means the refiner will be used for the last 20% of the generation process." + ] + }, + "refinerSteps": { + "heading": "Steps", + "paragraphs": [ + "Number of steps that will be performed during the refiner portion of the generation process.", + "Similar to the Generation Steps." + ] + }, + "refinerCfgScale": { + "heading": "CFG Scale", + "paragraphs": [ + "Controls how much the prompt influences the generation process.", + "Similar to the Generation CFG Scale." ] }, "scaleBeforeProcessing": { "heading": "Scale Before Processing", "paragraphs": [ - "Scales the selected area to the size best suited for the model before the image generation process." + "“Auto” scales the selected area to the size best suited for the model before the image generation process.", + "“Manual” allows you to choose the width and height the selected area will be scaled to before the image generation process." ] + }, + "seamlessTilingXAxis": { + "heading": "Seamless Tiling X Axis", + "paragraphs": ["Seamlessly tile an image along the horizontal axis."] + }, + "seamlessTilingYAxis": { + "heading": "Seamless Tiling Y Axis", + "paragraphs": ["Seamlessly tile an image along the vertical axis."] } }, "ui": { From e01769294f57930ae57ca1d51781a701f9fadf06 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 19 Feb 2024 19:12:51 +0100 Subject: [PATCH 029/411] translationBot(ui): update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 3 +-- invokeai/frontend/web/public/locales/it.json | 6 ++---- invokeai/frontend/web/public/locales/nl.json | 6 ++---- invokeai/frontend/web/public/locales/ru.json | 6 ++---- invokeai/frontend/web/public/locales/zh_CN.json | 6 ++---- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 44ae981b02..ae3da48518 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -985,8 +985,7 @@ "paramModel": { "heading": "Modell", "paragraphs": [ - "Modell für die Entrauschungsschritte.", - "Verschiedene Modelle werden in der Regel so trainiert, dass sie sich auf die Erzeugung bestimmter Ästhetik und/oder Inhalte spezialisiert." + "Modell für die Entrauschungsschritte." ] }, "paramIterations": { diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 8a427482dc..3d8fc79390 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -1411,8 +1411,7 @@ "clipSkip": { "paragraphs": [ "Scegli quanti livelli del modello CLIP saltare.", - "Alcuni modelli funzionano meglio con determinate impostazioni di CLIP Skip.", - "Un valore più alto in genere produce un'immagine meno dettagliata." + "Alcuni modelli funzionano meglio con determinate impostazioni di CLIP Skip." ] }, "compositingCoherencePass": { @@ -1528,8 +1527,7 @@ "paramModel": { "heading": "Modello", "paragraphs": [ - "Modello utilizzato per i passaggi di riduzione del rumore.", - "Diversi modelli sono generalmente addestrati per specializzarsi nella produzione di particolari risultati e contenuti estetici." + "Modello utilizzato per i passaggi di riduzione del rumore." ] }, "paramDenoisingStrength": { diff --git a/invokeai/frontend/web/public/locales/nl.json b/invokeai/frontend/web/public/locales/nl.json index aaf836604f..c23030bf54 100644 --- a/invokeai/frontend/web/public/locales/nl.json +++ b/invokeai/frontend/web/public/locales/nl.json @@ -1217,16 +1217,14 @@ "clipSkip": { "paragraphs": [ "Kies hoeveel CLIP-modellagen je wilt overslaan.", - "Bepaalde modellen werken beter met bepaalde Overslaan CLIP-instellingen.", - "Een hogere waarde geeft meestal een minder gedetailleerde afbeelding." + "Bepaalde modellen werken beter met bepaalde Overslaan CLIP-instellingen." ], "heading": "Overslaan CLIP" }, "paramModel": { "heading": "Model", "paragraphs": [ - "Model gebruikt voor de ontruisingsstappen.", - "Verschillende modellen zijn meestal getraind om zich te specialiseren in het maken van bepaalde esthetische resultaten en materiaal." + "Model gebruikt voor de ontruisingsstappen." ] }, "compositingCoherencePass": { diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index 18bfad7f02..af640e538a 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -1353,16 +1353,14 @@ "clipSkip": { "paragraphs": [ "Выберите, сколько слоев модели CLIP нужно пропустить.", - "Некоторые модели работают лучше с определенными настройками пропуска CLIP.", - "Более высокое значение обычно приводит к менее детализированному изображению." + "Некоторые модели работают лучше с определенными настройками пропуска CLIP." ], "heading": "CLIP пропуск" }, "paramModel": { "heading": "Модель", "paragraphs": [ - "Модель, используемая для шагов шумоподавления.", - "Различные модели обычно обучаются, чтобы специализироваться на достижении определенных эстетических результатов и содержания." + "Модель, используемая для шагов шумоподавления." ] }, "compositingCoherencePass": { diff --git a/invokeai/frontend/web/public/locales/zh_CN.json b/invokeai/frontend/web/public/locales/zh_CN.json index 65ebc7f01a..3e4319fef8 100644 --- a/invokeai/frontend/web/public/locales/zh_CN.json +++ b/invokeai/frontend/web/public/locales/zh_CN.json @@ -1444,16 +1444,14 @@ "clipSkip": { "paragraphs": [ "选择要跳过 CLIP 模型多少层。", - "部分模型跳过特定数值的层时效果会更好。", - "较高的数值通常会导致图像细节更少。" + "部分模型跳过特定数值的层时效果会更好。" ], "heading": "CLIP 跳过层" }, "paramModel": { "heading": "模型", "paragraphs": [ - "用于去噪过程的模型。", - "不同的模型一般会通过接受训练来专门产生特定的美学内容和结果。" + "用于去噪过程的模型。" ] }, "paramIterations": { From 01a6378dc113927877157f867ec5151b734dc2b4 Mon Sep 17 00:00:00 2001 From: B N Date: Thu, 22 Feb 2024 19:02:03 +0100 Subject: [PATCH 030/411] translationBot(ui): update translation (German) Currently translated at 78.8% (1159 of 1470 strings) Co-authored-by: B N Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 40 +++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index ae3da48518..789cedfda8 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -601,7 +601,7 @@ "setToOptimalSize": "Optimiere Größe für Modell" }, "settings": { - "displayInProgress": "Bilder in Bearbeitung anzeigen", + "displayInProgress": "Zwischenbilder anzeigen", "saveSteps": "Speichern der Bilder alle n Schritte", "confirmOnDelete": "Bestätigen beim Löschen", "displayHelpIcons": "Hilfesymbole anzeigen", @@ -614,9 +614,34 @@ "useSlidersForAll": "Schieberegler für alle Optionen verwenden", "showAdvancedOptions": "Erweiterte Optionen anzeigen", "alternateCanvasLayout": "Alternatives Leinwand-Layout", - "clearIntermediatesDesc1": "Das Löschen der Zwischenprodukte setzt Leinwand und ControlNet zurück.", + "clearIntermediatesDesc1": "Das Löschen der Zwischenbilder setzt Leinwand und ControlNet zurück.", "favoriteSchedulers": "Lieblings-Planer", - "favoriteSchedulersPlaceholder": "Keine Planer favorisiert" + "favoriteSchedulersPlaceholder": "Keine Planer favorisiert", + "generation": "Erzeugung", + "enableInformationalPopovers": "Info-Popouts anzeigen", + "shouldLogToConsole": "Konsole loggen", + "showProgressInViewer": "Zwischenbilder im Viewer anzeigen", + "clearIntermediatesDesc3": "Ihre Bilder werden nicht gelöscht.", + "clearIntermediatesWithCount_one": "Lösche {{count}} Zwischenbilder", + "clearIntermediatesWithCount_other": "Lösche {{count}} Zwischenbilder", + "reloadingIn": "Neuladen in", + "enableNodesEditor": "Nodes Editor aktivieren", + "autoChangeDimensions": "Breite/Höhe auf Modellstandard setzen", + "experimental": "Experimentell", + "intermediatesCleared_one": "{{count}} Zwischenbilder gelöscht", + "intermediatesCleared_other": "{{count}} Zwischenbilder gelöscht", + "enableInvisibleWatermark": "Unsichtbares Wasserzeichen aktivieren", + "general": "Allgemein", + "consoleLogLevel": "Protokollierungsstufe", + "clearIntermediatesDisabled": "Warteschlange muss leer sein, um Zwischenbilder zu löschen", + "developer": "Entwickler", + "antialiasProgressImages": "Zwischenbilder mit Anti-Alias", + "beta": "Beta", + "ui": "Benutzeroberfläche", + "clearIntermediatesDesc2": "Zwischenbilder sind Nebenprodukte der Erstellung. Sie zu löschen macht Festplattenspeicher frei.", + "clearIntermediates": "Zwischenbilder löschen", + "intermediatesClearedFailed": "Problem beim Löschen der Zwischenbilder", + "enableNSFWChecker": "Auf unangemessene Inhalte prüfen" }, "toast": { "tempFoldersEmptied": "Temp-Ordner geleert", @@ -661,7 +686,9 @@ "problemCopyingCanvas": "Problem beim Kopieren der Leinwand", "problemCopyingCanvasDesc": "Kann Basis-Layer nicht exportieren", "problemDownloadingCanvas": "Problem beim Herunterladen der Leinwand", - "setAsCanvasInitialImage": "Als Ausgangsbild gesetzt" + "setAsCanvasInitialImage": "Als Ausgangsbild gesetzt", + "addedToBoard": "Dem Board hinzugefügt", + "loadedWithWarnings": "Workflow mit Warnungen geladen" }, "tooltip": { "feature": { @@ -1397,7 +1424,10 @@ }, "sdxl": { "concatPromptStyle": "Verknüpfen von Prompt & Stil", - "scheduler": "Planer" + "scheduler": "Planer", + "steps": "Schritte", + "useRefiner": "Refiner verwenden", + "selectAModel": "Modell auswählen" }, "dynamicPrompts": { "showDynamicPrompts": "Dynamische Prompts anzeigen" From 228f1d7f629bab0d041adf3c2f9eba2982623f3d Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Thu, 22 Feb 2024 19:02:03 +0100 Subject: [PATCH 031/411] translationBot(ui): update translation (Italian) Currently translated at 95.6% (1406 of 1470 strings) translationBot(ui): update translation (Italian) Currently translated at 93.9% (1381 of 1470 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 100 +++++++++++++++---- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 3d8fc79390..406b16cf49 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -1379,7 +1379,8 @@ "popovers": { "paramScheduler": { "paragraphs": [ - "Il campionatore definisce come aggiungere in modo iterativo il rumore a un'immagine o come aggiornare un campione in base all'output di un modello." + "Il campionatore utilizzato durante il processo di generazione.", + "Ciascun campionatore definisce come aggiungere in modo iterativo il rumore a un'immagine o come aggiornare un campione in base all'output di un modello." ], "heading": "Campionatore" }, @@ -1392,8 +1393,8 @@ "compositingCoherenceSteps": { "heading": "Passi", "paragraphs": [ - "Numero di passi di riduzione del rumore utilizzati nel Passaggio di Coerenza.", - "Uguale al parametro principale Passi." + "Numero di passi utilizzati nel Passaggio di Coerenza.", + "Simile ai passi di generazione." ] }, "compositingBlur": { @@ -1405,7 +1406,7 @@ "compositingCoherenceMode": { "heading": "Modalità", "paragraphs": [ - "La modalità del Passaggio di Coerenza." + "Metodo utilizzato per creare un'immagine coerente con l'area mascherata appena generata." ] }, "clipSkip": { @@ -1423,8 +1424,8 @@ "compositingStrength": { "heading": "Forza", "paragraphs": [ - "Intensità di riduzione del rumore per il passaggio di coerenza.", - "Uguale al parametro intensità di riduzione del rumore da immagine a immagine." + "Quantità di rumore aggiunta per il Passaggio di Coerenza.", + "Simile alla forza di riduzione del rumore." ] }, "paramNegativeConditioning": { @@ -1450,8 +1451,8 @@ "controlNetBeginEnd": { "heading": "Percentuale passi Inizio / Fine", "paragraphs": [ - "A quali passi del processo di rimozione del rumore verrà applicato ControlNet.", - "I ControlNet applicati all'inizio del processo guidano la composizione, mentre i ControlNet applicati alla fine guidano i dettagli." + "La parte del processo di rimozione del rumore in cui verrà applicato l'adattatore di controllo.", + "In genere, gli adattatori di controllo applicati all'inizio del processo guidano la composizione, mentre quelli applicati alla fine guidano i dettagli." ] }, "noiseUseCPU": { @@ -1464,7 +1465,7 @@ }, "scaleBeforeProcessing": { "paragraphs": [ - "Ridimensiona l'area selezionata alla dimensione più adatta al modello prima del processo di generazione dell'immagine." + "\"Auto\" ridimensiona l'area selezionata alla dimensione più adatta al modello prima del processo di generazione dell'immagine." ], "heading": "Scala prima dell'elaborazione" }, @@ -1499,20 +1500,21 @@ "paramVAEPrecision": { "heading": "Precisione VAE", "paragraphs": [ - "La precisione utilizzata durante la codifica e decodifica VAE. FP16/mezza precisione è più efficiente, a scapito di minori variazioni dell'immagine." + "La precisione utilizzata durante la codifica e decodifica VAE.", + "Fp16/Mezza precisione è più efficiente, a scapito di minori variazioni dell'immagine." ] }, "paramSeed": { "paragraphs": [ "Controlla il rumore iniziale utilizzato per la generazione.", - "Disabilita seme \"Casuale\" per produrre risultati identici con le stesse impostazioni di generazione." + "Disabilita l'opzione \"Casuale\" per produrre risultati identici con le stesse impostazioni di generazione." ], "heading": "Seme" }, "controlNetResizeMode": { "heading": "Modalità ridimensionamento", "paragraphs": [ - "Come l'immagine ControlNet verrà adattata alle dimensioni di output dell'immagine." + "Metodo per adattare le dimensioni dell'immagine in ingresso dell'adattatore di controllo alle dimensioni della generazione di output." ] }, "dynamicPromptsSeedBehaviour": { @@ -1527,7 +1529,7 @@ "paramModel": { "heading": "Modello", "paragraphs": [ - "Modello utilizzato per i passaggi di riduzione del rumore." + "Modello utilizzato per la generazione. Diversi modelli vengono addestrati per specializzarsi nella produzione di risultati e contenuti estetici diversi." ] }, "paramDenoisingStrength": { @@ -1545,25 +1547,26 @@ }, "infillMethod": { "paragraphs": [ - "Metodo per riempire l'area selezionata." + "Metodo di riempimento durante il processo di Outpainting o Inpainting." ], "heading": "Metodo di riempimento" }, "controlNetWeight": { "heading": "Peso", "paragraphs": [ - "Quanto forte sarà l'impatto di ControlNet sull'immagine generata." + "Peso dell'adattatore di controllo. Un peso maggiore porterà a impatti maggiori sull'immagine finale." ] }, "paramCFGScale": { "heading": "Scala CFG", "paragraphs": [ - "Controlla quanto il tuo prompt influenza il processo di generazione." + "Controlla quanto il prompt influenza il processo di generazione.", + "Valori elevati della scala CFG possono provocare una saturazione eccessiva e distorsioni nei risultati della generazione. " ] }, "controlNetControlMode": { "paragraphs": [ - "Attribuisce più peso al prompt o a ControlNet." + "Attribuisce più peso al prompt oppure a ControlNet." ], "heading": "Modalità di controllo" }, @@ -1575,9 +1578,9 @@ ] }, "lora": { - "heading": "Peso LoRA", + "heading": "LoRA", "paragraphs": [ - "Un peso LoRA più elevato porterà a impatti maggiori sull'immagine finale." + "Modelli leggeri utilizzati insieme ai modelli base." ] }, "controlNet": { @@ -1589,8 +1592,65 @@ "paramCFGRescaleMultiplier": { "heading": "Moltiplicatore di riscala CFG", "paragraphs": [ - "Moltiplicatore di riscala per la guida CFG, utilizzato per modelli addestrati utilizzando SNR a terminale zero (ztsnr). Valore suggerito 0.7." + "Moltiplicatore di riscala per la guida CFG, utilizzato per modelli addestrati utilizzando SNR a terminale zero (ztsnr).", + "Valore suggerito di 0.7 per questi modelli." ] + }, + "controlNetProcessor": { + "heading": "Processore", + "paragraphs": [ + "Metodo di elaborazione dell'immagine di input per guidare il processo di generazione. Processori diversi forniranno effetti o stili diversi nelle immagini generate." + ] + }, + "imageFit": { + "heading": "Adatta l'immagine iniziale alle dimensioni di output", + "paragraphs": [ + "Ridimensiona l'immagine iniziale in base alla larghezza e all'altezza dell'immagine di output. Si consiglia di abilitarlo." + ] + }, + "loraWeight": { + "heading": "Peso", + "paragraphs": [ + "Peso del LoRA. Un peso maggiore comporterà un impatto maggiore sull'immagine finale." + ] + }, + "paramAspect": { + "heading": "Aspetto", + "paragraphs": [ + "Proporzioni dell'immagine generata. La modifica del rapporto aggiornerà di conseguenza la larghezza e l'altezza.", + "\"Ottimizza\" imposterà la larghezza e l'altezza alle dimensioni ottimali per il modello scelto." + ] + }, + "paramHeight": { + "heading": "Altezza", + "paragraphs": [ + "Altezza dell'immagine generata. Deve essere un multiplo di 8." + ] + }, + "paramHrf": { + "heading": "Abilita correzione alta risoluzione", + "paragraphs": [ + "Genera immagini di alta qualità con una risoluzione maggiore di quella ottimale per il modello. Generalmente utilizzato per impedire la duplicazione nell'immagine generata." + ] + }, + "paramUpscaleMethod": { + "heading": "Metodo di ampliamento", + "paragraphs": [ + "Metodo utilizzato per eseguire l'ampliamento dell'immagine per la correzione ad alta risoluzione." + ] + }, + "patchmatchDownScaleSize": { + "heading": "Ridimensiona", + "paragraphs": [ + "Quanto ridimensionamento avviene prima del riempimento.", + "Un ridimensionamento più elevato migliorerà le prestazioni e ridurrà la qualità." + ] + }, + "paramWidth": { + "paragraphs": [ + "Larghezza dell'immagine generata. Deve essere un multiplo di 8." + ], + "heading": "Larghezza" } }, "sdxl": { From 9986fce1a6e735fd28df6b0d56edc89cce040006 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Thu, 22 Feb 2024 19:02:03 +0100 Subject: [PATCH 032/411] translationBot(ui): update translation (German) Currently translated at 80.0% (1176 of 1470 strings) Co-authored-by: Alexander Eichhorn Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 789cedfda8..3b44c4c62f 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -116,7 +116,8 @@ "saveAs": "Speichern als", "updated": "Aktualisiert", "copy": "Kopieren", - "aboutHeading": "Nutzen Sie Ihre kreative Energie" + "aboutHeading": "Nutzen Sie Ihre kreative Energie", + "toResolve": "Lösen" }, "gallery": { "generations": "Erzeugungen", @@ -906,7 +907,11 @@ "small": "Klein", "base": "Basis", "depthAnything": "Depth Anything", - "depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth-Anything-Technik" + "depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth-Anything-Technik", + "face": "Gesicht", + "body": "Körper", + "hands": "Hände", + "dwOpenpose": "DW Openpose" }, "queue": { "status": "Status", @@ -1329,7 +1334,19 @@ "vaeFieldDescription": "VAE Submodell.", "unknownInput": "Unbekannte Eingabe: {{name}}", "unknownNodeType": "Unbekannter Knotentyp", - "float": "Kommazahlen" + "float": "Kommazahlen", + "latentsPolymorphic": "Latents Polymorph", + "integerPolymorphicDescription": "Eine Sammlung von ganzen Zahlen.", + "integerPolymorphic": "Ganze Zahl Polymorph", + "ipAdapterPolymorphic": "IP-Adapter Polymorph", + "floatPolymorphic": "Fließkommazahl Polymorph", + "enumDescription": "Aufzählungen sind Werte, die eine von mehreren Optionen sein können.", + "floatCollection": "Fließkommazahl Sammlung", + "enum": "Aufzählung", + "floatPolymorphicDescription": "Eine Sammlung von Fließkommazahlen", + "fullyContainNodes": "Vollständig ausgewählte Nodes auswählen", + "editMode": "Im Workflow-Editor bearbeiten", + "floatCollectionDescription": "Eine Sammlung von Fließkommazahlen" }, "hrf": { "enableHrf": "Korrektur für hohe Auflösungen", From 34b0ea20dc7741012a32d72d1b9f1c577d705081 Mon Sep 17 00:00:00 2001 From: B N Date: Sun, 25 Feb 2024 12:01:59 +0100 Subject: [PATCH 033/411] translationBot(ui): update translation (German) Currently translated at 80.3% (1181 of 1470 strings) translationBot(ui): update translation (German) Currently translated at 80.1% (1178 of 1470 strings) Co-authored-by: B N Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 3b44c4c62f..b0a3f799b3 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -599,7 +599,10 @@ "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (kann zu groß sein)", "lockAspectRatio": "Seitenverhältnis sperren", "swapDimensions": "Seitenverhältnis umkehren", - "setToOptimalSize": "Optimiere Größe für Modell" + "setToOptimalSize": "Optimiere Größe für Modell", + "useSize": "Maße übernehmen", + "remixImage": "Remix des Bilds erstellen", + "imageActions": "Weitere Bildaktionen" }, "settings": { "displayInProgress": "Zwischenbilder anzeigen", @@ -858,7 +861,7 @@ "colorMap": "Farbe", "lowThreshold": "Niedrige Schwelle", "highThreshold": "Hohe Schwelle", - "toggleControlNet": "Schalten ControlNet um", + "toggleControlNet": "Dieses ControlNet ein- oder ausschalten", "delete": "Löschen", "controlAdapter_one": "Control Adapter", "controlAdapter_other": "Control Adapter", @@ -911,14 +914,15 @@ "face": "Gesicht", "body": "Körper", "hands": "Hände", - "dwOpenpose": "DW Openpose" + "dwOpenpose": "DW Openpose", + "dwOpenposeDescription": "Posenschätzung mit DW Openpose" }, "queue": { "status": "Status", "cancelTooltip": "Aktuellen Aufgabe abbrechen", "queueEmpty": "Warteschlange leer", "in_progress": "In Arbeit", - "queueFront": "An den Anfang der Warteschlange tun", + "queueFront": "Am Anfang der Warteschlange einreihen", "completed": "Fertig", "queueBack": "In die Warteschlange", "clearFailed": "Probleme beim leeren der Warteschlange", @@ -1131,6 +1135,11 @@ "\"Planer\" definiert, wie iterativ Rauschen zu einem Bild hinzugefügt wird, oder wie ein Sample bei der Ausgabe eines Modells aktualisiert wird." ], "heading": "Planer" + }, + "imageFit": { + "paragraphs": [ + "Reduziert das Ausgangsbild auf die Breite und Höhe des Ausgangsbildes. Empfohlen zu aktivieren." + ] } }, "ui": { From 2ec6b51d8b680b341df180908ed43818032c5cd3 Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Sun, 25 Feb 2024 12:01:59 +0100 Subject: [PATCH 034/411] translationBot(ui): update translation (Italian) Currently translated at 97.2% (1430 of 1470 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 66 +++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 406b16cf49..9cd46874c1 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -934,7 +934,7 @@ "executionStateCompleted": "Completato", "boardFieldDescription": "Una bacheca della galleria", "addNodeToolTip": "Aggiungi nodo (Shift+A, Space)", - "sDXLRefinerModelField": "Modello Refiner", + "sDXLRefinerModelField": "Modello Affinatore", "problemReadingMetadata": "Problema durante la lettura dei metadati dall'immagine", "colorCodeEdgesHelp": "Bordi con codice colore in base ai campi collegati", "animatedEdges": "Bordi animati", @@ -1329,7 +1329,7 @@ "noModelsAvailable": "Nessun modello disponibile", "selectModel": "Seleziona un modello", "selectLoRA": "Seleziona un LoRA", - "noRefinerModelsInstalled": "Nessun modello SDXL Refiner installato", + "noRefinerModelsInstalled": "Nessun modello affinatore SDXL installato", "noLoRAsInstalled": "Nessun LoRA installato", "esrganModel": "Modello ESRGAN", "addLora": "Aggiungi LoRA", @@ -1465,7 +1465,8 @@ }, "scaleBeforeProcessing": { "paragraphs": [ - "\"Auto\" ridimensiona l'area selezionata alla dimensione più adatta al modello prima del processo di generazione dell'immagine." + "\"Auto\" ridimensiona l'area selezionata alla dimensione più adatta al modello prima del processo di generazione dell'immagine.", + "\"Manuale\" consente di scegliere la larghezza e l'altezza a cui verrà ridimensionata l'area selezionata prima del processo di generazione dell'immagine." ], "heading": "Scala prima dell'elaborazione" }, @@ -1651,6 +1652,65 @@ "Larghezza dell'immagine generata. Deve essere un multiplo di 8." ], "heading": "Larghezza" + }, + "refinerModel": { + "heading": "Modello Affinatore", + "paragraphs": [ + "Modello utilizzato durante la parte di affinamento del processo di generazione.", + "Simile al modello di generazione." + ] + }, + "refinerNegativeAestheticScore": { + "paragraphs": [ + "Valuta le generazioni in modo che siano più simili alle immagini con un punteggio estetico basso, in base ai dati di addestramento." + ], + "heading": "Punteggio estetico negativo" + }, + "refinerScheduler": { + "paragraphs": [ + "Campionatore utilizzato durante la parte di affinamento del processo di generazione.", + "Simile al campionatore di generazione." + ], + "heading": "Campionatore" + }, + "refinerStart": { + "heading": "Inizio affinamento", + "paragraphs": [ + "A che punto nel processo di generazione inizierà ad essere utilizzato l'affinatore.", + "0 significa che l'affinatore verrà utilizzato per l'intero processo di generazione, 0.8 significa che l'affinatore verrà utilizzato per l'ultimo 20% del processo di generazione." + ] + }, + "refinerSteps": { + "heading": "Passi", + "paragraphs": [ + "Numero di passi che verranno eseguiti durante la parte di affinamento del processo di generazione.", + "Simile ai passi di generazione." + ] + }, + "refinerCfgScale": { + "heading": "Scala CFG", + "paragraphs": [ + "Controlla quanto il prompt influenza il processo di generazione.", + "Simile alla scala CFG di generazione." + ] + }, + "seamlessTilingXAxis": { + "heading": "Asse X di piastrellatura senza cuciture", + "paragraphs": [ + "Affianca senza soluzione di continuità un'immagine lungo l'asse orizzontale." + ] + }, + "seamlessTilingYAxis": { + "heading": "Asse Y di piastrellatura senza cuciture", + "paragraphs": [ + "Affianca senza soluzione di continuità un'immagine lungo l'asse verticale." + ] + }, + "refinerPositiveAestheticScore": { + "heading": "Punteggio estetico positivo", + "paragraphs": [ + "Valuta le generazioni in modo che siano più simili alle immagini con un punteggio estetico elevato, in base ai dati di addestramento." + ] } }, "sdxl": { From 3a09bceea42c161a64272c246976840989cf0fc3 Mon Sep 17 00:00:00 2001 From: skunkworxdark Date: Mon, 26 Feb 2024 18:42:25 +0000 Subject: [PATCH 035/411] Update communityNodes.md Updated description of metadata nodes --- docs/nodes/communityNodes.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/nodes/communityNodes.md b/docs/nodes/communityNodes.md index 33dddad162..296fbb7ee6 100644 --- a/docs/nodes/communityNodes.md +++ b/docs/nodes/communityNodes.md @@ -354,12 +354,21 @@ See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/mai **Description:** A set of nodes for Metadata. Collect Metadata from within an `iterate` node & extract metadata from an image. -- `Metadata Item Linked` - Allows collecting of metadata while within an iterate node with no need for a collect node or conversion to metadata node. -- `Metadata From Image` - Provides Metadata from an image. -- `Metadata To String` - Extracts a String value of a label from metadata. -- `Metadata To Integer` - Extracts an Integer value of a label from metadata. -- `Metadata To Float` - Extracts a Float value of a label from metadata. -- `Metadata To Scheduler` - Extracts a Scheduler value of a label from metadata. +- `Metadata Item Linked` - Allows collecting of metadata while within an iterate node with no need for a collect node or conversion to metadata node +- `Metadata From Image` - Provides Metadata from an image +- `Metadata To String` - Extracts a String value of a label from metadata +- `Metadata To Integer` - Extracts an Integer value of a label from metadata +- `Metadata To Float` - Extracts a Float value of a label from metadata +- `Metadata To Scheduler` - Extracts a Scheduler value of a label from metadata +- `Metadata To Bool` - Extracts Bool types from metadata +- `Metadata To Model` - Extracts model types from metadata +- `Metadata To SDXL Model` - Extracts SDXL model types from metadata +- `Metadata To LoRAs` - Extracts Loras from metadata. +- `Metadata To SDXL LoRAs` - Extracts SDXL Loras from metadata +- `Metadata To ControlNets` - Extracts ControNets from metadata +- `Metadata To IP-Adapters` - Extracts IP-Adapters from metadata +- `Metadata To T2I-Adapters` - Extracts T2I-Adapters from metadata +- `Denoise Latents + Metadata` - This is an inherited version of the existing `Denoise Latents` node but with a metadata input and output. **Node Link:** https://github.com/skunkworxdark/metadata-linked-nodes From 1a3ffb6e94c0c399492a070fa22e0ffc1f8c79d4 Mon Sep 17 00:00:00 2001 From: Alexander Eichhorn Date: Wed, 28 Feb 2024 16:02:04 +0100 Subject: [PATCH 036/411] translationBot(ui): update translation (German) Currently translated at 80.4% (1183 of 1470 strings) Co-authored-by: Alexander Eichhorn Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index b0a3f799b3..65aa7b2a7a 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -1007,7 +1007,8 @@ "workflow": "Workflow", "scheduler": "Planer", "noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden", - "recallParameters": "Parameter wiederherstellen" + "recallParameters": "Parameter wiederherstellen", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)" }, "popovers": { "noiseUseCPU": { From 9a8a9c5848834f808b3fe8dc3e28bb950eb6c169 Mon Sep 17 00:00:00 2001 From: Samantha Morello Date: Wed, 28 Feb 2024 16:02:06 +0100 Subject: [PATCH 037/411] translationBot(ui): update translation (Italian) Currently translated at 98.0% (1441 of 1470 strings) Co-authored-by: Samantha Morello Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index 9cd46874c1..1a55f967f7 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -47,7 +47,7 @@ "statusModelConverted": "Modello Convertito", "statusConvertingModel": "Conversione Modello", "loading": "Caricamento in corso", - "loadingInvokeAI": "Caricamento Invoke AI", + "loadingInvokeAI": "Caricamento di Invoke AI", "postprocessing": "Post Elaborazione", "txt2img": "Testo a Immagine", "accept": "Accetta", @@ -61,7 +61,7 @@ "imagePrompt": "Prompt Immagine", "darkMode": "Modalità scura", "batch": "Gestione Lotto", - "modelManager": "Gestore modello", + "modelManager": "Gestore Modelli", "communityLabel": "Comunità", "nodeEditor": "Editor dei nodi", "statusProcessing": "Elaborazione in corso", @@ -81,7 +81,7 @@ "error": "Errore", "installed": "Installato", "template": "Schema", - "outputs": "Uscite", + "outputs": "Risultati", "data": "Dati", "somethingWentWrong": "Qualcosa è andato storto", "copyError": "$t(gallery.copy) Errore", @@ -93,7 +93,7 @@ "created": "Creato", "prevPage": "Pagina precedente", "delete": "Elimina", - "orderBy": "Ordinato per", + "orderBy": "Ordina per", "nextPage": "Pagina successiva", "saveAs": "Salva come", "unsaved": "Non salvato", @@ -109,7 +109,12 @@ "green": "Verde", "blue": "Blu", "alpha": "Alfa", - "copy": "Copia" + "copy": "Copia", + "on": "Attivato", + "checkpoint": "Checkpoint", + "safetensors": "Safetensors", + "ai": "ia", + "file": "File" }, "gallery": { "generations": "Generazioni", @@ -1249,7 +1254,12 @@ "dwOpenposeDescription": "Stima della posa umana utilizzando DW Openpose", "face": "Viso", "body": "Corpo", - "hands": "Mani" + "hands": "Mani", + "lineartAnime": "Linea Anime", + "base": "Base", + "lineart": "Linea", + "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", + "mediapipeFace": "Mediapipe Volto" }, "queue": { "queueFront": "Aggiungi all'inizio della coda", @@ -1758,7 +1768,8 @@ "steps": "Passi", "scheduler": "Campionatore", "recallParameters": "Richiama i parametri", - "noRecallParameters": "Nessun parametro da richiamare trovato" + "noRecallParameters": "Nessun parametro da richiamare trovato", + "cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)" }, "hrf": { "enableHrf": "Abilita Correzione Alta Risoluzione", From 63ab5ff5a29e5b46664e2642e636ca34b26a6192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B0=D1=81=D1=8F=D0=BD=D0=B0=D1=82=D0=BE=D1=80?= Date: Wed, 28 Feb 2024 16:02:07 +0100 Subject: [PATCH 038/411] translationBot(ui): update translation (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 98.3% (1398 of 1422 strings) Co-authored-by: Васянатор Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/ru.json | 93 +++++++++++++++----- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/public/locales/ru.json b/invokeai/frontend/web/public/locales/ru.json index af640e538a..8468554bab 100644 --- a/invokeai/frontend/web/public/locales/ru.json +++ b/invokeai/frontend/web/public/locales/ru.json @@ -108,7 +108,16 @@ "preferencesLabel": "Предпочтения", "or": "или", "advancedOptions": "Расширенные настройки", - "free": "Свободно" + "free": "Свободно", + "aboutHeading": "Владей своей творческой силой", + "red": "Красный", + "green": "Зеленый", + "blue": "Синий", + "alpha": "Альфа", + "toResolve": "Чтоб решить", + "copy": "Копировать", + "localSystem": "Локальная система", + "aboutDesc": "Используя Invoke для работы? Проверьте это:" }, "gallery": { "generations": "Генерации", @@ -152,17 +161,17 @@ }, "hotkeys": { "keyboardShortcuts": "Горячие клавиши", - "appHotkeys": "Горячие клавиши приложения", - "generalHotkeys": "Общие горячие клавиши", - "galleryHotkeys": "Горячие клавиши галереи", - "unifiedCanvasHotkeys": "Горячие клавиши Единого холста", + "appHotkeys": "Приложение", + "generalHotkeys": "Общее", + "galleryHotkeys": "Галлерея", + "unifiedCanvasHotkeys": "Единый холст", "invoke": { "title": "Invoke", "desc": "Сгенерировать изображение" }, "cancel": { "title": "Отменить", - "desc": "Отменить генерацию изображения" + "desc": "Отменить текущий элемент" }, "focusPrompt": { "title": "Переключиться на ввод запроса", @@ -352,7 +361,7 @@ "desc": "Открывает меню добавления узла", "title": "Добавление узлов" }, - "nodesHotkeys": "Горячие клавиши узлов", + "nodesHotkeys": "Узлы", "cancelAndClear": { "desc": "Отмена текущего элемента очереди и очистка всех ожидающих элементов", "title": "Отменить и очистить" @@ -367,7 +376,11 @@ "desc": "Открытие и закрытие панели опций и галереи", "title": "Переключить опции и галерею" }, - "clearSearch": "Очистить поиск" + "clearSearch": "Очистить поиск", + "remixImage": { + "desc": "Используйте все параметры, кроме сида из текущего изображения", + "title": "Ремикс изображения" + } }, "modelManager": { "modelManager": "Менеджер моделей", @@ -512,7 +525,8 @@ "modelType": "Тип модели", "customConfigFileLocation": "Расположение пользовательского файла конфигурации", "vaePrecision": "Точность VAE", - "noModelSelected": "Модель не выбрана" + "noModelSelected": "Модель не выбрана", + "configFile": "Файл конфигурации" }, "parameters": { "images": "Изображения", @@ -583,8 +597,8 @@ "copyImage": "Скопировать изображение", "showPreview": "Показать предпросмотр", "noiseSettings": "Шум", - "seamlessXAxis": "Горизонтальная", - "seamlessYAxis": "Вертикальная", + "seamlessXAxis": "Бесшовность по оси X", + "seamlessYAxis": "Бесшовность по оси Y", "scheduler": "Планировщик", "boundingBoxWidth": "Ширина ограничивающей рамки", "boundingBoxHeight": "Высота ограничивающей рамки", @@ -612,7 +626,7 @@ "noControlImageForControlAdapter": "Адаптер контроля #{{number}} не имеет изображения", "noModelForControlAdapter": "Не выбрана модель адаптера контроля #{{number}}.", "unableToInvoke": "Невозможно вызвать", - "incompatibleBaseModelForControlAdapter": "Модель контрольного адаптера №{{number}} недействительна для основной модели.", + "incompatibleBaseModelForControlAdapter": "Адаптер контроля №{{number}} несовместим с основной моделью.", "systemDisconnected": "Система отключена", "missingNodeTemplate": "Отсутствует шаблон узла", "readyToInvoke": "Готово к вызову", @@ -653,7 +667,10 @@ "setToOptimalSize": "Установить оптимальный для модели размер", "setToOptimalSizeTooSmall": "$t(parameters.setToOptimalSize) (может быть слишком маленьким)", "setToOptimalSizeTooLarge": "$t(parameters.setToOptimalSize) (может быть слишком большим)", - "lockAspectRatio": "Заблокировать соотношение" + "lockAspectRatio": "Заблокировать соотношение", + "boxBlur": "Размытие прямоугольника", + "gaussianBlur": "Размытие по Гауссу", + "remixImage": "Ремикс изображения" }, "settings": { "models": "Модели", @@ -787,7 +804,10 @@ "canvasSavedGallery": "Холст сохранен в галерею", "imageUploadFailed": "Не удалось загрузить изображение", "modelAdded": "Добавлена модель: {{modelName}}", - "problemImportingMask": "Проблема с импортом маски" + "problemImportingMask": "Проблема с импортом маски", + "problemDownloadingImage": "Не удается скачать изображение", + "uploadInitialImage": "Загрузить начальное изображение", + "resetInitialImage": "Сбросить начальное изображение" }, "tooltip": { "feature": { @@ -892,7 +912,8 @@ "mode": "Режим", "loadMore": "Загрузить больше", "resetUI": "$t(accessibility.reset) интерфейс", - "createIssue": "Сообщить о проблеме" + "createIssue": "Сообщить о проблеме", + "about": "Об этом" }, "ui": { "showProgressImages": "Показывать промежуточный итог", @@ -1117,7 +1138,18 @@ "unableToParseEdge": "Невозможно разобрать край", "unknownInput": "Неизвестный вход: {{name}}", "oNNXModelFieldDescription": "Поле модели ONNX.", - "imageCollection": "Коллекция изображений" + "imageCollection": "Коллекция изображений", + "newWorkflow": "Новый рабочий процесс", + "newWorkflowDesc": "Создать новый рабочий процесс?", + "clearWorkflow": "Очистить рабочий процесс", + "newWorkflowDesc2": "Текущий рабочий процесс имеет несохраненные изменения.", + "latentsCollection": "Коллекция латентов", + "clearWorkflowDesc": "Очистить этот рабочий процесс и создать новый?", + "clearWorkflowDesc2": "Текущий рабочий процесс имеет несохраненные измерения.", + "reorderLinearView": "Изменить порядок линейного просмотра", + "viewMode": "Использовать в линейном представлении", + "editMode": "Открыть в редакторе узлов", + "resetToDefaultValue": "Сбросить к стандартному значкнию" }, "controlnet": { "amult": "a_mult", @@ -1198,7 +1230,18 @@ "enableIPAdapter": "Включить IP Adapter", "maxFaces": "Макс Лица", "mlsdDescription": "Минималистичный детектор отрезков линии", - "resizeSimple": "Изменить размер (простой)" + "resizeSimple": "Изменить размер (простой)", + "megaControl": "Mega контроль", + "base": "Базовый", + "depthAnything": "Глубина всего", + "depthAnythingDescription": "Создание карты глубины с использованием метода Depth Anything", + "face": "Лицо", + "dwOpenposeDescription": "Оценка позы человека с помощью DW Openpose", + "large": "Большой", + "modelSize": "Размер модели", + "small": "Маленький", + "body": "Тело", + "hands": "Руки" }, "boards": { "autoAddBoard": "Авто добавление Доски", @@ -1281,7 +1324,7 @@ "compositingCoherenceSteps": { "heading": "Шаги", "paragraphs": [ - null, + "Количество шагов снижения шума, используемых при прохождении когерентности.", "То же, что и основной параметр «Шаги»." ] }, @@ -1319,7 +1362,10 @@ ] }, "compositingCoherenceMode": { - "heading": "Режим" + "heading": "Режим", + "paragraphs": [ + "Режим прохождения когерентности." + ] }, "paramSeed": { "paragraphs": [ @@ -1599,7 +1645,7 @@ "openWorkflow": "Открытый рабочий процесс", "clearWorkflowSearchFilter": "Очистить фильтр поиска рабочих процессов", "workflowLibrary": "Библиотека", - "downloadWorkflow": "Скачать рабочий процесс", + "downloadWorkflow": "Сохранить в файл", "noRecentWorkflows": "Нет недавних рабочих процессов", "workflowSaved": "Рабочий процесс сохранен", "workflowIsOpen": "Рабочий процесс открыт", @@ -1612,9 +1658,12 @@ "deleteWorkflow": "Удалить рабочий процесс", "workflows": "Рабочие процессы", "noDescription": "Без описания", - "uploadWorkflow": "Загрузить рабочий процесс", + "uploadWorkflow": "Загрузить из файла", "userWorkflows": "Мои рабочие процессы", - "newWorkflowCreated": "Создан новый рабочий процесс" + "newWorkflowCreated": "Создан новый рабочий процесс", + "saveWorkflowToProject": "Сохранить рабочий процесс в проект", + "workflowCleared": "Рабочий процесс очищен", + "noWorkflows": "Нет рабочих процессов" }, "embedding": { "noEmbeddingsLoaded": "встраивания не загружены", From 992b02aa65ac9ae1e7554ef80f51d18a17d569f9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:23:06 +1100 Subject: [PATCH 039/411] tidy(nodes): move all field things to fields.py Unfortunately, this is necessary to prevent circular imports at runtime. --- invokeai/app/api/routers/images.py | 2 +- invokeai/app/api_app.py | 3 +- invokeai/app/invocations/baseinvocation.py | 453 +--------------- invokeai/app/invocations/collections.py | 3 +- invokeai/app/invocations/compel.py | 6 +- .../controlnet_image_processors.py | 6 +- invokeai/app/invocations/cv.py | 3 +- invokeai/app/invocations/facetools.py | 4 +- invokeai/app/invocations/fields.py | 501 ++++++++++++++++++ invokeai/app/invocations/image.py | 5 +- invokeai/app/invocations/infill.py | 3 +- invokeai/app/invocations/ip_adapter.py | 5 +- invokeai/app/invocations/latent.py | 7 +- invokeai/app/invocations/math.py | 4 +- invokeai/app/invocations/metadata.py | 6 +- invokeai/app/invocations/model.py | 5 +- invokeai/app/invocations/noise.py | 4 +- invokeai/app/invocations/onnx.py | 16 +- invokeai/app/invocations/param_easing.py | 3 +- invokeai/app/invocations/primitives.py | 6 +- invokeai/app/invocations/prompt.py | 3 +- invokeai/app/invocations/sdxl.py | 6 +- invokeai/app/invocations/strings.py | 4 +- invokeai/app/invocations/t2i_adapter.py | 5 +- invokeai/app/invocations/tiles.py | 5 +- invokeai/app/invocations/upscale.py | 3 +- .../services/image_files/image_files_base.py | 2 +- .../services/image_files/image_files_disk.py | 2 +- .../image_records/image_records_base.py | 2 +- .../image_records/image_records_sqlite.py | 2 +- invokeai/app/services/images/images_base.py | 2 +- .../app/services/images/images_default.py | 2 +- invokeai/app/services/shared/graph.py | 5 +- invokeai/app/shared/fields.py | 67 --- invokeai/app/shared/models.py | 2 +- tests/aa_nodes/test_nodes.py | 3 +- 36 files changed, 552 insertions(+), 608 deletions(-) create mode 100644 invokeai/app/invocations/fields.py delete mode 100644 invokeai/app/shared/fields.py diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 125896b8d3..cc60ad1be8 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -8,7 +8,7 @@ from fastapi.routing import APIRouter from PIL import Image from pydantic import BaseModel, Field, ValidationError -from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator +from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 6294083d0e..f48074de7c 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -6,6 +6,7 @@ import sys from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles from invokeai.version.invokeai_version import __version__ +from .invocations.fields import InputFieldJSONSchemaExtra, OutputFieldJSONSchemaExtra from .services.config import InvokeAIAppConfig app_config = InvokeAIAppConfig.get_config() @@ -57,8 +58,6 @@ if True: # hack to make flake8 happy with imports coming after setting up the c from .api.sockets import SocketIO from .invocations.baseinvocation import ( BaseInvocation, - InputFieldJSONSchemaExtra, - OutputFieldJSONSchemaExtra, UIConfigBase, ) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index d9e0c7ba0d..395d5e9870 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -12,10 +12,11 @@ from types import UnionType from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Literal, Optional, Type, TypeVar, Union, cast import semver -from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter, create_model -from pydantic.fields import FieldInfo, _Unset +from pydantic import BaseModel, ConfigDict, Field, create_model +from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined +from invokeai.app.invocations.fields import FieldKind, Input from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.shared.fields import FieldDescriptions @@ -52,393 +53,6 @@ class Classification(str, Enum, metaclass=MetaEnum): Prototype = "prototype" -class Input(str, Enum, metaclass=MetaEnum): - """ - The type of input a field accepts. - - `Input.Direct`: The field must have its value provided directly, when the invocation and field \ - are instantiated. - - `Input.Connection`: The field must have its value provided by a connection. - - `Input.Any`: The field may have its value provided either directly or by a connection. - """ - - Connection = "connection" - Direct = "direct" - Any = "any" - - -class FieldKind(str, Enum, metaclass=MetaEnum): - """ - The kind of field. - - `Input`: An input field on a node. - - `Output`: An output field on a node. - - `Internal`: A field which is treated as an input, but cannot be used in node definitions. Metadata is - one example. It is provided to nodes via the WithMetadata class, and we want to reserve the field name - "metadata" for this on all nodes. `FieldKind` is used to short-circuit the field name validation logic, - allowing "metadata" for that field. - - `NodeAttribute`: The field is a node attribute. These are fields which are not inputs or outputs, - but which are used to store information about the node. For example, the `id` and `type` fields are node - attributes. - - The presence of this in `json_schema_extra["field_kind"]` is used when initializing node schemas on app - startup, and when generating the OpenAPI schema for the workflow editor. - """ - - Input = "input" - Output = "output" - Internal = "internal" - NodeAttribute = "node_attribute" - - -class UIType(str, Enum, metaclass=MetaEnum): - """ - Type hints for the UI for situations in which the field type is not enough to infer the correct UI type. - - - Model Fields - The most common node-author-facing use will be for model fields. Internally, there is no difference - between SD-1, SD-2 and SDXL model fields - they all use the class `MainModelField`. To ensure the - base-model-specific UI is rendered, use e.g. `ui_type=UIType.SDXLMainModelField` to indicate that - the field is an SDXL main model field. - - - Any Field - We cannot infer the usage of `typing.Any` via schema parsing, so you *must* use `ui_type=UIType.Any` to - indicate that the field accepts any type. Use with caution. This cannot be used on outputs. - - - Scheduler Field - Special handling in the UI is needed for this field, which otherwise would be parsed as a plain enum field. - - - Internal Fields - Similar to the Any Field, the `collect` and `iterate` nodes make use of `typing.Any`. To facilitate - handling these types in the client, we use `UIType._Collection` and `UIType._CollectionItem`. These - should not be used by node authors. - - - DEPRECATED Fields - These types are deprecated and should not be used by node authors. A warning will be logged if one is - used, and the type will be ignored. They are included here for backwards compatibility. - """ - - # region Model Field Types - SDXLMainModel = "SDXLMainModelField" - SDXLRefinerModel = "SDXLRefinerModelField" - ONNXModel = "ONNXModelField" - VaeModel = "VAEModelField" - LoRAModel = "LoRAModelField" - ControlNetModel = "ControlNetModelField" - IPAdapterModel = "IPAdapterModelField" - # endregion - - # region Misc Field Types - Scheduler = "SchedulerField" - Any = "AnyField" - # endregion - - # region Internal Field Types - _Collection = "CollectionField" - _CollectionItem = "CollectionItemField" - # endregion - - # region DEPRECATED - Boolean = "DEPRECATED_Boolean" - Color = "DEPRECATED_Color" - Conditioning = "DEPRECATED_Conditioning" - Control = "DEPRECATED_Control" - Float = "DEPRECATED_Float" - Image = "DEPRECATED_Image" - Integer = "DEPRECATED_Integer" - Latents = "DEPRECATED_Latents" - String = "DEPRECATED_String" - BooleanCollection = "DEPRECATED_BooleanCollection" - ColorCollection = "DEPRECATED_ColorCollection" - ConditioningCollection = "DEPRECATED_ConditioningCollection" - ControlCollection = "DEPRECATED_ControlCollection" - FloatCollection = "DEPRECATED_FloatCollection" - ImageCollection = "DEPRECATED_ImageCollection" - IntegerCollection = "DEPRECATED_IntegerCollection" - LatentsCollection = "DEPRECATED_LatentsCollection" - StringCollection = "DEPRECATED_StringCollection" - BooleanPolymorphic = "DEPRECATED_BooleanPolymorphic" - ColorPolymorphic = "DEPRECATED_ColorPolymorphic" - ConditioningPolymorphic = "DEPRECATED_ConditioningPolymorphic" - ControlPolymorphic = "DEPRECATED_ControlPolymorphic" - FloatPolymorphic = "DEPRECATED_FloatPolymorphic" - ImagePolymorphic = "DEPRECATED_ImagePolymorphic" - IntegerPolymorphic = "DEPRECATED_IntegerPolymorphic" - LatentsPolymorphic = "DEPRECATED_LatentsPolymorphic" - StringPolymorphic = "DEPRECATED_StringPolymorphic" - MainModel = "DEPRECATED_MainModel" - UNet = "DEPRECATED_UNet" - Vae = "DEPRECATED_Vae" - CLIP = "DEPRECATED_CLIP" - Collection = "DEPRECATED_Collection" - CollectionItem = "DEPRECATED_CollectionItem" - Enum = "DEPRECATED_Enum" - WorkflowField = "DEPRECATED_WorkflowField" - IsIntermediate = "DEPRECATED_IsIntermediate" - BoardField = "DEPRECATED_BoardField" - MetadataItem = "DEPRECATED_MetadataItem" - MetadataItemCollection = "DEPRECATED_MetadataItemCollection" - MetadataItemPolymorphic = "DEPRECATED_MetadataItemPolymorphic" - MetadataDict = "DEPRECATED_MetadataDict" - # endregion - - -class UIComponent(str, Enum, metaclass=MetaEnum): - """ - The type of UI component to use for a field, used to override the default components, which are - inferred from the field type. - """ - - None_ = "none" - Textarea = "textarea" - Slider = "slider" - - -class InputFieldJSONSchemaExtra(BaseModel): - """ - Extra attributes to be added to input fields and their OpenAPI schema. Used during graph execution, - and by the workflow editor during schema parsing and UI rendering. - """ - - input: Input - orig_required: bool - field_kind: FieldKind - default: Optional[Any] = None - orig_default: Optional[Any] = None - ui_hidden: bool = False - ui_type: Optional[UIType] = None - ui_component: Optional[UIComponent] = None - ui_order: Optional[int] = None - ui_choice_labels: Optional[dict[str, str]] = None - - model_config = ConfigDict( - validate_assignment=True, - json_schema_serialization_defaults_required=True, - ) - - -class OutputFieldJSONSchemaExtra(BaseModel): - """ - Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor - during schema parsing and UI rendering. - """ - - field_kind: FieldKind - ui_hidden: bool - ui_type: Optional[UIType] - ui_order: Optional[int] - - model_config = ConfigDict( - validate_assignment=True, - json_schema_serialization_defaults_required=True, - ) - - -def InputField( - # copied from pydantic's Field - # TODO: Can we support default_factory? - default: Any = _Unset, - default_factory: Callable[[], Any] | None = _Unset, - title: str | None = _Unset, - description: str | None = _Unset, - pattern: str | None = _Unset, - strict: bool | None = _Unset, - gt: float | None = _Unset, - ge: float | None = _Unset, - lt: float | None = _Unset, - le: float | None = _Unset, - multiple_of: float | None = _Unset, - allow_inf_nan: bool | None = _Unset, - max_digits: int | None = _Unset, - decimal_places: int | None = _Unset, - min_length: int | None = _Unset, - max_length: int | None = _Unset, - # custom - input: Input = Input.Any, - ui_type: Optional[UIType] = None, - ui_component: Optional[UIComponent] = None, - ui_hidden: bool = False, - ui_order: Optional[int] = None, - ui_choice_labels: Optional[dict[str, str]] = None, -) -> Any: - """ - Creates an input field for an invocation. - - This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field) \ - that adds a few extra parameters to support graph execution and the node editor UI. - - :param Input input: [Input.Any] The kind of input this field requires. \ - `Input.Direct` means a value must be provided on instantiation. \ - `Input.Connection` means the value must be provided by a connection. \ - `Input.Any` means either will do. - - :param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \ - In some situations, the field's type is not enough to infer the correct UI type. \ - For example, model selection fields should render a dropdown UI component to select a model. \ - Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \ - `MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \ - `UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field. - - :param UIComponent ui_component: [None] Optionally specifies a specific component to use in the UI. \ - The UI will always render a suitable component, but sometimes you want something different than the default. \ - For example, a `string` field will default to a single-line input, but you may want a multi-line textarea instead. \ - For this case, you could provide `UIComponent.Textarea`. - - :param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. - - :param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. - - :param dict[str, str] ui_choice_labels: [None] Specifies the labels to use for the choices in an enum field. - """ - - json_schema_extra_ = InputFieldJSONSchemaExtra( - input=input, - ui_type=ui_type, - ui_component=ui_component, - ui_hidden=ui_hidden, - ui_order=ui_order, - ui_choice_labels=ui_choice_labels, - field_kind=FieldKind.Input, - orig_required=True, - ) - - """ - There is a conflict between the typing of invocation definitions and the typing of an invocation's - `invoke()` function. - - On instantiation of a node, the invocation definition is used to create the python class. At this time, - any number of fields may be optional, because they may be provided by connections. - - On calling of `invoke()`, however, those fields may be required. - - For example, consider an ResizeImageInvocation with an `image: ImageField` field. - - `image` is required during the call to `invoke()`, but when the python class is instantiated, - the field may not be present. This is fine, because that image field will be provided by a - connection from an ancestor node, which outputs an image. - - This means we want to type the `image` field as optional for the node class definition, but required - for the `invoke()` function. - - If we use `typing.Optional` in the node class definition, the field will be typed as optional in the - `invoke()` method, and we'll have to do a lot of runtime checks to ensure the field is present - or - any static type analysis tools will complain. - - To get around this, in node class definitions, we type all fields correctly for the `invoke()` function, - but secretly make them optional in `InputField()`. We also store the original required bool and/or default - value. When we call `invoke()`, we use this stored information to do an additional check on the class. - """ - - if default_factory is not _Unset and default_factory is not None: - default = default_factory() - logger.warn('"default_factory" is not supported, calling it now to set "default"') - - # These are the args we may wish pass to the pydantic `Field()` function - field_args = { - "default": default, - "title": title, - "description": description, - "pattern": pattern, - "strict": strict, - "gt": gt, - "ge": ge, - "lt": lt, - "le": le, - "multiple_of": multiple_of, - "allow_inf_nan": allow_inf_nan, - "max_digits": max_digits, - "decimal_places": decimal_places, - "min_length": min_length, - "max_length": max_length, - } - - # We only want to pass the args that were provided, otherwise the `Field()`` function won't work as expected - provided_args = {k: v for (k, v) in field_args.items() if v is not PydanticUndefined} - - # Because we are manually making fields optional, we need to store the original required bool for reference later - json_schema_extra_.orig_required = default is PydanticUndefined - - # Make Input.Any and Input.Connection fields optional, providing None as a default if the field doesn't already have one - if input is Input.Any or input is Input.Connection: - default_ = None if default is PydanticUndefined else default - provided_args.update({"default": default_}) - if default is not PydanticUndefined: - # Before invoking, we'll check for the original default value and set it on the field if the field has no value - json_schema_extra_.default = default - json_schema_extra_.orig_default = default - elif default is not PydanticUndefined: - default_ = default - provided_args.update({"default": default_}) - json_schema_extra_.orig_default = default_ - - return Field( - **provided_args, - json_schema_extra=json_schema_extra_.model_dump(exclude_none=True), - ) - - -def OutputField( - # copied from pydantic's Field - default: Any = _Unset, - title: str | None = _Unset, - description: str | None = _Unset, - pattern: str | None = _Unset, - strict: bool | None = _Unset, - gt: float | None = _Unset, - ge: float | None = _Unset, - lt: float | None = _Unset, - le: float | None = _Unset, - multiple_of: float | None = _Unset, - allow_inf_nan: bool | None = _Unset, - max_digits: int | None = _Unset, - decimal_places: int | None = _Unset, - min_length: int | None = _Unset, - max_length: int | None = _Unset, - # custom - ui_type: Optional[UIType] = None, - ui_hidden: bool = False, - ui_order: Optional[int] = None, -) -> Any: - """ - Creates an output field for an invocation output. - - This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/1.10/usage/schema/#field-customization) \ - that adds a few extra parameters to support graph execution and the node editor UI. - - :param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \ - In some situations, the field's type is not enough to infer the correct UI type. \ - For example, model selection fields should render a dropdown UI component to select a model. \ - Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \ - `MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \ - `UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field. - - :param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. \ - - :param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \ - """ - return Field( - default=default, - title=title, - description=description, - pattern=pattern, - strict=strict, - gt=gt, - ge=ge, - lt=lt, - le=le, - multiple_of=multiple_of, - allow_inf_nan=allow_inf_nan, - max_digits=max_digits, - decimal_places=decimal_places, - min_length=min_length, - max_length=max_length, - json_schema_extra=OutputFieldJSONSchemaExtra( - ui_type=ui_type, - ui_hidden=ui_hidden, - ui_order=ui_order, - field_kind=FieldKind.Output, - ).model_dump(exclude_none=True), - ) - - class UIConfigBase(BaseModel): """ Provides additional node configuration to the UI. @@ -460,33 +74,6 @@ class UIConfigBase(BaseModel): ) -class InvocationContext: - """Initialized and provided to on execution of invocations.""" - - services: InvocationServices - graph_execution_state_id: str - queue_id: str - queue_item_id: int - queue_batch_id: str - workflow: Optional[WorkflowWithoutID] - - def __init__( - self, - services: InvocationServices, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - workflow: Optional[WorkflowWithoutID], - ): - self.services = services - self.graph_execution_state_id = graph_execution_state_id - self.queue_id = queue_id - self.queue_item_id = queue_item_id - self.queue_batch_id = queue_batch_id - self.workflow = workflow - - class BaseInvocationOutput(BaseModel): """ Base class for all invocation outputs. @@ -926,37 +513,3 @@ def invocation_output( return cls return wrapper - - -class MetadataField(RootModel): - """ - Pydantic model for metadata with custom root of type dict[str, Any]. - Metadata is stored without a strict schema. - """ - - root: dict[str, Any] = Field(description="The metadata") - - -MetadataFieldValidator = TypeAdapter(MetadataField) - - -class WithMetadata(BaseModel): - metadata: Optional[MetadataField] = Field( - default=None, - description=FieldDescriptions.metadata, - json_schema_extra=InputFieldJSONSchemaExtra( - field_kind=FieldKind.Internal, - input=Input.Connection, - orig_required=False, - ).model_dump(exclude_none=True), - ) - - -class WithWorkflow: - workflow = None - - def __init_subclass__(cls) -> None: - logger.warn( - f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow." - ) - super().__init_subclass__() diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index 4c7b6f94cd..d35a9d79c7 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -7,7 +7,8 @@ from pydantic import ValidationInfo, field_validator from invokeai.app.invocations.primitives import IntegerCollectionOutput from invokeai.app.util.misc import SEED_MAX -from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation +from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .fields import InputField @invocation( diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 49c62cff56..b386aef2cb 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -5,8 +5,8 @@ import torch from compel import Compel, ReturnedEmbeddingsType from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIComponent from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput -from invokeai.app.shared.fields import FieldDescriptions from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, ExtraConditioningInfo, @@ -20,11 +20,7 @@ from ..util.ti_utils import extract_ti_triggers_from_prompt from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, - UIComponent, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 1f9342985a..9b652b8eee 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -25,10 +25,10 @@ from controlnet_aux.util import HWC3, ade_palette from PIL import Image from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin -from invokeai.app.shared.fields import FieldDescriptions from invokeai.backend.image_util.depth_anything import DepthAnythingDetector from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector @@ -36,11 +36,7 @@ from ...backend.model_management import BaseModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, - WithMetadata, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index cb6828d21a..5865338e19 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -8,7 +8,8 @@ from PIL import Image, ImageOps from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin -from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, invocation +from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .fields import InputField, WithMetadata @invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.2.0") diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index e0c89b4de5..13f1066ec3 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -13,13 +13,11 @@ from pydantic import field_validator import invokeai.assets.fonts as font_assets from invokeai.app.invocations.baseinvocation import ( BaseInvocation, - InputField, InvocationContext, - OutputField, - WithMetadata, invocation, invocation_output, ) +from invokeai.app.invocations.fields import InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py new file mode 100644 index 0000000000..0cce8e3c6b --- /dev/null +++ b/invokeai/app/invocations/fields.py @@ -0,0 +1,501 @@ +from enum import Enum +from typing import Any, Callable, Optional + +from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter +from pydantic.fields import _Unset +from pydantic_core import PydanticUndefined + +from invokeai.app.util.metaenum import MetaEnum +from invokeai.backend.util.logging import InvokeAILogger + +logger = InvokeAILogger.get_logger() + + +class UIType(str, Enum, metaclass=MetaEnum): + """ + Type hints for the UI for situations in which the field type is not enough to infer the correct UI type. + + - Model Fields + The most common node-author-facing use will be for model fields. Internally, there is no difference + between SD-1, SD-2 and SDXL model fields - they all use the class `MainModelField`. To ensure the + base-model-specific UI is rendered, use e.g. `ui_type=UIType.SDXLMainModelField` to indicate that + the field is an SDXL main model field. + + - Any Field + We cannot infer the usage of `typing.Any` via schema parsing, so you *must* use `ui_type=UIType.Any` to + indicate that the field accepts any type. Use with caution. This cannot be used on outputs. + + - Scheduler Field + Special handling in the UI is needed for this field, which otherwise would be parsed as a plain enum field. + + - Internal Fields + Similar to the Any Field, the `collect` and `iterate` nodes make use of `typing.Any`. To facilitate + handling these types in the client, we use `UIType._Collection` and `UIType._CollectionItem`. These + should not be used by node authors. + + - DEPRECATED Fields + These types are deprecated and should not be used by node authors. A warning will be logged if one is + used, and the type will be ignored. They are included here for backwards compatibility. + """ + + # region Model Field Types + SDXLMainModel = "SDXLMainModelField" + SDXLRefinerModel = "SDXLRefinerModelField" + ONNXModel = "ONNXModelField" + VaeModel = "VAEModelField" + LoRAModel = "LoRAModelField" + ControlNetModel = "ControlNetModelField" + IPAdapterModel = "IPAdapterModelField" + # endregion + + # region Misc Field Types + Scheduler = "SchedulerField" + Any = "AnyField" + # endregion + + # region Internal Field Types + _Collection = "CollectionField" + _CollectionItem = "CollectionItemField" + # endregion + + # region DEPRECATED + Boolean = "DEPRECATED_Boolean" + Color = "DEPRECATED_Color" + Conditioning = "DEPRECATED_Conditioning" + Control = "DEPRECATED_Control" + Float = "DEPRECATED_Float" + Image = "DEPRECATED_Image" + Integer = "DEPRECATED_Integer" + Latents = "DEPRECATED_Latents" + String = "DEPRECATED_String" + BooleanCollection = "DEPRECATED_BooleanCollection" + ColorCollection = "DEPRECATED_ColorCollection" + ConditioningCollection = "DEPRECATED_ConditioningCollection" + ControlCollection = "DEPRECATED_ControlCollection" + FloatCollection = "DEPRECATED_FloatCollection" + ImageCollection = "DEPRECATED_ImageCollection" + IntegerCollection = "DEPRECATED_IntegerCollection" + LatentsCollection = "DEPRECATED_LatentsCollection" + StringCollection = "DEPRECATED_StringCollection" + BooleanPolymorphic = "DEPRECATED_BooleanPolymorphic" + ColorPolymorphic = "DEPRECATED_ColorPolymorphic" + ConditioningPolymorphic = "DEPRECATED_ConditioningPolymorphic" + ControlPolymorphic = "DEPRECATED_ControlPolymorphic" + FloatPolymorphic = "DEPRECATED_FloatPolymorphic" + ImagePolymorphic = "DEPRECATED_ImagePolymorphic" + IntegerPolymorphic = "DEPRECATED_IntegerPolymorphic" + LatentsPolymorphic = "DEPRECATED_LatentsPolymorphic" + StringPolymorphic = "DEPRECATED_StringPolymorphic" + MainModel = "DEPRECATED_MainModel" + UNet = "DEPRECATED_UNet" + Vae = "DEPRECATED_Vae" + CLIP = "DEPRECATED_CLIP" + Collection = "DEPRECATED_Collection" + CollectionItem = "DEPRECATED_CollectionItem" + Enum = "DEPRECATED_Enum" + WorkflowField = "DEPRECATED_WorkflowField" + IsIntermediate = "DEPRECATED_IsIntermediate" + BoardField = "DEPRECATED_BoardField" + MetadataItem = "DEPRECATED_MetadataItem" + MetadataItemCollection = "DEPRECATED_MetadataItemCollection" + MetadataItemPolymorphic = "DEPRECATED_MetadataItemPolymorphic" + MetadataDict = "DEPRECATED_MetadataDict" + + +class UIComponent(str, Enum, metaclass=MetaEnum): + """ + The type of UI component to use for a field, used to override the default components, which are + inferred from the field type. + """ + + None_ = "none" + Textarea = "textarea" + Slider = "slider" + + +class FieldDescriptions: + denoising_start = "When to start denoising, expressed a percentage of total steps" + denoising_end = "When to stop denoising, expressed a percentage of total steps" + cfg_scale = "Classifier-Free Guidance scale" + cfg_rescale_multiplier = "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR" + scheduler = "Scheduler to use during inference" + positive_cond = "Positive conditioning tensor" + negative_cond = "Negative conditioning tensor" + noise = "Noise tensor" + clip = "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count" + unet = "UNet (scheduler, LoRAs)" + vae = "VAE" + cond = "Conditioning tensor" + controlnet_model = "ControlNet model to load" + vae_model = "VAE model to load" + lora_model = "LoRA model to load" + main_model = "Main model (UNet, VAE, CLIP) to load" + sdxl_main_model = "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load" + sdxl_refiner_model = "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load" + onnx_main_model = "ONNX Main model (UNet, VAE, CLIP) to load" + lora_weight = "The weight at which the LoRA is applied to each model" + compel_prompt = "Prompt to be parsed by Compel to create a conditioning tensor" + raw_prompt = "Raw prompt text (no parsing)" + sdxl_aesthetic = "The aesthetic score to apply to the conditioning tensor" + skipped_layers = "Number of layers to skip in text encoder" + seed = "Seed for random number generation" + steps = "Number of steps to run" + width = "Width of output (px)" + height = "Height of output (px)" + control = "ControlNet(s) to apply" + ip_adapter = "IP-Adapter to apply" + t2i_adapter = "T2I-Adapter(s) to apply" + denoised_latents = "Denoised latents tensor" + latents = "Latents tensor" + strength = "Strength of denoising (proportional to steps)" + metadata = "Optional metadata to be saved with the image" + metadata_collection = "Collection of Metadata" + metadata_item_polymorphic = "A single metadata item or collection of metadata items" + metadata_item_label = "Label for this metadata item" + metadata_item_value = "The value for this metadata item (may be any type)" + workflow = "Optional workflow to be saved with the image" + interp_mode = "Interpolation mode" + torch_antialias = "Whether or not to apply antialiasing (bilinear or bicubic only)" + fp32 = "Whether or not to use full float32 precision" + precision = "Precision to use" + tiled = "Processing using overlapping tiles (reduce memory consumption)" + detect_res = "Pixel resolution for detection" + image_res = "Pixel resolution for output image" + safe_mode = "Whether or not to use safe mode" + scribble_mode = "Whether or not to use scribble mode" + scale_factor = "The factor by which to scale" + blend_alpha = ( + "Blending factor. 0.0 = use input A only, 1.0 = use input B only, 0.5 = 50% mix of input A and input B." + ) + num_1 = "The first number" + num_2 = "The second number" + mask = "The mask to use for the operation" + board = "The board to save the image to" + image = "The image to process" + tile_size = "Tile size" + inclusive_low = "The inclusive low value" + exclusive_high = "The exclusive high value" + decimal_places = "The number of decimal places to round to" + freeu_s1 = 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.' + freeu_s2 = 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.' + freeu_b1 = "Scaling factor for stage 1 to amplify the contributions of backbone features." + freeu_b2 = "Scaling factor for stage 2 to amplify the contributions of backbone features." + + +class MetadataField(RootModel): + """ + Pydantic model for metadata with custom root of type dict[str, Any]. + Metadata is stored without a strict schema. + """ + + root: dict[str, Any] = Field(description="The metadata") + + +MetadataFieldValidator = TypeAdapter(MetadataField) + + +class Input(str, Enum, metaclass=MetaEnum): + """ + The type of input a field accepts. + - `Input.Direct`: The field must have its value provided directly, when the invocation and field \ + are instantiated. + - `Input.Connection`: The field must have its value provided by a connection. + - `Input.Any`: The field may have its value provided either directly or by a connection. + """ + + Connection = "connection" + Direct = "direct" + Any = "any" + + +class FieldKind(str, Enum, metaclass=MetaEnum): + """ + The kind of field. + - `Input`: An input field on a node. + - `Output`: An output field on a node. + - `Internal`: A field which is treated as an input, but cannot be used in node definitions. Metadata is + one example. It is provided to nodes via the WithMetadata class, and we want to reserve the field name + "metadata" for this on all nodes. `FieldKind` is used to short-circuit the field name validation logic, + allowing "metadata" for that field. + - `NodeAttribute`: The field is a node attribute. These are fields which are not inputs or outputs, + but which are used to store information about the node. For example, the `id` and `type` fields are node + attributes. + + The presence of this in `json_schema_extra["field_kind"]` is used when initializing node schemas on app + startup, and when generating the OpenAPI schema for the workflow editor. + """ + + Input = "input" + Output = "output" + Internal = "internal" + NodeAttribute = "node_attribute" + + +class InputFieldJSONSchemaExtra(BaseModel): + """ + Extra attributes to be added to input fields and their OpenAPI schema. Used during graph execution, + and by the workflow editor during schema parsing and UI rendering. + """ + + input: Input + orig_required: bool + field_kind: FieldKind + default: Optional[Any] = None + orig_default: Optional[Any] = None + ui_hidden: bool = False + ui_type: Optional[UIType] = None + ui_component: Optional[UIComponent] = None + ui_order: Optional[int] = None + ui_choice_labels: Optional[dict[str, str]] = None + + model_config = ConfigDict( + validate_assignment=True, + json_schema_serialization_defaults_required=True, + ) + + +class WithMetadata(BaseModel): + metadata: Optional[MetadataField] = Field( + default=None, + description=FieldDescriptions.metadata, + json_schema_extra=InputFieldJSONSchemaExtra( + field_kind=FieldKind.Internal, + input=Input.Connection, + orig_required=False, + ).model_dump(exclude_none=True), + ) + + +class WithWorkflow: + workflow = None + + def __init_subclass__(cls) -> None: + logger.warn( + f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow." + ) + super().__init_subclass__() + + +class OutputFieldJSONSchemaExtra(BaseModel): + """ + Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor + during schema parsing and UI rendering. + """ + + field_kind: FieldKind + ui_hidden: bool + ui_type: Optional[UIType] + ui_order: Optional[int] + + model_config = ConfigDict( + validate_assignment=True, + json_schema_serialization_defaults_required=True, + ) + + +def InputField( + # copied from pydantic's Field + # TODO: Can we support default_factory? + default: Any = _Unset, + default_factory: Callable[[], Any] | None = _Unset, + title: str | None = _Unset, + description: str | None = _Unset, + pattern: str | None = _Unset, + strict: bool | None = _Unset, + gt: float | None = _Unset, + ge: float | None = _Unset, + lt: float | None = _Unset, + le: float | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + # custom + input: Input = Input.Any, + ui_type: Optional[UIType] = None, + ui_component: Optional[UIComponent] = None, + ui_hidden: bool = False, + ui_order: Optional[int] = None, + ui_choice_labels: Optional[dict[str, str]] = None, +) -> Any: + """ + Creates an input field for an invocation. + + This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field) \ + that adds a few extra parameters to support graph execution and the node editor UI. + + :param Input input: [Input.Any] The kind of input this field requires. \ + `Input.Direct` means a value must be provided on instantiation. \ + `Input.Connection` means the value must be provided by a connection. \ + `Input.Any` means either will do. + + :param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \ + In some situations, the field's type is not enough to infer the correct UI type. \ + For example, model selection fields should render a dropdown UI component to select a model. \ + Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \ + `MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \ + `UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field. + + :param UIComponent ui_component: [None] Optionally specifies a specific component to use in the UI. \ + The UI will always render a suitable component, but sometimes you want something different than the default. \ + For example, a `string` field will default to a single-line input, but you may want a multi-line textarea instead. \ + For this case, you could provide `UIComponent.Textarea`. + + :param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. + + :param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. + + :param dict[str, str] ui_choice_labels: [None] Specifies the labels to use for the choices in an enum field. + """ + + json_schema_extra_ = InputFieldJSONSchemaExtra( + input=input, + ui_type=ui_type, + ui_component=ui_component, + ui_hidden=ui_hidden, + ui_order=ui_order, + ui_choice_labels=ui_choice_labels, + field_kind=FieldKind.Input, + orig_required=True, + ) + + """ + There is a conflict between the typing of invocation definitions and the typing of an invocation's + `invoke()` function. + + On instantiation of a node, the invocation definition is used to create the python class. At this time, + any number of fields may be optional, because they may be provided by connections. + + On calling of `invoke()`, however, those fields may be required. + + For example, consider an ResizeImageInvocation with an `image: ImageField` field. + + `image` is required during the call to `invoke()`, but when the python class is instantiated, + the field may not be present. This is fine, because that image field will be provided by a + connection from an ancestor node, which outputs an image. + + This means we want to type the `image` field as optional for the node class definition, but required + for the `invoke()` function. + + If we use `typing.Optional` in the node class definition, the field will be typed as optional in the + `invoke()` method, and we'll have to do a lot of runtime checks to ensure the field is present - or + any static type analysis tools will complain. + + To get around this, in node class definitions, we type all fields correctly for the `invoke()` function, + but secretly make them optional in `InputField()`. We also store the original required bool and/or default + value. When we call `invoke()`, we use this stored information to do an additional check on the class. + """ + + if default_factory is not _Unset and default_factory is not None: + default = default_factory() + logger.warn('"default_factory" is not supported, calling it now to set "default"') + + # These are the args we may wish pass to the pydantic `Field()` function + field_args = { + "default": default, + "title": title, + "description": description, + "pattern": pattern, + "strict": strict, + "gt": gt, + "ge": ge, + "lt": lt, + "le": le, + "multiple_of": multiple_of, + "allow_inf_nan": allow_inf_nan, + "max_digits": max_digits, + "decimal_places": decimal_places, + "min_length": min_length, + "max_length": max_length, + } + + # We only want to pass the args that were provided, otherwise the `Field()`` function won't work as expected + provided_args = {k: v for (k, v) in field_args.items() if v is not PydanticUndefined} + + # Because we are manually making fields optional, we need to store the original required bool for reference later + json_schema_extra_.orig_required = default is PydanticUndefined + + # Make Input.Any and Input.Connection fields optional, providing None as a default if the field doesn't already have one + if input is Input.Any or input is Input.Connection: + default_ = None if default is PydanticUndefined else default + provided_args.update({"default": default_}) + if default is not PydanticUndefined: + # Before invoking, we'll check for the original default value and set it on the field if the field has no value + json_schema_extra_.default = default + json_schema_extra_.orig_default = default + elif default is not PydanticUndefined: + default_ = default + provided_args.update({"default": default_}) + json_schema_extra_.orig_default = default_ + + return Field( + **provided_args, + json_schema_extra=json_schema_extra_.model_dump(exclude_none=True), + ) + + +def OutputField( + # copied from pydantic's Field + default: Any = _Unset, + title: str | None = _Unset, + description: str | None = _Unset, + pattern: str | None = _Unset, + strict: bool | None = _Unset, + gt: float | None = _Unset, + ge: float | None = _Unset, + lt: float | None = _Unset, + le: float | None = _Unset, + multiple_of: float | None = _Unset, + allow_inf_nan: bool | None = _Unset, + max_digits: int | None = _Unset, + decimal_places: int | None = _Unset, + min_length: int | None = _Unset, + max_length: int | None = _Unset, + # custom + ui_type: Optional[UIType] = None, + ui_hidden: bool = False, + ui_order: Optional[int] = None, +) -> Any: + """ + Creates an output field for an invocation output. + + This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/1.10/usage/schema/#field-customization) \ + that adds a few extra parameters to support graph execution and the node editor UI. + + :param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \ + In some situations, the field's type is not enough to infer the correct UI type. \ + For example, model selection fields should render a dropdown UI component to select a model. \ + Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \ + `MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \ + `UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field. + + :param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. \ + + :param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \ + """ + return Field( + default=default, + title=title, + description=description, + pattern=pattern, + strict=strict, + gt=gt, + ge=ge, + lt=lt, + le=le, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + min_length=min_length, + max_length=max_length, + json_schema_extra=OutputFieldJSONSchemaExtra( + ui_type=ui_type, + ui_hidden=ui_hidden, + ui_order=ui_order, + field_kind=FieldKind.Output, + ).model_dump(exclude_none=True), + ) + # endregion diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index f729d60cdd..16d0f33dda 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -7,19 +7,16 @@ import cv2 import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, WithMetadata from invokeai.app.invocations.primitives import BoardField, ColorField, ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin -from invokeai.app.shared.fields import FieldDescriptions from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.safety_checker import SafetyChecker from .baseinvocation import ( BaseInvocation, Classification, - Input, - InputField, InvocationContext, - WithMetadata, invocation, ) diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index c3d00bb133..d4d3d5bea4 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -13,7 +13,8 @@ from invokeai.backend.image_util.cv2_inpaint import cv2_inpaint from invokeai.backend.image_util.lama import LaMA from invokeai.backend.image_util.patchmatch import PatchMatch -from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, invocation +from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .fields import InputField, WithMetadata from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index 6bd2889624..c01e0ed0fb 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -7,16 +7,13 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, invocation, invocation_output, ) +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights -from invokeai.app.shared.fields import FieldDescriptions from invokeai.backend.model_management.models.base import BaseModelType, ModelType from invokeai.backend.model_management.models.ip_adapter import get_ip_adapter_image_encoder_model_id diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index b77363ceb8..909c307481 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -23,6 +23,7 @@ from diffusers.schedulers import SchedulerMixin as Scheduler from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType, WithMetadata from invokeai.app.invocations.ip_adapter import IPAdapterField from invokeai.app.invocations.primitives import ( DenoiseMaskField, @@ -35,7 +36,6 @@ from invokeai.app.invocations.primitives import ( ) from invokeai.app.invocations.t2i_adapter import T2IAdapterField from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin -from invokeai.app.shared.fields import FieldDescriptions from invokeai.app.util.controlnet_utils import prepare_control_image from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus @@ -59,12 +59,7 @@ from ...backend.util.devices import choose_precision, choose_torch_device from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, - UIType, - WithMetadata, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/math.py b/invokeai/app/invocations/math.py index defc61275f..6ca53011f0 100644 --- a/invokeai/app/invocations/math.py +++ b/invokeai/app/invocations/math.py @@ -5,10 +5,10 @@ from typing import Literal import numpy as np from pydantic import ValidationInfo, field_validator +from invokeai.app.invocations.fields import FieldDescriptions, InputField from invokeai.app.invocations.primitives import FloatOutput, IntegerOutput -from invokeai.app.shared.fields import FieldDescriptions -from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation +from .baseinvocation import BaseInvocation, InvocationContext, invocation @invocation("add", title="Add Integers", tags=["math", "add"], category="math", version="1.0.0") diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py index 14d66f8ef6..399e217dc1 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -5,20 +5,16 @@ from pydantic import BaseModel, ConfigDict, Field from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InputField, InvocationContext, - MetadataField, - OutputField, - UIType, invocation, invocation_output, ) from invokeai.app.invocations.controlnet_image_processors import ControlField +from invokeai.app.invocations.fields import FieldDescriptions, InputField, MetadataField, OutputField, UIType from invokeai.app.invocations.ip_adapter import IPAdapterModelField from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.t2i_adapter import T2IAdapterField -from invokeai.app.shared.fields import FieldDescriptions from ...version import __version__ diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index 99dcc72999..c710c9761b 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -3,17 +3,14 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field -from invokeai.app.shared.fields import FieldDescriptions +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField from invokeai.app.shared.models import FreeUConfig from ...backend.model_management import BaseModelType, ModelType, SubModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index b1ee91e1cd..2e717ac561 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -4,17 +4,15 @@ import torch from pydantic import field_validator +from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField from invokeai.app.invocations.latent import LatentsField -from invokeai.app.shared.fields import FieldDescriptions from invokeai.app.util.misc import SEED_MAX from ...backend.util.devices import choose_torch_device, torch_dtype from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InputField, InvocationContext, - OutputField, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index 759cfde700..b43d7eaef2 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -11,9 +11,17 @@ from diffusers.image_processor import VaeImageProcessor from pydantic import BaseModel, ConfigDict, Field, field_validator from tqdm import tqdm +from invokeai.app.invocations.fields import ( + FieldDescriptions, + Input, + InputField, + OutputField, + UIComponent, + UIType, + WithMetadata, +) from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput, ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin -from invokeai.app.shared.fields import FieldDescriptions from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.backend import BaseModelType, ModelType, SubModelType @@ -24,13 +32,7 @@ from ..util.ti_utils import extract_ti_triggers_from_prompt from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, - UIComponent, - UIType, - WithMetadata, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/param_easing.py b/invokeai/app/invocations/param_easing.py index dccd18f754..dab9c3dc0f 100644 --- a/invokeai/app/invocations/param_easing.py +++ b/invokeai/app/invocations/param_easing.py @@ -41,7 +41,8 @@ from matplotlib.ticker import MaxNLocator from invokeai.app.invocations.primitives import FloatCollectionOutput -from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation +from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .fields import InputField @invocation( diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index afe8ff06d9..22f03454a5 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -5,16 +5,12 @@ from typing import Optional, Tuple import torch from pydantic import BaseModel, Field -from invokeai.app.shared.fields import FieldDescriptions +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIComponent from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, - UIComponent, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/prompt.py b/invokeai/app/invocations/prompt.py index 4778d98077..94b4a217ae 100644 --- a/invokeai/app/invocations/prompt.py +++ b/invokeai/app/invocations/prompt.py @@ -7,7 +7,8 @@ from pydantic import field_validator from invokeai.app.invocations.primitives import StringCollectionOutput -from .baseinvocation import BaseInvocation, InputField, InvocationContext, UIComponent, invocation +from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .fields import InputField, UIComponent @invocation( diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 68076fdfeb..62df5bc804 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -1,14 +1,10 @@ -from invokeai.app.shared.fields import FieldDescriptions +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType from ...backend.model_management import ModelType, SubModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, - UIType, invocation, invocation_output, ) diff --git a/invokeai/app/invocations/strings.py b/invokeai/app/invocations/strings.py index 3466206b37..ccbc2f6d92 100644 --- a/invokeai/app/invocations/strings.py +++ b/invokeai/app/invocations/strings.py @@ -5,13 +5,11 @@ import re from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InputField, InvocationContext, - OutputField, - UIComponent, invocation, invocation_output, ) +from .fields import InputField, OutputField, UIComponent from .primitives import StringOutput diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py index e055d23903..66ac87c37b 100644 --- a/invokeai/app/invocations/t2i_adapter.py +++ b/invokeai/app/invocations/t2i_adapter.py @@ -5,17 +5,14 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, invocation, invocation_output, ) from invokeai.app.invocations.controlnet_image_processors import CONTROLNET_RESIZE_VALUES +from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights -from invokeai.app.shared.fields import FieldDescriptions from invokeai.backend.model_management.models.base import BaseModelType diff --git a/invokeai/app/invocations/tiles.py b/invokeai/app/invocations/tiles.py index e51f891a8d..bdc23ef6ed 100644 --- a/invokeai/app/invocations/tiles.py +++ b/invokeai/app/invocations/tiles.py @@ -8,14 +8,11 @@ from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, Classification, - Input, - InputField, InvocationContext, - OutputField, - WithMetadata, invocation, invocation_output, ) +from invokeai.app.invocations.fields import Input, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.backend.tiles.tiles import ( diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index 5f715c1a7e..2cab279a9f 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -14,7 +14,8 @@ from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN from invokeai.backend.util.devices import choose_torch_device -from .baseinvocation import BaseInvocation, InputField, InvocationContext, WithMetadata, invocation +from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .fields import InputField, WithMetadata # TODO: Populate this from disk? # TODO: Use model manager to load? diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py index 27dd67531f..f4036277b7 100644 --- a/invokeai/app/services/image_files/image_files_base.py +++ b/invokeai/app/services/image_files/image_files_base.py @@ -4,7 +4,7 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.baseinvocation import MetadataField +from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py index 0844821672..fb687973ba 100644 --- a/invokeai/app/services/image_files/image_files_disk.py +++ b/invokeai/app/services/image_files/image_files_disk.py @@ -7,7 +7,7 @@ from PIL import Image, PngImagePlugin from PIL.Image import Image as PILImageType from send2trash import send2trash -from invokeai.app.invocations.baseinvocation import MetadataField +from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py index 727f4977fb..7b7b261eca 100644 --- a/invokeai/app/services/image_records/image_records_base.py +++ b/invokeai/app/services/image_records/image_records_base.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from datetime import datetime from typing import Optional -from invokeai.app.invocations.metadata import MetadataField +from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.shared.pagination import OffsetPaginatedResults from .image_records_common import ImageCategory, ImageRecord, ImageRecordChanges, ResourceOrigin diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py index 74f82e7d84..5b37913c8f 100644 --- a/invokeai/app/services/image_records/image_records_sqlite.py +++ b/invokeai/app/services/image_records/image_records_sqlite.py @@ -3,7 +3,7 @@ import threading from datetime import datetime from typing import Optional, Union, cast -from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator +from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py index df71dadb5b..42c4266774 100644 --- a/invokeai/app/services/images/images_base.py +++ b/invokeai/app/services/images/images_base.py @@ -3,7 +3,7 @@ from typing import Callable, Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.baseinvocation import MetadataField +from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.image_records.image_records_common import ( ImageCategory, ImageRecord, diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py index ff21731a50..adeed73811 100644 --- a/invokeai/app/services/images/images_default.py +++ b/invokeai/app/services/images/images_default.py @@ -2,7 +2,7 @@ from typing import Optional from PIL.Image import Image as PILImageType -from invokeai.app.invocations.baseinvocation import MetadataField +from invokeai.app.invocations.fields import MetadataField from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 1acf165aba..ba05b050c5 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -13,14 +13,11 @@ from invokeai.app.invocations import * # noqa: F401 F403 from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - Input, - InputField, InvocationContext, - OutputField, - UIType, invocation, invocation_output, ) +from invokeai.app.invocations.fields import Input, InputField, OutputField, UIType from invokeai.app.util.misc import uuid_string # in 3.10 this would be "from types import NoneType" diff --git a/invokeai/app/shared/fields.py b/invokeai/app/shared/fields.py deleted file mode 100644 index 3e841ffbf2..0000000000 --- a/invokeai/app/shared/fields.py +++ /dev/null @@ -1,67 +0,0 @@ -class FieldDescriptions: - denoising_start = "When to start denoising, expressed a percentage of total steps" - denoising_end = "When to stop denoising, expressed a percentage of total steps" - cfg_scale = "Classifier-Free Guidance scale" - cfg_rescale_multiplier = "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR" - scheduler = "Scheduler to use during inference" - positive_cond = "Positive conditioning tensor" - negative_cond = "Negative conditioning tensor" - noise = "Noise tensor" - clip = "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count" - unet = "UNet (scheduler, LoRAs)" - vae = "VAE" - cond = "Conditioning tensor" - controlnet_model = "ControlNet model to load" - vae_model = "VAE model to load" - lora_model = "LoRA model to load" - main_model = "Main model (UNet, VAE, CLIP) to load" - sdxl_main_model = "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load" - sdxl_refiner_model = "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load" - onnx_main_model = "ONNX Main model (UNet, VAE, CLIP) to load" - lora_weight = "The weight at which the LoRA is applied to each model" - compel_prompt = "Prompt to be parsed by Compel to create a conditioning tensor" - raw_prompt = "Raw prompt text (no parsing)" - sdxl_aesthetic = "The aesthetic score to apply to the conditioning tensor" - skipped_layers = "Number of layers to skip in text encoder" - seed = "Seed for random number generation" - steps = "Number of steps to run" - width = "Width of output (px)" - height = "Height of output (px)" - control = "ControlNet(s) to apply" - ip_adapter = "IP-Adapter to apply" - t2i_adapter = "T2I-Adapter(s) to apply" - denoised_latents = "Denoised latents tensor" - latents = "Latents tensor" - strength = "Strength of denoising (proportional to steps)" - metadata = "Optional metadata to be saved with the image" - metadata_collection = "Collection of Metadata" - metadata_item_polymorphic = "A single metadata item or collection of metadata items" - metadata_item_label = "Label for this metadata item" - metadata_item_value = "The value for this metadata item (may be any type)" - workflow = "Optional workflow to be saved with the image" - interp_mode = "Interpolation mode" - torch_antialias = "Whether or not to apply antialiasing (bilinear or bicubic only)" - fp32 = "Whether or not to use full float32 precision" - precision = "Precision to use" - tiled = "Processing using overlapping tiles (reduce memory consumption)" - detect_res = "Pixel resolution for detection" - image_res = "Pixel resolution for output image" - safe_mode = "Whether or not to use safe mode" - scribble_mode = "Whether or not to use scribble mode" - scale_factor = "The factor by which to scale" - blend_alpha = ( - "Blending factor. 0.0 = use input A only, 1.0 = use input B only, 0.5 = 50% mix of input A and input B." - ) - num_1 = "The first number" - num_2 = "The second number" - mask = "The mask to use for the operation" - board = "The board to save the image to" - image = "The image to process" - tile_size = "Tile size" - inclusive_low = "The inclusive low value" - exclusive_high = "The exclusive high value" - decimal_places = "The number of decimal places to round to" - freeu_s1 = 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.' - freeu_s2 = 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.' - freeu_b1 = "Scaling factor for stage 1 to amplify the contributions of backbone features." - freeu_b2 = "Scaling factor for stage 2 to amplify the contributions of backbone features." diff --git a/invokeai/app/shared/models.py b/invokeai/app/shared/models.py index ed68cb287e..1a11b480cc 100644 --- a/invokeai/app/shared/models.py +++ b/invokeai/app/shared/models.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, Field -from invokeai.app.shared.fields import FieldDescriptions +from invokeai.app.invocations.fields import FieldDescriptions class FreeUConfig(BaseModel): diff --git a/tests/aa_nodes/test_nodes.py b/tests/aa_nodes/test_nodes.py index bca4e1011f..e71daad3f3 100644 --- a/tests/aa_nodes/test_nodes.py +++ b/tests/aa_nodes/test_nodes.py @@ -3,12 +3,11 @@ from typing import Any, Callable, Union from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InputField, InvocationContext, - OutputField, invocation, invocation_output, ) +from invokeai.app.invocations.fields import InputField, OutputField from invokeai.app.invocations.image import ImageField From 3d98446d5d79828c4048a932877a4e1298d860a0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 13 Jan 2024 18:02:58 +1100 Subject: [PATCH 040/411] feat(nodes): restricts invocation context power Creates a low-power `InvocationContext` with simplified methods and data. See `invocation_context.py` for detailed comments. --- .../app/services/shared/invocation_context.py | 408 ++++++++++++++++++ invokeai/app/util/step_callback.py | 39 +- 2 files changed, 434 insertions(+), 13 deletions(-) create mode 100644 invokeai/app/services/shared/invocation_context.py diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py new file mode 100644 index 0000000000..c0aaac54f8 --- /dev/null +++ b/invokeai/app/services/shared/invocation_context.py @@ -0,0 +1,408 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Optional + +from PIL.Image import Image +from pydantic import ConfigDict +from torch import Tensor + +from invokeai.app.invocations.compel import ConditioningFieldData +from invokeai.app.invocations.fields import MetadataField, WithMetadata +from invokeai.app.services.config.config_default import InvokeAIAppConfig +from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin +from invokeai.app.services.images.images_common import ImageDTO +from invokeai.app.services.invocation_services import InvocationServices +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID +from invokeai.app.util.misc import uuid_string +from invokeai.app.util.step_callback import stable_diffusion_step_callback +from invokeai.backend.model_management.model_manager import ModelInfo +from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType +from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState + +if TYPE_CHECKING: + from invokeai.app.invocations.baseinvocation import BaseInvocation + +""" +The InvocationContext provides access to various services and data about the current invocation. + +We do not provide the invocation services directly, as their methods are both dangerous and +inconvenient to use. + +For example: +- The `images` service allows nodes to delete or unsafely modify existing images. +- The `configuration` service allows nodes to change the app's config at runtime. +- The `events` service allows nodes to emit arbitrary events. + +Wrapping these services provides a simpler and safer interface for nodes to use. + +When a node executes, a fresh `InvocationContext` is built for it, ensuring nodes cannot interfere +with each other. + +Note: The docstrings are in weird places, but that's where they must be to get IDEs to see them. +""" + + +@dataclass(frozen=True) +class InvocationContextData: + invocation: "BaseInvocation" + session_id: str + queue_id: str + source_node_id: str + queue_item_id: int + batch_id: str + workflow: Optional[WorkflowWithoutID] = None + + +class LoggerInterface: + def __init__(self, services: InvocationServices) -> None: + def debug(message: str) -> None: + """ + Logs a debug message. + + :param message: The message to log. + """ + services.logger.debug(message) + + def info(message: str) -> None: + """ + Logs an info message. + + :param message: The message to log. + """ + services.logger.info(message) + + def warning(message: str) -> None: + """ + Logs a warning message. + + :param message: The message to log. + """ + services.logger.warning(message) + + def error(message: str) -> None: + """ + Logs an error message. + + :param message: The message to log. + """ + services.logger.error(message) + + self.debug = debug + self.info = info + self.warning = warning + self.error = error + + +class ImagesInterface: + def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: + def save( + image: Image, + board_id: Optional[str] = None, + image_category: ImageCategory = ImageCategory.GENERAL, + metadata: Optional[MetadataField] = None, + ) -> ImageDTO: + """ + Saves an image, returning its DTO. + + If the current queue item has a workflow, it is automatically saved with the image. + + :param image: The image to save, as a PIL image. + :param board_id: The board ID to add the image to, if it should be added. + :param image_category: The category of the image. Only the GENERAL category is added to the gallery. + :param metadata: The metadata to save with the image, if it should have any. If the invocation inherits \ + from `WithMetadata`, that metadata will be used automatically. Provide this only if you want to \ + override or provide metadata manually. + """ + + # If the invocation inherits metadata, use that. Else, use the metadata passed in. + metadata_ = ( + context_data.invocation.metadata if isinstance(context_data.invocation, WithMetadata) else metadata + ) + + return services.images.create( + image=image, + is_intermediate=context_data.invocation.is_intermediate, + image_category=image_category, + board_id=board_id, + metadata=metadata_, + image_origin=ResourceOrigin.INTERNAL, + workflow=context_data.workflow, + session_id=context_data.session_id, + node_id=context_data.invocation.id, + ) + + def get_pil(image_name: str) -> Image: + """ + Gets an image as a PIL Image object. + + :param image_name: The name of the image to get. + """ + return services.images.get_pil_image(image_name) + + def get_metadata(image_name: str) -> Optional[MetadataField]: + """ + Gets an image's metadata, if it has any. + + :param image_name: The name of the image to get the metadata for. + """ + return services.images.get_metadata(image_name) + + def get_dto(image_name: str) -> ImageDTO: + """ + Gets an image as an ImageDTO object. + + :param image_name: The name of the image to get. + """ + return services.images.get_dto(image_name) + + def update( + image_name: str, + board_id: Optional[str] = None, + is_intermediate: Optional[bool] = False, + ) -> ImageDTO: + """ + Updates an image, returning its updated DTO. + + It is not suggested to update images saved by earlier nodes, as this can cause confusion for users. + + If you use this method, you *must* return the image as an :class:`ImageOutput` for the gallery to + get the updated image. + + :param image_name: The name of the image to update. + :param board_id: The board ID to add the image to, if it should be added. + :param is_intermediate: Whether the image is an intermediate. Intermediate images aren't added to the gallery. + """ + if is_intermediate is not None: + services.images.update(image_name, ImageRecordChanges(is_intermediate=is_intermediate)) + if board_id is None: + services.board_images.remove_image_from_board(image_name) + else: + services.board_images.add_image_to_board(image_name, board_id) + return services.images.get_dto(image_name) + + self.save = save + self.get_pil = get_pil + self.get_metadata = get_metadata + self.get_dto = get_dto + self.update = update + + +class LatentsKind(str, Enum): + IMAGE = "image" + NOISE = "noise" + MASK = "mask" + MASKED_IMAGE = "masked_image" + OTHER = "other" + + +class LatentsInterface: + def __init__( + self, + services: InvocationServices, + context_data: InvocationContextData, + ) -> None: + def save(tensor: Tensor) -> str: + """ + Saves a latents tensor, returning its name. + + :param tensor: The latents tensor to save. + """ + name = f"{context_data.session_id}__{context_data.invocation.id}__{uuid_string()[:7]}" + services.latents.save( + name=name, + data=tensor, + ) + return name + + def get(latents_name: str) -> Tensor: + """ + Gets a latents tensor by name. + + :param latents_name: The name of the latents tensor to get. + """ + return services.latents.get(latents_name) + + self.save = save + self.get = get + + +class ConditioningInterface: + def __init__( + self, + services: InvocationServices, + context_data: InvocationContextData, + ) -> None: + def save(conditioning_data: ConditioningFieldData) -> str: + """ + Saves a conditioning data object, returning its name. + + :param conditioning_data: The conditioning data to save. + """ + name = f"{context_data.session_id}__{context_data.invocation.id}__{uuid_string()[:7]}__conditioning" + services.latents.save( + name=name, + data=conditioning_data, # type: ignore [arg-type] + ) + return name + + def get(conditioning_name: str) -> Tensor: + """ + Gets conditioning data by name. + + :param conditioning_name: The name of the conditioning data to get. + """ + return services.latents.get(conditioning_name) + + self.save = save + self.get = get + + +class ModelsInterface: + def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: + def exists(model_name: str, base_model: BaseModelType, model_type: ModelType) -> bool: + """ + Checks if a model exists. + + :param model_name: The name of the model to check. + :param base_model: The base model of the model to check. + :param model_type: The type of the model to check. + """ + return services.model_manager.model_exists(model_name, base_model, model_type) + + def load( + model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None + ) -> ModelInfo: + """ + Loads a model, returning its `ModelInfo` object. + + :param model_name: The name of the model to get. + :param base_model: The base model of the model to get. + :param model_type: The type of the model to get. + :param submodel: The submodel of the model to get. + """ + return services.model_manager.get_model( + model_name, base_model, model_type, submodel, context_data=context_data + ) + + def get_info(model_name: str, base_model: BaseModelType, model_type: ModelType) -> dict: + """ + Gets a model's info, an dict-like object. + + :param model_name: The name of the model to get. + :param base_model: The base model of the model to get. + :param model_type: The type of the model to get. + """ + return services.model_manager.model_info(model_name, base_model, model_type) + + self.exists = exists + self.load = load + self.get_info = get_info + + +class ConfigInterface: + def __init__(self, services: InvocationServices) -> None: + def get() -> InvokeAIAppConfig: + """ + Gets the app's config. + """ + # The config can be changed at runtime. We don't want nodes doing this, so we make a + # frozen copy.. + config = services.configuration.get_config() + frozen_config = config.model_copy(update={"model_config": ConfigDict(frozen=True)}) + return frozen_config + + self.get = get + + +class UtilInterface: + def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: + def sd_step_callback( + intermediate_state: PipelineIntermediateState, + base_model: BaseModelType, + ) -> None: + """ + The step callback emits a progress event with the current step, the total number of + steps, a preview image, and some other internal metadata. + + This should be called after each step of the diffusion process. + + :param intermediate_state: The intermediate state of the diffusion pipeline. + :param base_model: The base model for the current denoising step. + """ + stable_diffusion_step_callback( + context_data=context_data, + intermediate_state=intermediate_state, + base_model=base_model, + invocation_queue=services.queue, + events=services.events, + ) + + self.sd_step_callback = sd_step_callback + + +class InvocationContext: + """ + The invocation context provides access to various services and data about the current invocation. + """ + + def __init__( + self, + images: ImagesInterface, + latents: LatentsInterface, + models: ModelsInterface, + config: ConfigInterface, + logger: LoggerInterface, + data: InvocationContextData, + util: UtilInterface, + conditioning: ConditioningInterface, + ) -> None: + self.images = images + "Provides methods to save, get and update images and their metadata." + self.logger = logger + "Provides access to the app logger." + self.latents = latents + "Provides methods to save and get latents tensors, including image, noise, masks, and masked images." + self.conditioning = conditioning + "Provides methods to save and get conditioning data." + self.models = models + "Provides methods to check if a model exists, get a model, and get a model's info." + self.config = config + "Provides access to the app's config." + self.data = data + "Provides data about the current queue item and invocation." + self.util = util + "Provides utility methods." + + +def build_invocation_context( + services: InvocationServices, + context_data: InvocationContextData, +) -> InvocationContext: + """ + Builds the invocation context. This is a wrapper around the invocation services that provides + a more convenient (and less dangerous) interface for nodes to use. + + :param invocation_services: The invocation services to wrap. + :param invocation_context_data: The invocation context data. + """ + + logger = LoggerInterface(services=services) + images = ImagesInterface(services=services, context_data=context_data) + latents = LatentsInterface(services=services, context_data=context_data) + models = ModelsInterface(services=services, context_data=context_data) + config = ConfigInterface(services=services) + util = UtilInterface(services=services, context_data=context_data) + conditioning = ConditioningInterface(services=services, context_data=context_data) + + ctx = InvocationContext( + images=images, + logger=logger, + config=config, + latents=latents, + models=models, + data=context_data, + util=util, + conditioning=conditioning, + ) + + return ctx diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index f166206d52..5cc3caa9ba 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -1,12 +1,25 @@ +from typing import Protocol + import torch from PIL import Image +from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException, ProgressImage +from invokeai.app.services.invocation_queue.invocation_queue_base import InvocationQueueABC +from invokeai.app.services.shared.invocation_context import InvocationContextData from ...backend.model_management.models import BaseModelType from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.util.util import image_to_dataURL -from ..invocations.baseinvocation import InvocationContext + + +class StepCallback(Protocol): + def __call__( + self, + intermediate_state: PipelineIntermediateState, + base_model: BaseModelType, + ) -> None: + ... def sample_to_lowres_estimated_image(samples, latent_rgb_factors, smooth_matrix=None): @@ -25,13 +38,13 @@ def sample_to_lowres_estimated_image(samples, latent_rgb_factors, smooth_matrix= def stable_diffusion_step_callback( - context: InvocationContext, + context_data: InvocationContextData, intermediate_state: PipelineIntermediateState, - node: dict, - source_node_id: str, base_model: BaseModelType, -): - if context.services.queue.is_canceled(context.graph_execution_state_id): + invocation_queue: InvocationQueueABC, + events: EventServiceBase, +) -> None: + if invocation_queue.is_canceled(context_data.session_id): raise CanceledException # Some schedulers report not only the noisy latents at the current timestep, @@ -108,13 +121,13 @@ def stable_diffusion_step_callback( dataURL = image_to_dataURL(image, image_format="JPEG") - context.services.events.emit_generator_progress( - queue_id=context.queue_id, - queue_item_id=context.queue_item_id, - queue_batch_id=context.queue_batch_id, - graph_execution_state_id=context.graph_execution_state_id, - node=node, - source_node_id=source_node_id, + events.emit_generator_progress( + queue_id=context_data.queue_id, + queue_item_id=context_data.queue_item_id, + queue_batch_id=context_data.batch_id, + graph_execution_state_id=context_data.session_id, + node_id=context_data.invocation.id, + source_node_id=context_data.source_node_id, progress_image=ProgressImage(width=width, height=height, dataURL=dataURL), step=intermediate_state.step, order=intermediate_state.order, From 9bc2d0988989a92a5736e8f78d21cca13cdc4646 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:23:01 +1100 Subject: [PATCH 041/411] feat: add pyright config I was having issues with mypy bother over- and under-reporting certain problems. I've added a pyright config. --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7f4b0d77f2..d063f1ad0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -280,3 +280,19 @@ module = [ "invokeai.frontend.install.model_install", ] #=== End: MyPy + +[tool.pyright] +include = [ + "invokeai/app/invocations/" +] +exclude = [ + "**/node_modules", + "**/__pycache__", + "invokeai/app/invocations/onnx.py", + "invokeai/app/api/routers/models.py", + "invokeai/app/services/invocation_stats/invocation_stats_default.py", + "invokeai/app/services/model_manager/model_manager_base.py", + "invokeai/app/services/model_manager/model_manager_default.py", + "invokeai/app/services/model_records/model_records_sql.py", + "invokeai/app/util/controlnet_utils.py", +] From 8637c40661617de2a792eae5c1495821205587a3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:23:16 +1100 Subject: [PATCH 042/411] feat(nodes): update all invocations to use new invocation context Update all invocations to use the new context. The changes are all fairly simple, but there are a lot of them. Supporting minor changes: - Patch bump for all nodes that use the context - Update invocation processor to provide new context - Minor change to `EventServiceBase` to accept a node's ID instead of the dict version of a node - Minor change to `ModelManagerService` to support the new wrapped context - Fanagling of imports to avoid circular dependencies --- invokeai/app/invocations/baseinvocation.py | 54 +- invokeai/app/invocations/collections.py | 8 +- invokeai/app/invocations/compel.py | 105 ++-- .../controlnet_image_processors.py | 56 +- invokeai/app/invocations/cv.py | 30 +- invokeai/app/invocations/facetools.py | 129 ++-- invokeai/app/invocations/fields.py | 57 +- invokeai/app/invocations/image.py | 586 +++++------------- invokeai/app/invocations/infill.py | 123 +--- invokeai/app/invocations/ip_adapter.py | 9 +- invokeai/app/invocations/latent.py | 238 +++---- invokeai/app/invocations/math.py | 22 +- invokeai/app/invocations/metadata.py | 19 +- invokeai/app/invocations/model.py | 29 +- invokeai/app/invocations/noise.py | 25 +- invokeai/app/invocations/onnx.py | 10 +- invokeai/app/invocations/param_easing.py | 44 +- invokeai/app/invocations/primitives.py | 136 ++-- invokeai/app/invocations/prompt.py | 6 +- invokeai/app/invocations/sdxl.py | 13 +- invokeai/app/invocations/strings.py | 11 +- invokeai/app/invocations/t2i_adapter.py | 6 +- invokeai/app/invocations/tiles.py | 38 +- invokeai/app/invocations/upscale.py | 33 +- invokeai/app/services/events/events_base.py | 4 +- .../invocation_processor_default.py | 24 +- .../model_manager/model_manager_base.py | 9 +- .../model_manager/model_manager_common.py | 0 .../model_manager/model_manager_default.py | 44 +- invokeai/app/services/shared/graph.py | 7 +- .../app/services/shared/invocation_context.py | 9 +- invokeai/app/util/step_callback.py | 23 +- 32 files changed, 716 insertions(+), 1191 deletions(-) create mode 100644 invokeai/app/services/model_manager/model_manager_common.py diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 395d5e9870..c4aed1fac5 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -16,10 +16,16 @@ from pydantic import BaseModel, ConfigDict, Field, create_model from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined -from invokeai.app.invocations.fields import FieldKind, Input +from invokeai.app.invocations.fields import ( + FieldDescriptions, + FieldKind, + Input, + InputFieldJSONSchemaExtra, + MetadataField, + logger, +) from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID -from invokeai.app.shared.fields import FieldDescriptions +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.metaenum import MetaEnum from invokeai.app.util.misc import uuid_string from invokeai.backend.util.logging import InvokeAILogger @@ -219,7 +225,7 @@ class BaseInvocation(ABC, BaseModel): """Invoke with provided context and return outputs.""" pass - def invoke_internal(self, context: InvocationContext) -> BaseInvocationOutput: + def invoke_internal(self, context: InvocationContext, services: "InvocationServices") -> BaseInvocationOutput: """ Internal invoke method, calls `invoke()` after some prep. Handles optional fields that are required to call `invoke()` and invocation cache. @@ -244,23 +250,23 @@ class BaseInvocation(ABC, BaseModel): raise MissingInputException(self.model_fields["type"].default, field_name) # skip node cache codepath if it's disabled - if context.services.configuration.node_cache_size == 0: + if services.configuration.node_cache_size == 0: return self.invoke(context) output: BaseInvocationOutput if self.use_cache: - key = context.services.invocation_cache.create_key(self) - cached_value = context.services.invocation_cache.get(key) + key = services.invocation_cache.create_key(self) + cached_value = services.invocation_cache.get(key) if cached_value is None: - context.services.logger.debug(f'Invocation cache miss for type "{self.get_type()}": {self.id}') + services.logger.debug(f'Invocation cache miss for type "{self.get_type()}": {self.id}') output = self.invoke(context) - context.services.invocation_cache.save(key, output) + services.invocation_cache.save(key, output) return output else: - context.services.logger.debug(f'Invocation cache hit for type "{self.get_type()}": {self.id}') + services.logger.debug(f'Invocation cache hit for type "{self.get_type()}": {self.id}') return cached_value else: - context.services.logger.debug(f'Skipping invocation cache for "{self.get_type()}": {self.id}') + services.logger.debug(f'Skipping invocation cache for "{self.get_type()}": {self.id}') return self.invoke(context) id: str = Field( @@ -513,3 +519,29 @@ def invocation_output( return cls return wrapper + + +class WithMetadata(BaseModel): + """ + Inherit from this class if your node needs a metadata input field. + """ + + metadata: Optional[MetadataField] = Field( + default=None, + description=FieldDescriptions.metadata, + json_schema_extra=InputFieldJSONSchemaExtra( + field_kind=FieldKind.Internal, + input=Input.Connection, + orig_required=False, + ).model_dump(exclude_none=True), + ) + + +class WithWorkflow: + workflow = None + + def __init_subclass__(cls) -> None: + logger.warn( + f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow." + ) + super().__init_subclass__() diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index d35a9d79c7..f5709b4ba3 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -7,7 +7,7 @@ from pydantic import ValidationInfo, field_validator from invokeai.app.invocations.primitives import IntegerCollectionOutput from invokeai.app.util.misc import SEED_MAX -from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .baseinvocation import BaseInvocation, invocation from .fields import InputField @@ -27,7 +27,7 @@ class RangeInvocation(BaseInvocation): raise ValueError("stop must be greater than start") return v - def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: + def invoke(self, context) -> IntegerCollectionOutput: return IntegerCollectionOutput(collection=list(range(self.start, self.stop, self.step))) @@ -45,7 +45,7 @@ class RangeOfSizeInvocation(BaseInvocation): size: int = InputField(default=1, gt=0, description="The number of values") step: int = InputField(default=1, description="The step of the range") - def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: + def invoke(self, context) -> IntegerCollectionOutput: return IntegerCollectionOutput( collection=list(range(self.start, self.start + (self.step * self.size), self.step)) ) @@ -72,6 +72,6 @@ class RandomRangeInvocation(BaseInvocation): description="The seed for the RNG (omit for random)", ) - def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: + def invoke(self, context) -> IntegerCollectionOutput: rng = np.random.default_rng(self.seed) return IntegerCollectionOutput(collection=list(rng.integers(low=self.low, high=self.high, size=self.size))) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index b386aef2cb..b4496031bc 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -1,12 +1,18 @@ -from dataclasses import dataclass -from typing import List, Optional, Union +from typing import TYPE_CHECKING, List, Optional, Union import torch from compel import Compel, ReturnedEmbeddingsType from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIComponent -from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput +from invokeai.app.invocations.fields import ( + ConditioningFieldData, + FieldDescriptions, + Input, + InputField, + OutputField, + UIComponent, +) +from invokeai.app.invocations.primitives import ConditioningOutput from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, ExtraConditioningInfo, @@ -20,16 +26,14 @@ from ..util.ti_utils import extract_ti_triggers_from_prompt from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) from .model import ClipField +if TYPE_CHECKING: + from invokeai.app.services.shared.invocation_context import InvocationContext -@dataclass -class ConditioningFieldData: - conditionings: List[BasicConditioningInfo] # unconditioned: Optional[torch.Tensor] @@ -44,7 +48,7 @@ class ConditioningFieldData: title="Prompt", tags=["prompt", "compel"], category="conditioning", - version="1.0.0", + version="1.0.1", ) class CompelInvocation(BaseInvocation): """Parse prompt using compel package to conditioning.""" @@ -61,26 +65,18 @@ class CompelInvocation(BaseInvocation): ) @torch.no_grad() - def invoke(self, context: InvocationContext) -> ConditioningOutput: - tokenizer_info = context.services.model_manager.get_model( - **self.clip.tokenizer.model_dump(), - context=context, - ) - text_encoder_info = context.services.model_manager.get_model( - **self.clip.text_encoder.model_dump(), - context=context, - ) + def invoke(self, context) -> ConditioningOutput: + tokenizer_info = context.models.load(**self.clip.tokenizer.model_dump()) + text_encoder_info = context.models.load(**self.clip.text_encoder.model_dump()) def _lora_loader(): for lora in self.clip.loras: - lora_info = context.services.model_manager.get_model( - **lora.model_dump(exclude={"weight"}), context=context - ) + lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) yield (lora_info.context.model, lora.weight) del lora_info return - # loras = [(context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras] + # loras = [(context.models.get(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras] ti_list = [] for trigger in extract_ti_triggers_from_prompt(self.prompt): @@ -89,11 +85,10 @@ class CompelInvocation(BaseInvocation): ti_list.append( ( name, - context.services.model_manager.get_model( + context.models.load( model_name=name, base_model=self.clip.text_encoder.base_model, model_type=ModelType.TextualInversion, - context=context, ).context.model, ) ) @@ -124,7 +119,7 @@ class CompelInvocation(BaseInvocation): conjunction = Compel.parse_prompt_string(self.prompt) - if context.services.configuration.log_tokenization: + if context.config.get().log_tokenization: log_tokenization_for_conjunction(conjunction, tokenizer) c, options = compel.build_conditioning_tensor_for_conjunction(conjunction) @@ -145,34 +140,23 @@ class CompelInvocation(BaseInvocation): ] ) - conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning" - context.services.latents.save(conditioning_name, conditioning_data) + conditioning_name = context.conditioning.save(conditioning_data) - return ConditioningOutput( - conditioning=ConditioningField( - conditioning_name=conditioning_name, - ), - ) + return ConditioningOutput.build(conditioning_name) class SDXLPromptInvocationBase: def run_clip_compel( self, - context: InvocationContext, + context: "InvocationContext", clip_field: ClipField, prompt: str, get_pooled: bool, lora_prefix: str, zero_on_empty: bool, ): - tokenizer_info = context.services.model_manager.get_model( - **clip_field.tokenizer.model_dump(), - context=context, - ) - text_encoder_info = context.services.model_manager.get_model( - **clip_field.text_encoder.model_dump(), - context=context, - ) + tokenizer_info = context.models.load(**clip_field.tokenizer.model_dump()) + text_encoder_info = context.models.load(**clip_field.text_encoder.model_dump()) # return zero on empty if prompt == "" and zero_on_empty: @@ -196,14 +180,12 @@ class SDXLPromptInvocationBase: def _lora_loader(): for lora in clip_field.loras: - lora_info = context.services.model_manager.get_model( - **lora.model_dump(exclude={"weight"}), context=context - ) + lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) yield (lora_info.context.model, lora.weight) del lora_info return - # loras = [(context.services.model_manager.get_model(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras] + # loras = [(context.models.get(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras] ti_list = [] for trigger in extract_ti_triggers_from_prompt(prompt): @@ -212,11 +194,10 @@ class SDXLPromptInvocationBase: ti_list.append( ( name, - context.services.model_manager.get_model( + context.models.load( model_name=name, base_model=clip_field.text_encoder.base_model, model_type=ModelType.TextualInversion, - context=context, ).context.model, ) ) @@ -249,7 +230,7 @@ class SDXLPromptInvocationBase: conjunction = Compel.parse_prompt_string(prompt) - if context.services.configuration.log_tokenization: + if context.config.get().log_tokenization: # TODO: better logging for and syntax log_tokenization_for_conjunction(conjunction, tokenizer) @@ -282,7 +263,7 @@ class SDXLPromptInvocationBase: title="SDXL Prompt", tags=["sdxl", "compel", "prompt"], category="conditioning", - version="1.0.0", + version="1.0.1", ) class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): """Parse prompt using compel package to conditioning.""" @@ -307,7 +288,7 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): clip2: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP 2") @torch.no_grad() - def invoke(self, context: InvocationContext) -> ConditioningOutput: + def invoke(self, context) -> ConditioningOutput: c1, c1_pooled, ec1 = self.run_clip_compel( context, self.clip, self.prompt, False, "lora_te1_", zero_on_empty=True ) @@ -364,14 +345,9 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): ] ) - conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning" - context.services.latents.save(conditioning_name, conditioning_data) + conditioning_name = context.conditioning.save(conditioning_data) - return ConditioningOutput( - conditioning=ConditioningField( - conditioning_name=conditioning_name, - ), - ) + return ConditioningOutput.build(conditioning_name) @invocation( @@ -379,7 +355,7 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): title="SDXL Refiner Prompt", tags=["sdxl", "compel", "prompt"], category="conditioning", - version="1.0.0", + version="1.0.1", ) class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): """Parse prompt using compel package to conditioning.""" @@ -397,7 +373,7 @@ class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase clip2: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection) @torch.no_grad() - def invoke(self, context: InvocationContext) -> ConditioningOutput: + def invoke(self, context) -> ConditioningOutput: # TODO: if there will appear lora for refiner - write proper prefix c2, c2_pooled, ec2 = self.run_clip_compel(context, self.clip2, self.style, True, "", zero_on_empty=False) @@ -417,14 +393,9 @@ class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase ] ) - conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning" - context.services.latents.save(conditioning_name, conditioning_data) + conditioning_name = context.conditioning.save(conditioning_data) - return ConditioningOutput( - conditioning=ConditioningField( - conditioning_name=conditioning_name, - ), - ) + return ConditioningOutput.build(conditioning_name) @invocation_output("clip_skip_output") @@ -447,7 +418,7 @@ class ClipSkipInvocation(BaseInvocation): clip: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP") skipped_layers: int = InputField(default=0, description=FieldDescriptions.skipped_layers) - def invoke(self, context: InvocationContext) -> ClipSkipInvocationOutput: + def invoke(self, context) -> ClipSkipInvocationOutput: self.clip.skipped_layers += self.skipped_layers return ClipSkipInvocationOutput( clip=self.clip, diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 9b652b8eee..3797722c93 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -25,18 +25,17 @@ from controlnet_aux.util import HWC3, ade_palette from PIL import Image from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, WithMetadata -from invokeai.app.invocations.primitives import ImageField, ImageOutput +from invokeai.app.invocations.baseinvocation import WithMetadata +from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField +from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.invocations.util import validate_begin_end_step, validate_weights -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.backend.image_util.depth_anything import DepthAnythingDetector from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector +from invokeai.backend.model_management.models.base import BaseModelType -from ...backend.model_management import BaseModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -121,7 +120,7 @@ class ControlNetInvocation(BaseInvocation): validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self - def invoke(self, context: InvocationContext) -> ControlOutput: + def invoke(self, context) -> ControlOutput: return ControlOutput( control=ControlField( image=self.image, @@ -145,23 +144,14 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata): # superclass just passes through image without processing return image - def invoke(self, context: InvocationContext) -> ImageOutput: - raw_image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + raw_image = context.images.get_pil(self.image.image_name) # image type should be PIL.PngImagePlugin.PngImageFile ? processed_image = self.run_processor(raw_image) # currently can't see processed image in node UI without a showImage node, # so for now setting image_type to RESULT instead of INTERMEDIATE so will get saved in gallery - image_dto = context.services.images.create( - image=processed_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.CONTROL, - session_id=context.graph_execution_state_id, - node_id=self.id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=processed_image) """Builds an ImageOutput and its ImageField""" processed_image_field = ImageField(image_name=image_dto.image_name) @@ -180,7 +170,7 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata): title="Canny Processor", tags=["controlnet", "canny"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class CannyImageProcessorInvocation(ImageProcessorInvocation): """Canny edge detection for ControlNet""" @@ -203,7 +193,7 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation): title="HED (softedge) Processor", tags=["controlnet", "hed", "softedge"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class HedImageProcessorInvocation(ImageProcessorInvocation): """Applies HED edge detection to image""" @@ -232,7 +222,7 @@ class HedImageProcessorInvocation(ImageProcessorInvocation): title="Lineart Processor", tags=["controlnet", "lineart"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class LineartImageProcessorInvocation(ImageProcessorInvocation): """Applies line art processing to image""" @@ -254,7 +244,7 @@ class LineartImageProcessorInvocation(ImageProcessorInvocation): title="Lineart Anime Processor", tags=["controlnet", "lineart", "anime"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): """Applies line art anime processing to image""" @@ -277,7 +267,7 @@ class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation): title="Midas Depth Processor", tags=["controlnet", "midas"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): """Applies Midas depth processing to image""" @@ -304,7 +294,7 @@ class MidasDepthImageProcessorInvocation(ImageProcessorInvocation): title="Normal BAE Processor", tags=["controlnet"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): """Applies NormalBae processing to image""" @@ -321,7 +311,7 @@ class NormalbaeImageProcessorInvocation(ImageProcessorInvocation): @invocation( - "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.2.0" + "mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.2.1" ) class MlsdImageProcessorInvocation(ImageProcessorInvocation): """Applies MLSD processing to image""" @@ -344,7 +334,7 @@ class MlsdImageProcessorInvocation(ImageProcessorInvocation): @invocation( - "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.2.0" + "pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.2.1" ) class PidiImageProcessorInvocation(ImageProcessorInvocation): """Applies PIDI processing to image""" @@ -371,7 +361,7 @@ class PidiImageProcessorInvocation(ImageProcessorInvocation): title="Content Shuffle Processor", tags=["controlnet", "contentshuffle"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): """Applies content shuffle processing to image""" @@ -401,7 +391,7 @@ class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation): title="Zoe (Depth) Processor", tags=["controlnet", "zoe", "depth"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): """Applies Zoe depth processing to image""" @@ -417,7 +407,7 @@ class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation): title="Mediapipe Face Processor", tags=["controlnet", "mediapipe", "face"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): """Applies mediapipe face processing to image""" @@ -440,7 +430,7 @@ class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): title="Leres (Depth) Processor", tags=["controlnet", "leres", "depth"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class LeresImageProcessorInvocation(ImageProcessorInvocation): """Applies leres processing to image""" @@ -469,7 +459,7 @@ class LeresImageProcessorInvocation(ImageProcessorInvocation): title="Tile Resample Processor", tags=["controlnet", "tile"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class TileResamplerProcessorInvocation(ImageProcessorInvocation): """Tile resampler processor""" @@ -509,7 +499,7 @@ class TileResamplerProcessorInvocation(ImageProcessorInvocation): title="Segment Anything Processor", tags=["controlnet", "segmentanything"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class SegmentAnythingProcessorInvocation(ImageProcessorInvocation): """Applies segment anything processing to image""" @@ -551,7 +541,7 @@ class SamDetectorReproducibleColors(SamDetector): title="Color Map Processor", tags=["controlnet"], category="controlnet", - version="1.2.0", + version="1.2.1", ) class ColorMapImageProcessorInvocation(ImageProcessorInvocation): """Generates a color map from the provided image""" diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 5865338e19..375b18f9c5 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -5,23 +5,23 @@ import cv2 as cv import numpy from PIL import Image, ImageOps -from invokeai.app.invocations.primitives import ImageField, ImageOutput -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin +from invokeai.app.invocations.fields import ImageField +from invokeai.app.invocations.primitives import ImageOutput -from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .baseinvocation import BaseInvocation, invocation from .fields import InputField, WithMetadata -@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.2.0") +@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.2.1") class CvInpaintInvocation(BaseInvocation, WithMetadata): """Simple inpaint using opencv.""" image: ImageField = InputField(description="The image to inpaint") mask: ImageField = InputField(description="The mask to use when inpainting") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) - mask = context.services.images.get_pil_image(self.mask.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) + mask = context.images.get_pil(self.mask.image_name) # Convert to cv image/mask # TODO: consider making these utility functions @@ -35,18 +35,6 @@ class CvInpaintInvocation(BaseInvocation, WithMetadata): # TODO: consider making a utility function image_inpainted = Image.fromarray(cv.cvtColor(cv_inpainted, cv.COLOR_BGR2RGB)) - image_dto = context.services.images.create( - image=image_inpainted, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image_inpainted) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index 13f1066ec3..2c92e28cfe 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -1,7 +1,7 @@ import math import re from pathlib import Path -from typing import Optional, TypedDict +from typing import TYPE_CHECKING, Optional, TypedDict import cv2 import numpy as np @@ -13,13 +13,16 @@ from pydantic import field_validator import invokeai.assets.fonts as font_assets from invokeai.app.invocations.baseinvocation import ( BaseInvocation, - InvocationContext, + WithMetadata, invocation, invocation_output, ) -from invokeai.app.invocations.fields import InputField, OutputField, WithMetadata -from invokeai.app.invocations.primitives import ImageField, ImageOutput -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin +from invokeai.app.invocations.fields import ImageField, InputField, OutputField +from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.image_records.image_records_common import ImageCategory + +if TYPE_CHECKING: + from invokeai.app.services.shared.invocation_context import InvocationContext @invocation_output("face_mask_output") @@ -174,7 +177,7 @@ def prepare_faces_list( def generate_face_box_mask( - context: InvocationContext, + context: "InvocationContext", minimum_confidence: float, x_offset: float, y_offset: float, @@ -273,7 +276,7 @@ def generate_face_box_mask( def extract_face( - context: InvocationContext, + context: "InvocationContext", image: ImageType, face: FaceResultData, padding: int, @@ -304,37 +307,37 @@ def extract_face( # Adjust the crop boundaries to stay within the original image's dimensions if x_min < 0: - context.services.logger.warning("FaceTools --> -X-axis padding reached image edge.") + context.logger.warning("FaceTools --> -X-axis padding reached image edge.") x_max -= x_min x_min = 0 elif x_max > mask.width: - context.services.logger.warning("FaceTools --> +X-axis padding reached image edge.") + context.logger.warning("FaceTools --> +X-axis padding reached image edge.") x_min -= x_max - mask.width x_max = mask.width if y_min < 0: - context.services.logger.warning("FaceTools --> +Y-axis padding reached image edge.") + context.logger.warning("FaceTools --> +Y-axis padding reached image edge.") y_max -= y_min y_min = 0 elif y_max > mask.height: - context.services.logger.warning("FaceTools --> -Y-axis padding reached image edge.") + context.logger.warning("FaceTools --> -Y-axis padding reached image edge.") y_min -= y_max - mask.height y_max = mask.height # Ensure the crop is square and adjust the boundaries if needed if x_max - x_min != crop_size: - context.services.logger.warning("FaceTools --> Limiting x-axis padding to constrain bounding box to a square.") + context.logger.warning("FaceTools --> Limiting x-axis padding to constrain bounding box to a square.") diff = crop_size - (x_max - x_min) x_min -= diff // 2 x_max += diff - diff // 2 if y_max - y_min != crop_size: - context.services.logger.warning("FaceTools --> Limiting y-axis padding to constrain bounding box to a square.") + context.logger.warning("FaceTools --> Limiting y-axis padding to constrain bounding box to a square.") diff = crop_size - (y_max - y_min) y_min -= diff // 2 y_max += diff - diff // 2 - context.services.logger.info(f"FaceTools --> Calculated bounding box (8 multiple): {crop_size}") + context.logger.info(f"FaceTools --> Calculated bounding box (8 multiple): {crop_size}") # Crop the output image to the specified size with the center of the face mesh as the center. mask = mask.crop((x_min, y_min, x_max, y_max)) @@ -354,7 +357,7 @@ def extract_face( def get_faces_list( - context: InvocationContext, + context: "InvocationContext", image: ImageType, should_chunk: bool, minimum_confidence: float, @@ -366,7 +369,7 @@ def get_faces_list( # Generate the face box mask and get the center of the face. if not should_chunk: - context.services.logger.info("FaceTools --> Attempting full image face detection.") + context.logger.info("FaceTools --> Attempting full image face detection.") result = generate_face_box_mask( context=context, minimum_confidence=minimum_confidence, @@ -378,7 +381,7 @@ def get_faces_list( draw_mesh=draw_mesh, ) if should_chunk or len(result) == 0: - context.services.logger.info("FaceTools --> Chunking image (chunk toggled on, or no face found in full image).") + context.logger.info("FaceTools --> Chunking image (chunk toggled on, or no face found in full image).") width, height = image.size image_chunks = [] x_offsets = [] @@ -397,7 +400,7 @@ def get_faces_list( x_offsets.append(x) y_offsets.append(0) fx += increment - context.services.logger.info(f"FaceTools --> Chunk starting at x = {x}") + context.logger.info(f"FaceTools --> Chunk starting at x = {x}") elif height > width: # Portrait - slice the image vertically fy = 0.0 @@ -409,10 +412,10 @@ def get_faces_list( x_offsets.append(0) y_offsets.append(y) fy += increment - context.services.logger.info(f"FaceTools --> Chunk starting at y = {y}") + context.logger.info(f"FaceTools --> Chunk starting at y = {y}") for idx in range(len(image_chunks)): - context.services.logger.info(f"FaceTools --> Evaluating faces in chunk {idx}") + context.logger.info(f"FaceTools --> Evaluating faces in chunk {idx}") result = result + generate_face_box_mask( context=context, minimum_confidence=minimum_confidence, @@ -426,7 +429,7 @@ def get_faces_list( if len(result) == 0: # Give up - context.services.logger.warning( + context.logger.warning( "FaceTools --> No face detected in chunked input image. Passing through original image." ) @@ -435,7 +438,7 @@ def get_faces_list( return all_faces -@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.2.0") +@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.2.1") class FaceOffInvocation(BaseInvocation, WithMetadata): """Bound, extract, and mask a face from an image using MediaPipe detection""" @@ -456,7 +459,7 @@ class FaceOffInvocation(BaseInvocation, WithMetadata): description="Whether to bypass full image face detection and default to image chunking. Chunking will occur if no faces are found in the full image.", ) - def faceoff(self, context: InvocationContext, image: ImageType) -> Optional[ExtractFaceData]: + def faceoff(self, context: "InvocationContext", image: ImageType) -> Optional[ExtractFaceData]: all_faces = get_faces_list( context=context, image=image, @@ -468,11 +471,11 @@ class FaceOffInvocation(BaseInvocation, WithMetadata): ) if len(all_faces) == 0: - context.services.logger.warning("FaceOff --> No faces detected. Passing through original image.") + context.logger.warning("FaceOff --> No faces detected. Passing through original image.") return None if self.face_id > len(all_faces) - 1: - context.services.logger.warning( + context.logger.warning( f"FaceOff --> Face ID {self.face_id} is outside of the number of faces detected ({len(all_faces)}). Passing through original image." ) return None @@ -483,8 +486,8 @@ class FaceOffInvocation(BaseInvocation, WithMetadata): return face_data - def invoke(self, context: InvocationContext) -> FaceOffOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> FaceOffOutput: + image = context.images.get_pil(self.image.image_name) result = self.faceoff(context=context, image=image) if result is None: @@ -498,24 +501,9 @@ class FaceOffInvocation(BaseInvocation, WithMetadata): x = result["x_min"] y = result["y_min"] - image_dto = context.services.images.create( - image=result_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - workflow=context.workflow, - ) + image_dto = context.images.save(image=result_image) - mask_dto = context.services.images.create( - image=result_mask, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.MASK, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - ) + mask_dto = context.images.save(image=result_mask, image_category=ImageCategory.MASK) output = FaceOffOutput( image=ImageField(image_name=image_dto.image_name), @@ -529,7 +517,7 @@ class FaceOffInvocation(BaseInvocation, WithMetadata): return output -@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.2.0") +@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.2.1") class FaceMaskInvocation(BaseInvocation, WithMetadata): """Face mask creation using mediapipe face detection""" @@ -556,7 +544,7 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata): raise ValueError('Face IDs must be a comma-separated list of integers (e.g. "1,2,3")') return v - def facemask(self, context: InvocationContext, image: ImageType) -> FaceMaskResult: + def facemask(self, context: "InvocationContext", image: ImageType) -> FaceMaskResult: all_faces = get_faces_list( context=context, image=image, @@ -578,7 +566,7 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata): if len(intersected_face_ids) == 0: id_range_str = ",".join([str(id) for id in id_range]) - context.services.logger.warning( + context.logger.warning( f"Face IDs must be in range of detected faces - requested {self.face_ids}, detected {id_range_str}. Passing through original image." ) return FaceMaskResult( @@ -613,28 +601,13 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata): mask=mask_pil, ) - def invoke(self, context: InvocationContext) -> FaceMaskOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> FaceMaskOutput: + image = context.images.get_pil(self.image.image_name) result = self.facemask(context=context, image=image) - image_dto = context.services.images.create( - image=result["image"], - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - workflow=context.workflow, - ) + image_dto = context.images.save(image=result["image"]) - mask_dto = context.services.images.create( - image=result["mask"], - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.MASK, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - ) + mask_dto = context.images.save(image=result["mask"], image_category=ImageCategory.MASK) output = FaceMaskOutput( image=ImageField(image_name=image_dto.image_name), @@ -647,7 +620,7 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata): @invocation( - "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.2.0" + "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.2.1" ) class FaceIdentifierInvocation(BaseInvocation, WithMetadata): """Outputs an image with detected face IDs printed on each face. For use with other FaceTools.""" @@ -661,7 +634,7 @@ class FaceIdentifierInvocation(BaseInvocation, WithMetadata): description="Whether to bypass full image face detection and default to image chunking. Chunking will occur if no faces are found in the full image.", ) - def faceidentifier(self, context: InvocationContext, image: ImageType) -> ImageType: + def faceidentifier(self, context: "InvocationContext", image: ImageType) -> ImageType: image = image.copy() all_faces = get_faces_list( @@ -702,22 +675,10 @@ class FaceIdentifierInvocation(BaseInvocation, WithMetadata): return image - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) result_image = self.faceidentifier(context=context, image=image) - image_dto = context.services.images.create( - image=result_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - workflow=context.workflow, - ) + image_dto = context.images.save(image=result_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 0cce8e3c6b..566babbb6b 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -1,11 +1,13 @@ +from dataclasses import dataclass from enum import Enum -from typing import Any, Callable, Optional +from typing import Any, Callable, List, Optional, Tuple from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter from pydantic.fields import _Unset from pydantic_core import PydanticUndefined from invokeai.app.util.metaenum import MetaEnum +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import BasicConditioningInfo from invokeai.backend.util.logging import InvokeAILogger logger = InvokeAILogger.get_logger() @@ -255,6 +257,10 @@ class InputFieldJSONSchemaExtra(BaseModel): class WithMetadata(BaseModel): + """ + Inherit from this class if your node needs a metadata input field. + """ + metadata: Optional[MetadataField] = Field( default=None, description=FieldDescriptions.metadata, @@ -498,4 +504,53 @@ def OutputField( field_kind=FieldKind.Output, ).model_dump(exclude_none=True), ) + + +class ImageField(BaseModel): + """An image primitive field""" + + image_name: str = Field(description="The name of the image") + + +class BoardField(BaseModel): + """A board primitive field""" + + board_id: str = Field(description="The id of the board") + + +class DenoiseMaskField(BaseModel): + """An inpaint mask field""" + + mask_name: str = Field(description="The name of the mask image") + masked_latents_name: Optional[str] = Field(default=None, description="The name of the masked image latents") + + +class LatentsField(BaseModel): + """A latents tensor primitive field""" + + latents_name: str = Field(description="The name of the latents") + seed: Optional[int] = Field(default=None, description="Seed used to generate this latents") + + +class ColorField(BaseModel): + """A color primitive field""" + + r: int = Field(ge=0, le=255, description="The red component") + g: int = Field(ge=0, le=255, description="The green component") + b: int = Field(ge=0, le=255, description="The blue component") + a: int = Field(ge=0, le=255, description="The alpha component") + + def tuple(self) -> Tuple[int, int, int, int]: + return (self.r, self.g, self.b, self.a) + + +@dataclass +class ConditioningFieldData: + conditionings: List[BasicConditioningInfo] + + +class ConditioningField(BaseModel): + """A conditioning tensor primitive value""" + + conditioning_name: str = Field(description="The name of conditioning tensor") # endregion diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 16d0f33dda..10ebd97ace 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -7,30 +7,36 @@ import cv2 import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, WithMetadata -from invokeai.app.invocations.primitives import BoardField, ColorField, ImageField, ImageOutput -from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin +from invokeai.app.invocations.baseinvocation import WithMetadata +from invokeai.app.invocations.fields import ( + BoardField, + ColorField, + FieldDescriptions, + ImageField, + Input, + InputField, +) +from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.safety_checker import SafetyChecker from .baseinvocation import ( BaseInvocation, Classification, - InvocationContext, invocation, ) -@invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.0") +@invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.1") class ShowImageInvocation(BaseInvocation): """Displays a provided image using the OS image viewer, and passes it forward in the pipeline.""" image: ImageField = InputField(description="The image to show") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) - if image: - image.show() + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) + image.show() # TODO: how to handle failure? @@ -46,7 +52,7 @@ class ShowImageInvocation(BaseInvocation): title="Blank Image", tags=["image"], category="image", - version="1.2.0", + version="1.2.1", ) class BlankImageInvocation(BaseInvocation, WithMetadata): """Creates a blank image and forwards it to the pipeline""" @@ -56,25 +62,12 @@ class BlankImageInvocation(BaseInvocation, WithMetadata): mode: Literal["RGB", "RGBA"] = InputField(default="RGB", description="The mode of the image") color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=255), description="The color of the image") - def invoke(self, context: InvocationContext) -> ImageOutput: + def invoke(self, context) -> ImageOutput: image = Image.new(mode=self.mode, size=(self.width, self.height), color=self.color.tuple()) - image_dto = context.services.images.create( - image=image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -82,7 +75,7 @@ class BlankImageInvocation(BaseInvocation, WithMetadata): title="Crop Image", tags=["image", "crop"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageCropInvocation(BaseInvocation, WithMetadata): """Crops an image to a specified box. The box can be outside of the image.""" @@ -93,28 +86,15 @@ class ImageCropInvocation(BaseInvocation, WithMetadata): width: int = InputField(default=512, gt=0, description="The width of the crop rectangle") height: int = InputField(default=512, gt=0, description="The height of the crop rectangle") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) image_crop = Image.new(mode="RGBA", size=(self.width, self.height), color=(0, 0, 0, 0)) image_crop.paste(image, (-self.x, -self.y)) - image_dto = context.services.images.create( - image=image_crop, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image_crop) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -145,8 +125,8 @@ class CenterPadCropInvocation(BaseInvocation): description="Number of pixels to pad/crop from the bottom (negative values crop inwards, positive values pad outwards)", ) - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) # Calculate and create new image dimensions new_width = image.width + self.right + self.left @@ -156,20 +136,9 @@ class CenterPadCropInvocation(BaseInvocation): # Paste new image onto input image_crop.paste(image, (self.left, self.top)) - image_dto = context.services.images.create( - image=image_crop, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - ) + image_dto = context.images.save(image=image_crop) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -177,7 +146,7 @@ class CenterPadCropInvocation(BaseInvocation): title="Paste Image", tags=["image", "paste"], category="image", - version="1.2.0", + version="1.2.1", ) class ImagePasteInvocation(BaseInvocation, WithMetadata): """Pastes an image into another image.""" @@ -192,12 +161,12 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata): y: int = InputField(default=0, description="The top y coordinate at which to paste the image") crop: bool = InputField(default=False, description="Crop to base image dimensions") - def invoke(self, context: InvocationContext) -> ImageOutput: - base_image = context.services.images.get_pil_image(self.base_image.image_name) - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + base_image = context.images.get_pil(self.base_image.image_name) + image = context.images.get_pil(self.image.image_name) mask = None if self.mask is not None: - mask = context.services.images.get_pil_image(self.mask.image_name) + mask = context.images.get_pil(self.mask.image_name) mask = ImageOps.invert(mask.convert("L")) # TODO: probably shouldn't invert mask here... should user be required to do it? @@ -214,22 +183,9 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata): base_w, base_h = base_image.size new_image = new_image.crop((abs(min_x), abs(min_y), abs(min_x) + base_w, abs(min_y) + base_h)) - image_dto = context.services.images.create( - image=new_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=new_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -237,7 +193,7 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata): title="Mask from Alpha", tags=["image", "mask"], category="image", - version="1.2.0", + version="1.2.1", ) class MaskFromAlphaInvocation(BaseInvocation, WithMetadata): """Extracts the alpha channel of an image as a mask.""" @@ -245,29 +201,16 @@ class MaskFromAlphaInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to create the mask from") invert: bool = InputField(default=False, description="Whether or not to invert the mask") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) image_mask = image.split()[-1] if self.invert: image_mask = ImageOps.invert(image_mask) - image_dto = context.services.images.create( - image=image_mask, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.MASK, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image_mask, image_category=ImageCategory.MASK) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -275,7 +218,7 @@ class MaskFromAlphaInvocation(BaseInvocation, WithMetadata): title="Multiply Images", tags=["image", "multiply"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageMultiplyInvocation(BaseInvocation, WithMetadata): """Multiplies two images together using `PIL.ImageChops.multiply()`.""" @@ -283,28 +226,15 @@ class ImageMultiplyInvocation(BaseInvocation, WithMetadata): image1: ImageField = InputField(description="The first image to multiply") image2: ImageField = InputField(description="The second image to multiply") - def invoke(self, context: InvocationContext) -> ImageOutput: - image1 = context.services.images.get_pil_image(self.image1.image_name) - image2 = context.services.images.get_pil_image(self.image2.image_name) + def invoke(self, context) -> ImageOutput: + image1 = context.images.get_pil(self.image1.image_name) + image2 = context.images.get_pil(self.image2.image_name) multiply_image = ImageChops.multiply(image1, image2) - image_dto = context.services.images.create( - image=multiply_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=multiply_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) IMAGE_CHANNELS = Literal["A", "R", "G", "B"] @@ -315,7 +245,7 @@ IMAGE_CHANNELS = Literal["A", "R", "G", "B"] title="Extract Image Channel", tags=["image", "channel"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageChannelInvocation(BaseInvocation, WithMetadata): """Gets a channel from an image.""" @@ -323,27 +253,14 @@ class ImageChannelInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to get the channel from") channel: IMAGE_CHANNELS = InputField(default="A", description="The channel to get") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) channel_image = image.getchannel(self.channel) - image_dto = context.services.images.create( - image=channel_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=channel_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] @@ -354,7 +271,7 @@ IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F title="Convert Image Mode", tags=["image", "convert"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageConvertInvocation(BaseInvocation, WithMetadata): """Converts an image to a different mode.""" @@ -362,27 +279,14 @@ class ImageConvertInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to convert") mode: IMAGE_MODES = InputField(default="L", description="The mode to convert to") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) converted_image = image.convert(self.mode) - image_dto = context.services.images.create( - image=converted_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=converted_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -390,7 +294,7 @@ class ImageConvertInvocation(BaseInvocation, WithMetadata): title="Blur Image", tags=["image", "blur"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageBlurInvocation(BaseInvocation, WithMetadata): """Blurs an image""" @@ -400,30 +304,17 @@ class ImageBlurInvocation(BaseInvocation, WithMetadata): # Metadata blur_type: Literal["gaussian", "box"] = InputField(default="gaussian", description="The type of blur") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) blur = ( ImageFilter.GaussianBlur(self.radius) if self.blur_type == "gaussian" else ImageFilter.BoxBlur(self.radius) ) blur_image = image.filter(blur) - image_dto = context.services.images.create( - image=blur_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=blur_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -431,7 +322,7 @@ class ImageBlurInvocation(BaseInvocation, WithMetadata): title="Unsharp Mask", tags=["image", "unsharp_mask"], category="image", - version="1.2.0", + version="1.2.1", classification=Classification.Beta, ) class UnsharpMaskInvocation(BaseInvocation, WithMetadata): @@ -447,8 +338,8 @@ class UnsharpMaskInvocation(BaseInvocation, WithMetadata): def array_from_pil(self, img): return numpy.array(img) / 255 - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) mode = image.mode alpha_channel = image.getchannel("A") if mode == "RGBA" else None @@ -466,16 +357,7 @@ class UnsharpMaskInvocation(BaseInvocation, WithMetadata): if alpha_channel is not None: image.putalpha(alpha_channel) - image_dto = context.services.images.create( - image=image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image) return ImageOutput( image=ImageField(image_name=image_dto.image_name), @@ -509,7 +391,7 @@ PIL_RESAMPLING_MAP = { title="Resize Image", tags=["image", "resize"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageResizeInvocation(BaseInvocation, WithMetadata): """Resizes an image to specific dimensions""" @@ -519,8 +401,8 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata): height: int = InputField(default=512, gt=0, description="The height to resize to (px)") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) resample_mode = PIL_RESAMPLING_MAP[self.resample_mode] @@ -529,22 +411,9 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata): resample=resample_mode, ) - image_dto = context.services.images.create( - image=resize_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=resize_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -552,7 +421,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata): title="Scale Image", tags=["image", "scale"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageScaleInvocation(BaseInvocation, WithMetadata): """Scales an image by a factor""" @@ -565,8 +434,8 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata): ) resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) resample_mode = PIL_RESAMPLING_MAP[self.resample_mode] width = int(image.width * self.scale_factor) @@ -577,22 +446,9 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata): resample=resample_mode, ) - image_dto = context.services.images.create( - image=resize_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=resize_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -600,7 +456,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata): title="Lerp Image", tags=["image", "lerp"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageLerpInvocation(BaseInvocation, WithMetadata): """Linear interpolation of all pixels of an image""" @@ -609,30 +465,17 @@ class ImageLerpInvocation(BaseInvocation, WithMetadata): min: int = InputField(default=0, ge=0, le=255, description="The minimum output value") max: int = InputField(default=255, ge=0, le=255, description="The maximum output value") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) image_arr = numpy.asarray(image, dtype=numpy.float32) / 255 image_arr = image_arr * (self.max - self.min) + self.min lerp_image = Image.fromarray(numpy.uint8(image_arr)) - image_dto = context.services.images.create( - image=lerp_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=lerp_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -640,7 +483,7 @@ class ImageLerpInvocation(BaseInvocation, WithMetadata): title="Inverse Lerp Image", tags=["image", "ilerp"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageInverseLerpInvocation(BaseInvocation, WithMetadata): """Inverse linear interpolation of all pixels of an image""" @@ -649,30 +492,17 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata): min: int = InputField(default=0, ge=0, le=255, description="The minimum input value") max: int = InputField(default=255, ge=0, le=255, description="The maximum input value") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) image_arr = numpy.asarray(image, dtype=numpy.float32) image_arr = numpy.minimum(numpy.maximum(image_arr - self.min, 0) / float(self.max - self.min), 1) * 255 # type: ignore [assignment] ilerp_image = Image.fromarray(numpy.uint8(image_arr)) - image_dto = context.services.images.create( - image=ilerp_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=ilerp_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -680,17 +510,17 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata): title="Blur NSFW Image", tags=["image", "nsfw"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata): """Add blur to NSFW-flagged images""" image: ImageField = InputField(description="The image to check") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) - logger = context.services.logger + logger = context.logger logger.debug("Running NSFW checker") if SafetyChecker.has_nsfw_concept(image): logger.info("A potentially NSFW image has been detected. Image will be blurred.") @@ -699,22 +529,9 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata): blurry_image.paste(caution, (0, 0), caution) image = blurry_image - image_dto = context.services.images.create( - image=image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) def _get_caution_img(self) -> Image.Image: import invokeai.app.assets.images as image_assets @@ -728,7 +545,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata): title="Add Invisible Watermark", tags=["image", "watermark"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageWatermarkInvocation(BaseInvocation, WithMetadata): """Add an invisible watermark to an image""" @@ -736,25 +553,12 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to check") text: str = InputField(default="InvokeAI", description="Watermark text") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) new_image = InvisibleWatermark.add_watermark(image, self.text) - image_dto = context.services.images.create( - image=new_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=new_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -762,7 +566,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata): title="Mask Edge", tags=["image", "mask", "inpaint"], category="image", - version="1.2.0", + version="1.2.1", ) class MaskEdgeInvocation(BaseInvocation, WithMetadata): """Applies an edge mask to an image""" @@ -775,8 +579,8 @@ class MaskEdgeInvocation(BaseInvocation, WithMetadata): description="Second threshold for the hysteresis procedure in Canny edge detection" ) - def invoke(self, context: InvocationContext) -> ImageOutput: - mask = context.services.images.get_pil_image(self.image.image_name).convert("L") + def invoke(self, context) -> ImageOutput: + mask = context.images.get_pil(self.image.image_name).convert("L") npimg = numpy.asarray(mask, dtype=numpy.uint8) npgradient = numpy.uint8(255 * (1.0 - numpy.floor(numpy.abs(0.5 - numpy.float32(npimg) / 255.0) * 2.0))) @@ -791,22 +595,9 @@ class MaskEdgeInvocation(BaseInvocation, WithMetadata): new_mask = ImageOps.invert(new_mask) - image_dto = context.services.images.create( - image=new_mask, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.MASK, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=new_mask, image_category=ImageCategory.MASK) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -814,7 +605,7 @@ class MaskEdgeInvocation(BaseInvocation, WithMetadata): title="Combine Masks", tags=["image", "mask", "multiply"], category="image", - version="1.2.0", + version="1.2.1", ) class MaskCombineInvocation(BaseInvocation, WithMetadata): """Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.""" @@ -822,28 +613,15 @@ class MaskCombineInvocation(BaseInvocation, WithMetadata): mask1: ImageField = InputField(description="The first mask to combine") mask2: ImageField = InputField(description="The second image to combine") - def invoke(self, context: InvocationContext) -> ImageOutput: - mask1 = context.services.images.get_pil_image(self.mask1.image_name).convert("L") - mask2 = context.services.images.get_pil_image(self.mask2.image_name).convert("L") + def invoke(self, context) -> ImageOutput: + mask1 = context.images.get_pil(self.mask1.image_name).convert("L") + mask2 = context.images.get_pil(self.mask2.image_name).convert("L") combined_mask = ImageChops.multiply(mask1, mask2) - image_dto = context.services.images.create( - image=combined_mask, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=combined_mask, image_category=ImageCategory.MASK) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -851,7 +629,7 @@ class MaskCombineInvocation(BaseInvocation, WithMetadata): title="Color Correct", tags=["image", "color"], category="image", - version="1.2.0", + version="1.2.1", ) class ColorCorrectInvocation(BaseInvocation, WithMetadata): """ @@ -864,14 +642,14 @@ class ColorCorrectInvocation(BaseInvocation, WithMetadata): mask: Optional[ImageField] = InputField(default=None, description="Mask to use when applying color-correction") mask_blur_radius: float = InputField(default=8, description="Mask blur radius") - def invoke(self, context: InvocationContext) -> ImageOutput: + def invoke(self, context) -> ImageOutput: pil_init_mask = None if self.mask is not None: - pil_init_mask = context.services.images.get_pil_image(self.mask.image_name).convert("L") + pil_init_mask = context.images.get_pil(self.mask.image_name).convert("L") - init_image = context.services.images.get_pil_image(self.reference.image_name) + init_image = context.images.get_pil(self.reference.image_name) - result = context.services.images.get_pil_image(self.image.image_name).convert("RGBA") + result = context.images.get_pil(self.image.image_name).convert("RGBA") # if init_image is None or init_mask is None: # return result @@ -945,22 +723,9 @@ class ColorCorrectInvocation(BaseInvocation, WithMetadata): # Paste original on color-corrected generation (using blurred mask) matched_result.paste(init_image, (0, 0), mask=multiplied_blurred_init_mask) - image_dto = context.services.images.create( - image=matched_result, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=matched_result) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -968,7 +733,7 @@ class ColorCorrectInvocation(BaseInvocation, WithMetadata): title="Adjust Image Hue", tags=["image", "hue"], category="image", - version="1.2.0", + version="1.2.1", ) class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata): """Adjusts the Hue of an image.""" @@ -976,8 +741,8 @@ class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to adjust") hue: int = InputField(default=0, description="The degrees by which to rotate the hue, 0-360") - def invoke(self, context: InvocationContext) -> ImageOutput: - pil_image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + pil_image = context.images.get_pil(self.image.image_name) # Convert image to HSV color space hsv_image = numpy.array(pil_image.convert("HSV")) @@ -991,24 +756,9 @@ class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata): # Convert back to PIL format and to original color mode pil_image = Image.fromarray(hsv_image, mode="HSV").convert("RGBA") - image_dto = context.services.images.create( - image=pil_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - is_intermediate=self.is_intermediate, - session_id=context.graph_execution_state_id, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=pil_image) - return ImageOutput( - image=ImageField( - image_name=image_dto.image_name, - ), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) COLOR_CHANNELS = Literal[ @@ -1072,7 +822,7 @@ CHANNEL_FORMATS = { "value", ], category="image", - version="1.2.0", + version="1.2.1", ) class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): """Add or subtract a value from a specific color channel of an image.""" @@ -1081,8 +831,8 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): channel: COLOR_CHANNELS = InputField(description="Which channel to adjust") offset: int = InputField(default=0, ge=-255, le=255, description="The amount to adjust the channel by") - def invoke(self, context: InvocationContext) -> ImageOutput: - pil_image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + pil_image = context.images.get_pil(self.image.image_name) # extract the channel and mode from the input and reference tuple mode = CHANNEL_FORMATS[self.channel][0] @@ -1101,24 +851,9 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): # Convert back to RGBA format and output pil_image = Image.fromarray(converted_image.astype(numpy.uint8), mode=mode).convert("RGBA") - image_dto = context.services.images.create( - image=pil_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - is_intermediate=self.is_intermediate, - session_id=context.graph_execution_state_id, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=pil_image) - return ImageOutput( - image=ImageField( - image_name=image_dto.image_name, - ), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -1143,7 +878,7 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): "value", ], category="image", - version="1.2.0", + version="1.2.1", ) class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): """Scale a specific color channel of an image.""" @@ -1153,8 +888,8 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): scale: float = InputField(default=1.0, ge=0.0, description="The amount to scale the channel by.") invert_channel: bool = InputField(default=False, description="Invert the channel after scaling") - def invoke(self, context: InvocationContext) -> ImageOutput: - pil_image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + pil_image = context.images.get_pil(self.image.image_name) # extract the channel and mode from the input and reference tuple mode = CHANNEL_FORMATS[self.channel][0] @@ -1177,24 +912,9 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): # Convert back to RGBA format and output pil_image = Image.fromarray(converted_image.astype(numpy.uint8), mode=mode).convert("RGBA") - image_dto = context.services.images.create( - image=pil_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - is_intermediate=self.is_intermediate, - session_id=context.graph_execution_state_id, - workflow=context.workflow, - metadata=self.metadata, - ) + image_dto = context.images.save(image=pil_image) - return ImageOutput( - image=ImageField( - image_name=image_dto.image_name, - ), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -1202,7 +922,7 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): title="Save Image", tags=["primitives", "image"], category="primitives", - version="1.2.0", + version="1.2.1", use_cache=False, ) class SaveImageInvocation(BaseInvocation, WithMetadata): @@ -1211,26 +931,12 @@ class SaveImageInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description=FieldDescriptions.image) board: BoardField = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) - image_dto = context.services.images.create( - image=image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - board_id=self.board.board_id if self.board else None, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image, board_id=self.board.board_id if self.board else None) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( @@ -1238,7 +944,7 @@ class SaveImageInvocation(BaseInvocation, WithMetadata): title="Linear UI Image Output", tags=["primitives", "image"], category="primitives", - version="1.0.1", + version="1.0.2", use_cache=False, ) class LinearUIOutputInvocation(BaseInvocation, WithMetadata): @@ -1247,19 +953,13 @@ class LinearUIOutputInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description=FieldDescriptions.image) board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) - def invoke(self, context: InvocationContext) -> ImageOutput: - image_dto = context.services.images.get_dto(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image_dto = context.images.get_dto(self.image.image_name) - if self.board: - context.services.board_images.add_image_to_board(self.board.board_id, self.image.image_name) - - if image_dto.is_intermediate != self.is_intermediate: - context.services.images.update( - self.image.image_name, changes=ImageRecordChanges(is_intermediate=self.is_intermediate) - ) - - return ImageOutput( - image=ImageField(image_name=self.image.image_name), - width=image_dto.width, - height=image_dto.height, + image_dto = context.images.update( + image_name=self.image.image_name, + board_id=self.board.board_id if self.board else None, + is_intermediate=self.is_intermediate, ) + + return ImageOutput.build(image_dto) diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index d4d3d5bea4..be51c8312f 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -6,15 +6,15 @@ from typing import Literal, Optional, get_args import numpy as np from PIL import Image, ImageOps -from invokeai.app.invocations.primitives import ColorField, ImageField, ImageOutput -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin +from invokeai.app.invocations.fields import ColorField, ImageField +from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.util.misc import SEED_MAX from invokeai.backend.image_util.cv2_inpaint import cv2_inpaint from invokeai.backend.image_util.lama import LaMA from invokeai.backend.image_util.patchmatch import PatchMatch -from .baseinvocation import BaseInvocation, InvocationContext, invocation -from .fields import InputField, WithMetadata +from .baseinvocation import BaseInvocation, WithMetadata, invocation +from .fields import InputField from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES @@ -119,7 +119,7 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int] return si -@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0") +@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") class InfillColorInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image with a solid color""" @@ -129,33 +129,20 @@ class InfillColorInvocation(BaseInvocation, WithMetadata): description="The color to use to infill", ) - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) solid_bg = Image.new("RGBA", image.size, self.color.tuple()) infilled = Image.alpha_composite(solid_bg, image.convert("RGBA")) infilled.paste(image, (0, 0), image.split()[-1]) - image_dto = context.services.images.create( - image=infilled, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=infilled) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) -@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") +@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2") class InfillTileInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image with tiles of the image""" @@ -168,32 +155,19 @@ class InfillTileInvocation(BaseInvocation, WithMetadata): description="The seed to use for tile generation (omit for random)", ) - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) infilled = tile_fill_missing(image.copy(), seed=self.seed, tile_size=self.tile_size) infilled.paste(image, (0, 0), image.split()[-1]) - image_dto = context.services.images.create( - image=infilled, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=infilled) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) @invocation( - "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0" + "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1" ) class InfillPatchMatchInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image using the PatchMatch algorithm""" @@ -202,8 +176,8 @@ class InfillPatchMatchInvocation(BaseInvocation, WithMetadata): downscale: float = InputField(default=2.0, gt=0, description="Run patchmatch on downscaled image to speedup infill") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name).convert("RGBA") + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name).convert("RGBA") resample_mode = PIL_RESAMPLING_MAP[self.resample_mode] @@ -228,77 +202,38 @@ class InfillPatchMatchInvocation(BaseInvocation, WithMetadata): infilled.paste(image, (0, 0), mask=image.split()[-1]) # image.paste(infilled, (0, 0), mask=image.split()[-1]) - image_dto = context.services.images.create( - image=infilled, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=infilled) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) -@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0") +@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") class LaMaInfillInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image using the LaMa model""" image: ImageField = InputField(description="The image to infill") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) infilled = infill_lama(image.copy()) - image_dto = context.services.images.create( - image=infilled, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=infilled) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) -@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.0") +@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") class CV2InfillInvocation(BaseInvocation, WithMetadata): """Infills transparent areas of an image using OpenCV Inpainting""" image: ImageField = InputField(description="The image to infill") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) infilled = infill_cv2(image.copy()) - image_dto = context.services.images.create( - image=infilled, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=infilled) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index c01e0ed0fb..b836be04b5 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -7,7 +7,6 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -62,7 +61,7 @@ class IPAdapterOutput(BaseInvocationOutput): ip_adapter: IPAdapterField = OutputField(description=FieldDescriptions.ip_adapter, title="IP-Adapter") -@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.1.1") +@invocation("ip_adapter", title="IP-Adapter", tags=["ip_adapter", "control"], category="ip_adapter", version="1.1.2") class IPAdapterInvocation(BaseInvocation): """Collects IP-Adapter info to pass to other nodes.""" @@ -93,9 +92,9 @@ class IPAdapterInvocation(BaseInvocation): validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self - def invoke(self, context: InvocationContext) -> IPAdapterOutput: + def invoke(self, context) -> IPAdapterOutput: # Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model. - ip_adapter_info = context.services.model_manager.model_info( + ip_adapter_info = context.models.get_info( self.ip_adapter_model.model_name, self.ip_adapter_model.base_model, ModelType.IPAdapter ) # HACK(ryand): This is bad for a couple of reasons: 1) we are bypassing the model manager to read the model @@ -104,7 +103,7 @@ class IPAdapterInvocation(BaseInvocation): # is currently messy due to differences between how the model info is generated when installing a model from # disk vs. downloading the model. image_encoder_model_id = get_ip_adapter_image_encoder_model_id( - os.path.join(context.services.configuration.get_config().models_path, ip_adapter_info["path"]) + os.path.join(context.config.get().models_path, ip_adapter_info["path"]) ) image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip() image_encoder_model = CLIPVisionModelField( diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 909c307481..0127a6521e 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -3,7 +3,7 @@ import math from contextlib import ExitStack from functools import singledispatchmethod -from typing import List, Literal, Optional, Union +from typing import TYPE_CHECKING, List, Literal, Optional, Union import einops import numpy as np @@ -23,21 +23,26 @@ from diffusers.schedulers import SchedulerMixin as Scheduler from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType, WithMetadata +from invokeai.app.invocations.fields import ( + ConditioningField, + DenoiseMaskField, + FieldDescriptions, + ImageField, + Input, + InputField, + LatentsField, + OutputField, + UIType, + WithMetadata, +) from invokeai.app.invocations.ip_adapter import IPAdapterField from invokeai.app.invocations.primitives import ( - DenoiseMaskField, DenoiseMaskOutput, - ImageField, ImageOutput, - LatentsField, LatentsOutput, - build_latents_output, ) from invokeai.app.invocations.t2i_adapter import T2IAdapterField -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.app.util.controlnet_utils import prepare_control_image -from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus from invokeai.backend.model_management.models import ModelType, SilenceWarnings from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningData, IPAdapterConditioningInfo @@ -59,14 +64,15 @@ from ...backend.util.devices import choose_precision, choose_torch_device from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) -from .compel import ConditioningField from .controlnet_image_processors import ControlField from .model import ModelInfo, UNetField, VaeField +if TYPE_CHECKING: + from invokeai.app.services.shared.invocation_context import InvocationContext + if choose_torch_device() == torch.device("mps"): from torch import mps @@ -102,7 +108,7 @@ class SchedulerInvocation(BaseInvocation): ui_type=UIType.Scheduler, ) - def invoke(self, context: InvocationContext) -> SchedulerOutput: + def invoke(self, context) -> SchedulerOutput: return SchedulerOutput(scheduler=self.scheduler) @@ -111,7 +117,7 @@ class SchedulerInvocation(BaseInvocation): title="Create Denoise Mask", tags=["mask", "denoise"], category="latents", - version="1.0.0", + version="1.0.1", ) class CreateDenoiseMaskInvocation(BaseInvocation): """Creates mask for denoising model run.""" @@ -137,9 +143,9 @@ class CreateDenoiseMaskInvocation(BaseInvocation): return mask_tensor @torch.no_grad() - def invoke(self, context: InvocationContext) -> DenoiseMaskOutput: + def invoke(self, context) -> DenoiseMaskOutput: if self.image is not None: - image = context.services.images.get_pil_image(self.image.image_name) + image = context.images.get_pil(self.image.image_name) image = image_resized_to_grid_as_tensor(image.convert("RGB")) if image.dim() == 3: image = image.unsqueeze(0) @@ -147,47 +153,37 @@ class CreateDenoiseMaskInvocation(BaseInvocation): image = None mask = self.prep_mask_tensor( - context.services.images.get_pil_image(self.mask.image_name), + context.images.get_pil(self.mask.image_name), ) if image is not None: - vae_info = context.services.model_manager.get_model( - **self.vae.vae.model_dump(), - context=context, - ) + vae_info = context.models.load(**self.vae.vae.model_dump()) img_mask = tv_resize(mask, image.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) masked_image = image * torch.where(img_mask < 0.5, 0.0, 1.0) # TODO: masked_latents = ImageToLatentsInvocation.vae_encode(vae_info, self.fp32, self.tiled, masked_image.clone()) - masked_latents_name = f"{context.graph_execution_state_id}__{self.id}_masked_latents" - context.services.latents.save(masked_latents_name, masked_latents) + masked_latents_name = context.latents.save(tensor=masked_latents) else: masked_latents_name = None - mask_name = f"{context.graph_execution_state_id}__{self.id}_mask" - context.services.latents.save(mask_name, mask) + mask_name = context.latents.save(tensor=mask) - return DenoiseMaskOutput( - denoise_mask=DenoiseMaskField( - mask_name=mask_name, - masked_latents_name=masked_latents_name, - ), + return DenoiseMaskOutput.build( + mask_name=mask_name, + masked_latents_name=masked_latents_name, ) def get_scheduler( - context: InvocationContext, + context: "InvocationContext", scheduler_info: ModelInfo, scheduler_name: str, seed: int, ) -> Scheduler: scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP["ddim"]) - orig_scheduler_info = context.services.model_manager.get_model( - **scheduler_info.model_dump(), - context=context, - ) + orig_scheduler_info = context.models.load(**scheduler_info.model_dump()) with orig_scheduler_info as orig_scheduler: scheduler_config = orig_scheduler.config @@ -216,7 +212,7 @@ def get_scheduler( title="Denoise Latents", tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"], category="latents", - version="1.5.1", + version="1.5.2", ) class DenoiseLatentsInvocation(BaseInvocation): """Denoises noisy latents to decodable images""" @@ -302,34 +298,18 @@ class DenoiseLatentsInvocation(BaseInvocation): raise ValueError("cfg_scale must be greater than 1") return v - # TODO: pass this an emitter method or something? or a session for dispatching? - def dispatch_progress( - self, - context: InvocationContext, - source_node_id: str, - intermediate_state: PipelineIntermediateState, - base_model: BaseModelType, - ) -> None: - stable_diffusion_step_callback( - context=context, - intermediate_state=intermediate_state, - node=self.model_dump(), - source_node_id=source_node_id, - base_model=base_model, - ) - def get_conditioning_data( self, - context: InvocationContext, + context: "InvocationContext", scheduler, unet, seed, ) -> ConditioningData: - positive_cond_data = context.services.latents.get(self.positive_conditioning.conditioning_name) + positive_cond_data = context.conditioning.get(self.positive_conditioning.conditioning_name) c = positive_cond_data.conditionings[0].to(device=unet.device, dtype=unet.dtype) extra_conditioning_info = c.extra_conditioning - negative_cond_data = context.services.latents.get(self.negative_conditioning.conditioning_name) + negative_cond_data = context.conditioning.get(self.negative_conditioning.conditioning_name) uc = negative_cond_data.conditionings[0].to(device=unet.device, dtype=unet.dtype) conditioning_data = ConditioningData( @@ -389,7 +369,7 @@ class DenoiseLatentsInvocation(BaseInvocation): def prep_control_data( self, - context: InvocationContext, + context: "InvocationContext", control_input: Union[ControlField, List[ControlField]], latents_shape: List[int], exit_stack: ExitStack, @@ -417,17 +397,16 @@ class DenoiseLatentsInvocation(BaseInvocation): controlnet_data = [] for control_info in control_list: control_model = exit_stack.enter_context( - context.services.model_manager.get_model( + context.models.load( model_name=control_info.control_model.model_name, model_type=ModelType.ControlNet, base_model=control_info.control_model.base_model, - context=context, ) ) # control_models.append(control_model) control_image_field = control_info.image - input_image = context.services.images.get_pil_image(control_image_field.image_name) + input_image = context.images.get_pil(control_image_field.image_name) # self.image.image_type, self.image.image_name # FIXME: still need to test with different widths, heights, devices, dtypes # and add in batch_size, num_images_per_prompt? @@ -463,7 +442,7 @@ class DenoiseLatentsInvocation(BaseInvocation): def prep_ip_adapter_data( self, - context: InvocationContext, + context: "InvocationContext", ip_adapter: Optional[Union[IPAdapterField, list[IPAdapterField]]], conditioning_data: ConditioningData, exit_stack: ExitStack, @@ -485,19 +464,17 @@ class DenoiseLatentsInvocation(BaseInvocation): conditioning_data.ip_adapter_conditioning = [] for single_ip_adapter in ip_adapter: ip_adapter_model: Union[IPAdapter, IPAdapterPlus] = exit_stack.enter_context( - context.services.model_manager.get_model( + context.models.load( model_name=single_ip_adapter.ip_adapter_model.model_name, model_type=ModelType.IPAdapter, base_model=single_ip_adapter.ip_adapter_model.base_model, - context=context, ) ) - image_encoder_model_info = context.services.model_manager.get_model( + image_encoder_model_info = context.models.load( model_name=single_ip_adapter.image_encoder_model.model_name, model_type=ModelType.CLIPVision, base_model=single_ip_adapter.image_encoder_model.base_model, - context=context, ) # `single_ip_adapter.image` could be a list or a single ImageField. Normalize to a list here. @@ -505,7 +482,7 @@ class DenoiseLatentsInvocation(BaseInvocation): if not isinstance(single_ipa_images, list): single_ipa_images = [single_ipa_images] - single_ipa_images = [context.services.images.get_pil_image(image.image_name) for image in single_ipa_images] + single_ipa_images = [context.images.get_pil(image.image_name) for image in single_ipa_images] # TODO(ryand): With some effort, the step of running the CLIP Vision encoder could be done before any other # models are needed in memory. This would help to reduce peak memory utilization in low-memory environments. @@ -532,7 +509,7 @@ class DenoiseLatentsInvocation(BaseInvocation): def run_t2i_adapters( self, - context: InvocationContext, + context: "InvocationContext", t2i_adapter: Optional[Union[T2IAdapterField, list[T2IAdapterField]]], latents_shape: list[int], do_classifier_free_guidance: bool, @@ -549,13 +526,12 @@ class DenoiseLatentsInvocation(BaseInvocation): t2i_adapter_data = [] for t2i_adapter_field in t2i_adapter: - t2i_adapter_model_info = context.services.model_manager.get_model( + t2i_adapter_model_info = context.models.load( model_name=t2i_adapter_field.t2i_adapter_model.model_name, model_type=ModelType.T2IAdapter, base_model=t2i_adapter_field.t2i_adapter_model.base_model, - context=context, ) - image = context.services.images.get_pil_image(t2i_adapter_field.image.image_name) + image = context.images.get_pil(t2i_adapter_field.image.image_name) # The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally. if t2i_adapter_field.t2i_adapter_model.base_model == BaseModelType.StableDiffusion1: @@ -642,30 +618,30 @@ class DenoiseLatentsInvocation(BaseInvocation): return num_inference_steps, timesteps, init_timestep - def prep_inpaint_mask(self, context, latents): + def prep_inpaint_mask(self, context: "InvocationContext", latents): if self.denoise_mask is None: return None, None - mask = context.services.latents.get(self.denoise_mask.mask_name) + mask = context.latents.get(self.denoise_mask.mask_name) mask = tv_resize(mask, latents.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) if self.denoise_mask.masked_latents_name is not None: - masked_latents = context.services.latents.get(self.denoise_mask.masked_latents_name) + masked_latents = context.latents.get(self.denoise_mask.masked_latents_name) else: masked_latents = None return 1 - mask, masked_latents @torch.no_grad() - def invoke(self, context: InvocationContext) -> LatentsOutput: + def invoke(self, context) -> LatentsOutput: with SilenceWarnings(): # this quenches NSFW nag from diffusers seed = None noise = None if self.noise is not None: - noise = context.services.latents.get(self.noise.latents_name) + noise = context.latents.get(self.noise.latents_name) seed = self.noise.seed if self.latents is not None: - latents = context.services.latents.get(self.latents.latents_name) + latents = context.latents.get(self.latents.latents_name) if seed is None: seed = self.latents.seed @@ -691,27 +667,17 @@ class DenoiseLatentsInvocation(BaseInvocation): do_classifier_free_guidance=True, ) - # Get the source node id (we are invoking the prepared node) - graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) - source_node_id = graph_execution_state.prepared_source_mapping[self.id] - def step_callback(state: PipelineIntermediateState): - self.dispatch_progress(context, source_node_id, state, self.unet.unet.base_model) + context.util.sd_step_callback(state, self.unet.unet.base_model) def _lora_loader(): for lora in self.unet.loras: - lora_info = context.services.model_manager.get_model( - **lora.model_dump(exclude={"weight"}), - context=context, - ) + lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) yield (lora_info.context.model, lora.weight) del lora_info return - unet_info = context.services.model_manager.get_model( - **self.unet.unet.model_dump(), - context=context, - ) + unet_info = context.models.load(**self.unet.unet.model_dump()) with ( ExitStack() as exit_stack, ModelPatcher.apply_freeu(unet_info.context.model, self.unet.freeu_config), @@ -787,9 +753,8 @@ class DenoiseLatentsInvocation(BaseInvocation): if choose_torch_device() == torch.device("mps"): mps.empty_cache() - name = f"{context.graph_execution_state_id}__{self.id}" - context.services.latents.save(name, result_latents) - return build_latents_output(latents_name=name, latents=result_latents, seed=seed) + name = context.latents.save(tensor=result_latents) + return LatentsOutput.build(latents_name=name, latents=result_latents, seed=seed) @invocation( @@ -797,7 +762,7 @@ class DenoiseLatentsInvocation(BaseInvocation): title="Latents to Image", tags=["latents", "image", "vae", "l2i"], category="latents", - version="1.2.0", + version="1.2.1", ) class LatentsToImageInvocation(BaseInvocation, WithMetadata): """Generates an image from latents.""" @@ -814,13 +779,10 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata): fp32: bool = InputField(default=DEFAULT_PRECISION == "float32", description=FieldDescriptions.fp32) @torch.no_grad() - def invoke(self, context: InvocationContext) -> ImageOutput: - latents = context.services.latents.get(self.latents.latents_name) + def invoke(self, context) -> ImageOutput: + latents = context.latents.get(self.latents.latents_name) - vae_info = context.services.model_manager.get_model( - **self.vae.vae.model_dump(), - context=context, - ) + vae_info = context.models.load(**self.vae.vae.model_dump()) with set_seamless(vae_info.context.model, self.vae.seamless_axes), vae_info as vae: latents = latents.to(vae.device) @@ -849,7 +811,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata): vae.to(dtype=torch.float16) latents = latents.half() - if self.tiled or context.services.configuration.tiled_decode: + if self.tiled or context.config.get().tiled_decode: vae.enable_tiling() else: vae.disable_tiling() @@ -873,22 +835,9 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata): if choose_torch_device() == torch.device("mps"): mps.empty_cache() - image_dto = context.services.images.create( - image=image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) LATENTS_INTERPOLATION_MODE = Literal["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"] @@ -899,7 +848,7 @@ LATENTS_INTERPOLATION_MODE = Literal["nearest", "linear", "bilinear", "bicubic", title="Resize Latents", tags=["latents", "resize"], category="latents", - version="1.0.0", + version="1.0.1", ) class ResizeLatentsInvocation(BaseInvocation): """Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8.""" @@ -921,8 +870,8 @@ class ResizeLatentsInvocation(BaseInvocation): mode: LATENTS_INTERPOLATION_MODE = InputField(default="bilinear", description=FieldDescriptions.interp_mode) antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) - def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.services.latents.get(self.latents.latents_name) + def invoke(self, context) -> LatentsOutput: + latents = context.latents.get(self.latents.latents_name) # TODO: device = choose_torch_device() @@ -940,10 +889,8 @@ class ResizeLatentsInvocation(BaseInvocation): if device == torch.device("mps"): mps.empty_cache() - name = f"{context.graph_execution_state_id}__{self.id}" - # context.services.latents.set(name, resized_latents) - context.services.latents.save(name, resized_latents) - return build_latents_output(latents_name=name, latents=resized_latents, seed=self.latents.seed) + name = context.latents.save(tensor=resized_latents) + return LatentsOutput.build(latents_name=name, latents=resized_latents, seed=self.latents.seed) @invocation( @@ -951,7 +898,7 @@ class ResizeLatentsInvocation(BaseInvocation): title="Scale Latents", tags=["latents", "resize"], category="latents", - version="1.0.0", + version="1.0.1", ) class ScaleLatentsInvocation(BaseInvocation): """Scales latents by a given factor.""" @@ -964,8 +911,8 @@ class ScaleLatentsInvocation(BaseInvocation): mode: LATENTS_INTERPOLATION_MODE = InputField(default="bilinear", description=FieldDescriptions.interp_mode) antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) - def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.services.latents.get(self.latents.latents_name) + def invoke(self, context) -> LatentsOutput: + latents = context.latents.get(self.latents.latents_name) # TODO: device = choose_torch_device() @@ -984,10 +931,8 @@ class ScaleLatentsInvocation(BaseInvocation): if device == torch.device("mps"): mps.empty_cache() - name = f"{context.graph_execution_state_id}__{self.id}" - # context.services.latents.set(name, resized_latents) - context.services.latents.save(name, resized_latents) - return build_latents_output(latents_name=name, latents=resized_latents, seed=self.latents.seed) + name = context.latents.save(tensor=resized_latents) + return LatentsOutput.build(latents_name=name, latents=resized_latents, seed=self.latents.seed) @invocation( @@ -995,7 +940,7 @@ class ScaleLatentsInvocation(BaseInvocation): title="Image to Latents", tags=["latents", "image", "vae", "i2l"], category="latents", - version="1.0.0", + version="1.0.1", ) class ImageToLatentsInvocation(BaseInvocation): """Encodes an image into latents.""" @@ -1055,13 +1000,10 @@ class ImageToLatentsInvocation(BaseInvocation): return latents @torch.no_grad() - def invoke(self, context: InvocationContext) -> LatentsOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> LatentsOutput: + image = context.images.get_pil(self.image.image_name) - vae_info = context.services.model_manager.get_model( - **self.vae.vae.model_dump(), - context=context, - ) + vae_info = context.models.load(**self.vae.vae.model_dump()) image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB")) if image_tensor.dim() == 3: @@ -1069,10 +1011,9 @@ class ImageToLatentsInvocation(BaseInvocation): latents = self.vae_encode(vae_info, self.fp32, self.tiled, image_tensor) - name = f"{context.graph_execution_state_id}__{self.id}" latents = latents.to("cpu") - context.services.latents.save(name, latents) - return build_latents_output(latents_name=name, latents=latents, seed=None) + name = context.latents.save(tensor=latents) + return LatentsOutput.build(latents_name=name, latents=latents, seed=None) @singledispatchmethod @staticmethod @@ -1092,7 +1033,7 @@ class ImageToLatentsInvocation(BaseInvocation): title="Blend Latents", tags=["latents", "blend"], category="latents", - version="1.0.0", + version="1.0.1", ) class BlendLatentsInvocation(BaseInvocation): """Blend two latents using a given alpha. Latents must have same size.""" @@ -1107,9 +1048,9 @@ class BlendLatentsInvocation(BaseInvocation): ) alpha: float = InputField(default=0.5, description=FieldDescriptions.blend_alpha) - def invoke(self, context: InvocationContext) -> LatentsOutput: - latents_a = context.services.latents.get(self.latents_a.latents_name) - latents_b = context.services.latents.get(self.latents_b.latents_name) + def invoke(self, context) -> LatentsOutput: + latents_a = context.latents.get(self.latents_a.latents_name) + latents_b = context.latents.get(self.latents_b.latents_name) if latents_a.shape != latents_b.shape: raise Exception("Latents to blend must be the same size.") @@ -1163,10 +1104,8 @@ class BlendLatentsInvocation(BaseInvocation): if device == torch.device("mps"): mps.empty_cache() - name = f"{context.graph_execution_state_id}__{self.id}" - # context.services.latents.set(name, resized_latents) - context.services.latents.save(name, blended_latents) - return build_latents_output(latents_name=name, latents=blended_latents) + name = context.latents.save(tensor=blended_latents) + return LatentsOutput.build(latents_name=name, latents=blended_latents) # The Crop Latents node was copied from @skunkworxdark's implementation here: @@ -1176,7 +1115,7 @@ class BlendLatentsInvocation(BaseInvocation): title="Crop Latents", tags=["latents", "crop"], category="latents", - version="1.0.0", + version="1.0.1", ) # TODO(ryand): Named `CropLatentsCoreInvocation` to prevent a conflict with custom node `CropLatentsInvocation`. # Currently, if the class names conflict then 'GET /openapi.json' fails. @@ -1210,8 +1149,8 @@ class CropLatentsCoreInvocation(BaseInvocation): description="The height (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.", ) - def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.services.latents.get(self.latents.latents_name) + def invoke(self, context) -> LatentsOutput: + latents = context.latents.get(self.latents.latents_name) x1 = self.x // LATENT_SCALE_FACTOR y1 = self.y // LATENT_SCALE_FACTOR @@ -1220,10 +1159,9 @@ class CropLatentsCoreInvocation(BaseInvocation): cropped_latents = latents[..., y1:y2, x1:x2] - name = f"{context.graph_execution_state_id}__{self.id}" - context.services.latents.save(name, cropped_latents) + name = context.latents.save(tensor=cropped_latents) - return build_latents_output(latents_name=name, latents=cropped_latents) + return LatentsOutput.build(latents_name=name, latents=cropped_latents) @invocation_output("ideal_size_output") diff --git a/invokeai/app/invocations/math.py b/invokeai/app/invocations/math.py index 6ca53011f0..d2dbf04981 100644 --- a/invokeai/app/invocations/math.py +++ b/invokeai/app/invocations/math.py @@ -8,7 +8,7 @@ from pydantic import ValidationInfo, field_validator from invokeai.app.invocations.fields import FieldDescriptions, InputField from invokeai.app.invocations.primitives import FloatOutput, IntegerOutput -from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .baseinvocation import BaseInvocation, invocation @invocation("add", title="Add Integers", tags=["math", "add"], category="math", version="1.0.0") @@ -18,7 +18,7 @@ class AddInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: return IntegerOutput(value=self.a + self.b) @@ -29,7 +29,7 @@ class SubtractInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: return IntegerOutput(value=self.a - self.b) @@ -40,7 +40,7 @@ class MultiplyInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: return IntegerOutput(value=self.a * self.b) @@ -51,7 +51,7 @@ class DivideInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: return IntegerOutput(value=int(self.a / self.b)) @@ -69,7 +69,7 @@ class RandomIntInvocation(BaseInvocation): low: int = InputField(default=0, description=FieldDescriptions.inclusive_low) high: int = InputField(default=np.iinfo(np.int32).max, description=FieldDescriptions.exclusive_high) - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: return IntegerOutput(value=np.random.randint(self.low, self.high)) @@ -88,7 +88,7 @@ class RandomFloatInvocation(BaseInvocation): high: float = InputField(default=1.0, description=FieldDescriptions.exclusive_high) decimals: int = InputField(default=2, description=FieldDescriptions.decimal_places) - def invoke(self, context: InvocationContext) -> FloatOutput: + def invoke(self, context) -> FloatOutput: random_float = np.random.uniform(self.low, self.high) rounded_float = round(random_float, self.decimals) return FloatOutput(value=rounded_float) @@ -110,7 +110,7 @@ class FloatToIntegerInvocation(BaseInvocation): default="Nearest", description="The method to use for rounding" ) - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: if self.method == "Nearest": return IntegerOutput(value=round(self.value / self.multiple) * self.multiple) elif self.method == "Floor": @@ -128,7 +128,7 @@ class RoundInvocation(BaseInvocation): value: float = InputField(default=0, description="The float value") decimals: int = InputField(default=0, description="The number of decimal places") - def invoke(self, context: InvocationContext) -> FloatOutput: + def invoke(self, context) -> FloatOutput: return FloatOutput(value=round(self.value, self.decimals)) @@ -196,7 +196,7 @@ class IntegerMathInvocation(BaseInvocation): raise ValueError("Result of exponentiation is not an integer") return v - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: # Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9 if self.operation == "ADD": return IntegerOutput(value=self.a + self.b) @@ -270,7 +270,7 @@ class FloatMathInvocation(BaseInvocation): raise ValueError("Root operation resulted in a complex number") return v - def invoke(self, context: InvocationContext) -> FloatOutput: + def invoke(self, context) -> FloatOutput: # Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9 if self.operation == "ADD": return FloatOutput(value=self.a + self.b) diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py index 399e217dc1..9d74abd8c1 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -5,15 +5,20 @@ from pydantic import BaseModel, ConfigDict, Field from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) from invokeai.app.invocations.controlnet_image_processors import ControlField -from invokeai.app.invocations.fields import FieldDescriptions, InputField, MetadataField, OutputField, UIType +from invokeai.app.invocations.fields import ( + FieldDescriptions, + ImageField, + InputField, + MetadataField, + OutputField, + UIType, +) from invokeai.app.invocations.ip_adapter import IPAdapterModelField from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField -from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.t2i_adapter import T2IAdapterField from ...version import __version__ @@ -59,7 +64,7 @@ class MetadataItemInvocation(BaseInvocation): label: str = InputField(description=FieldDescriptions.metadata_item_label) value: Any = InputField(description=FieldDescriptions.metadata_item_value, ui_type=UIType.Any) - def invoke(self, context: InvocationContext) -> MetadataItemOutput: + def invoke(self, context) -> MetadataItemOutput: return MetadataItemOutput(item=MetadataItemField(label=self.label, value=self.value)) @@ -76,7 +81,7 @@ class MetadataInvocation(BaseInvocation): description=FieldDescriptions.metadata_item_polymorphic ) - def invoke(self, context: InvocationContext) -> MetadataOutput: + def invoke(self, context) -> MetadataOutput: if isinstance(self.items, MetadataItemField): # single metadata item data = {self.items.label: self.items.value} @@ -95,7 +100,7 @@ class MergeMetadataInvocation(BaseInvocation): collection: list[MetadataField] = InputField(description=FieldDescriptions.metadata_collection) - def invoke(self, context: InvocationContext) -> MetadataOutput: + def invoke(self, context) -> MetadataOutput: data = {} for item in self.collection: data.update(item.model_dump()) @@ -213,7 +218,7 @@ class CoreMetadataInvocation(BaseInvocation): description="The start value used for refiner denoising", ) - def invoke(self, context: InvocationContext) -> MetadataOutput: + def invoke(self, context) -> MetadataOutput: """Collects and outputs a CoreMetadata object""" return MetadataOutput( diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index c710c9761b..f81e559e44 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -10,7 +10,6 @@ from ...backend.model_management import BaseModelType, ModelType, SubModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -102,7 +101,7 @@ class LoRAModelField(BaseModel): title="Main Model", tags=["model"], category="model", - version="1.0.0", + version="1.0.1", ) class MainModelLoaderInvocation(BaseInvocation): """Loads a main model, outputting its submodels.""" @@ -110,13 +109,13 @@ class MainModelLoaderInvocation(BaseInvocation): model: MainModelField = InputField(description=FieldDescriptions.main_model, input=Input.Direct) # TODO: precision? - def invoke(self, context: InvocationContext) -> ModelLoaderOutput: + def invoke(self, context) -> ModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.Main # TODO: not found exceptions - if not context.services.model_manager.model_exists( + if not context.models.exists( model_name=model_name, base_model=base_model, model_type=model_type, @@ -203,7 +202,7 @@ class LoraLoaderOutput(BaseInvocationOutput): clip: Optional[ClipField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP") -@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.0") +@invocation("lora_loader", title="LoRA", tags=["model"], category="model", version="1.0.1") class LoraLoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" @@ -222,14 +221,14 @@ class LoraLoaderInvocation(BaseInvocation): title="CLIP", ) - def invoke(self, context: InvocationContext) -> LoraLoaderOutput: + def invoke(self, context) -> LoraLoaderOutput: if self.lora is None: raise Exception("No LoRA provided") base_model = self.lora.base_model lora_name = self.lora.model_name - if not context.services.model_manager.model_exists( + if not context.models.exists( base_model=base_model, model_name=lora_name, model_type=ModelType.Lora, @@ -285,7 +284,7 @@ class SDXLLoraLoaderOutput(BaseInvocationOutput): title="SDXL LoRA", tags=["lora", "model"], category="model", - version="1.0.0", + version="1.0.1", ) class SDXLLoraLoaderInvocation(BaseInvocation): """Apply selected lora to unet and text_encoder.""" @@ -311,14 +310,14 @@ class SDXLLoraLoaderInvocation(BaseInvocation): title="CLIP 2", ) - def invoke(self, context: InvocationContext) -> SDXLLoraLoaderOutput: + def invoke(self, context) -> SDXLLoraLoaderOutput: if self.lora is None: raise Exception("No LoRA provided") base_model = self.lora.base_model lora_name = self.lora.model_name - if not context.services.model_manager.model_exists( + if not context.models.exists( base_model=base_model, model_name=lora_name, model_type=ModelType.Lora, @@ -384,7 +383,7 @@ class VAEModelField(BaseModel): model_config = ConfigDict(protected_namespaces=()) -@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.0") +@invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.1") class VaeLoaderInvocation(BaseInvocation): """Loads a VAE model, outputting a VaeLoaderOutput""" @@ -394,12 +393,12 @@ class VaeLoaderInvocation(BaseInvocation): title="VAE", ) - def invoke(self, context: InvocationContext) -> VAEOutput: + def invoke(self, context) -> VAEOutput: base_model = self.vae_model.base_model model_name = self.vae_model.model_name model_type = ModelType.Vae - if not context.services.model_manager.model_exists( + if not context.models.exists( base_model=base_model, model_name=model_name, model_type=model_type, @@ -449,7 +448,7 @@ class SeamlessModeInvocation(BaseInvocation): seamless_y: bool = InputField(default=True, input=Input.Any, description="Specify whether Y axis is seamless") seamless_x: bool = InputField(default=True, input=Input.Any, description="Specify whether X axis is seamless") - def invoke(self, context: InvocationContext) -> SeamlessModeOutput: + def invoke(self, context) -> SeamlessModeOutput: # Conditionally append 'x' and 'y' based on seamless_x and seamless_y unet = copy.deepcopy(self.unet) vae = copy.deepcopy(self.vae) @@ -485,6 +484,6 @@ class FreeUInvocation(BaseInvocation): s1: float = InputField(default=0.9, ge=-1, le=3, description=FieldDescriptions.freeu_s1) s2: float = InputField(default=0.2, ge=-1, le=3, description=FieldDescriptions.freeu_s2) - def invoke(self, context: InvocationContext) -> UNetOutput: + def invoke(self, context) -> UNetOutput: self.unet.freeu_config = FreeUConfig(s1=self.s1, s2=self.s2, b1=self.b1, b2=self.b2) return UNetOutput(unet=self.unet) diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index 2e717ac561..41641152f0 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -4,15 +4,13 @@ import torch from pydantic import field_validator -from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField -from invokeai.app.invocations.latent import LatentsField +from invokeai.app.invocations.fields import FieldDescriptions, InputField, LatentsField, OutputField from invokeai.app.util.misc import SEED_MAX from ...backend.util.devices import choose_torch_device, torch_dtype from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -67,13 +65,13 @@ class NoiseOutput(BaseInvocationOutput): width: int = OutputField(description=FieldDescriptions.width) height: int = OutputField(description=FieldDescriptions.height) - -def build_noise_output(latents_name: str, latents: torch.Tensor, seed: int): - return NoiseOutput( - noise=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * 8, - height=latents.size()[2] * 8, - ) + @classmethod + def build(cls, latents_name: str, latents: torch.Tensor, seed: int) -> "NoiseOutput": + return cls( + noise=LatentsField(latents_name=latents_name, seed=seed), + width=latents.size()[3] * 8, + height=latents.size()[2] * 8, + ) @invocation( @@ -114,7 +112,7 @@ class NoiseInvocation(BaseInvocation): """Returns the seed modulo (SEED_MAX + 1) to ensure it is within the valid range.""" return v % (SEED_MAX + 1) - def invoke(self, context: InvocationContext) -> NoiseOutput: + def invoke(self, context) -> NoiseOutput: noise = get_noise( width=self.width, height=self.height, @@ -122,6 +120,5 @@ class NoiseInvocation(BaseInvocation): seed=self.seed, use_cpu=self.use_cpu, ) - name = f"{context.graph_execution_state_id}__{self.id}" - context.services.latents.save(name, noise) - return build_noise_output(latents_name=name, latents=noise, seed=self.seed) + name = context.latents.save(tensor=noise) + return NoiseOutput.build(latents_name=name, latents=noise, seed=self.seed) diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index b43d7eaef2..3f8e6669ab 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -37,7 +37,7 @@ from .baseinvocation import ( invocation_output, ) from .controlnet_image_processors import ControlField -from .latent import SAMPLER_NAME_VALUES, LatentsField, LatentsOutput, build_latents_output, get_scheduler +from .latent import SAMPLER_NAME_VALUES, LatentsField, LatentsOutput, get_scheduler from .model import ClipField, ModelInfo, UNetField, VaeField ORT_TO_NP_TYPE = { @@ -63,7 +63,7 @@ class ONNXPromptInvocation(BaseInvocation): prompt: str = InputField(default="", description=FieldDescriptions.raw_prompt, ui_component=UIComponent.Textarea) clip: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection) - def invoke(self, context: InvocationContext) -> ConditioningOutput: + def invoke(self, context) -> ConditioningOutput: tokenizer_info = context.services.model_manager.get_model( **self.clip.tokenizer.model_dump(), ) @@ -201,7 +201,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation): # based on # https://github.com/huggingface/diffusers/blob/3ebbaf7c96801271f9e6c21400033b6aa5ffcf29/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py#L375 - def invoke(self, context: InvocationContext) -> LatentsOutput: + def invoke(self, context) -> LatentsOutput: c, _ = context.services.latents.get(self.positive_conditioning.conditioning_name) uc, _ = context.services.latents.get(self.negative_conditioning.conditioning_name) graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) @@ -342,7 +342,7 @@ class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata): ) # tiled: bool = InputField(default=False, description="Decode latents by overlaping tiles(less memory consumption)") - def invoke(self, context: InvocationContext) -> ImageOutput: + def invoke(self, context) -> ImageOutput: latents = context.services.latents.get(self.latents.latents_name) if self.vae.vae.submodel != SubModelType.VaeDecoder: @@ -417,7 +417,7 @@ class OnnxModelLoaderInvocation(BaseInvocation): description=FieldDescriptions.onnx_main_model, input=Input.Direct, ui_type=UIType.ONNXModel ) - def invoke(self, context: InvocationContext) -> ONNXModelLoaderOutput: + def invoke(self, context) -> ONNXModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.ONNX diff --git a/invokeai/app/invocations/param_easing.py b/invokeai/app/invocations/param_easing.py index dab9c3dc0f..bf59e87d27 100644 --- a/invokeai/app/invocations/param_easing.py +++ b/invokeai/app/invocations/param_easing.py @@ -41,7 +41,7 @@ from matplotlib.ticker import MaxNLocator from invokeai.app.invocations.primitives import FloatCollectionOutput -from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .baseinvocation import BaseInvocation, invocation from .fields import InputField @@ -62,7 +62,7 @@ class FloatLinearRangeInvocation(BaseInvocation): description="number of values to interpolate over (including start and stop)", ) - def invoke(self, context: InvocationContext) -> FloatCollectionOutput: + def invoke(self, context) -> FloatCollectionOutput: param_list = list(np.linspace(self.start, self.stop, self.steps)) return FloatCollectionOutput(collection=param_list) @@ -110,7 +110,7 @@ EASING_FUNCTION_KEYS = Literal[tuple(EASING_FUNCTIONS_MAP.keys())] title="Step Param Easing", tags=["step", "easing"], category="step", - version="1.0.0", + version="1.0.1", ) class StepParamEasingInvocation(BaseInvocation): """Experimental per-step parameter easing for denoising steps""" @@ -130,7 +130,7 @@ class StepParamEasingInvocation(BaseInvocation): # alt_mirror: bool = InputField(default=False, description="alternative mirroring by dual easing") show_easing_plot: bool = InputField(default=False, description="show easing plot") - def invoke(self, context: InvocationContext) -> FloatCollectionOutput: + def invoke(self, context) -> FloatCollectionOutput: log_diagnostics = False # convert from start_step_percent to nearest step <= (steps * start_step_percent) # start_step = int(np.floor(self.num_steps * self.start_step_percent)) @@ -149,19 +149,19 @@ class StepParamEasingInvocation(BaseInvocation): postlist = list(num_poststeps * [self.post_end_value]) if log_diagnostics: - context.services.logger.debug("start_step: " + str(start_step)) - context.services.logger.debug("end_step: " + str(end_step)) - context.services.logger.debug("num_easing_steps: " + str(num_easing_steps)) - context.services.logger.debug("num_presteps: " + str(num_presteps)) - context.services.logger.debug("num_poststeps: " + str(num_poststeps)) - context.services.logger.debug("prelist size: " + str(len(prelist))) - context.services.logger.debug("postlist size: " + str(len(postlist))) - context.services.logger.debug("prelist: " + str(prelist)) - context.services.logger.debug("postlist: " + str(postlist)) + context.logger.debug("start_step: " + str(start_step)) + context.logger.debug("end_step: " + str(end_step)) + context.logger.debug("num_easing_steps: " + str(num_easing_steps)) + context.logger.debug("num_presteps: " + str(num_presteps)) + context.logger.debug("num_poststeps: " + str(num_poststeps)) + context.logger.debug("prelist size: " + str(len(prelist))) + context.logger.debug("postlist size: " + str(len(postlist))) + context.logger.debug("prelist: " + str(prelist)) + context.logger.debug("postlist: " + str(postlist)) easing_class = EASING_FUNCTIONS_MAP[self.easing] if log_diagnostics: - context.services.logger.debug("easing class: " + str(easing_class)) + context.logger.debug("easing class: " + str(easing_class)) easing_list = [] if self.mirror: # "expected" mirroring # if number of steps is even, squeeze duration down to (number_of_steps)/2 @@ -172,7 +172,7 @@ class StepParamEasingInvocation(BaseInvocation): base_easing_duration = int(np.ceil(num_easing_steps / 2.0)) if log_diagnostics: - context.services.logger.debug("base easing duration: " + str(base_easing_duration)) + context.logger.debug("base easing duration: " + str(base_easing_duration)) even_num_steps = num_easing_steps % 2 == 0 # even number of steps easing_function = easing_class( start=self.start_value, @@ -184,14 +184,14 @@ class StepParamEasingInvocation(BaseInvocation): easing_val = easing_function.ease(step_index) base_easing_vals.append(easing_val) if log_diagnostics: - context.services.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(easing_val)) + context.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(easing_val)) if even_num_steps: mirror_easing_vals = list(reversed(base_easing_vals)) else: mirror_easing_vals = list(reversed(base_easing_vals[0:-1])) if log_diagnostics: - context.services.logger.debug("base easing vals: " + str(base_easing_vals)) - context.services.logger.debug("mirror easing vals: " + str(mirror_easing_vals)) + context.logger.debug("base easing vals: " + str(base_easing_vals)) + context.logger.debug("mirror easing vals: " + str(mirror_easing_vals)) easing_list = base_easing_vals + mirror_easing_vals # FIXME: add alt_mirror option (alternative to default or mirror), or remove entirely @@ -226,12 +226,12 @@ class StepParamEasingInvocation(BaseInvocation): step_val = easing_function.ease(step_index) easing_list.append(step_val) if log_diagnostics: - context.services.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(step_val)) + context.logger.debug("step_index: " + str(step_index) + ", easing_val: " + str(step_val)) if log_diagnostics: - context.services.logger.debug("prelist size: " + str(len(prelist))) - context.services.logger.debug("easing_list size: " + str(len(easing_list))) - context.services.logger.debug("postlist size: " + str(len(postlist))) + context.logger.debug("prelist size: " + str(len(prelist))) + context.logger.debug("easing_list size: " + str(len(easing_list))) + context.logger.debug("postlist size: " + str(len(postlist))) param_list = prelist + easing_list + postlist diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index 22f03454a5..ee04345eed 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -1,16 +1,26 @@ # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) -from typing import Optional, Tuple +from typing import Optional import torch -from pydantic import BaseModel, Field -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIComponent +from invokeai.app.invocations.fields import ( + ColorField, + ConditioningField, + DenoiseMaskField, + FieldDescriptions, + ImageField, + Input, + InputField, + LatentsField, + OutputField, + UIComponent, +) +from invokeai.app.services.images.images_common import ImageDTO from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -49,7 +59,7 @@ class BooleanInvocation(BaseInvocation): value: bool = InputField(default=False, description="The boolean value") - def invoke(self, context: InvocationContext) -> BooleanOutput: + def invoke(self, context) -> BooleanOutput: return BooleanOutput(value=self.value) @@ -65,7 +75,7 @@ class BooleanCollectionInvocation(BaseInvocation): collection: list[bool] = InputField(default=[], description="The collection of boolean values") - def invoke(self, context: InvocationContext) -> BooleanCollectionOutput: + def invoke(self, context) -> BooleanCollectionOutput: return BooleanCollectionOutput(collection=self.collection) @@ -98,7 +108,7 @@ class IntegerInvocation(BaseInvocation): value: int = InputField(default=0, description="The integer value") - def invoke(self, context: InvocationContext) -> IntegerOutput: + def invoke(self, context) -> IntegerOutput: return IntegerOutput(value=self.value) @@ -114,7 +124,7 @@ class IntegerCollectionInvocation(BaseInvocation): collection: list[int] = InputField(default=[], description="The collection of integer values") - def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: + def invoke(self, context) -> IntegerCollectionOutput: return IntegerCollectionOutput(collection=self.collection) @@ -145,7 +155,7 @@ class FloatInvocation(BaseInvocation): value: float = InputField(default=0.0, description="The float value") - def invoke(self, context: InvocationContext) -> FloatOutput: + def invoke(self, context) -> FloatOutput: return FloatOutput(value=self.value) @@ -161,7 +171,7 @@ class FloatCollectionInvocation(BaseInvocation): collection: list[float] = InputField(default=[], description="The collection of float values") - def invoke(self, context: InvocationContext) -> FloatCollectionOutput: + def invoke(self, context) -> FloatCollectionOutput: return FloatCollectionOutput(collection=self.collection) @@ -192,7 +202,7 @@ class StringInvocation(BaseInvocation): value: str = InputField(default="", description="The string value", ui_component=UIComponent.Textarea) - def invoke(self, context: InvocationContext) -> StringOutput: + def invoke(self, context) -> StringOutput: return StringOutput(value=self.value) @@ -208,7 +218,7 @@ class StringCollectionInvocation(BaseInvocation): collection: list[str] = InputField(default=[], description="The collection of string values") - def invoke(self, context: InvocationContext) -> StringCollectionOutput: + def invoke(self, context) -> StringCollectionOutput: return StringCollectionOutput(collection=self.collection) @@ -217,18 +227,6 @@ class StringCollectionInvocation(BaseInvocation): # region Image -class ImageField(BaseModel): - """An image primitive field""" - - image_name: str = Field(description="The name of the image") - - -class BoardField(BaseModel): - """A board primitive field""" - - board_id: str = Field(description="The id of the board") - - @invocation_output("image_output") class ImageOutput(BaseInvocationOutput): """Base class for nodes that output a single image""" @@ -237,6 +235,14 @@ class ImageOutput(BaseInvocationOutput): width: int = OutputField(description="The width of the image in pixels") height: int = OutputField(description="The height of the image in pixels") + @classmethod + def build(cls, image_dto: ImageDTO) -> "ImageOutput": + return cls( + image=ImageField(image_name=image_dto.image_name), + width=image_dto.width, + height=image_dto.height, + ) + @invocation_output("image_collection_output") class ImageCollectionOutput(BaseInvocationOutput): @@ -247,7 +253,7 @@ class ImageCollectionOutput(BaseInvocationOutput): ) -@invocation("image", title="Image Primitive", tags=["primitives", "image"], category="primitives", version="1.0.0") +@invocation("image", title="Image Primitive", tags=["primitives", "image"], category="primitives", version="1.0.1") class ImageInvocation( BaseInvocation, ): @@ -255,8 +261,8 @@ class ImageInvocation( image: ImageField = InputField(description="The image to load") - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) return ImageOutput( image=ImageField(image_name=self.image.image_name), @@ -277,7 +283,7 @@ class ImageCollectionInvocation(BaseInvocation): collection: list[ImageField] = InputField(description="The collection of image values") - def invoke(self, context: InvocationContext) -> ImageCollectionOutput: + def invoke(self, context) -> ImageCollectionOutput: return ImageCollectionOutput(collection=self.collection) @@ -286,32 +292,24 @@ class ImageCollectionInvocation(BaseInvocation): # region DenoiseMask -class DenoiseMaskField(BaseModel): - """An inpaint mask field""" - - mask_name: str = Field(description="The name of the mask image") - masked_latents_name: Optional[str] = Field(default=None, description="The name of the masked image latents") - - @invocation_output("denoise_mask_output") class DenoiseMaskOutput(BaseInvocationOutput): """Base class for nodes that output a single image""" denoise_mask: DenoiseMaskField = OutputField(description="Mask for denoise model run") + @classmethod + def build(cls, mask_name: str, masked_latents_name: Optional[str] = None) -> "DenoiseMaskOutput": + return cls( + denoise_mask=DenoiseMaskField(mask_name=mask_name, masked_latents_name=masked_latents_name), + ) + # endregion # region Latents -class LatentsField(BaseModel): - """A latents tensor primitive field""" - - latents_name: str = Field(description="The name of the latents") - seed: Optional[int] = Field(default=None, description="Seed used to generate this latents") - - @invocation_output("latents_output") class LatentsOutput(BaseInvocationOutput): """Base class for nodes that output a single latents tensor""" @@ -322,6 +320,14 @@ class LatentsOutput(BaseInvocationOutput): width: int = OutputField(description=FieldDescriptions.width) height: int = OutputField(description=FieldDescriptions.height) + @classmethod + def build(cls, latents_name: str, latents: torch.Tensor, seed: Optional[int] = None) -> "LatentsOutput": + return cls( + latents=LatentsField(latents_name=latents_name, seed=seed), + width=latents.size()[3] * 8, + height=latents.size()[2] * 8, + ) + @invocation_output("latents_collection_output") class LatentsCollectionOutput(BaseInvocationOutput): @@ -333,17 +339,17 @@ class LatentsCollectionOutput(BaseInvocationOutput): @invocation( - "latents", title="Latents Primitive", tags=["primitives", "latents"], category="primitives", version="1.0.0" + "latents", title="Latents Primitive", tags=["primitives", "latents"], category="primitives", version="1.0.1" ) class LatentsInvocation(BaseInvocation): """A latents tensor primitive value""" latents: LatentsField = InputField(description="The latents tensor", input=Input.Connection) - def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.services.latents.get(self.latents.latents_name) + def invoke(self, context) -> LatentsOutput: + latents = context.latents.get(self.latents.latents_name) - return build_latents_output(self.latents.latents_name, latents) + return LatentsOutput.build(self.latents.latents_name, latents) @invocation( @@ -360,35 +366,15 @@ class LatentsCollectionInvocation(BaseInvocation): description="The collection of latents tensors", ) - def invoke(self, context: InvocationContext) -> LatentsCollectionOutput: + def invoke(self, context) -> LatentsCollectionOutput: return LatentsCollectionOutput(collection=self.collection) -def build_latents_output(latents_name: str, latents: torch.Tensor, seed: Optional[int] = None): - return LatentsOutput( - latents=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * 8, - height=latents.size()[2] * 8, - ) - - # endregion # region Color -class ColorField(BaseModel): - """A color primitive field""" - - r: int = Field(ge=0, le=255, description="The red component") - g: int = Field(ge=0, le=255, description="The green component") - b: int = Field(ge=0, le=255, description="The blue component") - a: int = Field(ge=0, le=255, description="The alpha component") - - def tuple(self) -> Tuple[int, int, int, int]: - return (self.r, self.g, self.b, self.a) - - @invocation_output("color_output") class ColorOutput(BaseInvocationOutput): """Base class for nodes that output a single color""" @@ -411,7 +397,7 @@ class ColorInvocation(BaseInvocation): color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=255), description="The color value") - def invoke(self, context: InvocationContext) -> ColorOutput: + def invoke(self, context) -> ColorOutput: return ColorOutput(color=self.color) @@ -420,18 +406,16 @@ class ColorInvocation(BaseInvocation): # region Conditioning -class ConditioningField(BaseModel): - """A conditioning tensor primitive value""" - - conditioning_name: str = Field(description="The name of conditioning tensor") - - @invocation_output("conditioning_output") class ConditioningOutput(BaseInvocationOutput): """Base class for nodes that output a single conditioning tensor""" conditioning: ConditioningField = OutputField(description=FieldDescriptions.cond) + @classmethod + def build(cls, conditioning_name: str) -> "ConditioningOutput": + return cls(conditioning=ConditioningField(conditioning_name=conditioning_name)) + @invocation_output("conditioning_collection_output") class ConditioningCollectionOutput(BaseInvocationOutput): @@ -454,7 +438,7 @@ class ConditioningInvocation(BaseInvocation): conditioning: ConditioningField = InputField(description=FieldDescriptions.cond, input=Input.Connection) - def invoke(self, context: InvocationContext) -> ConditioningOutput: + def invoke(self, context) -> ConditioningOutput: return ConditioningOutput(conditioning=self.conditioning) @@ -473,7 +457,7 @@ class ConditioningCollectionInvocation(BaseInvocation): description="The collection of conditioning tensors", ) - def invoke(self, context: InvocationContext) -> ConditioningCollectionOutput: + def invoke(self, context) -> ConditioningCollectionOutput: return ConditioningCollectionOutput(collection=self.collection) diff --git a/invokeai/app/invocations/prompt.py b/invokeai/app/invocations/prompt.py index 94b4a217ae..4f5ef43a56 100644 --- a/invokeai/app/invocations/prompt.py +++ b/invokeai/app/invocations/prompt.py @@ -7,7 +7,7 @@ from pydantic import field_validator from invokeai.app.invocations.primitives import StringCollectionOutput -from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .baseinvocation import BaseInvocation, invocation from .fields import InputField, UIComponent @@ -29,7 +29,7 @@ class DynamicPromptInvocation(BaseInvocation): max_prompts: int = InputField(default=1, description="The number of prompts to generate") combinatorial: bool = InputField(default=False, description="Whether to use the combinatorial generator") - def invoke(self, context: InvocationContext) -> StringCollectionOutput: + def invoke(self, context) -> StringCollectionOutput: if self.combinatorial: generator = CombinatorialPromptGenerator() prompts = generator.generate(self.prompt, max_prompts=self.max_prompts) @@ -91,7 +91,7 @@ class PromptsFromFileInvocation(BaseInvocation): break return prompts - def invoke(self, context: InvocationContext) -> StringCollectionOutput: + def invoke(self, context) -> StringCollectionOutput: prompts = self.promptsFromFile( self.file_path, self.pre_prompt, diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 62df5bc804..75a526cfff 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -4,7 +4,6 @@ from ...backend.model_management import ModelType, SubModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -30,7 +29,7 @@ class SDXLRefinerModelLoaderOutput(BaseInvocationOutput): vae: VaeField = OutputField(description=FieldDescriptions.vae, title="VAE") -@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.0") +@invocation("sdxl_model_loader", title="SDXL Main Model", tags=["model", "sdxl"], category="model", version="1.0.1") class SDXLModelLoaderInvocation(BaseInvocation): """Loads an sdxl base model, outputting its submodels.""" @@ -39,13 +38,13 @@ class SDXLModelLoaderInvocation(BaseInvocation): ) # TODO: precision? - def invoke(self, context: InvocationContext) -> SDXLModelLoaderOutput: + def invoke(self, context) -> SDXLModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.Main # TODO: not found exceptions - if not context.services.model_manager.model_exists( + if not context.models.exists( model_name=model_name, base_model=base_model, model_type=model_type, @@ -116,7 +115,7 @@ class SDXLModelLoaderInvocation(BaseInvocation): title="SDXL Refiner Model", tags=["model", "sdxl", "refiner"], category="model", - version="1.0.0", + version="1.0.1", ) class SDXLRefinerModelLoaderInvocation(BaseInvocation): """Loads an sdxl refiner model, outputting its submodels.""" @@ -128,13 +127,13 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation): ) # TODO: precision? - def invoke(self, context: InvocationContext) -> SDXLRefinerModelLoaderOutput: + def invoke(self, context) -> SDXLRefinerModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.Main # TODO: not found exceptions - if not context.services.model_manager.model_exists( + if not context.models.exists( model_name=model_name, base_model=base_model, model_type=model_type, diff --git a/invokeai/app/invocations/strings.py b/invokeai/app/invocations/strings.py index ccbc2f6d92..a4c92d9de5 100644 --- a/invokeai/app/invocations/strings.py +++ b/invokeai/app/invocations/strings.py @@ -5,7 +5,6 @@ import re from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -33,7 +32,7 @@ class StringSplitNegInvocation(BaseInvocation): string: str = InputField(default="", description="String to split", ui_component=UIComponent.Textarea) - def invoke(self, context: InvocationContext) -> StringPosNegOutput: + def invoke(self, context) -> StringPosNegOutput: p_string = "" n_string = "" brackets_depth = 0 @@ -77,7 +76,7 @@ class StringSplitInvocation(BaseInvocation): default="", description="Delimiter to spilt with. blank will split on the first whitespace" ) - def invoke(self, context: InvocationContext) -> String2Output: + def invoke(self, context) -> String2Output: result = self.string.split(self.delimiter, 1) if len(result) == 2: part1, part2 = result @@ -95,7 +94,7 @@ class StringJoinInvocation(BaseInvocation): string_left: str = InputField(default="", description="String Left", ui_component=UIComponent.Textarea) string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea) - def invoke(self, context: InvocationContext) -> StringOutput: + def invoke(self, context) -> StringOutput: return StringOutput(value=((self.string_left or "") + (self.string_right or ""))) @@ -107,7 +106,7 @@ class StringJoinThreeInvocation(BaseInvocation): string_middle: str = InputField(default="", description="String Middle", ui_component=UIComponent.Textarea) string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea) - def invoke(self, context: InvocationContext) -> StringOutput: + def invoke(self, context) -> StringOutput: return StringOutput(value=((self.string_left or "") + (self.string_middle or "") + (self.string_right or ""))) @@ -126,7 +125,7 @@ class StringReplaceInvocation(BaseInvocation): default=False, description="Use search string as a regex expression (non regex is case insensitive)" ) - def invoke(self, context: InvocationContext) -> StringOutput: + def invoke(self, context) -> StringOutput: pattern = self.search_string or "" new_string = self.string or "" if len(pattern) > 0: diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py index 66ac87c37b..74a098a501 100644 --- a/invokeai/app/invocations/t2i_adapter.py +++ b/invokeai/app/invocations/t2i_adapter.py @@ -5,13 +5,11 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) from invokeai.app.invocations.controlnet_image_processors import CONTROLNET_RESIZE_VALUES -from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField -from invokeai.app.invocations.primitives import ImageField +from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.backend.model_management.models.base import BaseModelType @@ -91,7 +89,7 @@ class T2IAdapterInvocation(BaseInvocation): validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self - def invoke(self, context: InvocationContext) -> T2IAdapterOutput: + def invoke(self, context) -> T2IAdapterOutput: return T2IAdapterOutput( t2i_adapter=T2IAdapterField( image=self.image, diff --git a/invokeai/app/invocations/tiles.py b/invokeai/app/invocations/tiles.py index bdc23ef6ed..dd34c3dc09 100644 --- a/invokeai/app/invocations/tiles.py +++ b/invokeai/app/invocations/tiles.py @@ -8,13 +8,12 @@ from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, Classification, - InvocationContext, + WithMetadata, invocation, invocation_output, ) -from invokeai.app.invocations.fields import Input, InputField, OutputField, WithMetadata -from invokeai.app.invocations.primitives import ImageField, ImageOutput -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin +from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField +from invokeai.app.invocations.primitives import ImageOutput from invokeai.backend.tiles.tiles import ( calc_tiles_even_split, calc_tiles_min_overlap, @@ -58,7 +57,7 @@ class CalculateImageTilesInvocation(BaseInvocation): description="The target overlap, in pixels, between adjacent tiles. Adjacent tiles will overlap by at least this amount", ) - def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: + def invoke(self, context) -> CalculateImageTilesOutput: tiles = calc_tiles_with_overlap( image_height=self.image_height, image_width=self.image_width, @@ -101,7 +100,7 @@ class CalculateImageTilesEvenSplitInvocation(BaseInvocation): description="The overlap, in pixels, between adjacent tiles.", ) - def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: + def invoke(self, context) -> CalculateImageTilesOutput: tiles = calc_tiles_even_split( image_height=self.image_height, image_width=self.image_width, @@ -131,7 +130,7 @@ class CalculateImageTilesMinimumOverlapInvocation(BaseInvocation): tile_height: int = InputField(ge=1, default=576, description="The tile height, in pixels.") min_overlap: int = InputField(default=128, ge=0, description="Minimum overlap between adjacent tiles, in pixels.") - def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: + def invoke(self, context) -> CalculateImageTilesOutput: tiles = calc_tiles_min_overlap( image_height=self.image_height, image_width=self.image_width, @@ -176,7 +175,7 @@ class TileToPropertiesInvocation(BaseInvocation): tile: Tile = InputField(description="The tile to split into properties.") - def invoke(self, context: InvocationContext) -> TileToPropertiesOutput: + def invoke(self, context) -> TileToPropertiesOutput: return TileToPropertiesOutput( coords_left=self.tile.coords.left, coords_right=self.tile.coords.right, @@ -213,7 +212,7 @@ class PairTileImageInvocation(BaseInvocation): image: ImageField = InputField(description="The tile image.") tile: Tile = InputField(description="The tile properties.") - def invoke(self, context: InvocationContext) -> PairTileImageOutput: + def invoke(self, context) -> PairTileImageOutput: return PairTileImageOutput( tile_with_image=TileWithImage( tile=self.tile, @@ -249,7 +248,7 @@ class MergeTilesToImageInvocation(BaseInvocation, WithMetadata): description="The amount to blend adjacent tiles in pixels. Must be <= the amount of overlap between adjacent tiles.", ) - def invoke(self, context: InvocationContext) -> ImageOutput: + def invoke(self, context) -> ImageOutput: images = [twi.image for twi in self.tiles_with_images] tiles = [twi.tile for twi in self.tiles_with_images] @@ -265,7 +264,7 @@ class MergeTilesToImageInvocation(BaseInvocation, WithMetadata): # existed in memory at an earlier point in the graph. tile_np_images: list[np.ndarray] = [] for image in images: - pil_image = context.services.images.get_pil_image(image.image_name) + pil_image = context.images.get_pil(image.image_name) pil_image = pil_image.convert("RGB") tile_np_images.append(np.array(pil_image)) @@ -288,18 +287,5 @@ class MergeTilesToImageInvocation(BaseInvocation, WithMetadata): # Convert into a PIL image and save pil_image = Image.fromarray(np_image) - image_dto = context.services.images.create( - image=pil_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + image_dto = context.images.save(image=pil_image) + return ImageOutput.build(image_dto) diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index 2cab279a9f..ef17480986 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -8,13 +8,13 @@ import torch from PIL import Image from pydantic import ConfigDict -from invokeai.app.invocations.primitives import ImageField, ImageOutput -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin +from invokeai.app.invocations.fields import ImageField +from invokeai.app.invocations.primitives import ImageOutput from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN from invokeai.backend.util.devices import choose_torch_device -from .baseinvocation import BaseInvocation, InvocationContext, invocation +from .baseinvocation import BaseInvocation, invocation from .fields import InputField, WithMetadata # TODO: Populate this from disk? @@ -30,7 +30,7 @@ if choose_torch_device() == torch.device("mps"): from torch import mps -@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.3.0") +@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.3.1") class ESRGANInvocation(BaseInvocation, WithMetadata): """Upscales an image using RealESRGAN.""" @@ -42,9 +42,9 @@ class ESRGANInvocation(BaseInvocation, WithMetadata): model_config = ConfigDict(protected_namespaces=()) - def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.services.images.get_pil_image(self.image.image_name) - models_path = context.services.configuration.models_path + def invoke(self, context) -> ImageOutput: + image = context.images.get_pil(self.image.image_name) + models_path = context.config.get().models_path rrdbnet_model = None netscale = None @@ -88,7 +88,7 @@ class ESRGANInvocation(BaseInvocation, WithMetadata): netscale = 2 else: msg = f"Invalid RealESRGAN model: {self.model_name}" - context.services.logger.error(msg) + context.logger.error(msg) raise ValueError(msg) esrgan_model_path = Path(f"core/upscaling/realesrgan/{self.model_name}") @@ -111,19 +111,6 @@ class ESRGANInvocation(BaseInvocation, WithMetadata): if choose_torch_device() == torch.device("mps"): mps.empty_cache() - image_dto = context.services.images.create( - image=pil_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) + image_dto = context.images.save(image=pil_image) - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) + return ImageOutput.build(image_dto) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index e9365f3349..ad08ae0395 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -55,7 +55,7 @@ class EventServiceBase: queue_item_id: int, queue_batch_id: str, graph_execution_state_id: str, - node: dict, + node_id: str, source_node_id: str, progress_image: Optional[ProgressImage], step: int, @@ -70,7 +70,7 @@ class EventServiceBase: "queue_item_id": queue_item_id, "queue_batch_id": queue_batch_id, "graph_execution_state_id": graph_execution_state_id, - "node_id": node.get("id"), + "node_id": node_id, "source_node_id": source_node_id, "progress_image": progress_image.model_dump() if progress_image is not None else None, "step": step, diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py index 54342c0da1..d2ebe235e6 100644 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ b/invokeai/app/services/invocation_processor/invocation_processor_default.py @@ -5,11 +5,11 @@ from threading import BoundedSemaphore, Event, Thread from typing import Optional import invokeai.backend.util.logging as logger -from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.app.services.invocation_queue.invocation_queue_common import InvocationQueueItem from invokeai.app.services.invocation_stats.invocation_stats_common import ( GESStatsNotFoundError, ) +from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context from invokeai.app.util.profiler import Profiler from ..invoker import Invoker @@ -131,16 +131,20 @@ class DefaultInvocationProcessor(InvocationProcessorABC): # which handles a few things: # - nodes that require a value, but get it only from a connection # - referencing the invocation cache instead of executing the node - outputs = invocation.invoke_internal( - InvocationContext( - services=self.__invoker.services, - graph_execution_state_id=graph_execution_state.id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - queue_batch_id=queue_item.session_queue_batch_id, - workflow=queue_item.workflow, - ) + context_data = InvocationContextData( + invocation=invocation, + session_id=graph_id, + workflow=queue_item.workflow, + source_node_id=source_node_id, + queue_id=queue_item.session_queue_id, + queue_item_id=queue_item.session_queue_item_id, + batch_id=queue_item.session_queue_batch_id, ) + context = build_invocation_context( + services=self.__invoker.services, + context_data=context_data, + ) + outputs = invocation.invoke_internal(context=context, services=self.__invoker.services) # Check queue to see if this is canceled, and skip if so if self.__invoker.services.queue.is_canceled(graph_execution_state.id): diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index 4c2fc4c085..a9b53ae224 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -5,11 +5,12 @@ from __future__ import annotations from abc import ABC, abstractmethod from logging import Logger from pathlib import Path -from typing import TYPE_CHECKING, Callable, List, Literal, Optional, Tuple, Union +from typing import Callable, List, Literal, Optional, Tuple, Union from pydantic import Field from invokeai.app.services.config.config_default import InvokeAIAppConfig +from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_management import ( AddModelResult, BaseModelType, @@ -21,9 +22,6 @@ from invokeai.backend.model_management import ( ) from invokeai.backend.model_management.model_cache import CacheStats -if TYPE_CHECKING: - from invokeai.app.invocations.baseinvocation import BaseInvocation, InvocationContext - class ModelManagerServiceBase(ABC): """Responsible for managing models on disk and in memory""" @@ -49,8 +47,7 @@ class ModelManagerServiceBase(ABC): base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None, - node: Optional[BaseInvocation] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> ModelInfo: """Retrieve the indicated model with name and type. submodel can be used to get a part (such as the vae) diff --git a/invokeai/app/services/model_manager/model_manager_common.py b/invokeai/app/services/model_manager/model_manager_common.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index cdb3e59a91..b641dd3f1e 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -11,6 +11,8 @@ from pydantic import Field from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_management import ( AddModelResult, BaseModelType, @@ -30,7 +32,7 @@ from invokeai.backend.util import choose_precision, choose_torch_device from .model_manager_base import ModelManagerServiceBase if TYPE_CHECKING: - from invokeai.app.invocations.baseinvocation import InvocationContext + pass # simple implementation @@ -86,13 +88,16 @@ class ModelManagerService(ModelManagerServiceBase): ) logger.info("Model manager service initialized") + def start(self, invoker: Invoker) -> None: + self._invoker: Optional[Invoker] = invoker + def get_model( self, model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> ModelInfo: """ Retrieve the indicated model. submodel can be used to get a @@ -100,9 +105,9 @@ class ModelManagerService(ModelManagerServiceBase): """ # we can emit model loading events if we are executing with access to the invocation context - if context: + if context_data is not None: self._emit_load_event( - context=context, + context_data=context_data, model_name=model_name, base_model=base_model, model_type=model_type, @@ -116,9 +121,9 @@ class ModelManagerService(ModelManagerServiceBase): submodel, ) - if context: + if context_data is not None: self._emit_load_event( - context=context, + context_data=context_data, model_name=model_name, base_model=base_model, model_type=model_type, @@ -263,22 +268,25 @@ class ModelManagerService(ModelManagerServiceBase): def _emit_load_event( self, - context: InvocationContext, + context_data: InvocationContextData, model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None, model_info: Optional[ModelInfo] = None, ): - if context.services.queue.is_canceled(context.graph_execution_state_id): + if self._invoker is None: + return + + if self._invoker.services.queue.is_canceled(context_data.session_id): raise CanceledException() if model_info: - context.services.events.emit_model_load_completed( - queue_id=context.queue_id, - queue_item_id=context.queue_item_id, - queue_batch_id=context.queue_batch_id, - graph_execution_state_id=context.graph_execution_state_id, + self._invoker.services.events.emit_model_load_completed( + queue_id=context_data.queue_id, + queue_item_id=context_data.queue_item_id, + queue_batch_id=context_data.batch_id, + graph_execution_state_id=context_data.session_id, model_name=model_name, base_model=base_model, model_type=model_type, @@ -286,11 +294,11 @@ class ModelManagerService(ModelManagerServiceBase): model_info=model_info, ) else: - context.services.events.emit_model_load_started( - queue_id=context.queue_id, - queue_item_id=context.queue_item_id, - queue_batch_id=context.queue_batch_id, - graph_execution_state_id=context.graph_execution_state_id, + self._invoker.services.events.emit_model_load_started( + queue_id=context_data.queue_id, + queue_item_id=context_data.queue_item_id, + queue_batch_id=context_data.batch_id, + graph_execution_state_id=context_data.session_id, model_name=model_name, base_model=base_model, model_type=model_type, diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index ba05b050c5..c0699eb96b 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -13,7 +13,6 @@ from invokeai.app.invocations import * # noqa: F401 F403 from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -202,7 +201,7 @@ class GraphInvocation(BaseInvocation): # TODO: figure out how to create a default here graph: "Graph" = InputField(description="The graph to run", default=None) - def invoke(self, context: InvocationContext) -> GraphInvocationOutput: + def invoke(self, context) -> GraphInvocationOutput: """Invoke with provided services and return outputs.""" return GraphInvocationOutput() @@ -228,7 +227,7 @@ class IterateInvocation(BaseInvocation): ) index: int = InputField(description="The index, will be provided on executed iterators", default=0, ui_hidden=True) - def invoke(self, context: InvocationContext) -> IterateInvocationOutput: + def invoke(self, context) -> IterateInvocationOutput: """Produces the outputs as values""" return IterateInvocationOutput(item=self.collection[self.index], index=self.index, total=len(self.collection)) @@ -255,7 +254,7 @@ class CollectInvocation(BaseInvocation): description="The collection, will be provided on execution", default=[], ui_hidden=True ) - def invoke(self, context: InvocationContext) -> CollectInvocationOutput: + def invoke(self, context) -> CollectInvocationOutput: """Invoke with provided services and return outputs.""" return CollectInvocationOutput(collection=copy.copy(self.collection)) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index c0aaac54f8..b68e521c73 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -6,8 +6,7 @@ from PIL.Image import Image from pydantic import ConfigDict from torch import Tensor -from invokeai.app.invocations.compel import ConditioningFieldData -from invokeai.app.invocations.fields import MetadataField, WithMetadata +from invokeai.app.invocations.fields import ConditioningFieldData, MetadataField, WithMetadata from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO @@ -245,13 +244,15 @@ class ConditioningInterface: ) return name - def get(conditioning_name: str) -> Tensor: + def get(conditioning_name: str) -> ConditioningFieldData: """ Gets conditioning data by name. :param conditioning_name: The name of the conditioning data to get. """ - return services.latents.get(conditioning_name) + # TODO(sm): We are (ab)using the latents storage service as a general pickle storage + # service, but it is typed as returning tensors, so we need to ignore the type here. + return services.latents.get(conditioning_name) # type: ignore [return-value] self.save = save self.get = get diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index 5cc3caa9ba..d83b380d95 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -1,25 +1,18 @@ -from typing import Protocol +from typing import TYPE_CHECKING import torch from PIL import Image -from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException, ProgressImage -from invokeai.app.services.invocation_queue.invocation_queue_base import InvocationQueueABC -from invokeai.app.services.shared.invocation_context import InvocationContextData from ...backend.model_management.models import BaseModelType from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.util.util import image_to_dataURL - -class StepCallback(Protocol): - def __call__( - self, - intermediate_state: PipelineIntermediateState, - base_model: BaseModelType, - ) -> None: - ... +if TYPE_CHECKING: + from invokeai.app.services.events.events_base import EventServiceBase + from invokeai.app.services.invocation_queue.invocation_queue_base import InvocationQueueABC + from invokeai.app.services.shared.invocation_context import InvocationContextData def sample_to_lowres_estimated_image(samples, latent_rgb_factors, smooth_matrix=None): @@ -38,11 +31,11 @@ def sample_to_lowres_estimated_image(samples, latent_rgb_factors, smooth_matrix= def stable_diffusion_step_callback( - context_data: InvocationContextData, + context_data: "InvocationContextData", intermediate_state: PipelineIntermediateState, base_model: BaseModelType, - invocation_queue: InvocationQueueABC, - events: EventServiceBase, + invocation_queue: "InvocationQueueABC", + events: "EventServiceBase", ) -> None: if invocation_queue.is_canceled(context_data.session_id): raise CanceledException From a79a450e9df089c0591037a7ca3696c7945f3e34 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:23:27 +1100 Subject: [PATCH 043/411] docs: update INVOCATIONS.md --- docs/contributing/INVOCATIONS.md | 95 ++++++++++++-------------------- 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/docs/contributing/INVOCATIONS.md b/docs/contributing/INVOCATIONS.md index 124589f44c..5d9a3690ba 100644 --- a/docs/contributing/INVOCATIONS.md +++ b/docs/contributing/INVOCATIONS.md @@ -9,11 +9,15 @@ complex functionality. ## Invocations Directory -InvokeAI Nodes can be found in the `invokeai/app/invocations` directory. These can be used as examples to create your own nodes. +InvokeAI Nodes can be found in the `invokeai/app/invocations` directory. These +can be used as examples to create your own nodes. -New nodes should be added to a subfolder in `nodes` direction found at the root level of the InvokeAI installation location. Nodes added to this folder will be able to be used upon application startup. +New nodes should be added to a subfolder in `nodes` direction found at the root +level of the InvokeAI installation location. Nodes added to this folder will be +able to be used upon application startup. + +Example `nodes` subfolder structure: -Example `nodes` subfolder structure: ```py ├── __init__.py # Invoke-managed custom node loader │ @@ -30,14 +34,14 @@ Example `nodes` subfolder structure: └── fancy_node.py ``` -Each node folder must have an `__init__.py` file that imports its nodes. Only nodes imported in the `__init__.py` file are loaded. - See the README in the nodes folder for more examples: +Each node folder must have an `__init__.py` file that imports its nodes. Only +nodes imported in the `__init__.py` file are loaded. See the README in the nodes +folder for more examples: ```py from .cool_node import CoolInvocation ``` - ## Creating A New Invocation In order to understand the process of creating a new Invocation, let us actually @@ -131,7 +135,6 @@ from invokeai.app.invocations.primitives import ImageField class ResizeInvocation(BaseInvocation): '''Resizes an image''' - # Inputs image: ImageField = InputField(description="The input image") width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image") height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image") @@ -167,12 +170,11 @@ from invokeai.app.invocations.primitives import ImageField class ResizeInvocation(BaseInvocation): '''Resizes an image''' - # Inputs image: ImageField = InputField(description="The input image") width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image") height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image") - def invoke(self, context: InvocationContext): + def invoke(self, context): pass ``` @@ -197,12 +199,11 @@ from invokeai.app.invocations.image import ImageOutput class ResizeInvocation(BaseInvocation): '''Resizes an image''' - # Inputs image: ImageField = InputField(description="The input image") width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image") height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image") - def invoke(self, context: InvocationContext) -> ImageOutput: + def invoke(self, context) -> ImageOutput: pass ``` @@ -228,31 +229,18 @@ class ResizeInvocation(BaseInvocation): width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image") height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image") - def invoke(self, context: InvocationContext) -> ImageOutput: - # Load the image using InvokeAI's predefined Image Service. Returns the PIL image. - image = context.services.images.get_pil_image(self.image.image_name) + def invoke(self, context) -> ImageOutput: + # Load the input image as a PIL image + image = context.images.get_pil(self.image.image_name) - # Resizing the image + # Resize the image resized_image = image.resize((self.width, self.height)) - # Save the image using InvokeAI's predefined Image Service. Returns the prepared PIL image. - output_image = context.services.images.create( - image=resized_image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - ) + # Save the image + image_dto = context.images.save(image=resized_image) - # Returning the Image - return ImageOutput( - image=ImageField( - image_name=output_image.image_name, - ), - width=output_image.width, - height=output_image.height, - ) + # Return an ImageOutput + return ImageOutput.build(image_dto) ``` **Note:** Do not be overwhelmed by the `ImageOutput` process. InvokeAI has a @@ -343,27 +331,25 @@ class ImageColorStringOutput(BaseInvocationOutput): That's all there is to it. - +Custom fields only support connection inputs in the Workflow Editor. From ef2728356937574db1de80a9a2c8b27208ffd3ce Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:23:38 +1100 Subject: [PATCH 044/411] tests: fix tests for new invocation context --- tests/aa_nodes/test_graph_execution_state.py | 8 +------- tests/aa_nodes/test_nodes.py | 17 ++++++++--------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/tests/aa_nodes/test_graph_execution_state.py b/tests/aa_nodes/test_graph_execution_state.py index fab1fa4598..9cc30e43e1 100644 --- a/tests/aa_nodes/test_graph_execution_state.py +++ b/tests/aa_nodes/test_graph_execution_state.py @@ -21,7 +21,6 @@ from invokeai.app.services.invocation_processor.invocation_processor_default imp from invokeai.app.services.invocation_queue.invocation_queue_memory import MemoryInvocationQueue from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService -from invokeai.app.services.session_queue.session_queue_common import DEFAULT_QUEUE_ID from invokeai.app.services.shared.graph import ( CollectInvocation, Graph, @@ -86,12 +85,7 @@ def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[B print(f"invoking {n.id}: {type(n)}") o = n.invoke( InvocationContext( - queue_batch_id="1", - queue_item_id=1, - queue_id=DEFAULT_QUEUE_ID, - services=services, - graph_execution_state_id="1", - workflow=None, + conditioning=None, config=None, data=None, images=None, latents=None, logger=None, models=None, util=None ) ) g.complete(n.id, o) diff --git a/tests/aa_nodes/test_nodes.py b/tests/aa_nodes/test_nodes.py index e71daad3f3..559457c0e1 100644 --- a/tests/aa_nodes/test_nodes.py +++ b/tests/aa_nodes/test_nodes.py @@ -3,7 +3,6 @@ from typing import Any, Callable, Union from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, - InvocationContext, invocation, invocation_output, ) @@ -21,7 +20,7 @@ class ListPassThroughInvocationOutput(BaseInvocationOutput): class ListPassThroughInvocation(BaseInvocation): collection: list[ImageField] = InputField(default=[]) - def invoke(self, context: InvocationContext) -> ListPassThroughInvocationOutput: + def invoke(self, context) -> ListPassThroughInvocationOutput: return ListPassThroughInvocationOutput(collection=self.collection) @@ -34,13 +33,13 @@ class PromptTestInvocationOutput(BaseInvocationOutput): class PromptTestInvocation(BaseInvocation): prompt: str = InputField(default="") - def invoke(self, context: InvocationContext) -> PromptTestInvocationOutput: + def invoke(self, context) -> PromptTestInvocationOutput: return PromptTestInvocationOutput(prompt=self.prompt) @invocation("test_error", version="1.0.0") class ErrorInvocation(BaseInvocation): - def invoke(self, context: InvocationContext) -> PromptTestInvocationOutput: + def invoke(self, context) -> PromptTestInvocationOutput: raise Exception("This invocation is supposed to fail") @@ -54,7 +53,7 @@ class TextToImageTestInvocation(BaseInvocation): prompt: str = InputField(default="") prompt2: str = InputField(default="") - def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: + def invoke(self, context) -> ImageTestInvocationOutput: return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) @@ -63,7 +62,7 @@ class ImageToImageTestInvocation(BaseInvocation): prompt: str = InputField(default="") image: Union[ImageField, None] = InputField(default=None) - def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: + def invoke(self, context) -> ImageTestInvocationOutput: return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) @@ -76,7 +75,7 @@ class PromptCollectionTestInvocationOutput(BaseInvocationOutput): class PromptCollectionTestInvocation(BaseInvocation): collection: list[str] = InputField() - def invoke(self, context: InvocationContext) -> PromptCollectionTestInvocationOutput: + def invoke(self, context) -> PromptCollectionTestInvocationOutput: return PromptCollectionTestInvocationOutput(collection=self.collection.copy()) @@ -89,7 +88,7 @@ class AnyTypeTestInvocationOutput(BaseInvocationOutput): class AnyTypeTestInvocation(BaseInvocation): value: Any = InputField(default=None) - def invoke(self, context: InvocationContext) -> AnyTypeTestInvocationOutput: + def invoke(self, context) -> AnyTypeTestInvocationOutput: return AnyTypeTestInvocationOutput(value=self.value) @@ -97,7 +96,7 @@ class AnyTypeTestInvocation(BaseInvocation): class PolymorphicStringTestInvocation(BaseInvocation): value: Union[str, list[str]] = InputField(default="") - def invoke(self, context: InvocationContext) -> PromptCollectionTestInvocationOutput: + def invoke(self, context) -> PromptCollectionTestInvocationOutput: if isinstance(self.value, str): return PromptCollectionTestInvocationOutput(collection=[self.value]) return PromptCollectionTestInvocationOutput(collection=self.value) From 1616974b482bc7f68e648604035792b80fae0c28 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 14 Jan 2024 00:05:15 +1100 Subject: [PATCH 045/411] feat(nodes): tidy `invocation_context.py`, improve comments --- .../app/services/shared/invocation_context.py | 119 ++++++++++++------ 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index b68e521c73..7961c011af 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from enum import Enum from typing import TYPE_CHECKING, Optional from PIL.Image import Image @@ -37,6 +36,9 @@ Wrapping these services provides a simpler and safer interface for nodes to use. When a node executes, a fresh `InvocationContext` is built for it, ensuring nodes cannot interfere with each other. +Many of the wrappers have the same signature as the methods they wrap. This allows us to write +user-facing docstrings and not need to go and update the internal services to match. + Note: The docstrings are in weird places, but that's where they must be to get IDEs to see them. """ @@ -44,12 +46,19 @@ Note: The docstrings are in weird places, but that's where they must be to get I @dataclass(frozen=True) class InvocationContextData: invocation: "BaseInvocation" + """The invocation that is being executed.""" session_id: str + """The session that is being executed.""" queue_id: str + """The queue in which the session is being executed.""" source_node_id: str + """The ID of the node from which the currently executing invocation was prepared.""" queue_item_id: int + """The ID of the queue item that is being executed.""" batch_id: str + """The ID of the batch that is being executed.""" workflow: Optional[WorkflowWithoutID] = None + """The workflow associated with this queue item, if any.""" class LoggerInterface: @@ -103,14 +112,15 @@ class ImagesInterface: """ Saves an image, returning its DTO. - If the current queue item has a workflow, it is automatically saved with the image. + If the current queue item has a workflow or metadata, it is automatically saved with the image. :param image: The image to save, as a PIL image. :param board_id: The board ID to add the image to, if it should be added. - :param image_category: The category of the image. Only the GENERAL category is added to the gallery. - :param metadata: The metadata to save with the image, if it should have any. If the invocation inherits \ - from `WithMetadata`, that metadata will be used automatically. Provide this only if you want to \ - override or provide metadata manually. + :param image_category: The category of the image. Only the GENERAL category is added \ + to the gallery. + :param metadata: The metadata to save with the image, if it should have any. If the \ + invocation inherits from `WithMetadata`, that metadata will be used automatically. \ + **Use this only if you want to override or provide metadata manually!** """ # If the invocation inherits metadata, use that. Else, use the metadata passed in. @@ -186,14 +196,6 @@ class ImagesInterface: self.update = update -class LatentsKind(str, Enum): - IMAGE = "image" - NOISE = "noise" - MASK = "mask" - MASKED_IMAGE = "masked_image" - OTHER = "other" - - class LatentsInterface: def __init__( self, @@ -206,6 +208,22 @@ class LatentsInterface: :param tensor: The latents tensor to save. """ + + # Previously, we added a suffix indicating the type of Tensor we were saving, e.g. + # "mask", "noise", "masked_latents", etc. + # + # Retaining that capability in this wrapper would require either many different methods + # to save latents, or extra args for this method. Instead of complicating the API, we + # will use the same naming scheme for all latents. + # + # This has a very minor impact as we don't use them after a session completes. + + # Previously, invocations chose the name for their latents. This is a bit risky, so we + # will generate a name for them instead. We use a uuid to ensure the name is unique. + # + # Because the name of the latents file will includes the session and invocation IDs, + # we don't need to worry about collisions. A truncated UUIDv4 is fine. + name = f"{context_data.session_id}__{context_data.invocation.id}__{uuid_string()[:7]}" services.latents.save( name=name, @@ -231,12 +249,21 @@ class ConditioningInterface: services: InvocationServices, context_data: InvocationContextData, ) -> None: + # TODO(psyche): We are (ab)using the latents storage service as a general pickle storage + # service, but it is typed to work with Tensors only. We have to fudge the types here. + def save(conditioning_data: ConditioningFieldData) -> str: """ Saves a conditioning data object, returning its name. :param conditioning_data: The conditioning data to save. """ + + # Conditioning data is *not* a Tensor, so we will suffix it to indicate this. + # + # See comment for `LatentsInterface.save` for more info about this method (it's very + # similar). + name = f"{context_data.session_id}__{context_data.invocation.id}__{uuid_string()[:7]}__conditioning" services.latents.save( name=name, @@ -250,9 +277,8 @@ class ConditioningInterface: :param conditioning_name: The name of the conditioning data to get. """ - # TODO(sm): We are (ab)using the latents storage service as a general pickle storage - # service, but it is typed as returning tensors, so we need to ignore the type here. - return services.latents.get(conditioning_name) # type: ignore [return-value] + + return services.latents.get(conditioning_name) # type: ignore [return-value] self.save = save self.get = get @@ -281,6 +307,17 @@ class ModelsInterface: :param model_type: The type of the model to get. :param submodel: The submodel of the model to get. """ + + # During this call, the model manager emits events with model loading status. The model + # manager itself has access to the events services, but does not have access to the + # required metadata for the events. + # + # For example, it needs access to the node's ID so that the events can be associated + # with the execution of a specific node. + # + # While this is available within the node, it's tedious to need to pass it in on every + # call. We can avoid that by wrapping the method here. + return services.model_manager.get_model( model_name, base_model, model_type, submodel, context_data=context_data ) @@ -306,8 +343,11 @@ class ConfigInterface: """ Gets the app's config. """ - # The config can be changed at runtime. We don't want nodes doing this, so we make a - # frozen copy.. + + # The config can be changed at runtime. + # + # We don't want nodes doing this, so we make a frozen copy. + config = services.configuration.get_config() frozen_config = config.model_copy(update={"model_config": ConfigDict(frozen=True)}) return frozen_config @@ -330,6 +370,12 @@ class UtilInterface: :param intermediate_state: The intermediate state of the diffusion pipeline. :param base_model: The base model for the current denoising step. """ + + # The step callback needs access to the events and the invocation queue services, but this + # represents a dangerous level of access. + # + # We wrap the step callback so that nodes do not have direct access to these services. + stable_diffusion_step_callback( context_data=context_data, intermediate_state=intermediate_state, @@ -343,36 +389,36 @@ class UtilInterface: class InvocationContext: """ - The invocation context provides access to various services and data about the current invocation. + The `InvocationContext` provides access to various services and data for the current invocation. """ def __init__( self, images: ImagesInterface, latents: LatentsInterface, - models: ModelsInterface, - config: ConfigInterface, - logger: LoggerInterface, - data: InvocationContextData, - util: UtilInterface, conditioning: ConditioningInterface, + models: ModelsInterface, + logger: LoggerInterface, + config: ConfigInterface, + util: UtilInterface, + data: InvocationContextData, ) -> None: self.images = images - "Provides methods to save, get and update images and their metadata." - self.logger = logger - "Provides access to the app logger." + """Provides methods to save, get and update images and their metadata.""" self.latents = latents - "Provides methods to save and get latents tensors, including image, noise, masks, and masked images." + """Provides methods to save and get latents tensors, including image, noise, masks, and masked images.""" self.conditioning = conditioning - "Provides methods to save and get conditioning data." + """Provides methods to save and get conditioning data.""" self.models = models - "Provides methods to check if a model exists, get a model, and get a model's info." + """Provides methods to check if a model exists, get a model, and get a model's info.""" + self.logger = logger + """Provides access to the app logger.""" self.config = config - "Provides access to the app's config." - self.data = data - "Provides data about the current queue item and invocation." + """Provides access to the app's config.""" self.util = util - "Provides utility methods." + """Provides utility methods.""" + self.data = data + """Provides data about the current queue item and invocation.""" def build_invocation_context( @@ -380,8 +426,7 @@ def build_invocation_context( context_data: InvocationContextData, ) -> InvocationContext: """ - Builds the invocation context. This is a wrapper around the invocation services that provides - a more convenient (and less dangerous) interface for nodes to use. + Builds the invocation context for a specific invocation execution. :param invocation_services: The invocation services to wrap. :param invocation_context_data: The invocation context data. From 9af05536522b131a82b0b4e3a5ca4f9bfb8709c7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 14 Jan 2024 00:34:56 +1100 Subject: [PATCH 046/411] chore: ruff --- invokeai/app/invocations/baseinvocation.py | 1 - invokeai/app/invocations/onnx.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index c4aed1fac5..df0596c9a1 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -22,7 +22,6 @@ from invokeai.app.invocations.fields import ( Input, InputFieldJSONSchemaExtra, MetadataField, - logger, ) from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.shared.invocation_context import InvocationContext diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index 3f8e6669ab..a1e318a380 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -318,7 +318,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation): name = f"{context.graph_execution_state_id}__{self.id}" context.services.latents.save(name, latents) - return build_latents_output(latents_name=name, latents=torch.from_numpy(latents)) + # return build_latents_output(latents_name=name, latents=torch.from_numpy(latents)) # Latent to image From f612a96afdc9ddcbf32c67edeb63513a45648678 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:16:51 +1100 Subject: [PATCH 047/411] feat(nodes): restore previous invocation context methods with deprecation warnings --- .../app/services/shared/invocation_context.py | 117 +++++++++++++++++- pyproject.toml | 1 + 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 7961c011af..023274d49f 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Optional +from deprecated import deprecated from PIL.Image import Image from pydantic import ConfigDict from torch import Tensor @@ -365,7 +366,7 @@ class UtilInterface: The step callback emits a progress event with the current step, the total number of steps, a preview image, and some other internal metadata. - This should be called after each step of the diffusion process. + This should be called after each denoising step. :param intermediate_state: The intermediate state of the diffusion pipeline. :param base_model: The base model for the current denoising step. @@ -387,6 +388,30 @@ class UtilInterface: self.sd_step_callback = sd_step_callback +deprecation_version = "3.7.0" +removed_version = "3.8.0" + + +def get_deprecation_reason(property_name: str, alternative: Optional[str] = None) -> str: + msg = f"{property_name} is deprecated as of v{deprecation_version}. It will be removed in v{removed_version}." + if alternative is not None: + msg += f" Use {alternative} instead." + msg += " See PLACEHOLDER_URL for details." + return msg + + +# Deprecation docstrings template. I don't think we can implement these programmatically with +# __doc__ because the IDE won't see them. + +""" +**DEPRECATED as of v3.7.0** + +PROPERTY_NAME will be removed in v3.8.0. Use ALTERNATIVE instead. See PLACEHOLDER_URL for details. + +OG_DOCSTRING +""" + + class InvocationContext: """ The `InvocationContext` provides access to various services and data for the current invocation. @@ -402,6 +427,7 @@ class InvocationContext: config: ConfigInterface, util: UtilInterface, data: InvocationContextData, + services: InvocationServices, ) -> None: self.images = images """Provides methods to save, get and update images and their metadata.""" @@ -419,6 +445,94 @@ class InvocationContext: """Provides utility methods.""" self.data = data """Provides data about the current queue item and invocation.""" + self.__services = services + + @property + @deprecated(version=deprecation_version, reason=get_deprecation_reason("`context.services`")) + def services(self) -> InvocationServices: + """ + **DEPRECATED as of v3.7.0** + + `context.services` will be removed in v3.8.0. See PLACEHOLDER_URL for details. + + The invocation services. + """ + return self.__services + + @property + @deprecated( + version=deprecation_version, + reason=get_deprecation_reason("`context.graph_execution_state_api`", "`context.data.session_id`"), + ) + def graph_execution_state_id(self) -> str: + """ + **DEPRECATED as of v3.7.0** + + `context.graph_execution_state_api` will be removed in v3.8.0. Use `context.data.session_id` instead. See PLACEHOLDER_URL for details. + + The ID of the session (aka graph execution state). + """ + return self.data.session_id + + @property + @deprecated( + version=deprecation_version, + reason=get_deprecation_reason("`context.queue_id`", "`context.data.queue_id`"), + ) + def queue_id(self) -> str: + """ + **DEPRECATED as of v3.7.0** + + `context.queue_id` will be removed in v3.8.0. Use `context.data.queue_id` instead. See PLACEHOLDER_URL for details. + + The ID of the queue. + """ + return self.data.queue_id + + @property + @deprecated( + version=deprecation_version, + reason=get_deprecation_reason("`context.queue_item_id`", "`context.data.queue_item_id`"), + ) + def queue_item_id(self) -> int: + """ + **DEPRECATED as of v3.7.0** + + `context.queue_item_id` will be removed in v3.8.0. Use `context.data.queue_item_id` instead. See PLACEHOLDER_URL for details. + + The ID of the queue item. + """ + return self.data.queue_item_id + + @property + @deprecated( + version=deprecation_version, + reason=get_deprecation_reason("`context.queue_batch_id`", "`context.data.batch_id`"), + ) + def queue_batch_id(self) -> str: + """ + **DEPRECATED as of v3.7.0** + + `context.queue_batch_id` will be removed in v3.8.0. Use `context.data.batch_id` instead. See PLACEHOLDER_URL for details. + + The ID of the batch. + """ + return self.data.batch_id + + @property + @deprecated( + version=deprecation_version, + reason=get_deprecation_reason("`context.workflow`", "`context.data.workflow`"), + ) + def workflow(self) -> Optional[WorkflowWithoutID]: + """ + **DEPRECATED as of v3.7.0** + + `context.workflow` will be removed in v3.8.0. Use `context.data.workflow` instead. See PLACEHOLDER_URL for details. + + The workflow associated with this queue item, if any. + """ + return self.data.workflow def build_invocation_context( @@ -449,6 +563,7 @@ def build_invocation_context( data=context_data, util=util, conditioning=conditioning, + services=services, ) return ctx diff --git a/pyproject.toml b/pyproject.toml index d063f1ad0e..8d25ed2091 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ dependencies = [ "albumentations", "click", "datasets", + "Deprecated", "dnspython~=2.4.0", "dynamicprompts", "easing-functions", From 6452c706e1269ca40100e2216c39a9ca2b5371c3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:37:05 +1100 Subject: [PATCH 048/411] tests: fix missing arg for InvocationContext --- tests/aa_nodes/test_graph_execution_state.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/aa_nodes/test_graph_execution_state.py b/tests/aa_nodes/test_graph_execution_state.py index 9cc30e43e1..3577a78ae2 100644 --- a/tests/aa_nodes/test_graph_execution_state.py +++ b/tests/aa_nodes/test_graph_execution_state.py @@ -85,7 +85,15 @@ def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[B print(f"invoking {n.id}: {type(n)}") o = n.invoke( InvocationContext( - conditioning=None, config=None, data=None, images=None, latents=None, logger=None, models=None, util=None + conditioning=None, + config=None, + data=None, + images=None, + latents=None, + logger=None, + models=None, + util=None, + services=None, ) ) g.complete(n.id, o) From 05fb485d33dca9df114fbc35aa33170918ea619a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:41:25 +1100 Subject: [PATCH 049/411] feat(nodes): move `ConditioningFieldData` to `conditioning_data.py` --- invokeai/app/invocations/compel.py | 2 +- invokeai/app/invocations/fields.py | 9 +-------- invokeai/app/services/shared/invocation_context.py | 3 ++- .../stable_diffusion/diffusion/conditioning_data.py | 5 +++++ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index b4496031bc..94caf4128d 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -5,7 +5,6 @@ from compel import Compel, ReturnedEmbeddingsType from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment from invokeai.app.invocations.fields import ( - ConditioningFieldData, FieldDescriptions, Input, InputField, @@ -15,6 +14,7 @@ from invokeai.app.invocations.fields import ( from invokeai.app.invocations.primitives import ConditioningOutput from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, + ConditioningFieldData, ExtraConditioningInfo, SDXLConditioningInfo, ) diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 566babbb6b..8879f76077 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -1,13 +1,11 @@ -from dataclasses import dataclass from enum import Enum -from typing import Any, Callable, List, Optional, Tuple +from typing import Any, Callable, Optional, Tuple from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter from pydantic.fields import _Unset from pydantic_core import PydanticUndefined from invokeai.app.util.metaenum import MetaEnum -from invokeai.backend.stable_diffusion.diffusion.conditioning_data import BasicConditioningInfo from invokeai.backend.util.logging import InvokeAILogger logger = InvokeAILogger.get_logger() @@ -544,11 +542,6 @@ class ColorField(BaseModel): return (self.r, self.g, self.b, self.a) -@dataclass -class ConditioningFieldData: - conditionings: List[BasicConditioningInfo] - - class ConditioningField(BaseModel): """A conditioning tensor primitive value""" diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 023274d49f..3cf3952de0 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -6,7 +6,7 @@ from PIL.Image import Image from pydantic import ConfigDict from torch import Tensor -from invokeai.app.invocations.fields import ConditioningFieldData, MetadataField, WithMetadata +from invokeai.app.invocations.fields import MetadataField, WithMetadata from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO @@ -17,6 +17,7 @@ from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.backend.model_management.model_manager import ModelInfo from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData if TYPE_CHECKING: from invokeai.app.invocations.baseinvocation import BaseInvocation diff --git a/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py b/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py index 3e38f9f78d..0676555f7a 100644 --- a/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py +++ b/invokeai/backend/stable_diffusion/diffusion/conditioning_data.py @@ -32,6 +32,11 @@ class BasicConditioningInfo: return self +@dataclass +class ConditioningFieldData: + conditionings: List[BasicConditioningInfo] + + @dataclass class SDXLConditioningInfo(BasicConditioningInfo): pooled_embeds: torch.Tensor From a466f7a94b7ecf41e0a41c862ab34b72ccf5076c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:48:33 +1100 Subject: [PATCH 050/411] feat(nodes): create invocation_api.py This is the public API for invocations. Everything a custom node might need should be re-exported from this file. --- .../controlnet_image_processors.py | 3 +- invokeai/app/invocations/facetools.py | 3 +- invokeai/app/invocations/image.py | 2 +- invokeai/app/invocations/infill.py | 4 +- invokeai/app/invocations/tiles.py | 3 +- invokeai/invocation_api/__init__.py | 109 ++++++++++++++++++ pyproject.toml | 1 + 7 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 invokeai/invocation_api/__init__.py diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 3797722c93..e993ceffde 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -25,8 +25,7 @@ from controlnet_aux.util import HWC3, ade_palette from PIL import Image from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator -from invokeai.app.invocations.baseinvocation import WithMetadata -from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField +from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.backend.image_util.depth_anything import DepthAnythingDetector diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index 2c92e28cfe..dad6308981 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -13,11 +13,10 @@ from pydantic import field_validator import invokeai.assets.fonts as font_assets from invokeai.app.invocations.baseinvocation import ( BaseInvocation, - WithMetadata, invocation, invocation_output, ) -from invokeai.app.invocations.fields import ImageField, InputField, OutputField +from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 10ebd97ace..3b8b0b4b80 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -7,7 +7,6 @@ import cv2 import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps -from invokeai.app.invocations.baseinvocation import WithMetadata from invokeai.app.invocations.fields import ( BoardField, ColorField, @@ -15,6 +14,7 @@ from invokeai.app.invocations.fields import ( ImageField, Input, InputField, + WithMetadata, ) from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index be51c8312f..159bdb5f7a 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -13,8 +13,8 @@ from invokeai.backend.image_util.cv2_inpaint import cv2_inpaint from invokeai.backend.image_util.lama import LaMA from invokeai.backend.image_util.patchmatch import PatchMatch -from .baseinvocation import BaseInvocation, WithMetadata, invocation -from .fields import InputField +from .baseinvocation import BaseInvocation, invocation +from .fields import InputField, WithMetadata from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES diff --git a/invokeai/app/invocations/tiles.py b/invokeai/app/invocations/tiles.py index dd34c3dc09..0b4c472696 100644 --- a/invokeai/app/invocations/tiles.py +++ b/invokeai/app/invocations/tiles.py @@ -8,11 +8,10 @@ from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, Classification, - WithMetadata, invocation, invocation_output, ) -from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField +from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageOutput from invokeai.backend.tiles.tiles import ( calc_tiles_even_split, diff --git a/invokeai/invocation_api/__init__.py b/invokeai/invocation_api/__init__.py new file mode 100644 index 0000000000..e867ec3cc4 --- /dev/null +++ b/invokeai/invocation_api/__init__.py @@ -0,0 +1,109 @@ +""" +This file re-exports all the public API for invocations. This is the only file that should be imported by custom nodes. + +TODO(psyche): Do we want to dogfood this? +""" + +from invokeai.app.invocations.baseinvocation import ( + BaseInvocation, + BaseInvocationOutput, + invocation, + invocation_output, +) +from invokeai.app.invocations.fields import ( + BoardField, + ColorField, + ConditioningField, + DenoiseMaskField, + FieldDescriptions, + FieldKind, + ImageField, + Input, + InputField, + LatentsField, + MetadataField, + OutputField, + UIComponent, + UIType, + WithMetadata, + WithWorkflow, +) +from invokeai.app.invocations.primitives import ( + BooleanCollectionOutput, + BooleanOutput, + ColorCollectionOutput, + ColorOutput, + ConditioningCollectionOutput, + ConditioningOutput, + DenoiseMaskOutput, + FloatCollectionOutput, + FloatOutput, + ImageCollectionOutput, + ImageOutput, + IntegerCollectionOutput, + IntegerOutput, + LatentsCollectionOutput, + LatentsOutput, + StringCollectionOutput, + StringOutput, +) +from invokeai.app.services.image_records.image_records_common import ImageCategory +from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( + BasicConditioningInfo, + ConditioningFieldData, + ExtraConditioningInfo, + SDXLConditioningInfo, +) + +__all__ = [ + # invokeai.app.invocations.baseinvocation + "BaseInvocation", + "BaseInvocationOutput", + "invocation", + "invocation_output", + # invokeai.app.services.shared.invocation_context + "InvocationContext", + # invokeai.app.invocations.fields + "BoardField", + "ColorField", + "ConditioningField", + "DenoiseMaskField", + "FieldDescriptions", + "FieldKind", + "ImageField", + "Input", + "InputField", + "LatentsField", + "MetadataField", + "OutputField", + "UIComponent", + "UIType", + "WithMetadata", + "WithWorkflow", + # invokeai.app.invocations.primitives + "BooleanCollectionOutput", + "BooleanOutput", + "ColorCollectionOutput", + "ColorOutput", + "ConditioningCollectionOutput", + "ConditioningOutput", + "DenoiseMaskOutput", + "FloatCollectionOutput", + "FloatOutput", + "ImageCollectionOutput", + "ImageOutput", + "IntegerCollectionOutput", + "IntegerOutput", + "LatentsCollectionOutput", + "LatentsOutput", + "StringCollectionOutput", + "StringOutput", + # invokeai.app.services.image_records.image_records_common + "ImageCategory", + # invokeai.backend.stable_diffusion.diffusion.conditioning_data + "BasicConditioningInfo", + "ConditioningFieldData", + "ExtraConditioningInfo", + "SDXLConditioningInfo", +] diff --git a/pyproject.toml b/pyproject.toml index 8d25ed2091..69958064c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,6 +170,7 @@ version = { attr = "invokeai.version.__version__" } "invokeai.frontend.web.static*", "invokeai.configs*", "invokeai.app*", + "invokeai.invocation_api*", ] [tool.setuptools.package-data] From 282b483d14192a8b031eb07b8ce6da40dc75fde3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jan 2024 19:19:49 +1100 Subject: [PATCH 051/411] feat: tweak pyright config --- pyproject.toml | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 69958064c6..8b28375e29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -284,17 +284,36 @@ module = [ #=== End: MyPy [tool.pyright] -include = [ - "invokeai/app/invocations/" -] -exclude = [ - "**/node_modules", - "**/__pycache__", - "invokeai/app/invocations/onnx.py", - "invokeai/app/api/routers/models.py", - "invokeai/app/services/invocation_stats/invocation_stats_default.py", - "invokeai/app/services/model_manager/model_manager_base.py", - "invokeai/app/services/model_manager/model_manager_default.py", - "invokeai/app/services/model_records/model_records_sql.py", - "invokeai/app/util/controlnet_utils.py", -] +# Start from strict mode +typeCheckingMode = "strict" +# This errors whenever an import is missing a type stub file - way too noisy +reportMissingTypeStubs = "none" +# These are the rest of the rules enabled by strict mode - enable them @ warning +reportConstantRedefinition = "warning" +reportDeprecated = "warning" +reportDuplicateImport = "warning" +reportIncompleteStub = "warning" +reportInconsistentConstructor = "warning" +reportInvalidStubStatement = "warning" +reportMatchNotExhaustive = "warning" +reportMissingParameterType = "warning" +reportMissingTypeArgument = "warning" +reportPrivateUsage = "warning" +reportTypeCommentUsage = "warning" +reportUnknownArgumentType = "warning" +reportUnknownLambdaType = "warning" +reportUnknownMemberType = "warning" +reportUnknownParameterType = "warning" +reportUnknownVariableType = "warning" +reportUnnecessaryCast = "warning" +reportUnnecessaryComparison = "warning" +reportUnnecessaryContains = "warning" +reportUnnecessaryIsInstance = "warning" +reportUnusedClass = "warning" +reportUnusedImport = "warning" +reportUnusedFunction = "warning" +reportUnusedVariable = "warning" +reportUntypedBaseClass = "warning" +reportUntypedClassDecorator = "warning" +reportUntypedFunctionDecorator = "warning" +reportUntypedNamedTuple = "warning" From 281c3345311114e5e0cf2daef5ef1f3b86394f67 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:02:38 +1100 Subject: [PATCH 052/411] feat(nodes): do not freeze InvocationContextData, prevents it from being subclassesd --- invokeai/app/services/shared/invocation_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 3cf3952de0..a849d6b17a 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -45,7 +45,7 @@ Note: The docstrings are in weird places, but that's where they must be to get I """ -@dataclass(frozen=True) +@dataclass class InvocationContextData: invocation: "BaseInvocation" """The invocation that is being executed.""" From 4ce21087d3c28e0a0d45e5077e7766a871c1a6fa Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:16:35 +1100 Subject: [PATCH 053/411] fix(nodes): restore type annotations for `InvocationContext` --- docs/contributing/INVOCATIONS.md | 6 +-- invokeai/app/invocations/collections.py | 7 +-- invokeai/app/invocations/compel.py | 18 +++---- .../controlnet_image_processors.py | 5 +- invokeai/app/invocations/cv.py | 3 +- invokeai/app/invocations/facetools.py | 24 ++++----- invokeai/app/invocations/image.py | 51 ++++++++++--------- invokeai/app/invocations/infill.py | 11 ++-- invokeai/app/invocations/ip_adapter.py | 3 +- invokeai/app/invocations/latent.py | 18 +++---- invokeai/app/invocations/math.py | 21 ++++---- invokeai/app/invocations/metadata.py | 9 ++-- invokeai/app/invocations/model.py | 13 ++--- invokeai/app/invocations/noise.py | 3 +- invokeai/app/invocations/onnx.py | 8 +-- invokeai/app/invocations/param_easing.py | 5 +- invokeai/app/invocations/primitives.py | 31 +++++------ invokeai/app/invocations/prompt.py | 5 +- invokeai/app/invocations/sdxl.py | 5 +- invokeai/app/invocations/strings.py | 12 +++-- invokeai/app/invocations/t2i_adapter.py | 3 +- invokeai/app/invocations/tiles.py | 13 ++--- invokeai/app/invocations/upscale.py | 3 +- invokeai/app/services/shared/graph.py | 7 +-- tests/aa_nodes/test_nodes.py | 17 ++++--- 25 files changed, 158 insertions(+), 143 deletions(-) diff --git a/docs/contributing/INVOCATIONS.md b/docs/contributing/INVOCATIONS.md index 5d9a3690ba..ce1ee9e808 100644 --- a/docs/contributing/INVOCATIONS.md +++ b/docs/contributing/INVOCATIONS.md @@ -174,7 +174,7 @@ class ResizeInvocation(BaseInvocation): width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image") height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image") - def invoke(self, context): + def invoke(self, context: InvocationContext): pass ``` @@ -203,7 +203,7 @@ class ResizeInvocation(BaseInvocation): width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image") height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: pass ``` @@ -229,7 +229,7 @@ class ResizeInvocation(BaseInvocation): width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image") height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: # Load the input image as a PIL image image = context.images.get_pil(self.image.image_name) diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index f5709b4ba3..e02291980f 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -5,6 +5,7 @@ import numpy as np from pydantic import ValidationInfo, field_validator from invokeai.app.invocations.primitives import IntegerCollectionOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import SEED_MAX from .baseinvocation import BaseInvocation, invocation @@ -27,7 +28,7 @@ class RangeInvocation(BaseInvocation): raise ValueError("stop must be greater than start") return v - def invoke(self, context) -> IntegerCollectionOutput: + def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: return IntegerCollectionOutput(collection=list(range(self.start, self.stop, self.step))) @@ -45,7 +46,7 @@ class RangeOfSizeInvocation(BaseInvocation): size: int = InputField(default=1, gt=0, description="The number of values") step: int = InputField(default=1, description="The step of the range") - def invoke(self, context) -> IntegerCollectionOutput: + def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: return IntegerCollectionOutput( collection=list(range(self.start, self.start + (self.step * self.size), self.step)) ) @@ -72,6 +73,6 @@ class RandomRangeInvocation(BaseInvocation): description="The seed for the RNG (omit for random)", ) - def invoke(self, context) -> IntegerCollectionOutput: + def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: rng = np.random.default_rng(self.seed) return IntegerCollectionOutput(collection=list(rng.integers(low=self.low, high=self.high, size=self.size))) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 94caf4128d..978c6dcb17 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, Optional, Union +from typing import List, Optional, Union import torch from compel import Compel, ReturnedEmbeddingsType @@ -12,6 +12,7 @@ from invokeai.app.invocations.fields import ( UIComponent, ) from invokeai.app.invocations.primitives import ConditioningOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, ConditioningFieldData, @@ -31,10 +32,7 @@ from .baseinvocation import ( ) from .model import ClipField -if TYPE_CHECKING: - from invokeai.app.services.shared.invocation_context import InvocationContext - - # unconditioned: Optional[torch.Tensor] +# unconditioned: Optional[torch.Tensor] # class ConditioningAlgo(str, Enum): @@ -65,7 +63,7 @@ class CompelInvocation(BaseInvocation): ) @torch.no_grad() - def invoke(self, context) -> ConditioningOutput: + def invoke(self, context: InvocationContext) -> ConditioningOutput: tokenizer_info = context.models.load(**self.clip.tokenizer.model_dump()) text_encoder_info = context.models.load(**self.clip.text_encoder.model_dump()) @@ -148,7 +146,7 @@ class CompelInvocation(BaseInvocation): class SDXLPromptInvocationBase: def run_clip_compel( self, - context: "InvocationContext", + context: InvocationContext, clip_field: ClipField, prompt: str, get_pooled: bool, @@ -288,7 +286,7 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): clip2: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP 2") @torch.no_grad() - def invoke(self, context) -> ConditioningOutput: + def invoke(self, context: InvocationContext) -> ConditioningOutput: c1, c1_pooled, ec1 = self.run_clip_compel( context, self.clip, self.prompt, False, "lora_te1_", zero_on_empty=True ) @@ -373,7 +371,7 @@ class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase clip2: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection) @torch.no_grad() - def invoke(self, context) -> ConditioningOutput: + def invoke(self, context: InvocationContext) -> ConditioningOutput: # TODO: if there will appear lora for refiner - write proper prefix c2, c2_pooled, ec2 = self.run_clip_compel(context, self.clip2, self.style, True, "", zero_on_empty=False) @@ -418,7 +416,7 @@ class ClipSkipInvocation(BaseInvocation): clip: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP") skipped_layers: int = InputField(default=0, description=FieldDescriptions.skipped_layers) - def invoke(self, context) -> ClipSkipInvocationOutput: + def invoke(self, context: InvocationContext) -> ClipSkipInvocationOutput: self.clip.skipped_layers += self.skipped_layers return ClipSkipInvocationOutput( clip=self.clip, diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index e993ceffde..f8bdf14117 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -28,6 +28,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.invocations.util import validate_begin_end_step, validate_weights +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.image_util.depth_anything import DepthAnythingDetector from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector from invokeai.backend.model_management.models.base import BaseModelType @@ -119,7 +120,7 @@ class ControlNetInvocation(BaseInvocation): validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self - def invoke(self, context) -> ControlOutput: + def invoke(self, context: InvocationContext) -> ControlOutput: return ControlOutput( control=ControlField( image=self.image, @@ -143,7 +144,7 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata): # superclass just passes through image without processing return image - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: raw_image = context.images.get_pil(self.image.image_name) # image type should be PIL.PngImagePlugin.PngImageFile ? processed_image = self.run_processor(raw_image) diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 375b18f9c5..1ebabf5e06 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -7,6 +7,7 @@ from PIL import Image, ImageOps from invokeai.app.invocations.fields import ImageField from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from .baseinvocation import BaseInvocation, invocation from .fields import InputField, WithMetadata @@ -19,7 +20,7 @@ class CvInpaintInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to inpaint") mask: ImageField = InputField(description="The mask to use when inpainting") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) mask = context.images.get_pil(self.mask.image_name) diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index dad6308981..a1702d6517 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -1,7 +1,7 @@ import math import re from pathlib import Path -from typing import TYPE_CHECKING, Optional, TypedDict +from typing import Optional, TypedDict import cv2 import numpy as np @@ -19,9 +19,7 @@ from invokeai.app.invocations.baseinvocation import ( from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory - -if TYPE_CHECKING: - from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.app.services.shared.invocation_context import InvocationContext @invocation_output("face_mask_output") @@ -176,7 +174,7 @@ def prepare_faces_list( def generate_face_box_mask( - context: "InvocationContext", + context: InvocationContext, minimum_confidence: float, x_offset: float, y_offset: float, @@ -275,7 +273,7 @@ def generate_face_box_mask( def extract_face( - context: "InvocationContext", + context: InvocationContext, image: ImageType, face: FaceResultData, padding: int, @@ -356,7 +354,7 @@ def extract_face( def get_faces_list( - context: "InvocationContext", + context: InvocationContext, image: ImageType, should_chunk: bool, minimum_confidence: float, @@ -458,7 +456,7 @@ class FaceOffInvocation(BaseInvocation, WithMetadata): description="Whether to bypass full image face detection and default to image chunking. Chunking will occur if no faces are found in the full image.", ) - def faceoff(self, context: "InvocationContext", image: ImageType) -> Optional[ExtractFaceData]: + def faceoff(self, context: InvocationContext, image: ImageType) -> Optional[ExtractFaceData]: all_faces = get_faces_list( context=context, image=image, @@ -485,7 +483,7 @@ class FaceOffInvocation(BaseInvocation, WithMetadata): return face_data - def invoke(self, context) -> FaceOffOutput: + def invoke(self, context: InvocationContext) -> FaceOffOutput: image = context.images.get_pil(self.image.image_name) result = self.faceoff(context=context, image=image) @@ -543,7 +541,7 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata): raise ValueError('Face IDs must be a comma-separated list of integers (e.g. "1,2,3")') return v - def facemask(self, context: "InvocationContext", image: ImageType) -> FaceMaskResult: + def facemask(self, context: InvocationContext, image: ImageType) -> FaceMaskResult: all_faces = get_faces_list( context=context, image=image, @@ -600,7 +598,7 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata): mask=mask_pil, ) - def invoke(self, context) -> FaceMaskOutput: + def invoke(self, context: InvocationContext) -> FaceMaskOutput: image = context.images.get_pil(self.image.image_name) result = self.facemask(context=context, image=image) @@ -633,7 +631,7 @@ class FaceIdentifierInvocation(BaseInvocation, WithMetadata): description="Whether to bypass full image face detection and default to image chunking. Chunking will occur if no faces are found in the full image.", ) - def faceidentifier(self, context: "InvocationContext", image: ImageType) -> ImageType: + def faceidentifier(self, context: InvocationContext, image: ImageType) -> ImageType: image = image.copy() all_faces = get_faces_list( @@ -674,7 +672,7 @@ class FaceIdentifierInvocation(BaseInvocation, WithMetadata): return image - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) result_image = self.faceidentifier(context=context, image=image) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 3b8b0b4b80..7b74e4d96d 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -18,6 +18,7 @@ from invokeai.app.invocations.fields import ( ) from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark from invokeai.backend.image_util.safety_checker import SafetyChecker @@ -34,7 +35,7 @@ class ShowImageInvocation(BaseInvocation): image: ImageField = InputField(description="The image to show") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) image.show() @@ -62,7 +63,7 @@ class BlankImageInvocation(BaseInvocation, WithMetadata): mode: Literal["RGB", "RGBA"] = InputField(default="RGB", description="The mode of the image") color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=255), description="The color of the image") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = Image.new(mode=self.mode, size=(self.width, self.height), color=self.color.tuple()) image_dto = context.images.save(image=image) @@ -86,7 +87,7 @@ class ImageCropInvocation(BaseInvocation, WithMetadata): width: int = InputField(default=512, gt=0, description="The width of the crop rectangle") height: int = InputField(default=512, gt=0, description="The height of the crop rectangle") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) image_crop = Image.new(mode="RGBA", size=(self.width, self.height), color=(0, 0, 0, 0)) @@ -125,7 +126,7 @@ class CenterPadCropInvocation(BaseInvocation): description="Number of pixels to pad/crop from the bottom (negative values crop inwards, positive values pad outwards)", ) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) # Calculate and create new image dimensions @@ -161,7 +162,7 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata): y: int = InputField(default=0, description="The top y coordinate at which to paste the image") crop: bool = InputField(default=False, description="Crop to base image dimensions") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: base_image = context.images.get_pil(self.base_image.image_name) image = context.images.get_pil(self.image.image_name) mask = None @@ -201,7 +202,7 @@ class MaskFromAlphaInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to create the mask from") invert: bool = InputField(default=False, description="Whether or not to invert the mask") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) image_mask = image.split()[-1] @@ -226,7 +227,7 @@ class ImageMultiplyInvocation(BaseInvocation, WithMetadata): image1: ImageField = InputField(description="The first image to multiply") image2: ImageField = InputField(description="The second image to multiply") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image1 = context.images.get_pil(self.image1.image_name) image2 = context.images.get_pil(self.image2.image_name) @@ -253,7 +254,7 @@ class ImageChannelInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to get the channel from") channel: IMAGE_CHANNELS = InputField(default="A", description="The channel to get") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) channel_image = image.getchannel(self.channel) @@ -279,7 +280,7 @@ class ImageConvertInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to convert") mode: IMAGE_MODES = InputField(default="L", description="The mode to convert to") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) converted_image = image.convert(self.mode) @@ -304,7 +305,7 @@ class ImageBlurInvocation(BaseInvocation, WithMetadata): # Metadata blur_type: Literal["gaussian", "box"] = InputField(default="gaussian", description="The type of blur") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) blur = ( @@ -338,7 +339,7 @@ class UnsharpMaskInvocation(BaseInvocation, WithMetadata): def array_from_pil(self, img): return numpy.array(img) / 255 - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) mode = image.mode @@ -401,7 +402,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata): height: int = InputField(default=512, gt=0, description="The height to resize to (px)") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) resample_mode = PIL_RESAMPLING_MAP[self.resample_mode] @@ -434,7 +435,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata): ) resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) resample_mode = PIL_RESAMPLING_MAP[self.resample_mode] @@ -465,7 +466,7 @@ class ImageLerpInvocation(BaseInvocation, WithMetadata): min: int = InputField(default=0, ge=0, le=255, description="The minimum output value") max: int = InputField(default=255, ge=0, le=255, description="The maximum output value") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) image_arr = numpy.asarray(image, dtype=numpy.float32) / 255 @@ -492,7 +493,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata): min: int = InputField(default=0, ge=0, le=255, description="The minimum input value") max: int = InputField(default=255, ge=0, le=255, description="The maximum input value") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) image_arr = numpy.asarray(image, dtype=numpy.float32) @@ -517,7 +518,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to check") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) logger = context.logger @@ -553,7 +554,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to check") text: str = InputField(default="InvokeAI", description="Watermark text") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) new_image = InvisibleWatermark.add_watermark(image, self.text) image_dto = context.images.save(image=new_image) @@ -579,7 +580,7 @@ class MaskEdgeInvocation(BaseInvocation, WithMetadata): description="Second threshold for the hysteresis procedure in Canny edge detection" ) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: mask = context.images.get_pil(self.image.image_name).convert("L") npimg = numpy.asarray(mask, dtype=numpy.uint8) @@ -613,7 +614,7 @@ class MaskCombineInvocation(BaseInvocation, WithMetadata): mask1: ImageField = InputField(description="The first mask to combine") mask2: ImageField = InputField(description="The second image to combine") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: mask1 = context.images.get_pil(self.mask1.image_name).convert("L") mask2 = context.images.get_pil(self.mask2.image_name).convert("L") @@ -642,7 +643,7 @@ class ColorCorrectInvocation(BaseInvocation, WithMetadata): mask: Optional[ImageField] = InputField(default=None, description="Mask to use when applying color-correction") mask_blur_radius: float = InputField(default=8, description="Mask blur radius") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: pil_init_mask = None if self.mask is not None: pil_init_mask = context.images.get_pil(self.mask.image_name).convert("L") @@ -741,7 +742,7 @@ class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to adjust") hue: int = InputField(default=0, description="The degrees by which to rotate the hue, 0-360") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: pil_image = context.images.get_pil(self.image.image_name) # Convert image to HSV color space @@ -831,7 +832,7 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): channel: COLOR_CHANNELS = InputField(description="Which channel to adjust") offset: int = InputField(default=0, ge=-255, le=255, description="The amount to adjust the channel by") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: pil_image = context.images.get_pil(self.image.image_name) # extract the channel and mode from the input and reference tuple @@ -888,7 +889,7 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): scale: float = InputField(default=1.0, ge=0.0, description="The amount to scale the channel by.") invert_channel: bool = InputField(default=False, description="Invert the channel after scaling") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: pil_image = context.images.get_pil(self.image.image_name) # extract the channel and mode from the input and reference tuple @@ -931,7 +932,7 @@ class SaveImageInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description=FieldDescriptions.image) board: BoardField = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) image_dto = context.images.save(image=image, board_id=self.board.board_id if self.board else None) @@ -953,7 +954,7 @@ class LinearUIOutputInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description=FieldDescriptions.image) board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image_dto = context.images.get_dto(self.image.image_name) image_dto = context.images.update( diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index 159bdb5f7a..b007edd9e4 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -8,6 +8,7 @@ from PIL import Image, ImageOps from invokeai.app.invocations.fields import ColorField, ImageField from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import SEED_MAX from invokeai.backend.image_util.cv2_inpaint import cv2_inpaint from invokeai.backend.image_util.lama import LaMA @@ -129,7 +130,7 @@ class InfillColorInvocation(BaseInvocation, WithMetadata): description="The color to use to infill", ) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) solid_bg = Image.new("RGBA", image.size, self.color.tuple()) @@ -155,7 +156,7 @@ class InfillTileInvocation(BaseInvocation, WithMetadata): description="The seed to use for tile generation (omit for random)", ) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) infilled = tile_fill_missing(image.copy(), seed=self.seed, tile_size=self.tile_size) @@ -176,7 +177,7 @@ class InfillPatchMatchInvocation(BaseInvocation, WithMetadata): downscale: float = InputField(default=2.0, gt=0, description="Run patchmatch on downscaled image to speedup infill") resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name).convert("RGBA") resample_mode = PIL_RESAMPLING_MAP[self.resample_mode] @@ -213,7 +214,7 @@ class LaMaInfillInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to infill") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) infilled = infill_lama(image.copy()) @@ -229,7 +230,7 @@ class CV2InfillInvocation(BaseInvocation, WithMetadata): image: ImageField = InputField(description="The image to infill") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) infilled = infill_cv2(image.copy()) diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index b836be04b5..845fcfa284 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -13,6 +13,7 @@ from invokeai.app.invocations.baseinvocation import ( from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.model_management.models.base import BaseModelType, ModelType from invokeai.backend.model_management.models.ip_adapter import get_ip_adapter_image_encoder_model_id @@ -92,7 +93,7 @@ class IPAdapterInvocation(BaseInvocation): validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self - def invoke(self, context) -> IPAdapterOutput: + def invoke(self, context: InvocationContext) -> IPAdapterOutput: # Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model. ip_adapter_info = context.models.get_info( self.ip_adapter_model.model_name, self.ip_adapter_model.base_model, ModelType.IPAdapter diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 0127a6521e..2cc84f80a7 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -3,7 +3,7 @@ import math from contextlib import ExitStack from functools import singledispatchmethod -from typing import TYPE_CHECKING, List, Literal, Optional, Union +from typing import List, Literal, Optional, Union import einops import numpy as np @@ -42,6 +42,7 @@ from invokeai.app.invocations.primitives import ( LatentsOutput, ) from invokeai.app.invocations.t2i_adapter import T2IAdapterField +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.controlnet_utils import prepare_control_image from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus from invokeai.backend.model_management.models import ModelType, SilenceWarnings @@ -70,9 +71,6 @@ from .baseinvocation import ( from .controlnet_image_processors import ControlField from .model import ModelInfo, UNetField, VaeField -if TYPE_CHECKING: - from invokeai.app.services.shared.invocation_context import InvocationContext - if choose_torch_device() == torch.device("mps"): from torch import mps @@ -177,7 +175,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation): def get_scheduler( - context: "InvocationContext", + context: InvocationContext, scheduler_info: ModelInfo, scheduler_name: str, seed: int, @@ -300,7 +298,7 @@ class DenoiseLatentsInvocation(BaseInvocation): def get_conditioning_data( self, - context: "InvocationContext", + context: InvocationContext, scheduler, unet, seed, @@ -369,7 +367,7 @@ class DenoiseLatentsInvocation(BaseInvocation): def prep_control_data( self, - context: "InvocationContext", + context: InvocationContext, control_input: Union[ControlField, List[ControlField]], latents_shape: List[int], exit_stack: ExitStack, @@ -442,7 +440,7 @@ class DenoiseLatentsInvocation(BaseInvocation): def prep_ip_adapter_data( self, - context: "InvocationContext", + context: InvocationContext, ip_adapter: Optional[Union[IPAdapterField, list[IPAdapterField]]], conditioning_data: ConditioningData, exit_stack: ExitStack, @@ -509,7 +507,7 @@ class DenoiseLatentsInvocation(BaseInvocation): def run_t2i_adapters( self, - context: "InvocationContext", + context: InvocationContext, t2i_adapter: Optional[Union[T2IAdapterField, list[T2IAdapterField]]], latents_shape: list[int], do_classifier_free_guidance: bool, @@ -618,7 +616,7 @@ class DenoiseLatentsInvocation(BaseInvocation): return num_inference_steps, timesteps, init_timestep - def prep_inpaint_mask(self, context: "InvocationContext", latents): + def prep_inpaint_mask(self, context: InvocationContext, latents): if self.denoise_mask is None: return None, None diff --git a/invokeai/app/invocations/math.py b/invokeai/app/invocations/math.py index d2dbf04981..83a092be69 100644 --- a/invokeai/app/invocations/math.py +++ b/invokeai/app/invocations/math.py @@ -7,6 +7,7 @@ from pydantic import ValidationInfo, field_validator from invokeai.app.invocations.fields import FieldDescriptions, InputField from invokeai.app.invocations.primitives import FloatOutput, IntegerOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from .baseinvocation import BaseInvocation, invocation @@ -18,7 +19,7 @@ class AddInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: return IntegerOutput(value=self.a + self.b) @@ -29,7 +30,7 @@ class SubtractInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: return IntegerOutput(value=self.a - self.b) @@ -40,7 +41,7 @@ class MultiplyInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: return IntegerOutput(value=self.a * self.b) @@ -51,7 +52,7 @@ class DivideInvocation(BaseInvocation): a: int = InputField(default=0, description=FieldDescriptions.num_1) b: int = InputField(default=0, description=FieldDescriptions.num_2) - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: return IntegerOutput(value=int(self.a / self.b)) @@ -69,7 +70,7 @@ class RandomIntInvocation(BaseInvocation): low: int = InputField(default=0, description=FieldDescriptions.inclusive_low) high: int = InputField(default=np.iinfo(np.int32).max, description=FieldDescriptions.exclusive_high) - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: return IntegerOutput(value=np.random.randint(self.low, self.high)) @@ -88,7 +89,7 @@ class RandomFloatInvocation(BaseInvocation): high: float = InputField(default=1.0, description=FieldDescriptions.exclusive_high) decimals: int = InputField(default=2, description=FieldDescriptions.decimal_places) - def invoke(self, context) -> FloatOutput: + def invoke(self, context: InvocationContext) -> FloatOutput: random_float = np.random.uniform(self.low, self.high) rounded_float = round(random_float, self.decimals) return FloatOutput(value=rounded_float) @@ -110,7 +111,7 @@ class FloatToIntegerInvocation(BaseInvocation): default="Nearest", description="The method to use for rounding" ) - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: if self.method == "Nearest": return IntegerOutput(value=round(self.value / self.multiple) * self.multiple) elif self.method == "Floor": @@ -128,7 +129,7 @@ class RoundInvocation(BaseInvocation): value: float = InputField(default=0, description="The float value") decimals: int = InputField(default=0, description="The number of decimal places") - def invoke(self, context) -> FloatOutput: + def invoke(self, context: InvocationContext) -> FloatOutput: return FloatOutput(value=round(self.value, self.decimals)) @@ -196,7 +197,7 @@ class IntegerMathInvocation(BaseInvocation): raise ValueError("Result of exponentiation is not an integer") return v - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: # Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9 if self.operation == "ADD": return IntegerOutput(value=self.a + self.b) @@ -270,7 +271,7 @@ class FloatMathInvocation(BaseInvocation): raise ValueError("Root operation resulted in a complex number") return v - def invoke(self, context) -> FloatOutput: + def invoke(self, context: InvocationContext) -> FloatOutput: # Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9 if self.operation == "ADD": return FloatOutput(value=self.a + self.b) diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py index 9d74abd8c1..58edfab711 100644 --- a/invokeai/app/invocations/metadata.py +++ b/invokeai/app/invocations/metadata.py @@ -20,6 +20,7 @@ from invokeai.app.invocations.fields import ( from invokeai.app.invocations.ip_adapter import IPAdapterModelField from invokeai.app.invocations.model import LoRAModelField, MainModelField, VAEModelField from invokeai.app.invocations.t2i_adapter import T2IAdapterField +from invokeai.app.services.shared.invocation_context import InvocationContext from ...version import __version__ @@ -64,7 +65,7 @@ class MetadataItemInvocation(BaseInvocation): label: str = InputField(description=FieldDescriptions.metadata_item_label) value: Any = InputField(description=FieldDescriptions.metadata_item_value, ui_type=UIType.Any) - def invoke(self, context) -> MetadataItemOutput: + def invoke(self, context: InvocationContext) -> MetadataItemOutput: return MetadataItemOutput(item=MetadataItemField(label=self.label, value=self.value)) @@ -81,7 +82,7 @@ class MetadataInvocation(BaseInvocation): description=FieldDescriptions.metadata_item_polymorphic ) - def invoke(self, context) -> MetadataOutput: + def invoke(self, context: InvocationContext) -> MetadataOutput: if isinstance(self.items, MetadataItemField): # single metadata item data = {self.items.label: self.items.value} @@ -100,7 +101,7 @@ class MergeMetadataInvocation(BaseInvocation): collection: list[MetadataField] = InputField(description=FieldDescriptions.metadata_collection) - def invoke(self, context) -> MetadataOutput: + def invoke(self, context: InvocationContext) -> MetadataOutput: data = {} for item in self.collection: data.update(item.model_dump()) @@ -218,7 +219,7 @@ class CoreMetadataInvocation(BaseInvocation): description="The start value used for refiner denoising", ) - def invoke(self, context) -> MetadataOutput: + def invoke(self, context: InvocationContext) -> MetadataOutput: """Collects and outputs a CoreMetadata object""" return MetadataOutput( diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index f81e559e44..6a1fd6d36b 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -4,6 +4,7 @@ from typing import List, Optional from pydantic import BaseModel, ConfigDict, Field from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.shared.models import FreeUConfig from ...backend.model_management import BaseModelType, ModelType, SubModelType @@ -109,7 +110,7 @@ class MainModelLoaderInvocation(BaseInvocation): model: MainModelField = InputField(description=FieldDescriptions.main_model, input=Input.Direct) # TODO: precision? - def invoke(self, context) -> ModelLoaderOutput: + def invoke(self, context: InvocationContext) -> ModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.Main @@ -221,7 +222,7 @@ class LoraLoaderInvocation(BaseInvocation): title="CLIP", ) - def invoke(self, context) -> LoraLoaderOutput: + def invoke(self, context: InvocationContext) -> LoraLoaderOutput: if self.lora is None: raise Exception("No LoRA provided") @@ -310,7 +311,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): title="CLIP 2", ) - def invoke(self, context) -> SDXLLoraLoaderOutput: + def invoke(self, context: InvocationContext) -> SDXLLoraLoaderOutput: if self.lora is None: raise Exception("No LoRA provided") @@ -393,7 +394,7 @@ class VaeLoaderInvocation(BaseInvocation): title="VAE", ) - def invoke(self, context) -> VAEOutput: + def invoke(self, context: InvocationContext) -> VAEOutput: base_model = self.vae_model.base_model model_name = self.vae_model.model_name model_type = ModelType.Vae @@ -448,7 +449,7 @@ class SeamlessModeInvocation(BaseInvocation): seamless_y: bool = InputField(default=True, input=Input.Any, description="Specify whether Y axis is seamless") seamless_x: bool = InputField(default=True, input=Input.Any, description="Specify whether X axis is seamless") - def invoke(self, context) -> SeamlessModeOutput: + def invoke(self, context: InvocationContext) -> SeamlessModeOutput: # Conditionally append 'x' and 'y' based on seamless_x and seamless_y unet = copy.deepcopy(self.unet) vae = copy.deepcopy(self.vae) @@ -484,6 +485,6 @@ class FreeUInvocation(BaseInvocation): s1: float = InputField(default=0.9, ge=-1, le=3, description=FieldDescriptions.freeu_s1) s2: float = InputField(default=0.2, ge=-1, le=3, description=FieldDescriptions.freeu_s2) - def invoke(self, context) -> UNetOutput: + def invoke(self, context: InvocationContext) -> UNetOutput: self.unet.freeu_config = FreeUConfig(s1=self.s1, s2=self.s2, b1=self.b1, b2=self.b2) return UNetOutput(unet=self.unet) diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index 41641152f0..78f13cc52d 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -5,6 +5,7 @@ import torch from pydantic import field_validator from invokeai.app.invocations.fields import FieldDescriptions, InputField, LatentsField, OutputField +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import SEED_MAX from ...backend.util.devices import choose_torch_device, torch_dtype @@ -112,7 +113,7 @@ class NoiseInvocation(BaseInvocation): """Returns the seed modulo (SEED_MAX + 1) to ensure it is within the valid range.""" return v % (SEED_MAX + 1) - def invoke(self, context) -> NoiseOutput: + def invoke(self, context: InvocationContext) -> NoiseOutput: noise = get_noise( width=self.width, height=self.height, diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py index a1e318a380..e7b4d3d9fc 100644 --- a/invokeai/app/invocations/onnx.py +++ b/invokeai/app/invocations/onnx.py @@ -63,7 +63,7 @@ class ONNXPromptInvocation(BaseInvocation): prompt: str = InputField(default="", description=FieldDescriptions.raw_prompt, ui_component=UIComponent.Textarea) clip: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection) - def invoke(self, context) -> ConditioningOutput: + def invoke(self, context: InvocationContext) -> ConditioningOutput: tokenizer_info = context.services.model_manager.get_model( **self.clip.tokenizer.model_dump(), ) @@ -201,7 +201,7 @@ class ONNXTextToLatentsInvocation(BaseInvocation): # based on # https://github.com/huggingface/diffusers/blob/3ebbaf7c96801271f9e6c21400033b6aa5ffcf29/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py#L375 - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: c, _ = context.services.latents.get(self.positive_conditioning.conditioning_name) uc, _ = context.services.latents.get(self.negative_conditioning.conditioning_name) graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) @@ -342,7 +342,7 @@ class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata): ) # tiled: bool = InputField(default=False, description="Decode latents by overlaping tiles(less memory consumption)") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: latents = context.services.latents.get(self.latents.latents_name) if self.vae.vae.submodel != SubModelType.VaeDecoder: @@ -417,7 +417,7 @@ class OnnxModelLoaderInvocation(BaseInvocation): description=FieldDescriptions.onnx_main_model, input=Input.Direct, ui_type=UIType.ONNXModel ) - def invoke(self, context) -> ONNXModelLoaderOutput: + def invoke(self, context: InvocationContext) -> ONNXModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.ONNX diff --git a/invokeai/app/invocations/param_easing.py b/invokeai/app/invocations/param_easing.py index bf59e87d27..6845637de9 100644 --- a/invokeai/app/invocations/param_easing.py +++ b/invokeai/app/invocations/param_easing.py @@ -40,6 +40,7 @@ from easing_functions import ( from matplotlib.ticker import MaxNLocator from invokeai.app.invocations.primitives import FloatCollectionOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from .baseinvocation import BaseInvocation, invocation from .fields import InputField @@ -62,7 +63,7 @@ class FloatLinearRangeInvocation(BaseInvocation): description="number of values to interpolate over (including start and stop)", ) - def invoke(self, context) -> FloatCollectionOutput: + def invoke(self, context: InvocationContext) -> FloatCollectionOutput: param_list = list(np.linspace(self.start, self.stop, self.steps)) return FloatCollectionOutput(collection=param_list) @@ -130,7 +131,7 @@ class StepParamEasingInvocation(BaseInvocation): # alt_mirror: bool = InputField(default=False, description="alternative mirroring by dual easing") show_easing_plot: bool = InputField(default=False, description="show easing plot") - def invoke(self, context) -> FloatCollectionOutput: + def invoke(self, context: InvocationContext) -> FloatCollectionOutput: log_diagnostics = False # convert from start_step_percent to nearest step <= (steps * start_step_percent) # start_step = int(np.floor(self.num_steps * self.start_step_percent)) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index ee04345eed..c90d3230b2 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -17,6 +17,7 @@ from invokeai.app.invocations.fields import ( UIComponent, ) from invokeai.app.services.images.images_common import ImageDTO +from invokeai.app.services.shared.invocation_context import InvocationContext from .baseinvocation import ( BaseInvocation, @@ -59,7 +60,7 @@ class BooleanInvocation(BaseInvocation): value: bool = InputField(default=False, description="The boolean value") - def invoke(self, context) -> BooleanOutput: + def invoke(self, context: InvocationContext) -> BooleanOutput: return BooleanOutput(value=self.value) @@ -75,7 +76,7 @@ class BooleanCollectionInvocation(BaseInvocation): collection: list[bool] = InputField(default=[], description="The collection of boolean values") - def invoke(self, context) -> BooleanCollectionOutput: + def invoke(self, context: InvocationContext) -> BooleanCollectionOutput: return BooleanCollectionOutput(collection=self.collection) @@ -108,7 +109,7 @@ class IntegerInvocation(BaseInvocation): value: int = InputField(default=0, description="The integer value") - def invoke(self, context) -> IntegerOutput: + def invoke(self, context: InvocationContext) -> IntegerOutput: return IntegerOutput(value=self.value) @@ -124,7 +125,7 @@ class IntegerCollectionInvocation(BaseInvocation): collection: list[int] = InputField(default=[], description="The collection of integer values") - def invoke(self, context) -> IntegerCollectionOutput: + def invoke(self, context: InvocationContext) -> IntegerCollectionOutput: return IntegerCollectionOutput(collection=self.collection) @@ -155,7 +156,7 @@ class FloatInvocation(BaseInvocation): value: float = InputField(default=0.0, description="The float value") - def invoke(self, context) -> FloatOutput: + def invoke(self, context: InvocationContext) -> FloatOutput: return FloatOutput(value=self.value) @@ -171,7 +172,7 @@ class FloatCollectionInvocation(BaseInvocation): collection: list[float] = InputField(default=[], description="The collection of float values") - def invoke(self, context) -> FloatCollectionOutput: + def invoke(self, context: InvocationContext) -> FloatCollectionOutput: return FloatCollectionOutput(collection=self.collection) @@ -202,7 +203,7 @@ class StringInvocation(BaseInvocation): value: str = InputField(default="", description="The string value", ui_component=UIComponent.Textarea) - def invoke(self, context) -> StringOutput: + def invoke(self, context: InvocationContext) -> StringOutput: return StringOutput(value=self.value) @@ -218,7 +219,7 @@ class StringCollectionInvocation(BaseInvocation): collection: list[str] = InputField(default=[], description="The collection of string values") - def invoke(self, context) -> StringCollectionOutput: + def invoke(self, context: InvocationContext) -> StringCollectionOutput: return StringCollectionOutput(collection=self.collection) @@ -261,7 +262,7 @@ class ImageInvocation( image: ImageField = InputField(description="The image to load") - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) return ImageOutput( @@ -283,7 +284,7 @@ class ImageCollectionInvocation(BaseInvocation): collection: list[ImageField] = InputField(description="The collection of image values") - def invoke(self, context) -> ImageCollectionOutput: + def invoke(self, context: InvocationContext) -> ImageCollectionOutput: return ImageCollectionOutput(collection=self.collection) @@ -346,7 +347,7 @@ class LatentsInvocation(BaseInvocation): latents: LatentsField = InputField(description="The latents tensor", input=Input.Connection) - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: latents = context.latents.get(self.latents.latents_name) return LatentsOutput.build(self.latents.latents_name, latents) @@ -366,7 +367,7 @@ class LatentsCollectionInvocation(BaseInvocation): description="The collection of latents tensors", ) - def invoke(self, context) -> LatentsCollectionOutput: + def invoke(self, context: InvocationContext) -> LatentsCollectionOutput: return LatentsCollectionOutput(collection=self.collection) @@ -397,7 +398,7 @@ class ColorInvocation(BaseInvocation): color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=255), description="The color value") - def invoke(self, context) -> ColorOutput: + def invoke(self, context: InvocationContext) -> ColorOutput: return ColorOutput(color=self.color) @@ -438,7 +439,7 @@ class ConditioningInvocation(BaseInvocation): conditioning: ConditioningField = InputField(description=FieldDescriptions.cond, input=Input.Connection) - def invoke(self, context) -> ConditioningOutput: + def invoke(self, context: InvocationContext) -> ConditioningOutput: return ConditioningOutput(conditioning=self.conditioning) @@ -457,7 +458,7 @@ class ConditioningCollectionInvocation(BaseInvocation): description="The collection of conditioning tensors", ) - def invoke(self, context) -> ConditioningCollectionOutput: + def invoke(self, context: InvocationContext) -> ConditioningCollectionOutput: return ConditioningCollectionOutput(collection=self.collection) diff --git a/invokeai/app/invocations/prompt.py b/invokeai/app/invocations/prompt.py index 4f5ef43a56..234743a003 100644 --- a/invokeai/app/invocations/prompt.py +++ b/invokeai/app/invocations/prompt.py @@ -6,6 +6,7 @@ from dynamicprompts.generators import CombinatorialPromptGenerator, RandomPrompt from pydantic import field_validator from invokeai.app.invocations.primitives import StringCollectionOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from .baseinvocation import BaseInvocation, invocation from .fields import InputField, UIComponent @@ -29,7 +30,7 @@ class DynamicPromptInvocation(BaseInvocation): max_prompts: int = InputField(default=1, description="The number of prompts to generate") combinatorial: bool = InputField(default=False, description="Whether to use the combinatorial generator") - def invoke(self, context) -> StringCollectionOutput: + def invoke(self, context: InvocationContext) -> StringCollectionOutput: if self.combinatorial: generator = CombinatorialPromptGenerator() prompts = generator.generate(self.prompt, max_prompts=self.max_prompts) @@ -91,7 +92,7 @@ class PromptsFromFileInvocation(BaseInvocation): break return prompts - def invoke(self, context) -> StringCollectionOutput: + def invoke(self, context: InvocationContext) -> StringCollectionOutput: prompts = self.promptsFromFile( self.file_path, self.pre_prompt, diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 75a526cfff..8d51674a04 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -1,4 +1,5 @@ from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType +from invokeai.app.services.shared.invocation_context import InvocationContext from ...backend.model_management import ModelType, SubModelType from .baseinvocation import ( @@ -38,7 +39,7 @@ class SDXLModelLoaderInvocation(BaseInvocation): ) # TODO: precision? - def invoke(self, context) -> SDXLModelLoaderOutput: + def invoke(self, context: InvocationContext) -> SDXLModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.Main @@ -127,7 +128,7 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation): ) # TODO: precision? - def invoke(self, context) -> SDXLRefinerModelLoaderOutput: + def invoke(self, context: InvocationContext) -> SDXLRefinerModelLoaderOutput: base_model = self.model.base_model model_name = self.model.model_name model_type = ModelType.Main diff --git a/invokeai/app/invocations/strings.py b/invokeai/app/invocations/strings.py index a4c92d9de5..182c976cd7 100644 --- a/invokeai/app/invocations/strings.py +++ b/invokeai/app/invocations/strings.py @@ -2,6 +2,8 @@ import re +from invokeai.app.services.shared.invocation_context import InvocationContext + from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, @@ -32,7 +34,7 @@ class StringSplitNegInvocation(BaseInvocation): string: str = InputField(default="", description="String to split", ui_component=UIComponent.Textarea) - def invoke(self, context) -> StringPosNegOutput: + def invoke(self, context: InvocationContext) -> StringPosNegOutput: p_string = "" n_string = "" brackets_depth = 0 @@ -76,7 +78,7 @@ class StringSplitInvocation(BaseInvocation): default="", description="Delimiter to spilt with. blank will split on the first whitespace" ) - def invoke(self, context) -> String2Output: + def invoke(self, context: InvocationContext) -> String2Output: result = self.string.split(self.delimiter, 1) if len(result) == 2: part1, part2 = result @@ -94,7 +96,7 @@ class StringJoinInvocation(BaseInvocation): string_left: str = InputField(default="", description="String Left", ui_component=UIComponent.Textarea) string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea) - def invoke(self, context) -> StringOutput: + def invoke(self, context: InvocationContext) -> StringOutput: return StringOutput(value=((self.string_left or "") + (self.string_right or ""))) @@ -106,7 +108,7 @@ class StringJoinThreeInvocation(BaseInvocation): string_middle: str = InputField(default="", description="String Middle", ui_component=UIComponent.Textarea) string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea) - def invoke(self, context) -> StringOutput: + def invoke(self, context: InvocationContext) -> StringOutput: return StringOutput(value=((self.string_left or "") + (self.string_middle or "") + (self.string_right or ""))) @@ -125,7 +127,7 @@ class StringReplaceInvocation(BaseInvocation): default=False, description="Use search string as a regex expression (non regex is case insensitive)" ) - def invoke(self, context) -> StringOutput: + def invoke(self, context: InvocationContext) -> StringOutput: pattern = self.search_string or "" new_string = self.string or "" if len(pattern) > 0: diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py index 74a098a501..0f4fe66ada 100644 --- a/invokeai/app/invocations/t2i_adapter.py +++ b/invokeai/app/invocations/t2i_adapter.py @@ -11,6 +11,7 @@ from invokeai.app.invocations.baseinvocation import ( from invokeai.app.invocations.controlnet_image_processors import CONTROLNET_RESIZE_VALUES from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.model_management.models.base import BaseModelType @@ -89,7 +90,7 @@ class T2IAdapterInvocation(BaseInvocation): validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self - def invoke(self, context) -> T2IAdapterOutput: + def invoke(self, context: InvocationContext) -> T2IAdapterOutput: return T2IAdapterOutput( t2i_adapter=T2IAdapterField( image=self.image, diff --git a/invokeai/app/invocations/tiles.py b/invokeai/app/invocations/tiles.py index 0b4c472696..19ece42376 100644 --- a/invokeai/app/invocations/tiles.py +++ b/invokeai/app/invocations/tiles.py @@ -13,6 +13,7 @@ from invokeai.app.invocations.baseinvocation import ( ) from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField, WithMetadata from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.tiles.tiles import ( calc_tiles_even_split, calc_tiles_min_overlap, @@ -56,7 +57,7 @@ class CalculateImageTilesInvocation(BaseInvocation): description="The target overlap, in pixels, between adjacent tiles. Adjacent tiles will overlap by at least this amount", ) - def invoke(self, context) -> CalculateImageTilesOutput: + def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: tiles = calc_tiles_with_overlap( image_height=self.image_height, image_width=self.image_width, @@ -99,7 +100,7 @@ class CalculateImageTilesEvenSplitInvocation(BaseInvocation): description="The overlap, in pixels, between adjacent tiles.", ) - def invoke(self, context) -> CalculateImageTilesOutput: + def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: tiles = calc_tiles_even_split( image_height=self.image_height, image_width=self.image_width, @@ -129,7 +130,7 @@ class CalculateImageTilesMinimumOverlapInvocation(BaseInvocation): tile_height: int = InputField(ge=1, default=576, description="The tile height, in pixels.") min_overlap: int = InputField(default=128, ge=0, description="Minimum overlap between adjacent tiles, in pixels.") - def invoke(self, context) -> CalculateImageTilesOutput: + def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput: tiles = calc_tiles_min_overlap( image_height=self.image_height, image_width=self.image_width, @@ -174,7 +175,7 @@ class TileToPropertiesInvocation(BaseInvocation): tile: Tile = InputField(description="The tile to split into properties.") - def invoke(self, context) -> TileToPropertiesOutput: + def invoke(self, context: InvocationContext) -> TileToPropertiesOutput: return TileToPropertiesOutput( coords_left=self.tile.coords.left, coords_right=self.tile.coords.right, @@ -211,7 +212,7 @@ class PairTileImageInvocation(BaseInvocation): image: ImageField = InputField(description="The tile image.") tile: Tile = InputField(description="The tile properties.") - def invoke(self, context) -> PairTileImageOutput: + def invoke(self, context: InvocationContext) -> PairTileImageOutput: return PairTileImageOutput( tile_with_image=TileWithImage( tile=self.tile, @@ -247,7 +248,7 @@ class MergeTilesToImageInvocation(BaseInvocation, WithMetadata): description="The amount to blend adjacent tiles in pixels. Must be <= the amount of overlap between adjacent tiles.", ) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: images = [twi.image for twi in self.tiles_with_images] tiles = [twi.tile for twi in self.tiles_with_images] diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index ef17480986..71ef7ca3aa 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -10,6 +10,7 @@ from pydantic import ConfigDict from invokeai.app.invocations.fields import ImageField from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN from invokeai.backend.util.devices import choose_torch_device @@ -42,7 +43,7 @@ class ESRGANInvocation(BaseInvocation, WithMetadata): model_config = ConfigDict(protected_namespaces=()) - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) models_path = context.config.get().models_path diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index c0699eb96b..3df230f5ee 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -17,6 +17,7 @@ from invokeai.app.invocations.baseinvocation import ( invocation_output, ) from invokeai.app.invocations.fields import Input, InputField, OutputField, UIType +from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import uuid_string # in 3.10 this would be "from types import NoneType" @@ -201,7 +202,7 @@ class GraphInvocation(BaseInvocation): # TODO: figure out how to create a default here graph: "Graph" = InputField(description="The graph to run", default=None) - def invoke(self, context) -> GraphInvocationOutput: + def invoke(self, context: InvocationContext) -> GraphInvocationOutput: """Invoke with provided services and return outputs.""" return GraphInvocationOutput() @@ -227,7 +228,7 @@ class IterateInvocation(BaseInvocation): ) index: int = InputField(description="The index, will be provided on executed iterators", default=0, ui_hidden=True) - def invoke(self, context) -> IterateInvocationOutput: + def invoke(self, context: InvocationContext) -> IterateInvocationOutput: """Produces the outputs as values""" return IterateInvocationOutput(item=self.collection[self.index], index=self.index, total=len(self.collection)) @@ -254,7 +255,7 @@ class CollectInvocation(BaseInvocation): description="The collection, will be provided on execution", default=[], ui_hidden=True ) - def invoke(self, context) -> CollectInvocationOutput: + def invoke(self, context: InvocationContext) -> CollectInvocationOutput: """Invoke with provided services and return outputs.""" return CollectInvocationOutput(collection=copy.copy(self.collection)) diff --git a/tests/aa_nodes/test_nodes.py b/tests/aa_nodes/test_nodes.py index 559457c0e1..aab3d9c7b4 100644 --- a/tests/aa_nodes/test_nodes.py +++ b/tests/aa_nodes/test_nodes.py @@ -8,6 +8,7 @@ from invokeai.app.invocations.baseinvocation import ( ) from invokeai.app.invocations.fields import InputField, OutputField from invokeai.app.invocations.image import ImageField +from invokeai.app.services.shared.invocation_context import InvocationContext # Define test invocations before importing anything that uses invocations @@ -20,7 +21,7 @@ class ListPassThroughInvocationOutput(BaseInvocationOutput): class ListPassThroughInvocation(BaseInvocation): collection: list[ImageField] = InputField(default=[]) - def invoke(self, context) -> ListPassThroughInvocationOutput: + def invoke(self, context: InvocationContext) -> ListPassThroughInvocationOutput: return ListPassThroughInvocationOutput(collection=self.collection) @@ -33,13 +34,13 @@ class PromptTestInvocationOutput(BaseInvocationOutput): class PromptTestInvocation(BaseInvocation): prompt: str = InputField(default="") - def invoke(self, context) -> PromptTestInvocationOutput: + def invoke(self, context: InvocationContext) -> PromptTestInvocationOutput: return PromptTestInvocationOutput(prompt=self.prompt) @invocation("test_error", version="1.0.0") class ErrorInvocation(BaseInvocation): - def invoke(self, context) -> PromptTestInvocationOutput: + def invoke(self, context: InvocationContext) -> PromptTestInvocationOutput: raise Exception("This invocation is supposed to fail") @@ -53,7 +54,7 @@ class TextToImageTestInvocation(BaseInvocation): prompt: str = InputField(default="") prompt2: str = InputField(default="") - def invoke(self, context) -> ImageTestInvocationOutput: + def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) @@ -62,7 +63,7 @@ class ImageToImageTestInvocation(BaseInvocation): prompt: str = InputField(default="") image: Union[ImageField, None] = InputField(default=None) - def invoke(self, context) -> ImageTestInvocationOutput: + def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) @@ -75,7 +76,7 @@ class PromptCollectionTestInvocationOutput(BaseInvocationOutput): class PromptCollectionTestInvocation(BaseInvocation): collection: list[str] = InputField() - def invoke(self, context) -> PromptCollectionTestInvocationOutput: + def invoke(self, context: InvocationContext) -> PromptCollectionTestInvocationOutput: return PromptCollectionTestInvocationOutput(collection=self.collection.copy()) @@ -88,7 +89,7 @@ class AnyTypeTestInvocationOutput(BaseInvocationOutput): class AnyTypeTestInvocation(BaseInvocation): value: Any = InputField(default=None) - def invoke(self, context) -> AnyTypeTestInvocationOutput: + def invoke(self, context: InvocationContext) -> AnyTypeTestInvocationOutput: return AnyTypeTestInvocationOutput(value=self.value) @@ -96,7 +97,7 @@ class AnyTypeTestInvocation(BaseInvocation): class PolymorphicStringTestInvocation(BaseInvocation): value: Union[str, list[str]] = InputField(default="") - def invoke(self, context) -> PromptCollectionTestInvocationOutput: + def invoke(self, context: InvocationContext) -> PromptCollectionTestInvocationOutput: if isinstance(self.value, str): return PromptCollectionTestInvocationOutput(collection=[self.value]) return PromptCollectionTestInvocationOutput(collection=self.value) From 95dd5aad16287cc92b8503e6c321fc178361833e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:40:49 +1100 Subject: [PATCH 054/411] feat(nodes): add boards interface to invocation context --- .../app/services/shared/invocation_context.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index a849d6b17a..cbcaa6a548 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -7,6 +7,7 @@ from pydantic import ConfigDict from torch import Tensor from invokeai.app.invocations.fields import MetadataField, WithMetadata +from invokeai.app.services.boards.boards_common import BoardDTO from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO @@ -63,6 +64,54 @@ class InvocationContextData: """The workflow associated with this queue item, if any.""" +class BoardsInterface: + def __init__(self, services: InvocationServices) -> None: + def create(board_name: str) -> BoardDTO: + """ + Creates a board. + + :param board_name: The name of the board to create. + """ + return services.boards.create(board_name) + + def get_dto(board_id: str) -> BoardDTO: + """ + Gets a board DTO. + + :param board_id: The ID of the board to get. + """ + return services.boards.get_dto(board_id) + + def get_all() -> list[BoardDTO]: + """ + Gets all boards. + """ + return services.boards.get_all() + + def add_image_to_board(board_id: str, image_name: str) -> None: + """ + Adds an image to a board. + + :param board_id: The ID of the board to add the image to. + :param image_name: The name of the image to add to the board. + """ + services.board_images.add_image_to_board(board_id, image_name) + + def get_all_image_names_for_board(board_id: str) -> list[str]: + """ + Gets all image names for a board. + + :param board_id: The ID of the board to get the image names for. + """ + return services.board_images.get_all_board_image_names_for_board(board_id) + + self.create = create + self.get_dto = get_dto + self.get_all = get_all + self.add_image_to_board = add_image_to_board + self.get_all_image_names_for_board = get_all_image_names_for_board + + class LoggerInterface: def __init__(self, services: InvocationServices) -> None: def debug(message: str) -> None: @@ -427,6 +476,7 @@ class InvocationContext: logger: LoggerInterface, config: ConfigInterface, util: UtilInterface, + boards: BoardsInterface, data: InvocationContextData, services: InvocationServices, ) -> None: @@ -444,6 +494,8 @@ class InvocationContext: """Provides access to the app's config.""" self.util = util """Provides utility methods.""" + self.boards = boards + """Provides methods to interact with boards.""" self.data = data """Provides data about the current queue item and invocation.""" self.__services = services @@ -554,6 +606,7 @@ def build_invocation_context( config = ConfigInterface(services=services) util = UtilInterface(services=services, context_data=context_data) conditioning = ConditioningInterface(services=services, context_data=context_data) + boards = BoardsInterface(services=services) ctx = InvocationContext( images=images, @@ -565,6 +618,7 @@ def build_invocation_context( util=util, conditioning=conditioning, services=services, + boards=boards, ) return ctx From e11af7de9b665f70a334758f25c1013371ad603a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:48:32 +1100 Subject: [PATCH 055/411] feat(nodes): export more things from `invocation_api" --- invokeai/invocation_api/__init__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/invokeai/invocation_api/__init__.py b/invokeai/invocation_api/__init__.py index e867ec3cc4..e80bc26a00 100644 --- a/invokeai/invocation_api/__init__.py +++ b/invokeai/invocation_api/__init__.py @@ -47,8 +47,14 @@ from invokeai.app.invocations.primitives import ( StringCollectionOutput, StringOutput, ) +from invokeai.app.services.boards.boards_common import BoardDTO +from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID +from invokeai.backend.model_management.model_manager import ModelInfo +from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType +from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, ConditioningFieldData, @@ -101,9 +107,23 @@ __all__ = [ "StringOutput", # invokeai.app.services.image_records.image_records_common "ImageCategory", + # invokeai.app.services.boards.boards_common + "BoardDTO", # invokeai.backend.stable_diffusion.diffusion.conditioning_data "BasicConditioningInfo", "ConditioningFieldData", "ExtraConditioningInfo", "SDXLConditioningInfo", + # invokeai.backend.stable_diffusion.diffusers_pipeline + "PipelineIntermediateState", + # invokeai.app.services.workflow_records.workflow_records_common + "WorkflowWithoutID", + # invokeai.app.services.config.config_default + "InvokeAIAppConfig", + # invokeai.backend.model_management.model_manager + "ModelInfo", + # invokeai.backend.model_management.models.base + "BaseModelType", + "ModelType", + "SubModelType", ] From cbf22d8a804fc6aa7685b7a041cce9db72fbe631 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 5 Feb 2024 18:06:01 +1100 Subject: [PATCH 056/411] chore(nodes): add comments for ConfigInterface --- invokeai/app/services/shared/invocation_context.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index cbcaa6a548..cb989cb15e 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -392,7 +392,7 @@ class ConfigInterface: def __init__(self, services: InvocationServices) -> None: def get() -> InvokeAIAppConfig: """ - Gets the app's config. + Gets the app's config. The config is read-only; attempts to mutate it will raise an error. """ # The config can be changed at runtime. @@ -400,6 +400,7 @@ class ConfigInterface: # We don't want nodes doing this, so we make a frozen copy. config = services.configuration.get_config() + # TODO(psyche): If config cannot be changed at runtime, should we cache this? frozen_config = config.model_copy(update={"model_config": ConfigDict(frozen=True)}) return frozen_config From 59c77832d8e557f3c111e35e02a3426bffee1bac Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Feb 2024 00:37:18 +1100 Subject: [PATCH 057/411] tests(nodes): fix mock InvocationContext --- tests/aa_nodes/test_graph_execution_state.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/aa_nodes/test_graph_execution_state.py b/tests/aa_nodes/test_graph_execution_state.py index 3577a78ae2..1612cbe719 100644 --- a/tests/aa_nodes/test_graph_execution_state.py +++ b/tests/aa_nodes/test_graph_execution_state.py @@ -93,6 +93,7 @@ def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[B logger=None, models=None, util=None, + boards=None, services=None, ) ) From cc8d713c57e4486496024d4d71942e772bd7628f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:22:58 +1100 Subject: [PATCH 058/411] fix(nodes): restore missing context type annotations --- invokeai/app/invocations/latent.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 2cc84f80a7..5e36e73ec8 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -106,7 +106,7 @@ class SchedulerInvocation(BaseInvocation): ui_type=UIType.Scheduler, ) - def invoke(self, context) -> SchedulerOutput: + def invoke(self, context: InvocationContext) -> SchedulerOutput: return SchedulerOutput(scheduler=self.scheduler) @@ -141,7 +141,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation): return mask_tensor @torch.no_grad() - def invoke(self, context) -> DenoiseMaskOutput: + def invoke(self, context: InvocationContext) -> DenoiseMaskOutput: if self.image is not None: image = context.images.get_pil(self.image.image_name) image = image_resized_to_grid_as_tensor(image.convert("RGB")) @@ -630,7 +630,7 @@ class DenoiseLatentsInvocation(BaseInvocation): return 1 - mask, masked_latents @torch.no_grad() - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: with SilenceWarnings(): # this quenches NSFW nag from diffusers seed = None noise = None @@ -777,7 +777,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata): fp32: bool = InputField(default=DEFAULT_PRECISION == "float32", description=FieldDescriptions.fp32) @torch.no_grad() - def invoke(self, context) -> ImageOutput: + def invoke(self, context: InvocationContext) -> ImageOutput: latents = context.latents.get(self.latents.latents_name) vae_info = context.models.load(**self.vae.vae.model_dump()) @@ -868,7 +868,7 @@ class ResizeLatentsInvocation(BaseInvocation): mode: LATENTS_INTERPOLATION_MODE = InputField(default="bilinear", description=FieldDescriptions.interp_mode) antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: latents = context.latents.get(self.latents.latents_name) # TODO: @@ -909,7 +909,7 @@ class ScaleLatentsInvocation(BaseInvocation): mode: LATENTS_INTERPOLATION_MODE = InputField(default="bilinear", description=FieldDescriptions.interp_mode) antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: latents = context.latents.get(self.latents.latents_name) # TODO: @@ -998,7 +998,7 @@ class ImageToLatentsInvocation(BaseInvocation): return latents @torch.no_grad() - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: image = context.images.get_pil(self.image.image_name) vae_info = context.models.load(**self.vae.vae.model_dump()) @@ -1046,7 +1046,7 @@ class BlendLatentsInvocation(BaseInvocation): ) alpha: float = InputField(default=0.5, description=FieldDescriptions.blend_alpha) - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: latents_a = context.latents.get(self.latents_a.latents_name) latents_b = context.latents.get(self.latents_b.latents_name) @@ -1147,7 +1147,7 @@ class CropLatentsCoreInvocation(BaseInvocation): description="The height (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.", ) - def invoke(self, context) -> LatentsOutput: + def invoke(self, context: InvocationContext) -> LatentsOutput: latents = context.latents.get(self.latents.latents_name) x1 = self.x // LATENT_SCALE_FACTOR From dcafbb998890548b24a40a8a415f582dd572fd58 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:24:05 +1100 Subject: [PATCH 059/411] feat(nodes): do not hide `services` in invocation context interfaces --- .../app/services/shared/invocation_context.py | 637 ++++++++---------- 1 file changed, 298 insertions(+), 339 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index cb989cb15e..54c50bcf76 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -64,379 +64,338 @@ class InvocationContextData: """The workflow associated with this queue item, if any.""" -class BoardsInterface: - def __init__(self, services: InvocationServices) -> None: - def create(board_name: str) -> BoardDTO: - """ - Creates a board. - - :param board_name: The name of the board to create. - """ - return services.boards.create(board_name) - - def get_dto(board_id: str) -> BoardDTO: - """ - Gets a board DTO. - - :param board_id: The ID of the board to get. - """ - return services.boards.get_dto(board_id) - - def get_all() -> list[BoardDTO]: - """ - Gets all boards. - """ - return services.boards.get_all() - - def add_image_to_board(board_id: str, image_name: str) -> None: - """ - Adds an image to a board. - - :param board_id: The ID of the board to add the image to. - :param image_name: The name of the image to add to the board. - """ - services.board_images.add_image_to_board(board_id, image_name) - - def get_all_image_names_for_board(board_id: str) -> list[str]: - """ - Gets all image names for a board. - - :param board_id: The ID of the board to get the image names for. - """ - return services.board_images.get_all_board_image_names_for_board(board_id) - - self.create = create - self.get_dto = get_dto - self.get_all = get_all - self.add_image_to_board = add_image_to_board - self.get_all_image_names_for_board = get_all_image_names_for_board - - -class LoggerInterface: - def __init__(self, services: InvocationServices) -> None: - def debug(message: str) -> None: - """ - Logs a debug message. - - :param message: The message to log. - """ - services.logger.debug(message) - - def info(message: str) -> None: - """ - Logs an info message. - - :param message: The message to log. - """ - services.logger.info(message) - - def warning(message: str) -> None: - """ - Logs a warning message. - - :param message: The message to log. - """ - services.logger.warning(message) - - def error(message: str) -> None: - """ - Logs an error message. - - :param message: The message to log. - """ - services.logger.error(message) - - self.debug = debug - self.info = info - self.warning = warning - self.error = error - - -class ImagesInterface: +class InvocationContextInterface: def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: - def save( - image: Image, - board_id: Optional[str] = None, - image_category: ImageCategory = ImageCategory.GENERAL, - metadata: Optional[MetadataField] = None, - ) -> ImageDTO: - """ - Saves an image, returning its DTO. - - If the current queue item has a workflow or metadata, it is automatically saved with the image. - - :param image: The image to save, as a PIL image. - :param board_id: The board ID to add the image to, if it should be added. - :param image_category: The category of the image. Only the GENERAL category is added \ - to the gallery. - :param metadata: The metadata to save with the image, if it should have any. If the \ - invocation inherits from `WithMetadata`, that metadata will be used automatically. \ - **Use this only if you want to override or provide metadata manually!** - """ - - # If the invocation inherits metadata, use that. Else, use the metadata passed in. - metadata_ = ( - context_data.invocation.metadata if isinstance(context_data.invocation, WithMetadata) else metadata - ) - - return services.images.create( - image=image, - is_intermediate=context_data.invocation.is_intermediate, - image_category=image_category, - board_id=board_id, - metadata=metadata_, - image_origin=ResourceOrigin.INTERNAL, - workflow=context_data.workflow, - session_id=context_data.session_id, - node_id=context_data.invocation.id, - ) - - def get_pil(image_name: str) -> Image: - """ - Gets an image as a PIL Image object. - - :param image_name: The name of the image to get. - """ - return services.images.get_pil_image(image_name) - - def get_metadata(image_name: str) -> Optional[MetadataField]: - """ - Gets an image's metadata, if it has any. - - :param image_name: The name of the image to get the metadata for. - """ - return services.images.get_metadata(image_name) - - def get_dto(image_name: str) -> ImageDTO: - """ - Gets an image as an ImageDTO object. - - :param image_name: The name of the image to get. - """ - return services.images.get_dto(image_name) - - def update( - image_name: str, - board_id: Optional[str] = None, - is_intermediate: Optional[bool] = False, - ) -> ImageDTO: - """ - Updates an image, returning its updated DTO. - - It is not suggested to update images saved by earlier nodes, as this can cause confusion for users. - - If you use this method, you *must* return the image as an :class:`ImageOutput` for the gallery to - get the updated image. - - :param image_name: The name of the image to update. - :param board_id: The board ID to add the image to, if it should be added. - :param is_intermediate: Whether the image is an intermediate. Intermediate images aren't added to the gallery. - """ - if is_intermediate is not None: - services.images.update(image_name, ImageRecordChanges(is_intermediate=is_intermediate)) - if board_id is None: - services.board_images.remove_image_from_board(image_name) - else: - services.board_images.add_image_to_board(image_name, board_id) - return services.images.get_dto(image_name) - - self.save = save - self.get_pil = get_pil - self.get_metadata = get_metadata - self.get_dto = get_dto - self.update = update + self._services = services + self._context_data = context_data -class LatentsInterface: - def __init__( +class BoardsInterface(InvocationContextInterface): + def create(self, board_name: str) -> BoardDTO: + """ + Creates a board. + + :param board_name: The name of the board to create. + """ + return self._services.boards.create(board_name) + + def get_dto(self, board_id: str) -> BoardDTO: + """ + Gets a board DTO. + + :param board_id: The ID of the board to get. + """ + return self._services.boards.get_dto(board_id) + + def get_all(self) -> list[BoardDTO]: + """ + Gets all boards. + """ + return self._services.boards.get_all() + + def add_image_to_board(self, board_id: str, image_name: str) -> None: + """ + Adds an image to a board. + + :param board_id: The ID of the board to add the image to. + :param image_name: The name of the image to add to the board. + """ + return self._services.board_images.add_image_to_board(board_id, image_name) + + def get_all_image_names_for_board(self, board_id: str) -> list[str]: + """ + Gets all image names for a board. + + :param board_id: The ID of the board to get the image names for. + """ + return self._services.board_images.get_all_board_image_names_for_board(board_id) + + +class LoggerInterface(InvocationContextInterface): + def debug(self, message: str) -> None: + """ + Logs a debug message. + + :param message: The message to log. + """ + self._services.logger.debug(message) + + def info(self, message: str) -> None: + """ + Logs an info message. + + :param message: The message to log. + """ + self._services.logger.info(message) + + def warning(self, message: str) -> None: + """ + Logs a warning message. + + :param message: The message to log. + """ + self._services.logger.warning(message) + + def error(self, message: str) -> None: + """ + Logs an error message. + + :param message: The message to log. + """ + self._services.logger.error(message) + + +class ImagesInterface(InvocationContextInterface): + def save( self, - services: InvocationServices, - context_data: InvocationContextData, - ) -> None: - def save(tensor: Tensor) -> str: - """ - Saves a latents tensor, returning its name. + image: Image, + board_id: Optional[str] = None, + image_category: ImageCategory = ImageCategory.GENERAL, + metadata: Optional[MetadataField] = None, + ) -> ImageDTO: + """ + Saves an image, returning its DTO. - :param tensor: The latents tensor to save. - """ + If the current queue item has a workflow or metadata, it is automatically saved with the image. - # Previously, we added a suffix indicating the type of Tensor we were saving, e.g. - # "mask", "noise", "masked_latents", etc. - # - # Retaining that capability in this wrapper would require either many different methods - # to save latents, or extra args for this method. Instead of complicating the API, we - # will use the same naming scheme for all latents. - # - # This has a very minor impact as we don't use them after a session completes. + :param image: The image to save, as a PIL image. + :param board_id: The board ID to add the image to, if it should be added. + :param image_category: The category of the image. Only the GENERAL category is added \ + to the gallery. + :param metadata: The metadata to save with the image, if it should have any. If the \ + invocation inherits from `WithMetadata`, that metadata will be used automatically. \ + **Use this only if you want to override or provide metadata manually!** + """ - # Previously, invocations chose the name for their latents. This is a bit risky, so we - # will generate a name for them instead. We use a uuid to ensure the name is unique. - # - # Because the name of the latents file will includes the session and invocation IDs, - # we don't need to worry about collisions. A truncated UUIDv4 is fine. + # If the invocation inherits metadata, use that. Else, use the metadata passed in. + metadata_ = ( + self._context_data.invocation.metadata + if isinstance(self._context_data.invocation, WithMetadata) + else metadata + ) - name = f"{context_data.session_id}__{context_data.invocation.id}__{uuid_string()[:7]}" - services.latents.save( - name=name, - data=tensor, - ) - return name + return self._services.images.create( + image=image, + is_intermediate=self._context_data.invocation.is_intermediate, + image_category=image_category, + board_id=board_id, + metadata=metadata_, + image_origin=ResourceOrigin.INTERNAL, + workflow=self._context_data.workflow, + session_id=self._context_data.session_id, + node_id=self._context_data.invocation.id, + ) - def get(latents_name: str) -> Tensor: - """ - Gets a latents tensor by name. + def get_pil(self, image_name: str) -> Image: + """ + Gets an image as a PIL Image object. - :param latents_name: The name of the latents tensor to get. - """ - return services.latents.get(latents_name) + :param image_name: The name of the image to get. + """ + return self._services.images.get_pil_image(image_name) - self.save = save - self.get = get + def get_metadata(self, image_name: str) -> Optional[MetadataField]: + """ + Gets an image's metadata, if it has any. + :param image_name: The name of the image to get the metadata for. + """ + return self._services.images.get_metadata(image_name) -class ConditioningInterface: - def __init__( + def get_dto(self, image_name: str) -> ImageDTO: + """ + Gets an image as an ImageDTO object. + + :param image_name: The name of the image to get. + """ + return self._services.images.get_dto(image_name) + + def update( self, - services: InvocationServices, - context_data: InvocationContextData, - ) -> None: - # TODO(psyche): We are (ab)using the latents storage service as a general pickle storage - # service, but it is typed to work with Tensors only. We have to fudge the types here. + image_name: str, + board_id: Optional[str] = None, + is_intermediate: Optional[bool] = False, + ) -> ImageDTO: + """ + Updates an image, returning its updated DTO. - def save(conditioning_data: ConditioningFieldData) -> str: - """ - Saves a conditioning data object, returning its name. + It is not suggested to update images saved by earlier nodes, as this can cause confusion for users. - :param conditioning_data: The conditioning data to save. - """ + If you use this method, you *must* return the image as an :class:`ImageOutput` for the gallery to + get the updated image. - # Conditioning data is *not* a Tensor, so we will suffix it to indicate this. - # - # See comment for `LatentsInterface.save` for more info about this method (it's very - # similar). - - name = f"{context_data.session_id}__{context_data.invocation.id}__{uuid_string()[:7]}__conditioning" - services.latents.save( - name=name, - data=conditioning_data, # type: ignore [arg-type] - ) - return name - - def get(conditioning_name: str) -> ConditioningFieldData: - """ - Gets conditioning data by name. - - :param conditioning_name: The name of the conditioning data to get. - """ - - return services.latents.get(conditioning_name) # type: ignore [return-value] - - self.save = save - self.get = get + :param image_name: The name of the image to update. + :param board_id: The board ID to add the image to, if it should be added. + :param is_intermediate: Whether the image is an intermediate. Intermediate images aren't added to the gallery. + """ + if is_intermediate is not None: + self._services.images.update(image_name, ImageRecordChanges(is_intermediate=is_intermediate)) + if board_id is None: + self._services.board_images.remove_image_from_board(image_name) + else: + self._services.board_images.add_image_to_board(image_name, board_id) + return self._services.images.get_dto(image_name) -class ModelsInterface: - def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: - def exists(model_name: str, base_model: BaseModelType, model_type: ModelType) -> bool: - """ - Checks if a model exists. +class LatentsInterface(InvocationContextInterface): + def save(self, tensor: Tensor) -> str: + """ + Saves a latents tensor, returning its name. - :param model_name: The name of the model to check. - :param base_model: The base model of the model to check. - :param model_type: The type of the model to check. - """ - return services.model_manager.model_exists(model_name, base_model, model_type) + :param tensor: The latents tensor to save. + """ - def load( - model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None - ) -> ModelInfo: - """ - Loads a model, returning its `ModelInfo` object. + # Previously, we added a suffix indicating the type of Tensor we were saving, e.g. + # "mask", "noise", "masked_latents", etc. + # + # Retaining that capability in this wrapper would require either many different methods + # to save latents, or extra args for this method. Instead of complicating the API, we + # will use the same naming scheme for all latents. + # + # This has a very minor impact as we don't use them after a session completes. - :param model_name: The name of the model to get. - :param base_model: The base model of the model to get. - :param model_type: The type of the model to get. - :param submodel: The submodel of the model to get. - """ + # Previously, invocations chose the name for their latents. This is a bit risky, so we + # will generate a name for them instead. We use a uuid to ensure the name is unique. + # + # Because the name of the latents file will includes the session and invocation IDs, + # we don't need to worry about collisions. A truncated UUIDv4 is fine. - # During this call, the model manager emits events with model loading status. The model - # manager itself has access to the events services, but does not have access to the - # required metadata for the events. - # - # For example, it needs access to the node's ID so that the events can be associated - # with the execution of a specific node. - # - # While this is available within the node, it's tedious to need to pass it in on every - # call. We can avoid that by wrapping the method here. + name = f"{self._context_data.session_id}__{self._context_data.invocation.id}__{uuid_string()[:7]}" + self._services.latents.save( + name=name, + data=tensor, + ) + return name - return services.model_manager.get_model( - model_name, base_model, model_type, submodel, context_data=context_data - ) + def get(self, latents_name: str) -> Tensor: + """ + Gets a latents tensor by name. - def get_info(model_name: str, base_model: BaseModelType, model_type: ModelType) -> dict: - """ - Gets a model's info, an dict-like object. - - :param model_name: The name of the model to get. - :param base_model: The base model of the model to get. - :param model_type: The type of the model to get. - """ - return services.model_manager.model_info(model_name, base_model, model_type) - - self.exists = exists - self.load = load - self.get_info = get_info + :param latents_name: The name of the latents tensor to get. + """ + return self._services.latents.get(latents_name) -class ConfigInterface: - def __init__(self, services: InvocationServices) -> None: - def get() -> InvokeAIAppConfig: - """ - Gets the app's config. The config is read-only; attempts to mutate it will raise an error. - """ +class ConditioningInterface(InvocationContextInterface): + # TODO(psyche): We are (ab)using the latents storage service as a general pickle storage + # service, but it is typed to work with Tensors only. We have to fudge the types here. + def save(self, conditioning_data: ConditioningFieldData) -> str: + """ + Saves a conditioning data object, returning its name. - # The config can be changed at runtime. - # - # We don't want nodes doing this, so we make a frozen copy. + :param conditioning_context_data: The conditioning data to save. + """ - config = services.configuration.get_config() - # TODO(psyche): If config cannot be changed at runtime, should we cache this? - frozen_config = config.model_copy(update={"model_config": ConfigDict(frozen=True)}) - return frozen_config + # Conditioning data is *not* a Tensor, so we will suffix it to indicate this. + # + # See comment for `LatentsInterface.save` for more info about this method (it's very + # similar). - self.get = get + name = f"{self._context_data.session_id}__{self._context_data.invocation.id}__{uuid_string()[:7]}__conditioning" + self._services.latents.save( + name=name, + data=conditioning_data, # type: ignore [arg-type] + ) + return name + + def get(self, conditioning_name: str) -> ConditioningFieldData: + """ + Gets conditioning data by name. + + :param conditioning_name: The name of the conditioning data to get. + """ + + return self._services.latents.get(conditioning_name) # type: ignore [return-value] -class UtilInterface: - def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: - def sd_step_callback( - intermediate_state: PipelineIntermediateState, - base_model: BaseModelType, - ) -> None: - """ - The step callback emits a progress event with the current step, the total number of - steps, a preview image, and some other internal metadata. +class ModelsInterface(InvocationContextInterface): + def exists(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> bool: + """ + Checks if a model exists. - This should be called after each denoising step. + :param model_name: The name of the model to check. + :param base_model: The base model of the model to check. + :param model_type: The type of the model to check. + """ + return self._services.model_manager.model_exists(model_name, base_model, model_type) - :param intermediate_state: The intermediate state of the diffusion pipeline. - :param base_model: The base model for the current denoising step. - """ + def load( + self, model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None + ) -> ModelInfo: + """ + Loads a model, returning its `ModelInfo` object. - # The step callback needs access to the events and the invocation queue services, but this - # represents a dangerous level of access. - # - # We wrap the step callback so that nodes do not have direct access to these services. + :param model_name: The name of the model to get. + :param base_model: The base model of the model to get. + :param model_type: The type of the model to get. + :param submodel: The submodel of the model to get. + """ - stable_diffusion_step_callback( - context_data=context_data, - intermediate_state=intermediate_state, - base_model=base_model, - invocation_queue=services.queue, - events=services.events, - ) + # During this call, the model manager emits events with model loading status. The model + # manager itself has access to the events services, but does not have access to the + # required metadata for the events. + # + # For example, it needs access to the node's ID so that the events can be associated + # with the execution of a specific node. + # + # While this is available within the node, it's tedious to need to pass it in on every + # call. We can avoid that by wrapping the method here. - self.sd_step_callback = sd_step_callback + return self._services.model_manager.get_model( + model_name, base_model, model_type, submodel, context_data=self._context_data + ) + + def get_info(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> dict: + """ + Gets a model's info, an dict-like object. + + :param model_name: The name of the model to get. + :param base_model: The base model of the model to get. + :param model_type: The type of the model to get. + """ + return self._services.model_manager.model_info(model_name, base_model, model_type) + + +class ConfigInterface(InvocationContextInterface): + def get(self) -> InvokeAIAppConfig: + """ + Gets the app's config. The config is read-only; attempts to mutate it will raise an error. + """ + + # The config can be changed at runtime. + # + # We don't want nodes doing this, so we make a frozen copy. + + config = self._services.configuration.get_config() + # TODO(psyche): If config cannot be changed at runtime, should we cache this? + frozen_config = config.model_copy(update={"model_config": ConfigDict(frozen=True)}) + return frozen_config + + +class UtilInterface(InvocationContextInterface): + def sd_step_callback(self, intermediate_state: PipelineIntermediateState, base_model: BaseModelType) -> None: + """ + The step callback emits a progress event with the current step, the total number of + steps, a preview image, and some other internal metadata. + + This should be called after each denoising step. + + :param intermediate_state: The intermediate state of the diffusion pipeline. + :param base_model: The base model for the current denoising step. + """ + + # The step callback needs access to the events and the invocation queue services, but this + # represents a dangerous level of access. + # + # We wrap the step callback so that nodes do not have direct access to these services. + + stable_diffusion_step_callback( + context_data=self._context_data, + intermediate_state=intermediate_state, + base_model=base_model, + invocation_queue=self._services.queue, + events=self._services.events, + ) deprecation_version = "3.7.0" @@ -600,14 +559,14 @@ def build_invocation_context( :param invocation_context_data: The invocation context data. """ - logger = LoggerInterface(services=services) + logger = LoggerInterface(services=services, context_data=context_data) images = ImagesInterface(services=services, context_data=context_data) latents = LatentsInterface(services=services, context_data=context_data) models = ModelsInterface(services=services, context_data=context_data) - config = ConfigInterface(services=services) + config = ConfigInterface(services=services, context_data=context_data) util = UtilInterface(services=services, context_data=context_data) conditioning = ConditioningInterface(services=services, context_data=context_data) - boards = BoardsInterface(services=services) + boards = BoardsInterface(services=services, context_data=context_data) ctx = InvocationContext( images=images, From 60e2eff94d4e9e4e4d889cb49539de696b5de600 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:28:29 +1100 Subject: [PATCH 060/411] feat(nodes): cache invocation interface config --- .../app/services/shared/invocation_context.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 54c50bcf76..99e439ad96 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -357,19 +357,24 @@ class ModelsInterface(InvocationContextInterface): class ConfigInterface(InvocationContextInterface): + def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: + super().__init__(services, context_data) + # Config cache, only populated at runtime if requested + self._frozen_config: Optional[InvokeAIAppConfig] = None + def get(self) -> InvokeAIAppConfig: """ Gets the app's config. The config is read-only; attempts to mutate it will raise an error. """ - # The config can be changed at runtime. - # - # We don't want nodes doing this, so we make a frozen copy. + if self._frozen_config is None: + # The config is a live pydantic model and can be changed at runtime. + # We don't want nodes doing this, so we make a frozen copy. + self._frozen_config = self._services.configuration.get_config().model_copy( + update={"model_config": ConfigDict(frozen=True)} + ) - config = self._services.configuration.get_config() - # TODO(psyche): If config cannot be changed at runtime, should we cache this? - frozen_config = config.model_copy(update={"model_config": ConfigDict(frozen=True)}) - return frozen_config + return self._frozen_config class UtilInterface(InvocationContextInterface): From 5730ae9b96021b327888642f872901984ac2d597 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:36:42 +1100 Subject: [PATCH 061/411] feat(nodes): context.__services -> context._services --- invokeai/app/services/shared/invocation_context.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 99e439ad96..5da8593167 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -463,7 +463,8 @@ class InvocationContext: """Provides methods to interact with boards.""" self.data = data """Provides data about the current queue item and invocation.""" - self.__services = services + self._services = services + """Provides access to the full application services. This is an internal API and may change without warning.""" @property @deprecated(version=deprecation_version, reason=get_deprecation_reason("`context.services`")) @@ -475,7 +476,7 @@ class InvocationContext: The invocation services. """ - return self.__services + return self._services @property @deprecated( From 958b80acddecea011f00514bb5a552e3fe79fa17 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:39:26 +1100 Subject: [PATCH 062/411] feat(nodes): context.data -> context._data --- .../app/services/shared/invocation_context.py | 38 +++++++++---------- tests/aa_nodes/test_graph_execution_state.py | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 5da8593167..b48a6acc54 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -442,7 +442,7 @@ class InvocationContext: config: ConfigInterface, util: UtilInterface, boards: BoardsInterface, - data: InvocationContextData, + context_data: InvocationContextData, services: InvocationServices, ) -> None: self.images = images @@ -461,8 +461,8 @@ class InvocationContext: """Provides utility methods.""" self.boards = boards """Provides methods to interact with boards.""" - self.data = data - """Provides data about the current queue item and invocation.""" + self._data = context_data + """Provides data about the current queue item and invocation. This is an internal API and may change without warning.""" self._services = services """Provides access to the full application services. This is an internal API and may change without warning.""" @@ -481,77 +481,77 @@ class InvocationContext: @property @deprecated( version=deprecation_version, - reason=get_deprecation_reason("`context.graph_execution_state_api`", "`context.data.session_id`"), + reason=get_deprecation_reason("`context.graph_execution_state_id", "`context._data.session_id`"), ) def graph_execution_state_id(self) -> str: """ **DEPRECATED as of v3.7.0** - `context.graph_execution_state_api` will be removed in v3.8.0. Use `context.data.session_id` instead. See PLACEHOLDER_URL for details. + `context.graph_execution_state_api` will be removed in v3.8.0. Use `context._data.session_id` instead. See PLACEHOLDER_URL for details. The ID of the session (aka graph execution state). """ - return self.data.session_id + return self._data.session_id @property @deprecated( version=deprecation_version, - reason=get_deprecation_reason("`context.queue_id`", "`context.data.queue_id`"), + reason=get_deprecation_reason("`context.queue_id`", "`context._data.queue_id`"), ) def queue_id(self) -> str: """ **DEPRECATED as of v3.7.0** - `context.queue_id` will be removed in v3.8.0. Use `context.data.queue_id` instead. See PLACEHOLDER_URL for details. + `context.queue_id` will be removed in v3.8.0. Use `context._data.queue_id` instead. See PLACEHOLDER_URL for details. The ID of the queue. """ - return self.data.queue_id + return self._data.queue_id @property @deprecated( version=deprecation_version, - reason=get_deprecation_reason("`context.queue_item_id`", "`context.data.queue_item_id`"), + reason=get_deprecation_reason("`context.queue_item_id`", "`context._data.queue_item_id`"), ) def queue_item_id(self) -> int: """ **DEPRECATED as of v3.7.0** - `context.queue_item_id` will be removed in v3.8.0. Use `context.data.queue_item_id` instead. See PLACEHOLDER_URL for details. + `context.queue_item_id` will be removed in v3.8.0. Use `context._data.queue_item_id` instead. See PLACEHOLDER_URL for details. The ID of the queue item. """ - return self.data.queue_item_id + return self._data.queue_item_id @property @deprecated( version=deprecation_version, - reason=get_deprecation_reason("`context.queue_batch_id`", "`context.data.batch_id`"), + reason=get_deprecation_reason("`context.queue_batch_id`", "`context._data.batch_id`"), ) def queue_batch_id(self) -> str: """ **DEPRECATED as of v3.7.0** - `context.queue_batch_id` will be removed in v3.8.0. Use `context.data.batch_id` instead. See PLACEHOLDER_URL for details. + `context.queue_batch_id` will be removed in v3.8.0. Use `context._data.batch_id` instead. See PLACEHOLDER_URL for details. The ID of the batch. """ - return self.data.batch_id + return self._data.batch_id @property @deprecated( version=deprecation_version, - reason=get_deprecation_reason("`context.workflow`", "`context.data.workflow`"), + reason=get_deprecation_reason("`context.workflow`", "`context._data.workflow`"), ) def workflow(self) -> Optional[WorkflowWithoutID]: """ **DEPRECATED as of v3.7.0** - `context.workflow` will be removed in v3.8.0. Use `context.data.workflow` instead. See PLACEHOLDER_URL for details. + `context.workflow` will be removed in v3.8.0. Use `context._data.workflow` instead. See PLACEHOLDER_URL for details. The workflow associated with this queue item, if any. """ - return self.data.workflow + return self._data.workflow def build_invocation_context( @@ -580,7 +580,7 @@ def build_invocation_context( config=config, latents=latents, models=models, - data=context_data, + context_data=context_data, util=util, conditioning=conditioning, services=services, diff --git a/tests/aa_nodes/test_graph_execution_state.py b/tests/aa_nodes/test_graph_execution_state.py index 1612cbe719..aba7c5694f 100644 --- a/tests/aa_nodes/test_graph_execution_state.py +++ b/tests/aa_nodes/test_graph_execution_state.py @@ -87,7 +87,7 @@ def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[B InvocationContext( conditioning=None, config=None, - data=None, + context_data=None, images=None, latents=None, logger=None, From 47d05fdd81010e11bb202db9a98c644539c5e0f4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:58:46 +1100 Subject: [PATCH 063/411] fix(nodes): do not freeze or cache config in context wrapper - The config is already cached by the config class's `get_config()` method. - The config mutates itself in its `root_path` property getter. Freezing the class makes any attempt to grab a path from the config error. Unfortunately this means we cannot easily freeze the class without fiddling with the inner workings of `InvokeAIAppConfig`, which is outside the scope here. --- .../app/services/shared/invocation_context.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index b48a6acc54..cd88ec876d 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -357,24 +357,10 @@ class ModelsInterface(InvocationContextInterface): class ConfigInterface(InvocationContextInterface): - def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: - super().__init__(services, context_data) - # Config cache, only populated at runtime if requested - self._frozen_config: Optional[InvokeAIAppConfig] = None - def get(self) -> InvokeAIAppConfig: - """ - Gets the app's config. The config is read-only; attempts to mutate it will raise an error. - """ + """Gets the app's config.""" - if self._frozen_config is None: - # The config is a live pydantic model and can be changed at runtime. - # We don't want nodes doing this, so we make a frozen copy. - self._frozen_config = self._services.configuration.get_config().model_copy( - update={"model_config": ConfigDict(frozen=True)} - ) - - return self._frozen_config + return self._services.configuration.get_config() class UtilInterface(InvocationContextInterface): From 5d2f70b3ef37fb1dabfe8186cb417573f9207d35 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:14:35 +1100 Subject: [PATCH 064/411] fix(ui): remove original l2i node in HRF graph --- .../web/src/features/nodes/util/graph/addHrfToGraph.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts index 7413302fa5..8a4448833c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts @@ -314,6 +314,10 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = ); copyConnectionsToDenoiseLatentsHrf(graph); + // The original l2i node is unnecessary now, remove it + graph.edges = graph.edges.filter((edge) => edge.destination.node_id !== LATENTS_TO_IMAGE); + delete graph.nodes[LATENTS_TO_IMAGE]; + graph.nodes[LATENTS_TO_IMAGE_HRF_HR] = { type: 'l2i', id: LATENTS_TO_IMAGE_HRF_HR, From e137071543c5ac65baf21b444badc44904d87981 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:23:57 +1100 Subject: [PATCH 065/411] remove unused configdict import --- invokeai/app/services/shared/invocation_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index cd88ec876d..8aaa5233af 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Optional from deprecated import deprecated from PIL.Image import Image -from pydantic import ConfigDict from torch import Tensor from invokeai.app.invocations.fields import MetadataField, WithMetadata From 7fbdfbf9e5e504bc8311e307211d1c7064e5a347 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:33:55 +1100 Subject: [PATCH 066/411] feat(nodes): add `WithBoard` field helper class This class works the same way as `WithMetadata` - it simply adds a `board` field to the node. The context wrapper function is able to pull the board id from this. This allows image-outputting nodes to get a board field "for free", and have their outputs automatically saved to it. This is a breaking change for node authors who may have a field called `board`, because it makes `board` a reserved field name. I'll look into how to avoid this - maybe by naming this invoke-managed field `_board` to avoid collisions? Supporting changes: - `WithBoard` is added to all image-outputting nodes, giving them the ability to save to board. - Unused, duplicate `WithMetadata` and `WithWorkflow` classes are deleted from `baseinvocation.py`. The "real" versions are in `fields.py`. - Remove `LinearUIOutputInvocation`. Now that all nodes that output images also have a `board` field by default, this node is no longer necessary. See comment here for context: https://github.com/invoke-ai/InvokeAI/pull/5491#discussion_r1480760629 - Without `LinearUIOutputInvocation`, the `ImagesInferface.update` method is no longer needed, and removed. Note: This commit does not bump all node versions. I will ensure that is done correctly before merging the PR of which this commit is a part. Note: A followup commit will implement the frontend changes to support this change. --- invokeai/app/invocations/baseinvocation.py | 33 +------- .../controlnet_image_processors.py | 12 ++- invokeai/app/invocations/cv.py | 4 +- invokeai/app/invocations/facetools.py | 4 +- invokeai/app/invocations/fields.py | 16 ++++ invokeai/app/invocations/image.py | 76 ++++++------------- invokeai/app/invocations/infill.py | 12 +-- invokeai/app/invocations/latent.py | 3 +- invokeai/app/invocations/primitives.py | 4 +- invokeai/app/invocations/tiles.py | 4 +- invokeai/app/invocations/upscale.py | 4 +- .../app/services/shared/invocation_context.py | 40 +++------- 12 files changed, 78 insertions(+), 134 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index df0596c9a1..3243714937 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -17,11 +17,8 @@ from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined from invokeai.app.invocations.fields import ( - FieldDescriptions, FieldKind, Input, - InputFieldJSONSchemaExtra, - MetadataField, ) from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.shared.invocation_context import InvocationContext @@ -306,9 +303,7 @@ RESERVED_NODE_ATTRIBUTE_FIELD_NAMES = { "workflow", } -RESERVED_INPUT_FIELD_NAMES = { - "metadata", -} +RESERVED_INPUT_FIELD_NAMES = {"metadata", "board"} RESERVED_OUTPUT_FIELD_NAMES = {"type"} @@ -518,29 +513,3 @@ def invocation_output( return cls return wrapper - - -class WithMetadata(BaseModel): - """ - Inherit from this class if your node needs a metadata input field. - """ - - metadata: Optional[MetadataField] = Field( - default=None, - description=FieldDescriptions.metadata, - json_schema_extra=InputFieldJSONSchemaExtra( - field_kind=FieldKind.Internal, - input=Input.Connection, - orig_required=False, - ).model_dump(exclude_none=True), - ) - - -class WithWorkflow: - workflow = None - - def __init_subclass__(cls) -> None: - logger.warn( - f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow." - ) - super().__init_subclass__() diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index f8bdf14117..37954c1097 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -25,7 +25,15 @@ from controlnet_aux.util import HWC3, ade_palette from PIL import Image from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator -from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, WithMetadata +from invokeai.app.invocations.fields import ( + FieldDescriptions, + ImageField, + Input, + InputField, + OutputField, + WithBoard, + WithMetadata, +) from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.app.services.shared.invocation_context import InvocationContext @@ -135,7 +143,7 @@ class ControlNetInvocation(BaseInvocation): # This invocation exists for other invocations to subclass it - do not register with @invocation! -class ImageProcessorInvocation(BaseInvocation, WithMetadata): +class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithBoard): """Base class for invocations that preprocess images for ControlNet""" image: ImageField = InputField(description="The image to process") diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 1ebabf5e06..8174f19b64 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -10,11 +10,11 @@ from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.services.shared.invocation_context import InvocationContext from .baseinvocation import BaseInvocation, invocation -from .fields import InputField, WithMetadata +from .fields import InputField, WithBoard, WithMetadata @invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.2.1") -class CvInpaintInvocation(BaseInvocation, WithMetadata): +class CvInpaintInvocation(BaseInvocation, WithMetadata, WithBoard): """Simple inpaint using opencv.""" image: ImageField = InputField(description="The image to inpaint") diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index a1702d6517..fed2ed5e4f 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -16,7 +16,7 @@ from invokeai.app.invocations.baseinvocation import ( invocation, invocation_output, ) -from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithMetadata +from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithBoard, WithMetadata from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.app.services.shared.invocation_context import InvocationContext @@ -619,7 +619,7 @@ class FaceMaskInvocation(BaseInvocation, WithMetadata): @invocation( "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.2.1" ) -class FaceIdentifierInvocation(BaseInvocation, WithMetadata): +class FaceIdentifierInvocation(BaseInvocation, WithMetadata, WithBoard): """Outputs an image with detected face IDs printed on each face. For use with other FaceTools.""" image: ImageField = InputField(description="Image to face detect") diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 8879f76077..c42d2f8312 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -280,6 +280,22 @@ class WithWorkflow: super().__init_subclass__() +class WithBoard(BaseModel): + """ + Inherit from this class if your node needs a board input field. + """ + + board: Optional["BoardField"] = Field( + default=None, + description=FieldDescriptions.board, + json_schema_extra=InputFieldJSONSchemaExtra( + field_kind=FieldKind.Internal, + input=Input.Direct, + orig_required=False, + ).model_dump(exclude_none=True), + ) + + class OutputFieldJSONSchemaExtra(BaseModel): """ Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 7b74e4d96d..f5ad5515a6 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -8,12 +8,11 @@ import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps from invokeai.app.invocations.fields import ( - BoardField, ColorField, FieldDescriptions, ImageField, - Input, InputField, + WithBoard, WithMetadata, ) from invokeai.app.invocations.primitives import ImageOutput @@ -55,7 +54,7 @@ class ShowImageInvocation(BaseInvocation): category="image", version="1.2.1", ) -class BlankImageInvocation(BaseInvocation, WithMetadata): +class BlankImageInvocation(BaseInvocation, WithMetadata, WithBoard): """Creates a blank image and forwards it to the pipeline""" width: int = InputField(default=512, description="The width of the image") @@ -78,7 +77,7 @@ class BlankImageInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageCropInvocation(BaseInvocation, WithMetadata): +class ImageCropInvocation(BaseInvocation, WithMetadata, WithBoard): """Crops an image to a specified box. The box can be outside of the image.""" image: ImageField = InputField(description="The image to crop") @@ -149,7 +148,7 @@ class CenterPadCropInvocation(BaseInvocation): category="image", version="1.2.1", ) -class ImagePasteInvocation(BaseInvocation, WithMetadata): +class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard): """Pastes an image into another image.""" base_image: ImageField = InputField(description="The base image") @@ -196,7 +195,7 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class MaskFromAlphaInvocation(BaseInvocation, WithMetadata): +class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard): """Extracts the alpha channel of an image as a mask.""" image: ImageField = InputField(description="The image to create the mask from") @@ -221,7 +220,7 @@ class MaskFromAlphaInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageMultiplyInvocation(BaseInvocation, WithMetadata): +class ImageMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard): """Multiplies two images together using `PIL.ImageChops.multiply()`.""" image1: ImageField = InputField(description="The first image to multiply") @@ -248,7 +247,7 @@ IMAGE_CHANNELS = Literal["A", "R", "G", "B"] category="image", version="1.2.1", ) -class ImageChannelInvocation(BaseInvocation, WithMetadata): +class ImageChannelInvocation(BaseInvocation, WithMetadata, WithBoard): """Gets a channel from an image.""" image: ImageField = InputField(description="The image to get the channel from") @@ -274,7 +273,7 @@ IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F category="image", version="1.2.1", ) -class ImageConvertInvocation(BaseInvocation, WithMetadata): +class ImageConvertInvocation(BaseInvocation, WithMetadata, WithBoard): """Converts an image to a different mode.""" image: ImageField = InputField(description="The image to convert") @@ -297,7 +296,7 @@ class ImageConvertInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageBlurInvocation(BaseInvocation, WithMetadata): +class ImageBlurInvocation(BaseInvocation, WithMetadata, WithBoard): """Blurs an image""" image: ImageField = InputField(description="The image to blur") @@ -326,7 +325,7 @@ class ImageBlurInvocation(BaseInvocation, WithMetadata): version="1.2.1", classification=Classification.Beta, ) -class UnsharpMaskInvocation(BaseInvocation, WithMetadata): +class UnsharpMaskInvocation(BaseInvocation, WithMetadata, WithBoard): """Applies an unsharp mask filter to an image""" image: ImageField = InputField(description="The image to use") @@ -394,7 +393,7 @@ PIL_RESAMPLING_MAP = { category="image", version="1.2.1", ) -class ImageResizeInvocation(BaseInvocation, WithMetadata): +class ImageResizeInvocation(BaseInvocation, WithMetadata, WithBoard): """Resizes an image to specific dimensions""" image: ImageField = InputField(description="The image to resize") @@ -424,7 +423,7 @@ class ImageResizeInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageScaleInvocation(BaseInvocation, WithMetadata): +class ImageScaleInvocation(BaseInvocation, WithMetadata, WithBoard): """Scales an image by a factor""" image: ImageField = InputField(description="The image to scale") @@ -459,7 +458,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageLerpInvocation(BaseInvocation, WithMetadata): +class ImageLerpInvocation(BaseInvocation, WithMetadata, WithBoard): """Linear interpolation of all pixels of an image""" image: ImageField = InputField(description="The image to lerp") @@ -486,7 +485,7 @@ class ImageLerpInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageInverseLerpInvocation(BaseInvocation, WithMetadata): +class ImageInverseLerpInvocation(BaseInvocation, WithMetadata, WithBoard): """Inverse linear interpolation of all pixels of an image""" image: ImageField = InputField(description="The image to lerp") @@ -513,7 +512,7 @@ class ImageInverseLerpInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata): +class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard): """Add blur to NSFW-flagged images""" image: ImageField = InputField(description="The image to check") @@ -548,7 +547,7 @@ class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageWatermarkInvocation(BaseInvocation, WithMetadata): +class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithBoard): """Add an invisible watermark to an image""" image: ImageField = InputField(description="The image to check") @@ -569,7 +568,7 @@ class ImageWatermarkInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class MaskEdgeInvocation(BaseInvocation, WithMetadata): +class MaskEdgeInvocation(BaseInvocation, WithMetadata, WithBoard): """Applies an edge mask to an image""" image: ImageField = InputField(description="The image to apply the mask to") @@ -608,7 +607,7 @@ class MaskEdgeInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class MaskCombineInvocation(BaseInvocation, WithMetadata): +class MaskCombineInvocation(BaseInvocation, WithMetadata, WithBoard): """Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`.""" mask1: ImageField = InputField(description="The first mask to combine") @@ -632,7 +631,7 @@ class MaskCombineInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ColorCorrectInvocation(BaseInvocation, WithMetadata): +class ColorCorrectInvocation(BaseInvocation, WithMetadata, WithBoard): """ Shifts the colors of a target image to match the reference image, optionally using a mask to only color-correct certain regions of the target image. @@ -736,7 +735,7 @@ class ColorCorrectInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata): +class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata, WithBoard): """Adjusts the Hue of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -825,7 +824,7 @@ CHANNEL_FORMATS = { category="image", version="1.2.1", ) -class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): +class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata, WithBoard): """Add or subtract a value from a specific color channel of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -881,7 +880,7 @@ class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata): category="image", version="1.2.1", ) -class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): +class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard): """Scale a specific color channel of an image.""" image: ImageField = InputField(description="The image to adjust") @@ -926,41 +925,14 @@ class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata): version="1.2.1", use_cache=False, ) -class SaveImageInvocation(BaseInvocation, WithMetadata): +class SaveImageInvocation(BaseInvocation, WithMetadata, WithBoard): """Saves an image. Unlike an image primitive, this invocation stores a copy of the image.""" image: ImageField = InputField(description=FieldDescriptions.image) - board: BoardField = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) - image_dto = context.images.save(image=image, board_id=self.board.board_id if self.board else None) - - return ImageOutput.build(image_dto) - - -@invocation( - "linear_ui_output", - title="Linear UI Image Output", - tags=["primitives", "image"], - category="primitives", - version="1.0.2", - use_cache=False, -) -class LinearUIOutputInvocation(BaseInvocation, WithMetadata): - """Handles Linear UI Image Outputting tasks.""" - - image: ImageField = InputField(description=FieldDescriptions.image) - board: Optional[BoardField] = InputField(default=None, description=FieldDescriptions.board, input=Input.Direct) - - def invoke(self, context: InvocationContext) -> ImageOutput: - image_dto = context.images.get_dto(self.image.image_name) - - image_dto = context.images.update( - image_name=self.image.image_name, - board_id=self.board.board_id if self.board else None, - is_intermediate=self.is_intermediate, - ) + image_dto = context.images.save(image=image) return ImageOutput.build(image_dto) diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index b007edd9e4..53f6f4732f 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -15,7 +15,7 @@ from invokeai.backend.image_util.lama import LaMA from invokeai.backend.image_util.patchmatch import PatchMatch from .baseinvocation import BaseInvocation, invocation -from .fields import InputField, WithMetadata +from .fields import InputField, WithBoard, WithMetadata from .image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES @@ -121,7 +121,7 @@ def tile_fill_missing(im: Image.Image, tile_size: int = 16, seed: Optional[int] @invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") -class InfillColorInvocation(BaseInvocation, WithMetadata): +class InfillColorInvocation(BaseInvocation, WithMetadata, WithBoard): """Infills transparent areas of an image with a solid color""" image: ImageField = InputField(description="The image to infill") @@ -144,7 +144,7 @@ class InfillColorInvocation(BaseInvocation, WithMetadata): @invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2") -class InfillTileInvocation(BaseInvocation, WithMetadata): +class InfillTileInvocation(BaseInvocation, WithMetadata, WithBoard): """Infills transparent areas of an image with tiles of the image""" image: ImageField = InputField(description="The image to infill") @@ -170,7 +170,7 @@ class InfillTileInvocation(BaseInvocation, WithMetadata): @invocation( "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1" ) -class InfillPatchMatchInvocation(BaseInvocation, WithMetadata): +class InfillPatchMatchInvocation(BaseInvocation, WithMetadata, WithBoard): """Infills transparent areas of an image using the PatchMatch algorithm""" image: ImageField = InputField(description="The image to infill") @@ -209,7 +209,7 @@ class InfillPatchMatchInvocation(BaseInvocation, WithMetadata): @invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") -class LaMaInfillInvocation(BaseInvocation, WithMetadata): +class LaMaInfillInvocation(BaseInvocation, WithMetadata, WithBoard): """Infills transparent areas of an image using the LaMa model""" image: ImageField = InputField(description="The image to infill") @@ -225,7 +225,7 @@ class LaMaInfillInvocation(BaseInvocation, WithMetadata): @invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.1") -class CV2InfillInvocation(BaseInvocation, WithMetadata): +class CV2InfillInvocation(BaseInvocation, WithMetadata, WithBoard): """Infills transparent areas of an image using OpenCV Inpainting""" image: ImageField = InputField(description="The image to infill") diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 5e36e73ec8..5449ec9af7 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -33,6 +33,7 @@ from invokeai.app.invocations.fields import ( LatentsField, OutputField, UIType, + WithBoard, WithMetadata, ) from invokeai.app.invocations.ip_adapter import IPAdapterField @@ -762,7 +763,7 @@ class DenoiseLatentsInvocation(BaseInvocation): category="latents", version="1.2.1", ) -class LatentsToImageInvocation(BaseInvocation, WithMetadata): +class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): """Generates an image from latents.""" latents: LatentsField = InputField( diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index c90d3230b2..a77939943a 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -255,9 +255,7 @@ class ImageCollectionOutput(BaseInvocationOutput): @invocation("image", title="Image Primitive", tags=["primitives", "image"], category="primitives", version="1.0.1") -class ImageInvocation( - BaseInvocation, -): +class ImageInvocation(BaseInvocation): """An image primitive value""" image: ImageField = InputField(description="The image to load") diff --git a/invokeai/app/invocations/tiles.py b/invokeai/app/invocations/tiles.py index 19ece42376..cb5373bbf7 100644 --- a/invokeai/app/invocations/tiles.py +++ b/invokeai/app/invocations/tiles.py @@ -11,7 +11,7 @@ from invokeai.app.invocations.baseinvocation import ( invocation, invocation_output, ) -from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField, WithMetadata +from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField, WithBoard, WithMetadata from invokeai.app.invocations.primitives import ImageOutput from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.tiles.tiles import ( @@ -232,7 +232,7 @@ BLEND_MODES = Literal["Linear", "Seam"] version="1.1.0", classification=Classification.Beta, ) -class MergeTilesToImageInvocation(BaseInvocation, WithMetadata): +class MergeTilesToImageInvocation(BaseInvocation, WithMetadata, WithBoard): """Merge multiple tile images into a single image.""" # Inputs diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index 71ef7ca3aa..2e2a6ce881 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -16,7 +16,7 @@ from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN from invokeai.backend.util.devices import choose_torch_device from .baseinvocation import BaseInvocation, invocation -from .fields import InputField, WithMetadata +from .fields import InputField, WithBoard, WithMetadata # TODO: Populate this from disk? # TODO: Use model manager to load? @@ -32,7 +32,7 @@ if choose_torch_device() == torch.device("mps"): @invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="esrgan", version="1.3.1") -class ESRGANInvocation(BaseInvocation, WithMetadata): +class ESRGANInvocation(BaseInvocation, WithMetadata, WithBoard): """Upscales an image using RealESRGAN.""" image: ImageField = InputField(description="The input image") diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 8aaa5233af..97a62246fb 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -5,10 +5,10 @@ from deprecated import deprecated from PIL.Image import Image from torch import Tensor -from invokeai.app.invocations.fields import MetadataField, WithMetadata +from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata from invokeai.app.services.boards.boards_common import BoardDTO from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin +from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID @@ -158,7 +158,9 @@ class ImagesInterface(InvocationContextInterface): If the current queue item has a workflow or metadata, it is automatically saved with the image. :param image: The image to save, as a PIL image. - :param board_id: The board ID to add the image to, if it should be added. + :param board_id: The board ID to add the image to, if it should be added. It the invocation \ + inherits from `WithBoard`, that board will be used automatically. **Use this only if \ + you want to override or provide a board manually!** :param image_category: The category of the image. Only the GENERAL category is added \ to the gallery. :param metadata: The metadata to save with the image, if it should have any. If the \ @@ -173,11 +175,15 @@ class ImagesInterface(InvocationContextInterface): else metadata ) + # If the invocation inherits WithBoard, use that. Else, use the board_id passed in. + board_ = self._context_data.invocation.board if isinstance(self._context_data.invocation, WithBoard) else None + board_id_ = board_.board_id if board_ is not None else board_id + return self._services.images.create( image=image, is_intermediate=self._context_data.invocation.is_intermediate, image_category=image_category, - board_id=board_id, + board_id=board_id_, metadata=metadata_, image_origin=ResourceOrigin.INTERNAL, workflow=self._context_data.workflow, @@ -209,32 +215,6 @@ class ImagesInterface(InvocationContextInterface): """ return self._services.images.get_dto(image_name) - def update( - self, - image_name: str, - board_id: Optional[str] = None, - is_intermediate: Optional[bool] = False, - ) -> ImageDTO: - """ - Updates an image, returning its updated DTO. - - It is not suggested to update images saved by earlier nodes, as this can cause confusion for users. - - If you use this method, you *must* return the image as an :class:`ImageOutput` for the gallery to - get the updated image. - - :param image_name: The name of the image to update. - :param board_id: The board ID to add the image to, if it should be added. - :param is_intermediate: Whether the image is an intermediate. Intermediate images aren't added to the gallery. - """ - if is_intermediate is not None: - self._services.images.update(image_name, ImageRecordChanges(is_intermediate=is_intermediate)) - if board_id is None: - self._services.board_images.remove_image_from_board(image_name) - else: - self._services.board_images.add_image_to_board(image_name, board_id) - return self._services.images.get_dto(image_name) - class LatentsInterface(InvocationContextInterface): def save(self, tensor: Tensor) -> str: From d60f1965d11c2529315d85fea9b664adcae9f490 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:34:40 +1100 Subject: [PATCH 067/411] chore(ui): regen types --- .../frontend/web/src/services/api/schema.ts | 223 ++++++++---------- 1 file changed, 96 insertions(+), 127 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index da036b6d40..45358ed97d 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -968,6 +968,8 @@ export type components = { * @description Creates a blank image and forwards it to the pipeline */ BlankImageInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -1860,6 +1862,8 @@ export type components = { * @description Infills transparent areas of an image using OpenCV Inpainting */ CV2InfillInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -2095,6 +2099,8 @@ export type components = { * @description Canny edge detection for ControlNet */ CannyImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -2482,6 +2488,8 @@ export type components = { * using a mask to only color-correct certain regions of the target image. */ ColorCorrectInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -2590,6 +2598,8 @@ export type components = { * @description Generates a color map from the provided image */ ColorMapImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -2797,6 +2807,8 @@ export type components = { * @description Applies content shuffle processing to image */ ContentShuffleImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -3442,6 +3454,8 @@ export type components = { * @description Simple inpaint using opencv. */ CvInpaintInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -3677,6 +3691,8 @@ export type components = { * @description Generates a depth map based on the Depth Anything algorithm */ DepthAnythingImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -3910,6 +3926,8 @@ export type components = { * @description Upscales an image using RealESRGAN. */ ESRGANInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -4041,6 +4059,8 @@ export type components = { * @description Outputs an image with detected face IDs printed on each face. For use with other FaceTools. */ FaceIdentifierInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -4873,6 +4893,8 @@ export type components = { * @description Applies HED edge detection to image */ HedImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5324,6 +5346,8 @@ export type components = { * @description Blurs an image */ ImageBlurInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5382,6 +5406,8 @@ export type components = { * @description Gets a channel from an image. */ ImageChannelInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5422,6 +5448,8 @@ export type components = { * @description Scale a specific color channel of an image. */ ImageChannelMultiplyInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5473,6 +5501,8 @@ export type components = { * @description Add or subtract a value from a specific color channel of an image. */ ImageChannelOffsetInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5640,6 +5670,8 @@ export type components = { * @description Converts an image to a different mode. */ ImageConvertInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5680,6 +5712,8 @@ export type components = { * @description Crops an image to a specified box. The box can be outside of the image. */ ImageCropInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5949,6 +5983,8 @@ export type components = { * @description Adjusts the Hue of an image. */ ImageHueAdjustmentInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -5988,6 +6024,8 @@ export type components = { * @description Inverse linear interpolation of all pixels of an image */ ImageInverseLerpInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6064,6 +6102,8 @@ export type components = { * @description Linear interpolation of all pixels of an image */ ImageLerpInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6109,6 +6149,8 @@ export type components = { * @description Multiplies two images together using `PIL.ImageChops.multiply()`. */ ImageMultiplyInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6144,6 +6186,8 @@ export type components = { * @description Add blur to NSFW-flagged images */ ImageNSFWBlurInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6252,6 +6296,8 @@ export type components = { * @description Pastes an image into another image. */ ImagePasteInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6337,6 +6383,8 @@ export type components = { * @description Resizes an image to specific dimensions */ ImageResizeInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6446,6 +6494,8 @@ export type components = { * @description Scales an image by a factor */ ImageScaleInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6621,6 +6671,8 @@ export type components = { * @description Add an invisible watermark to an image */ ImageWatermarkInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6676,6 +6728,8 @@ export type components = { * @description Infills transparent areas of an image with a solid color */ InfillColorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6719,6 +6773,8 @@ export type components = { * @description Infills transparent areas of an image using the PatchMatch algorithm */ InfillPatchMatchInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -6765,6 +6821,8 @@ export type components = { * @description Infills transparent areas of an image with tiles of the image */ InfillTileInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7065,6 +7123,8 @@ export type components = { * @description Infills transparent areas of an image using the LaMa model */ LaMaInfillInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7093,96 +7153,6 @@ export type components = { */ type: "infill_lama"; }; - /** - * Latent Consistency MonoNode - * @description Wrapper node around diffusers LatentConsistencyTxt2ImgPipeline - */ - LatentConsistencyInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** - * Prompt - * @description The prompt to use - */ - prompt?: string; - /** - * Num Inference Steps - * @description The number of inference steps to use, 4-8 recommended - * @default 8 - */ - num_inference_steps?: number; - /** - * Guidance Scale - * @description The guidance scale to use - * @default 8 - */ - guidance_scale?: number; - /** - * Batches - * @description The number of batches to use - * @default 1 - */ - batches?: number; - /** - * Images Per Batch - * @description The number of images per batch to use - * @default 1 - */ - images_per_batch?: number; - /** - * Seeds - * @description List of noise seeds to use - */ - seeds?: number[]; - /** - * Lcm Origin Steps - * @description The lcm origin steps to use - * @default 50 - */ - lcm_origin_steps?: number; - /** - * Width - * @description The width to use - * @default 512 - */ - width?: number; - /** - * Height - * @description The height to use - * @default 512 - */ - height?: number; - /** - * Precision - * @description floating point precision - * @default fp16 - * @enum {string} - */ - precision?: "fp16" | "fp32"; - /** @description The board to save the image to */ - board?: components["schemas"]["BoardField"]; - /** - * type - * @default latent_consistency_mononode - * @constant - */ - type: "latent_consistency_mononode"; - }; /** * Latents Collection Primitive * @description A collection of latents tensor primitive values @@ -7310,6 +7280,8 @@ export type components = { * @description Generates an image from latents. */ LatentsToImageInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7357,6 +7329,8 @@ export type components = { * @description Applies leres processing to image */ LeresImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7441,46 +7415,13 @@ export type components = { /** @description Type of commercial use allowed or 'No' if no commercial use is allowed. */ AllowCommercialUse?: components["schemas"]["CommercialUsage"]; }; - /** - * Linear UI Image Output - * @description Handles Linear UI Image Outputting tasks. - */ - LinearUIOutputInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default false - */ - use_cache?: boolean; - /** @description The image to process */ - image?: components["schemas"]["ImageField"]; - /** @description The board to save the image to */ - board?: components["schemas"]["BoardField"] | null; - /** - * type - * @default linear_ui_output - * @constant - */ - type: "linear_ui_output"; - }; /** * Lineart Anime Processor * @description Applies line art anime processing to image */ LineartAnimeImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7526,6 +7467,8 @@ export type components = { * @description Applies line art processing to image */ LineartImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7954,6 +7897,8 @@ export type components = { * @description Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`. */ MaskCombineInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -7989,6 +7934,8 @@ export type components = { * @description Applies an edge mask to an image */ MaskEdgeInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -8042,6 +7989,8 @@ export type components = { * @description Extracts the alpha channel of an image as a mask. */ MaskFromAlphaInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -8122,6 +8071,8 @@ export type components = { * @description Applies mediapipe face processing to image */ MediapipeFaceProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -8238,6 +8189,8 @@ export type components = { * @description Merge multiple tile images into a single image. */ MergeTilesToImageInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -8404,6 +8357,8 @@ export type components = { * @description Applies Midas depth processing to image */ MidasDepthImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -8449,6 +8404,8 @@ export type components = { * @description Applies MLSD processing to image */ MlsdImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -8961,6 +8918,8 @@ export type components = { * @description Applies NormalBae processing to image */ NormalbaeImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -9591,6 +9550,8 @@ export type components = { * @description Applies PIDI processing to image */ PidiImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -10443,6 +10404,8 @@ export type components = { * @description Saves an image. Unlike an image primitive, this invocation stores a copy of the image. */ SaveImageInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -10464,8 +10427,6 @@ export type components = { use_cache?: boolean; /** @description The image to process */ image?: components["schemas"]["ImageField"]; - /** @description The board to save the image to */ - board?: components["schemas"]["BoardField"]; /** * type * @default save_image @@ -10651,6 +10612,8 @@ export type components = { * @description Applies segment anything processing to image */ SegmentAnythingProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -12189,6 +12152,8 @@ export type components = { * @description Tile resampler processor */ TileResamplerProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -12378,6 +12343,8 @@ export type components = { * @description Applies an unsharp mask filter to an image */ UnsharpMaskInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -12846,6 +12813,8 @@ export type components = { * @description Applies Zoe depth processing to image */ ZoeDepthImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** From 70034d26e2e3df971215f35198d98b28511510eb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 16:41:24 +1100 Subject: [PATCH 068/411] feat(ui): revise graphs to not use `LinearUIOutputInvocation` See this comment for context: https://github.com/invoke-ai/InvokeAI/pull/5491#discussion_r1480760629 - Remove this now-unnecessary node from all graphs - Update graphs' terminal image-outputting nodes' `is_intermediate` and `board` fields appropriately - Add util function to prepare the `board` field, tidy the utils - Update `socketInvocationComplete` listener to work correctly with this change I've manually tested all graph permutations that were changed (I think this is all...) to ensure images go to the gallery as expected: - ad-hoc upscaling - t2i w/ sd1.5 - t2i w/ sd1.5 & hrf - t2i w/ sdxl - t2i w/ sdxl + refiner - i2i w/ sd1.5 - i2i w/ sdxl - i2i w/ sdxl + refiner - canvas t2i w/ sd1.5 - canvas t2i w/ sdxl - canvas t2i w/ sdxl + refiner - canvas i2i w/ sd1.5 - canvas i2i w/ sdxl - canvas i2i w/ sdxl + refiner - canvas inpaint w/ sd1.5 - canvas inpaint w/ sdxl - canvas inpaint w/ sdxl + refiner - canvas outpaint w/ sd1.5 - canvas outpaint w/ sdxl - canvas outpaint w/ sdxl + refiner --- .../socketio/socketInvocationComplete.ts | 9 +-- .../listeners/upscaleRequested.ts | 6 +- .../nodes/util/graph/addHrfToGraph.ts | 4 +- .../nodes/util/graph/addLinearUIOutputNode.ts | 78 ------------------- .../nodes/util/graph/addNSFWCheckerToGraph.ts | 4 +- .../nodes/util/graph/addSDXLRefinerToGraph.ts | 2 +- .../nodes/util/graph/addWatermarkerToGraph.ts | 9 +-- .../util/graph/buildAdHocUpscaleGraph.ts | 40 +++------- .../graph/buildCanvasImageToImageGraph.ts | 13 ++-- .../util/graph/buildCanvasInpaintGraph.ts | 7 +- .../util/graph/buildCanvasOutpaintGraph.ts | 7 +- .../graph/buildCanvasSDXLImageToImageGraph.ts | 8 +- .../util/graph/buildCanvasSDXLInpaintGraph.ts | 8 +- .../graph/buildCanvasSDXLOutpaintGraph.ts | 8 +- .../graph/buildCanvasSDXLTextToImageGraph.ts | 11 ++- .../util/graph/buildCanvasTextToImageGraph.ts | 10 +-- .../graph/buildLinearImageToImageGraph.ts | 7 +- .../graph/buildLinearSDXLImageToImageGraph.ts | 8 +- .../graph/buildLinearSDXLTextToImageGraph.ts | 8 +- .../util/graph/buildLinearTextToImageGraph.ts | 7 +- .../features/nodes/util/graph/constants.ts | 1 - .../nodes/util/graph/getSDXLStylePrompt.ts | 11 --- .../nodes/util/graph/graphBuilderUtils.ts | 38 +++++++++ .../frontend/web/src/services/api/types.ts | 1 - 24 files changed, 108 insertions(+), 197 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/addLinearUIOutputNode.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/getSDXLStylePrompt.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index d49f35cd2a..75fa9e1094 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -4,7 +4,7 @@ import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { boardIdSelected, galleryViewChanged, imageSelected } from 'features/gallery/store/gallerySlice'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { isImageOutput } from 'features/nodes/types/common'; -import { LINEAR_UI_OUTPUT, nodeIDDenyList } from 'features/nodes/util/graph/constants'; +import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import { imagesAdapter } from 'services/api/util'; @@ -24,10 +24,9 @@ export const addInvocationCompleteEventListener = () => { const { data } = action.payload; log.debug({ data: parseify(data) }, `Invocation complete (${action.payload.data.node.type})`); - const { result, node, queue_batch_id, source_node_id } = data; - + const { result, node, queue_batch_id } = data; // This complete event has an associated image output - if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type) && !nodeIDDenyList.includes(source_node_id)) { + if (isImageOutput(result) && !nodeTypeDenylist.includes(node.type)) { const { image_name } = result.image; const { canvas, gallery } = getState(); @@ -42,7 +41,7 @@ export const addInvocationCompleteEventListener = () => { imageDTORequest.unsubscribe(); // Add canvas images to the staging area - if (canvas.batchIds.includes(queue_batch_id) && [LINEAR_UI_OUTPUT].includes(data.source_node_id)) { + if (canvas.batchIds.includes(queue_batch_id) && data.source_node_id === CANVAS_OUTPUT) { dispatch(addImageToStagingArea(imageDTO)); } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts index 46f55ef21f..ab98930179 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/upscaleRequested.ts @@ -39,16 +39,12 @@ export const addUpscaleRequestedListener = () => { return; } - const { esrganModelName } = state.postprocessing; - const { autoAddBoardId } = state.gallery; - const enqueueBatchArg: BatchConfig = { prepend: true, batch: { graph: buildAdHocUpscaleGraph({ image_name, - esrganModelName, - autoAddBoardId, + state, }), runs: 1, }, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts index 8a4448833c..5632cfd112 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addHrfToGraph.ts @@ -1,6 +1,7 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; import { roundToMultiple } from 'common/util/roundDownToMultiple'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import { selectOptimalDimension } from 'features/parameters/store/generationSlice'; import type { DenoiseLatentsInvocation, @@ -322,7 +323,8 @@ export const addHrfToGraph = (state: RootState, graph: NonNullableGraph): void = type: 'l2i', id: LATENTS_TO_IMAGE_HRF_HR, fp32: originalLatentsToImageNode?.fp32, - is_intermediate: true, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), }; graph.edges.push( { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addLinearUIOutputNode.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addLinearUIOutputNode.ts deleted file mode 100644 index 5c78ad804e..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addLinearUIOutputNode.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { RootState } from 'app/store/store'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import type { LinearUIOutputInvocation, NonNullableGraph } from 'services/api/types'; - -import { - CANVAS_OUTPUT, - LATENTS_TO_IMAGE, - LATENTS_TO_IMAGE_HRF_HR, - LINEAR_UI_OUTPUT, - NSFW_CHECKER, - WATERMARKER, -} from './constants'; - -/** - * Set the `use_cache` field on the linear/canvas graph's final image output node to False. - */ -export const addLinearUIOutputNode = (state: RootState, graph: NonNullableGraph): void => { - const activeTabName = activeTabNameSelector(state); - const is_intermediate = activeTabName === 'unifiedCanvas' ? !state.canvas.shouldAutoSave : false; - const { autoAddBoardId } = state.gallery; - - const linearUIOutputNode: LinearUIOutputInvocation = { - id: LINEAR_UI_OUTPUT, - type: 'linear_ui_output', - is_intermediate, - use_cache: false, - board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId }, - }; - - graph.nodes[LINEAR_UI_OUTPUT] = linearUIOutputNode; - - const destination = { - node_id: LINEAR_UI_OUTPUT, - field: 'image', - }; - - if (WATERMARKER in graph.nodes) { - graph.edges.push({ - source: { - node_id: WATERMARKER, - field: 'image', - }, - destination, - }); - } else if (NSFW_CHECKER in graph.nodes) { - graph.edges.push({ - source: { - node_id: NSFW_CHECKER, - field: 'image', - }, - destination, - }); - } else if (CANVAS_OUTPUT in graph.nodes) { - graph.edges.push({ - source: { - node_id: CANVAS_OUTPUT, - field: 'image', - }, - destination, - }); - } else if (LATENTS_TO_IMAGE_HRF_HR in graph.nodes) { - graph.edges.push({ - source: { - node_id: LATENTS_TO_IMAGE_HRF_HR, - field: 'image', - }, - destination, - }); - } else if (LATENTS_TO_IMAGE in graph.nodes) { - graph.edges.push({ - source: { - node_id: LATENTS_TO_IMAGE, - field: 'image', - }, - destination, - }); - } -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addNSFWCheckerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addNSFWCheckerToGraph.ts index 4a8e77abfa..35fc324689 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addNSFWCheckerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addNSFWCheckerToGraph.ts @@ -2,6 +2,7 @@ import type { RootState } from 'app/store/store'; import type { ImageNSFWBlurInvocation, LatentsToImageInvocation, NonNullableGraph } from 'services/api/types'; import { LATENTS_TO_IMAGE, NSFW_CHECKER } from './constants'; +import { getBoardField, getIsIntermediate } from './graphBuilderUtils'; export const addNSFWCheckerToGraph = ( state: RootState, @@ -21,7 +22,8 @@ export const addNSFWCheckerToGraph = ( const nsfwCheckerNode: ImageNSFWBlurInvocation = { id: NSFW_CHECKER, type: 'img_nsfw', - is_intermediate: true, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), }; graph.nodes[NSFW_CHECKER] = nsfwCheckerNode as ImageNSFWBlurInvocation; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLRefinerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLRefinerToGraph.ts index 708353e4d6..fc4d998969 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLRefinerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLRefinerToGraph.ts @@ -24,7 +24,7 @@ import { SDXL_REFINER_POSITIVE_CONDITIONING, SDXL_REFINER_SEAMLESS, } from './constants'; -import { getSDXLStylePrompts } from './getSDXLStylePrompt'; +import { getSDXLStylePrompts } from './graphBuilderUtils'; import { upsertMetadata } from './metadata'; export const addSDXLRefinerToGraph = ( diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addWatermarkerToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addWatermarkerToGraph.ts index 99c5c07be4..61beb11df4 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addWatermarkerToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addWatermarkerToGraph.ts @@ -1,5 +1,4 @@ import type { RootState } from 'app/store/store'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import type { ImageNSFWBlurInvocation, ImageWatermarkInvocation, @@ -8,16 +7,13 @@ import type { } from 'services/api/types'; import { LATENTS_TO_IMAGE, NSFW_CHECKER, WATERMARKER } from './constants'; +import { getBoardField, getIsIntermediate } from './graphBuilderUtils'; export const addWatermarkerToGraph = ( state: RootState, graph: NonNullableGraph, nodeIdToAddTo = LATENTS_TO_IMAGE ): void => { - const activeTabName = activeTabNameSelector(state); - - const is_intermediate = activeTabName === 'unifiedCanvas' ? !state.canvas.shouldAutoSave : false; - const nodeToAddTo = graph.nodes[nodeIdToAddTo] as LatentsToImageInvocation | undefined; const nsfwCheckerNode = graph.nodes[NSFW_CHECKER] as ImageNSFWBlurInvocation | undefined; @@ -30,7 +26,8 @@ export const addWatermarkerToGraph = ( const watermarkerNode: ImageWatermarkInvocation = { id: WATERMARKER, type: 'img_watermark', - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), }; graph.nodes[WATERMARKER] = watermarkerNode; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts index fa20206d91..52c09b1db0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildAdHocUpscaleGraph.ts @@ -1,51 +1,33 @@ -import type { BoardId } from 'features/gallery/store/types'; -import type { ParamESRGANModelName } from 'features/parameters/store/postprocessingSlice'; -import type { ESRGANInvocation, Graph, LinearUIOutputInvocation, NonNullableGraph } from 'services/api/types'; +import type { RootState } from 'app/store/store'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; +import type { ESRGANInvocation, Graph, NonNullableGraph } from 'services/api/types'; -import { ESRGAN, LINEAR_UI_OUTPUT } from './constants'; +import { ESRGAN } from './constants'; import { addCoreMetadataNode, upsertMetadata } from './metadata'; type Arg = { image_name: string; - esrganModelName: ParamESRGANModelName; - autoAddBoardId: BoardId; + state: RootState; }; -export const buildAdHocUpscaleGraph = ({ image_name, esrganModelName, autoAddBoardId }: Arg): Graph => { +export const buildAdHocUpscaleGraph = ({ image_name, state }: Arg): Graph => { + const { esrganModelName } = state.postprocessing; + const realesrganNode: ESRGANInvocation = { id: ESRGAN, type: 'esrgan', image: { image_name }, model_name: esrganModelName, - is_intermediate: true, - }; - - const linearUIOutputNode: LinearUIOutputInvocation = { - id: LINEAR_UI_OUTPUT, - type: 'linear_ui_output', - use_cache: false, - is_intermediate: false, - board: autoAddBoardId === 'none' ? undefined : { board_id: autoAddBoardId }, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), }; const graph: NonNullableGraph = { id: `adhoc-esrgan-graph`, nodes: { [ESRGAN]: realesrganNode, - [LINEAR_UI_OUTPUT]: linearUIOutputNode, }, - edges: [ - { - source: { - node_id: ESRGAN, - field: 'image', - }, - destination: { - node_id: LINEAR_UI_OUTPUT, - field: 'image', - }, - }, - ], + edges: [], }; addCoreMetadataNode(graph, {}, ESRGAN); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts index 3002e05441..bc6a83f4fa 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts @@ -1,10 +1,10 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ImageDTO, ImageToLatentsInvocation, NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; @@ -132,7 +132,8 @@ export const buildCanvasImageToImageGraph = (state: RootState, initialImage: Ima [CANVAS_OUTPUT]: { type: 'l2i', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), use_cache: false, }, }, @@ -242,7 +243,8 @@ export const buildCanvasImageToImageGraph = (state: RootState, initialImage: Ima graph.nodes[CANVAS_OUTPUT] = { id: CANVAS_OUTPUT, type: 'img_resize', - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), width: width, height: height, use_cache: false, @@ -284,7 +286,8 @@ export const buildCanvasImageToImageGraph = (state: RootState, initialImage: Ima graph.nodes[CANVAS_OUTPUT] = { type: 'l2i', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), fp32, use_cache: false, }; @@ -355,7 +358,5 @@ export const buildCanvasImageToImageGraph = (state: RootState, initialImage: Ima addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts index bb52a44a8e..d983b9cf4f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { CreateDenoiseMaskInvocation, ImageBlurInvocation, @@ -12,7 +13,6 @@ import type { import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; @@ -191,7 +191,8 @@ export const buildCanvasInpaintGraph = ( [CANVAS_OUTPUT]: { type: 'color_correct', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), reference: canvasInitImage, use_cache: false, }, @@ -663,7 +664,5 @@ export const buildCanvasInpaintGraph = ( addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts index b82b55cfee..1d02894381 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts @@ -1,5 +1,6 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ImageDTO, ImageToLatentsInvocation, @@ -11,7 +12,6 @@ import type { import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; @@ -200,7 +200,8 @@ export const buildCanvasOutpaintGraph = ( [CANVAS_OUTPUT]: { type: 'color_correct', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), use_cache: false, }, }, @@ -769,7 +770,5 @@ export const buildCanvasOutpaintGraph = ( addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts index 1b586371a0..58269afce3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts @@ -4,7 +4,6 @@ import type { ImageDTO, ImageToLatentsInvocation, NonNullableGraph } from 'servi import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; @@ -26,7 +25,7 @@ import { SDXL_REFINER_SEAMLESS, SEAMLESS, } from './constants'; -import { getSDXLStylePrompts } from './getSDXLStylePrompt'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; import { addCoreMetadataNode } from './metadata'; /** @@ -246,7 +245,8 @@ export const buildCanvasSDXLImageToImageGraph = (state: RootState, initialImage: graph.nodes[CANVAS_OUTPUT] = { id: CANVAS_OUTPUT, type: 'img_resize', - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), width: width, height: height, use_cache: false, @@ -368,7 +368,5 @@ export const buildCanvasSDXLImageToImageGraph = (state: RootState, initialImage: addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts index 00fea9a37e..5902dee2fc 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts @@ -12,7 +12,6 @@ import type { import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; @@ -44,7 +43,7 @@ import { SDXL_REFINER_SEAMLESS, SEAMLESS, } from './constants'; -import { getSDXLStylePrompts } from './getSDXLStylePrompt'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; /** * Builds the Canvas tab's Inpaint graph. @@ -190,7 +189,8 @@ export const buildCanvasSDXLInpaintGraph = ( [CANVAS_OUTPUT]: { type: 'color_correct', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), reference: canvasInitImage, use_cache: false, }, @@ -687,7 +687,5 @@ export const buildCanvasSDXLInpaintGraph = ( addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts index f85760d8f2..7a78750e8d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts @@ -11,7 +11,6 @@ import type { import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; @@ -46,7 +45,7 @@ import { SDXL_REFINER_SEAMLESS, SEAMLESS, } from './constants'; -import { getSDXLStylePrompts } from './getSDXLStylePrompt'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; /** * Builds the Canvas tab's Outpaint graph. @@ -199,7 +198,8 @@ export const buildCanvasSDXLOutpaintGraph = ( [CANVAS_OUTPUT]: { type: 'color_correct', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), use_cache: false, }, }, @@ -786,7 +786,5 @@ export const buildCanvasSDXLOutpaintGraph = ( addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts index 91d9da4cb5..22da39c67d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts @@ -4,7 +4,6 @@ import type { NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; @@ -24,7 +23,7 @@ import { SDXL_REFINER_SEAMLESS, SEAMLESS, } from './constants'; -import { getSDXLStylePrompts } from './getSDXLStylePrompt'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; import { addCoreMetadataNode } from './metadata'; /** @@ -222,7 +221,8 @@ export const buildCanvasSDXLTextToImageGraph = (state: RootState): NonNullableGr graph.nodes[CANVAS_OUTPUT] = { id: CANVAS_OUTPUT, type: 'img_resize', - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), width: width, height: height, use_cache: false, @@ -254,7 +254,8 @@ export const buildCanvasSDXLTextToImageGraph = (state: RootState): NonNullableGr graph.nodes[CANVAS_OUTPUT] = { type: 'l2i', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), fp32, use_cache: false, }; @@ -330,7 +331,5 @@ export const buildCanvasSDXLTextToImageGraph = (state: RootState): NonNullableGr addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts index 967dd3ff4a..93f0470c7a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts @@ -1,10 +1,10 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; @@ -211,7 +211,8 @@ export const buildCanvasTextToImageGraph = (state: RootState): NonNullableGraph graph.nodes[CANVAS_OUTPUT] = { id: CANVAS_OUTPUT, type: 'img_resize', - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), width: width, height: height, use_cache: false, @@ -243,7 +244,8 @@ export const buildCanvasTextToImageGraph = (state: RootState): NonNullableGraph graph.nodes[CANVAS_OUTPUT] = { type: 'l2i', id: CANVAS_OUTPUT, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), fp32, use_cache: false, }; @@ -310,7 +312,5 @@ export const buildCanvasTextToImageGraph = (state: RootState): NonNullableGraph addWatermarkerToGraph(state, graph, CANVAS_OUTPUT); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts index c76776d94d..d1f1546b23 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts @@ -1,10 +1,10 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { ImageResizeInvocation, ImageToLatentsInvocation, NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; @@ -117,7 +117,8 @@ export const buildLinearImageToImageGraph = (state: RootState): NonNullableGraph type: 'l2i', id: LATENTS_TO_IMAGE, fp32, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), }, [DENOISE_LATENTS]: { type: 'denoise_latents', @@ -358,7 +359,5 @@ export const buildLinearImageToImageGraph = (state: RootState): NonNullableGraph addWatermarkerToGraph(state, graph); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts index 9ae602bcac..de4ad7cece 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts @@ -4,7 +4,6 @@ import type { ImageResizeInvocation, ImageToLatentsInvocation, NonNullableGraph import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; @@ -25,7 +24,7 @@ import { SDXL_REFINER_SEAMLESS, SEAMLESS, } from './constants'; -import { getSDXLStylePrompts } from './getSDXLStylePrompt'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; import { addCoreMetadataNode } from './metadata'; /** @@ -120,7 +119,8 @@ export const buildLinearSDXLImageToImageGraph = (state: RootState): NonNullableG type: 'l2i', id: LATENTS_TO_IMAGE, fp32, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), }, [SDXL_DENOISE_LATENTS]: { type: 'denoise_latents', @@ -380,7 +380,5 @@ export const buildLinearSDXLImageToImageGraph = (state: RootState): NonNullableG addWatermarkerToGraph(state, graph); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts index 222dc1a359..58b97b07c7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts @@ -4,7 +4,6 @@ import type { NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSDXLLoRAsToGraph } from './addSDXLLoRAstoGraph'; import { addSDXLRefinerToGraph } from './addSDXLRefinerToGraph'; @@ -23,7 +22,7 @@ import { SDXL_TEXT_TO_IMAGE_GRAPH, SEAMLESS, } from './constants'; -import { getSDXLStylePrompts } from './getSDXLStylePrompt'; +import { getBoardField, getIsIntermediate, getSDXLStylePrompts } from './graphBuilderUtils'; import { addCoreMetadataNode } from './metadata'; export const buildLinearSDXLTextToImageGraph = (state: RootState): NonNullableGraph => { @@ -120,7 +119,8 @@ export const buildLinearSDXLTextToImageGraph = (state: RootState): NonNullableGr type: 'l2i', id: LATENTS_TO_IMAGE, fp32, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), use_cache: false, }, }, @@ -281,7 +281,5 @@ export const buildLinearSDXLTextToImageGraph = (state: RootState): NonNullableGr addWatermarkerToGraph(state, graph); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts index 0a45d91deb..b2b84cfdad 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts @@ -1,11 +1,11 @@ import { logger } from 'app/logging/logger'; import type { RootState } from 'app/store/store'; +import { getBoardField, getIsIntermediate } from 'features/nodes/util/graph/graphBuilderUtils'; import type { NonNullableGraph } from 'services/api/types'; import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addHrfToGraph } from './addHrfToGraph'; import { addIPAdapterToLinearGraph } from './addIPAdapterToLinearGraph'; -import { addLinearUIOutputNode } from './addLinearUIOutputNode'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addNSFWCheckerToGraph } from './addNSFWCheckerToGraph'; import { addSeamlessToLinearGraph } from './addSeamlessToLinearGraph'; @@ -119,7 +119,8 @@ export const buildLinearTextToImageGraph = (state: RootState): NonNullableGraph type: 'l2i', id: LATENTS_TO_IMAGE, fp32, - is_intermediate, + is_intermediate: getIsIntermediate(state), + board: getBoardField(state), use_cache: false, }, }, @@ -267,7 +268,5 @@ export const buildLinearTextToImageGraph = (state: RootState): NonNullableGraph addWatermarkerToGraph(state, graph); } - addLinearUIOutputNode(state, graph); - return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts b/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts index 363d319121..767bf25df0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/constants.ts @@ -9,7 +9,6 @@ export const LATENTS_TO_IMAGE_HRF_LR = 'latents_to_image_hrf_lr'; export const IMAGE_TO_LATENTS_HRF = 'image_to_latents_hrf'; export const RESIZE_HRF = 'resize_hrf'; export const ESRGAN_HRF = 'esrgan_hrf'; -export const LINEAR_UI_OUTPUT = 'linear_ui_output'; export const NSFW_CHECKER = 'nsfw_checker'; export const WATERMARKER = 'invisible_watermark'; export const NOISE = 'noise'; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/getSDXLStylePrompt.ts b/invokeai/frontend/web/src/features/nodes/util/graph/getSDXLStylePrompt.ts deleted file mode 100644 index e1cd8518fd..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/graph/getSDXLStylePrompt.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { RootState } from 'app/store/store'; - -export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: string; negativeStylePrompt: string } => { - const { positivePrompt, negativePrompt } = state.generation; - const { positiveStylePrompt, negativeStylePrompt, shouldConcatSDXLStylePrompt } = state.sdxl; - - return { - positiveStylePrompt: shouldConcatSDXLStylePrompt ? positivePrompt : positiveStylePrompt, - negativeStylePrompt: shouldConcatSDXLStylePrompt ? negativePrompt : negativeStylePrompt, - }; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts new file mode 100644 index 0000000000..cb6fc9acf1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graph/graphBuilderUtils.ts @@ -0,0 +1,38 @@ +import type { RootState } from 'app/store/store'; +import type { BoardField } from 'features/nodes/types/common'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; + +/** + * Gets the board field, based on the autoAddBoardId setting. + */ +export const getBoardField = (state: RootState): BoardField | undefined => { + const { autoAddBoardId } = state.gallery; + if (autoAddBoardId === 'none') { + return undefined; + } + return { board_id: autoAddBoardId }; +}; + +/** + * Gets the SDXL style prompts, based on the concat setting. + */ +export const getSDXLStylePrompts = (state: RootState): { positiveStylePrompt: string; negativeStylePrompt: string } => { + const { positivePrompt, negativePrompt } = state.generation; + const { positiveStylePrompt, negativeStylePrompt, shouldConcatSDXLStylePrompt } = state.sdxl; + + return { + positiveStylePrompt: shouldConcatSDXLStylePrompt ? positivePrompt : positiveStylePrompt, + negativeStylePrompt: shouldConcatSDXLStylePrompt ? negativePrompt : negativeStylePrompt, + }; +}; + +/** + * Gets the is_intermediate field, based on the active tab and shouldAutoSave setting. + */ +export const getIsIntermediate = (state: RootState) => { + const activeTabName = activeTabNameSelector(state); + if (activeTabName === 'unifiedCanvas') { + return !state.canvas.shouldAutoSave; + } + return false; +}; diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 1382fbe275..55ff808b40 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -132,7 +132,6 @@ export type DivideInvocation = s['DivideInvocation']; export type ImageNSFWBlurInvocation = s['ImageNSFWBlurInvocation']; export type ImageWatermarkInvocation = s['ImageWatermarkInvocation']; export type SeamlessModeInvocation = s['SeamlessModeInvocation']; -export type LinearUIOutputInvocation = s['LinearUIOutputInvocation']; export type MetadataInvocation = s['MetadataInvocation']; export type CoreMetadataInvocation = s['CoreMetadataInvocation']; export type MetadataItemInvocation = s['MetadataItemInvocation']; From b386b1b8af0c5d0bb790b770ac4e68bb1b8c30c4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:01:39 +1100 Subject: [PATCH 069/411] tidy(nodes): remove unnecessary, shadowing class attr declarations --- invokeai/app/services/invocation_services.py | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 11a4de99d6..51bfd5d77a 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -36,33 +36,6 @@ if TYPE_CHECKING: class InvocationServices: """Services that can be used by invocations""" - # TODO: Just forward-declared everything due to circular dependencies. Fix structure. - board_images: "BoardImagesServiceABC" - board_image_record_storage: "BoardImageRecordStorageBase" - boards: "BoardServiceABC" - board_records: "BoardRecordStorageBase" - configuration: "InvokeAIAppConfig" - events: "EventServiceBase" - graph_execution_manager: "ItemStorageABC[GraphExecutionState]" - images: "ImageServiceABC" - image_records: "ImageRecordStorageBase" - image_files: "ImageFileStorageBase" - latents: "LatentsStorageBase" - logger: "Logger" - model_manager: "ModelManagerServiceBase" - model_records: "ModelRecordServiceBase" - download_queue: "DownloadQueueServiceBase" - model_install: "ModelInstallServiceBase" - processor: "InvocationProcessorABC" - performance_statistics: "InvocationStatsServiceBase" - queue: "InvocationQueueABC" - session_queue: "SessionQueueBase" - session_processor: "SessionProcessorBase" - invocation_cache: "InvocationCacheBase" - names: "NameServiceBase" - urls: "UrlServiceBase" - workflow_records: "WorkflowRecordsStorageBase" - def __init__( self, board_images: "BoardImagesServiceABC", From 322a60f48fe3c58b2fa5b582a0681f1b7f756fdb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:10:25 +1100 Subject: [PATCH 070/411] fix(nodes): rearrange fields.py to avoid needing forward refs --- invokeai/app/invocations/fields.py | 92 +++++++++++++++--------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index c42d2f8312..40d403c03d 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -182,6 +182,51 @@ class FieldDescriptions: freeu_b2 = "Scaling factor for stage 2 to amplify the contributions of backbone features." +class ImageField(BaseModel): + """An image primitive field""" + + image_name: str = Field(description="The name of the image") + + +class BoardField(BaseModel): + """A board primitive field""" + + board_id: str = Field(description="The id of the board") + + +class DenoiseMaskField(BaseModel): + """An inpaint mask field""" + + mask_name: str = Field(description="The name of the mask image") + masked_latents_name: Optional[str] = Field(default=None, description="The name of the masked image latents") + + +class LatentsField(BaseModel): + """A latents tensor primitive field""" + + latents_name: str = Field(description="The name of the latents") + seed: Optional[int] = Field(default=None, description="Seed used to generate this latents") + + +class ColorField(BaseModel): + """A color primitive field""" + + r: int = Field(ge=0, le=255, description="The red component") + g: int = Field(ge=0, le=255, description="The green component") + b: int = Field(ge=0, le=255, description="The blue component") + a: int = Field(ge=0, le=255, description="The alpha component") + + def tuple(self) -> Tuple[int, int, int, int]: + return (self.r, self.g, self.b, self.a) + + +class ConditioningField(BaseModel): + """A conditioning tensor primitive value""" + + conditioning_name: str = Field(description="The name of conditioning tensor") + # endregion + + class MetadataField(RootModel): """ Pydantic model for metadata with custom root of type dict[str, Any]. @@ -285,7 +330,7 @@ class WithBoard(BaseModel): Inherit from this class if your node needs a board input field. """ - board: Optional["BoardField"] = Field( + board: Optional[BoardField] = Field( default=None, description=FieldDescriptions.board, json_schema_extra=InputFieldJSONSchemaExtra( @@ -518,48 +563,3 @@ def OutputField( field_kind=FieldKind.Output, ).model_dump(exclude_none=True), ) - - -class ImageField(BaseModel): - """An image primitive field""" - - image_name: str = Field(description="The name of the image") - - -class BoardField(BaseModel): - """A board primitive field""" - - board_id: str = Field(description="The id of the board") - - -class DenoiseMaskField(BaseModel): - """An inpaint mask field""" - - mask_name: str = Field(description="The name of the mask image") - masked_latents_name: Optional[str] = Field(default=None, description="The name of the masked image latents") - - -class LatentsField(BaseModel): - """A latents tensor primitive field""" - - latents_name: str = Field(description="The name of the latents") - seed: Optional[int] = Field(default=None, description="Seed used to generate this latents") - - -class ColorField(BaseModel): - """A color primitive field""" - - r: int = Field(ge=0, le=255, description="The red component") - g: int = Field(ge=0, le=255, description="The green component") - b: int = Field(ge=0, le=255, description="The blue component") - a: int = Field(ge=0, le=255, description="The alpha component") - - def tuple(self) -> Tuple[int, int, int, int]: - return (self.r, self.g, self.b, self.a) - - -class ConditioningField(BaseModel): - """A conditioning tensor primitive value""" - - conditioning_name: str = Field(description="The name of conditioning tensor") - # endregion From 31db62ba9975d9d57bb852877231a6ec471d3dd6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:11:22 +1100 Subject: [PATCH 071/411] tidy(nodes): delete onnx.py It doesn't work and keeping it updated to prevent the app from starting was getting tedious. Deleted. --- invokeai/app/invocations/onnx.py | 510 ------------------------------- 1 file changed, 510 deletions(-) delete mode 100644 invokeai/app/invocations/onnx.py diff --git a/invokeai/app/invocations/onnx.py b/invokeai/app/invocations/onnx.py deleted file mode 100644 index e7b4d3d9fc..0000000000 --- a/invokeai/app/invocations/onnx.py +++ /dev/null @@ -1,510 +0,0 @@ -# Copyright (c) 2023 Borisov Sergey (https://github.com/StAlKeR7779) - -import inspect - -# from contextlib import ExitStack -from typing import List, Literal, Union - -import numpy as np -import torch -from diffusers.image_processor import VaeImageProcessor -from pydantic import BaseModel, ConfigDict, Field, field_validator -from tqdm import tqdm - -from invokeai.app.invocations.fields import ( - FieldDescriptions, - Input, - InputField, - OutputField, - UIComponent, - UIType, - WithMetadata, -) -from invokeai.app.invocations.primitives import ConditioningField, ConditioningOutput, ImageField, ImageOutput -from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin -from invokeai.app.util.step_callback import stable_diffusion_step_callback -from invokeai.backend import BaseModelType, ModelType, SubModelType - -from ...backend.model_management import ONNXModelPatcher -from ...backend.stable_diffusion import PipelineIntermediateState -from ...backend.util import choose_torch_device -from ..util.ti_utils import extract_ti_triggers_from_prompt -from .baseinvocation import ( - BaseInvocation, - BaseInvocationOutput, - InvocationContext, - invocation, - invocation_output, -) -from .controlnet_image_processors import ControlField -from .latent import SAMPLER_NAME_VALUES, LatentsField, LatentsOutput, get_scheduler -from .model import ClipField, ModelInfo, UNetField, VaeField - -ORT_TO_NP_TYPE = { - "tensor(bool)": np.bool_, - "tensor(int8)": np.int8, - "tensor(uint8)": np.uint8, - "tensor(int16)": np.int16, - "tensor(uint16)": np.uint16, - "tensor(int32)": np.int32, - "tensor(uint32)": np.uint32, - "tensor(int64)": np.int64, - "tensor(uint64)": np.uint64, - "tensor(float16)": np.float16, - "tensor(float)": np.float32, - "tensor(double)": np.float64, -} - -PRECISION_VALUES = Literal[tuple(ORT_TO_NP_TYPE.keys())] - - -@invocation("prompt_onnx", title="ONNX Prompt (Raw)", tags=["prompt", "onnx"], category="conditioning", version="1.0.0") -class ONNXPromptInvocation(BaseInvocation): - prompt: str = InputField(default="", description=FieldDescriptions.raw_prompt, ui_component=UIComponent.Textarea) - clip: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection) - - def invoke(self, context: InvocationContext) -> ConditioningOutput: - tokenizer_info = context.services.model_manager.get_model( - **self.clip.tokenizer.model_dump(), - ) - text_encoder_info = context.services.model_manager.get_model( - **self.clip.text_encoder.model_dump(), - ) - with tokenizer_info as orig_tokenizer, text_encoder_info as text_encoder: # , ExitStack() as stack: - loras = [ - ( - context.services.model_manager.get_model(**lora.model_dump(exclude={"weight"})).context.model, - lora.weight, - ) - for lora in self.clip.loras - ] - - ti_list = [] - for trigger in extract_ti_triggers_from_prompt(self.prompt): - name = trigger[1:-1] - try: - ti_list.append( - ( - name, - context.services.model_manager.get_model( - model_name=name, - base_model=self.clip.text_encoder.base_model, - model_type=ModelType.TextualInversion, - ).context.model, - ) - ) - except Exception: - # print(e) - # import traceback - # print(traceback.format_exc()) - print(f'Warn: trigger: "{trigger}" not found') - if loras or ti_list: - text_encoder.release_session() - with ( - ONNXModelPatcher.apply_lora_text_encoder(text_encoder, loras), - ONNXModelPatcher.apply_ti(orig_tokenizer, text_encoder, ti_list) as (tokenizer, ti_manager), - ): - text_encoder.create_session() - - # copy from - # https://github.com/huggingface/diffusers/blob/3ebbaf7c96801271f9e6c21400033b6aa5ffcf29/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py#L153 - text_inputs = tokenizer( - self.prompt, - padding="max_length", - max_length=tokenizer.model_max_length, - truncation=True, - return_tensors="np", - ) - text_input_ids = text_inputs.input_ids - """ - untruncated_ids = tokenizer(prompt, padding="max_length", return_tensors="np").input_ids - - if not np.array_equal(text_input_ids, untruncated_ids): - removed_text = self.tokenizer.batch_decode( - untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1] - ) - logger.warning( - "The following part of your input was truncated because CLIP can only handle sequences up to" - f" {self.tokenizer.model_max_length} tokens: {removed_text}" - ) - """ - - prompt_embeds = text_encoder(input_ids=text_input_ids.astype(np.int32))[0] - - conditioning_name = f"{context.graph_execution_state_id}_{self.id}_conditioning" - - # TODO: hacky but works ;D maybe rename latents somehow? - context.services.latents.save(conditioning_name, (prompt_embeds, None)) - - return ConditioningOutput( - conditioning=ConditioningField( - conditioning_name=conditioning_name, - ), - ) - - -# Text to image -@invocation( - "t2l_onnx", - title="ONNX Text to Latents", - tags=["latents", "inference", "txt2img", "onnx"], - category="latents", - version="1.0.0", -) -class ONNXTextToLatentsInvocation(BaseInvocation): - """Generates latents from conditionings.""" - - positive_conditioning: ConditioningField = InputField( - description=FieldDescriptions.positive_cond, - input=Input.Connection, - ) - negative_conditioning: ConditioningField = InputField( - description=FieldDescriptions.negative_cond, - input=Input.Connection, - ) - noise: LatentsField = InputField( - description=FieldDescriptions.noise, - input=Input.Connection, - ) - steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps) - cfg_scale: Union[float, List[float]] = InputField( - default=7.5, - ge=1, - description=FieldDescriptions.cfg_scale, - ) - scheduler: SAMPLER_NAME_VALUES = InputField( - default="euler", description=FieldDescriptions.scheduler, input=Input.Direct, ui_type=UIType.Scheduler - ) - precision: PRECISION_VALUES = InputField(default="tensor(float16)", description=FieldDescriptions.precision) - unet: UNetField = InputField( - description=FieldDescriptions.unet, - input=Input.Connection, - ) - control: Union[ControlField, list[ControlField]] = InputField( - default=None, - description=FieldDescriptions.control, - ) - # seamless: bool = InputField(default=False, description="Whether or not to generate an image that can tile without seams", ) - # seamless_axes: str = InputField(default="", description="The axes to tile the image on, 'x' and/or 'y'") - - @field_validator("cfg_scale") - def ge_one(cls, v): - """validate that all cfg_scale values are >= 1""" - if isinstance(v, list): - for i in v: - if i < 1: - raise ValueError("cfg_scale must be greater than 1") - else: - if v < 1: - raise ValueError("cfg_scale must be greater than 1") - return v - - # based on - # https://github.com/huggingface/diffusers/blob/3ebbaf7c96801271f9e6c21400033b6aa5ffcf29/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py#L375 - def invoke(self, context: InvocationContext) -> LatentsOutput: - c, _ = context.services.latents.get(self.positive_conditioning.conditioning_name) - uc, _ = context.services.latents.get(self.negative_conditioning.conditioning_name) - graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) - source_node_id = graph_execution_state.prepared_source_mapping[self.id] - if isinstance(c, torch.Tensor): - c = c.cpu().numpy() - if isinstance(uc, torch.Tensor): - uc = uc.cpu().numpy() - device = torch.device(choose_torch_device()) - prompt_embeds = np.concatenate([uc, c]) - - latents = context.services.latents.get(self.noise.latents_name) - if isinstance(latents, torch.Tensor): - latents = latents.cpu().numpy() - - # TODO: better execution device handling - latents = latents.astype(ORT_TO_NP_TYPE[self.precision]) - - # get the initial random noise unless the user supplied it - do_classifier_free_guidance = True - # latents_dtype = prompt_embeds.dtype - # latents_shape = (batch_size * num_images_per_prompt, 4, height // 8, width // 8) - # if latents.shape != latents_shape: - # raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {latents_shape}") - - scheduler = get_scheduler( - context=context, - scheduler_info=self.unet.scheduler, - scheduler_name=self.scheduler, - seed=0, # TODO: refactor this node - ) - - def torch2numpy(latent: torch.Tensor): - return latent.cpu().numpy() - - def numpy2torch(latent, device): - return torch.from_numpy(latent).to(device) - - def dispatch_progress( - self, context: InvocationContext, source_node_id: str, intermediate_state: PipelineIntermediateState - ) -> None: - stable_diffusion_step_callback( - context=context, - intermediate_state=intermediate_state, - node=self.model_dump(), - source_node_id=source_node_id, - ) - - scheduler.set_timesteps(self.steps) - latents = latents * np.float64(scheduler.init_noise_sigma) - - extra_step_kwargs = {} - if "eta" in set(inspect.signature(scheduler.step).parameters.keys()): - extra_step_kwargs.update( - eta=0.0, - ) - - unet_info = context.services.model_manager.get_model(**self.unet.unet.model_dump()) - - with unet_info as unet: # , ExitStack() as stack: - # loras = [(stack.enter_context(context.services.model_manager.get_model(**lora.dict(exclude={"weight"}))), lora.weight) for lora in self.unet.loras] - loras = [ - ( - context.services.model_manager.get_model(**lora.model_dump(exclude={"weight"})).context.model, - lora.weight, - ) - for lora in self.unet.loras - ] - - if loras: - unet.release_session() - with ONNXModelPatcher.apply_lora_unet(unet, loras): - # TODO: - _, _, h, w = latents.shape - unet.create_session(h, w) - - timestep_dtype = next( - (input.type for input in unet.session.get_inputs() if input.name == "timestep"), "tensor(float16)" - ) - timestep_dtype = ORT_TO_NP_TYPE[timestep_dtype] - for i in tqdm(range(len(scheduler.timesteps))): - t = scheduler.timesteps[i] - # expand the latents if we are doing classifier free guidance - latent_model_input = np.concatenate([latents] * 2) if do_classifier_free_guidance else latents - latent_model_input = scheduler.scale_model_input(numpy2torch(latent_model_input, device), t) - latent_model_input = latent_model_input.cpu().numpy() - - # predict the noise residual - timestep = np.array([t], dtype=timestep_dtype) - noise_pred = unet(sample=latent_model_input, timestep=timestep, encoder_hidden_states=prompt_embeds) - noise_pred = noise_pred[0] - - # perform guidance - if do_classifier_free_guidance: - noise_pred_uncond, noise_pred_text = np.split(noise_pred, 2) - noise_pred = noise_pred_uncond + self.cfg_scale * (noise_pred_text - noise_pred_uncond) - - # compute the previous noisy sample x_t -> x_t-1 - scheduler_output = scheduler.step( - numpy2torch(noise_pred, device), t, numpy2torch(latents, device), **extra_step_kwargs - ) - latents = torch2numpy(scheduler_output.prev_sample) - - state = PipelineIntermediateState( - run_id="test", step=i, timestep=timestep, latents=scheduler_output.prev_sample - ) - dispatch_progress(self, context=context, source_node_id=source_node_id, intermediate_state=state) - - # call the callback, if provided - # if callback is not None and i % callback_steps == 0: - # callback(i, t, latents) - - torch.cuda.empty_cache() - - name = f"{context.graph_execution_state_id}__{self.id}" - context.services.latents.save(name, latents) - # return build_latents_output(latents_name=name, latents=torch.from_numpy(latents)) - - -# Latent to image -@invocation( - "l2i_onnx", - title="ONNX Latents to Image", - tags=["latents", "image", "vae", "onnx"], - category="image", - version="1.2.0", -) -class ONNXLatentsToImageInvocation(BaseInvocation, WithMetadata): - """Generates an image from latents.""" - - latents: LatentsField = InputField( - description=FieldDescriptions.denoised_latents, - input=Input.Connection, - ) - vae: VaeField = InputField( - description=FieldDescriptions.vae, - input=Input.Connection, - ) - # tiled: bool = InputField(default=False, description="Decode latents by overlaping tiles(less memory consumption)") - - def invoke(self, context: InvocationContext) -> ImageOutput: - latents = context.services.latents.get(self.latents.latents_name) - - if self.vae.vae.submodel != SubModelType.VaeDecoder: - raise Exception(f"Expected vae_decoder, found: {self.vae.vae.model_type}") - - vae_info = context.services.model_manager.get_model( - **self.vae.vae.model_dump(), - ) - - # clear memory as vae decode can request a lot - torch.cuda.empty_cache() - - with vae_info as vae: - vae.create_session() - - # copied from - # https://github.com/huggingface/diffusers/blob/3ebbaf7c96801271f9e6c21400033b6aa5ffcf29/src/diffusers/pipelines/stable_diffusion/pipeline_onnx_stable_diffusion.py#L427 - latents = 1 / 0.18215 * latents - # image = self.vae_decoder(latent_sample=latents)[0] - # it seems likes there is a strange result for using half-precision vae decoder if batchsize>1 - image = np.concatenate([vae(latent_sample=latents[i : i + 1])[0] for i in range(latents.shape[0])]) - - image = np.clip(image / 2 + 0.5, 0, 1) - image = image.transpose((0, 2, 3, 1)) - image = VaeImageProcessor.numpy_to_pil(image)[0] - - torch.cuda.empty_cache() - - image_dto = context.services.images.create( - image=image, - image_origin=ResourceOrigin.INTERNAL, - image_category=ImageCategory.GENERAL, - node_id=self.id, - session_id=context.graph_execution_state_id, - is_intermediate=self.is_intermediate, - metadata=self.metadata, - workflow=context.workflow, - ) - - return ImageOutput( - image=ImageField(image_name=image_dto.image_name), - width=image_dto.width, - height=image_dto.height, - ) - - -@invocation_output("model_loader_output_onnx") -class ONNXModelLoaderOutput(BaseInvocationOutput): - """Model loader output""" - - unet: UNetField = OutputField(default=None, description=FieldDescriptions.unet, title="UNet") - clip: ClipField = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP") - vae_decoder: VaeField = OutputField(default=None, description=FieldDescriptions.vae, title="VAE Decoder") - vae_encoder: VaeField = OutputField(default=None, description=FieldDescriptions.vae, title="VAE Encoder") - - -class OnnxModelField(BaseModel): - """Onnx model field""" - - model_name: str = Field(description="Name of the model") - base_model: BaseModelType = Field(description="Base model") - model_type: ModelType = Field(description="Model Type") - - model_config = ConfigDict(protected_namespaces=()) - - -@invocation("onnx_model_loader", title="ONNX Main Model", tags=["onnx", "model"], category="model", version="1.0.0") -class OnnxModelLoaderInvocation(BaseInvocation): - """Loads a main model, outputting its submodels.""" - - model: OnnxModelField = InputField( - description=FieldDescriptions.onnx_main_model, input=Input.Direct, ui_type=UIType.ONNXModel - ) - - def invoke(self, context: InvocationContext) -> ONNXModelLoaderOutput: - base_model = self.model.base_model - model_name = self.model.model_name - model_type = ModelType.ONNX - - # TODO: not found exceptions - if not context.services.model_manager.model_exists( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ): - raise Exception(f"Unknown {base_model} {model_type} model: {model_name}") - - """ - if not context.services.model_manager.model_exists( - model_name=self.model_name, - model_type=SDModelType.Diffusers, - submodel=SDModelType.Tokenizer, - ): - raise Exception( - f"Failed to find tokenizer submodel in {self.model_name}! Check if model corrupted" - ) - - if not context.services.model_manager.model_exists( - model_name=self.model_name, - model_type=SDModelType.Diffusers, - submodel=SDModelType.TextEncoder, - ): - raise Exception( - f"Failed to find text_encoder submodel in {self.model_name}! Check if model corrupted" - ) - - if not context.services.model_manager.model_exists( - model_name=self.model_name, - model_type=SDModelType.Diffusers, - submodel=SDModelType.UNet, - ): - raise Exception( - f"Failed to find unet submodel from {self.model_name}! Check if model corrupted" - ) - """ - - return ONNXModelLoaderOutput( - unet=UNetField( - unet=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=SubModelType.UNet, - ), - scheduler=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=SubModelType.Scheduler, - ), - loras=[], - ), - clip=ClipField( - tokenizer=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=SubModelType.Tokenizer, - ), - text_encoder=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=SubModelType.TextEncoder, - ), - loras=[], - skipped_layers=0, - ), - vae_decoder=VaeField( - vae=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=SubModelType.VaeDecoder, - ), - ), - vae_encoder=VaeField( - vae=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=SubModelType.VaeEncoder, - ), - ), - ) From 0710fb3fb0486ab901465bb3a6a6ca07cb6e498e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 17:41:23 +1100 Subject: [PATCH 072/411] feat(nodes): replace latents service with tensors and conditioning services - New generic class `PickleStorageBase`, implements the same API as `LatentsStorageBase`, use for storing non-serializable data via pickling - Implementation `PickleStorageTorch` uses `torch.save` and `torch.load`, same as `LatentsStorageDisk` - Add `tensors: PickleStorageBase[torch.Tensor]` to `InvocationServices` - Add `conditioning: PickleStorageBase[ConditioningFieldData]` to `InvocationServices` - Remove `latents` service and all `LatentsStorage` classes - Update `InvocationContext` and all usage of old `latents` service to use the new services/context wrapper methods --- invokeai/app/api/dependencies.py | 18 +++-- invokeai/app/invocations/latent.py | 36 +++++----- invokeai/app/invocations/noise.py | 2 +- invokeai/app/invocations/primitives.py | 6 +- .../invocation_cache_memory.py | 3 +- invokeai/app/services/invocation_services.py | 12 +++- .../app/services/latents_storage/__init__.py | 0 .../latents_storage/latents_storage_disk.py | 58 ---------------- .../latents_storage_forward_cache.py | 68 ------------------- .../pickle_storage_base.py} | 18 ++--- .../pickle_storage_forward_cache.py | 58 ++++++++++++++++ .../pickle_storage/pickle_storage_torch.py | 62 +++++++++++++++++ .../app/services/shared/invocation_context.py | 49 ++++++------- 13 files changed, 197 insertions(+), 193 deletions(-) delete mode 100644 invokeai/app/services/latents_storage/__init__.py delete mode 100644 invokeai/app/services/latents_storage/latents_storage_disk.py delete mode 100644 invokeai/app/services/latents_storage/latents_storage_forward_cache.py rename invokeai/app/services/{latents_storage/latents_storage_base.py => pickle_storage/pickle_storage_base.py} (68%) create mode 100644 invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py create mode 100644 invokeai/app/services/pickle_storage/pickle_storage_torch.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index c8309e1729..6bb0915cb6 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -2,9 +2,14 @@ from logging import Logger +import torch + from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory +from invokeai.app.services.pickle_storage.pickle_storage_forward_cache import PickleStorageForwardCache +from invokeai.app.services.pickle_storage.pickle_storage_torch import PickleStorageTorch from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.backend.model_manager.metadata import ModelMetadataStore +from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData from invokeai.backend.util.logging import InvokeAILogger from invokeai.version.invokeai_version import __version__ @@ -23,8 +28,6 @@ from ..services.invocation_queue.invocation_queue_memory import MemoryInvocation from ..services.invocation_services import InvocationServices from ..services.invocation_stats.invocation_stats_default import InvocationStatsService from ..services.invoker import Invoker -from ..services.latents_storage.latents_storage_disk import DiskLatentsStorage -from ..services.latents_storage.latents_storage_forward_cache import ForwardCacheLatentsStorage from ..services.model_install import ModelInstallService from ..services.model_manager.model_manager_default import ModelManagerService from ..services.model_records import ModelRecordServiceSQL @@ -68,6 +71,9 @@ class ApiDependencies: logger.debug(f"Internet connectivity is {config.internet_available}") output_folder = config.output_path + if output_folder is None: + raise ValueError("Output folder is not set") + image_files = DiskImageFileStorage(f"{output_folder}/images") db = init_db(config=config, logger=logger, image_files=image_files) @@ -84,7 +90,10 @@ class ApiDependencies: image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) - latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f"{output_folder}/latents")) + tensors = PickleStorageForwardCache(PickleStorageTorch[torch.Tensor](output_folder / "tensors", "tensor")) + conditioning = PickleStorageForwardCache( + PickleStorageTorch[ConditioningFieldData](output_folder / "conditioning", "conditioning") + ) model_manager = ModelManagerService(config, logger) model_record_service = ModelRecordServiceSQL(db=db) download_queue_service = DownloadQueueService(event_bus=events) @@ -117,7 +126,6 @@ class ApiDependencies: image_records=image_records, images=images, invocation_cache=invocation_cache, - latents=latents, logger=logger, model_manager=model_manager, model_records=model_record_service, @@ -131,6 +139,8 @@ class ApiDependencies: session_queue=session_queue, urls=urls, workflow_records=workflow_records, + tensors=tensors, + conditioning=conditioning, ) ApiDependencies.invoker = Invoker(services) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 5449ec9af7..94440d3e2a 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -163,11 +163,11 @@ class CreateDenoiseMaskInvocation(BaseInvocation): # TODO: masked_latents = ImageToLatentsInvocation.vae_encode(vae_info, self.fp32, self.tiled, masked_image.clone()) - masked_latents_name = context.latents.save(tensor=masked_latents) + masked_latents_name = context.tensors.save(tensor=masked_latents) else: masked_latents_name = None - mask_name = context.latents.save(tensor=mask) + mask_name = context.tensors.save(tensor=mask) return DenoiseMaskOutput.build( mask_name=mask_name, @@ -621,10 +621,10 @@ class DenoiseLatentsInvocation(BaseInvocation): if self.denoise_mask is None: return None, None - mask = context.latents.get(self.denoise_mask.mask_name) + mask = context.tensors.get(self.denoise_mask.mask_name) mask = tv_resize(mask, latents.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) if self.denoise_mask.masked_latents_name is not None: - masked_latents = context.latents.get(self.denoise_mask.masked_latents_name) + masked_latents = context.tensors.get(self.denoise_mask.masked_latents_name) else: masked_latents = None @@ -636,11 +636,11 @@ class DenoiseLatentsInvocation(BaseInvocation): seed = None noise = None if self.noise is not None: - noise = context.latents.get(self.noise.latents_name) + noise = context.tensors.get(self.noise.latents_name) seed = self.noise.seed if self.latents is not None: - latents = context.latents.get(self.latents.latents_name) + latents = context.tensors.get(self.latents.latents_name) if seed is None: seed = self.latents.seed @@ -752,7 +752,7 @@ class DenoiseLatentsInvocation(BaseInvocation): if choose_torch_device() == torch.device("mps"): mps.empty_cache() - name = context.latents.save(tensor=result_latents) + name = context.tensors.save(tensor=result_latents) return LatentsOutput.build(latents_name=name, latents=result_latents, seed=seed) @@ -779,7 +779,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): @torch.no_grad() def invoke(self, context: InvocationContext) -> ImageOutput: - latents = context.latents.get(self.latents.latents_name) + latents = context.tensors.get(self.latents.latents_name) vae_info = context.models.load(**self.vae.vae.model_dump()) @@ -870,7 +870,7 @@ class ResizeLatentsInvocation(BaseInvocation): antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.latents.get(self.latents.latents_name) + latents = context.tensors.get(self.latents.latents_name) # TODO: device = choose_torch_device() @@ -888,7 +888,7 @@ class ResizeLatentsInvocation(BaseInvocation): if device == torch.device("mps"): mps.empty_cache() - name = context.latents.save(tensor=resized_latents) + name = context.tensors.save(tensor=resized_latents) return LatentsOutput.build(latents_name=name, latents=resized_latents, seed=self.latents.seed) @@ -911,7 +911,7 @@ class ScaleLatentsInvocation(BaseInvocation): antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.latents.get(self.latents.latents_name) + latents = context.tensors.get(self.latents.latents_name) # TODO: device = choose_torch_device() @@ -930,7 +930,7 @@ class ScaleLatentsInvocation(BaseInvocation): if device == torch.device("mps"): mps.empty_cache() - name = context.latents.save(tensor=resized_latents) + name = context.tensors.save(tensor=resized_latents) return LatentsOutput.build(latents_name=name, latents=resized_latents, seed=self.latents.seed) @@ -1011,7 +1011,7 @@ class ImageToLatentsInvocation(BaseInvocation): latents = self.vae_encode(vae_info, self.fp32, self.tiled, image_tensor) latents = latents.to("cpu") - name = context.latents.save(tensor=latents) + name = context.tensors.save(tensor=latents) return LatentsOutput.build(latents_name=name, latents=latents, seed=None) @singledispatchmethod @@ -1048,8 +1048,8 @@ class BlendLatentsInvocation(BaseInvocation): alpha: float = InputField(default=0.5, description=FieldDescriptions.blend_alpha) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents_a = context.latents.get(self.latents_a.latents_name) - latents_b = context.latents.get(self.latents_b.latents_name) + latents_a = context.tensors.get(self.latents_a.latents_name) + latents_b = context.tensors.get(self.latents_b.latents_name) if latents_a.shape != latents_b.shape: raise Exception("Latents to blend must be the same size.") @@ -1103,7 +1103,7 @@ class BlendLatentsInvocation(BaseInvocation): if device == torch.device("mps"): mps.empty_cache() - name = context.latents.save(tensor=blended_latents) + name = context.tensors.save(tensor=blended_latents) return LatentsOutput.build(latents_name=name, latents=blended_latents) @@ -1149,7 +1149,7 @@ class CropLatentsCoreInvocation(BaseInvocation): ) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.latents.get(self.latents.latents_name) + latents = context.tensors.get(self.latents.latents_name) x1 = self.x // LATENT_SCALE_FACTOR y1 = self.y // LATENT_SCALE_FACTOR @@ -1158,7 +1158,7 @@ class CropLatentsCoreInvocation(BaseInvocation): cropped_latents = latents[..., y1:y2, x1:x2] - name = context.latents.save(tensor=cropped_latents) + name = context.tensors.save(tensor=cropped_latents) return LatentsOutput.build(latents_name=name, latents=cropped_latents) diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index 78f13cc52d..74b3d6e4cb 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -121,5 +121,5 @@ class NoiseInvocation(BaseInvocation): seed=self.seed, use_cpu=self.use_cpu, ) - name = context.latents.save(tensor=noise) + name = context.tensors.save(tensor=noise) return NoiseOutput.build(latents_name=name, latents=noise, seed=self.seed) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index a77939943a..082d5432cc 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -313,9 +313,7 @@ class DenoiseMaskOutput(BaseInvocationOutput): class LatentsOutput(BaseInvocationOutput): """Base class for nodes that output a single latents tensor""" - latents: LatentsField = OutputField( - description=FieldDescriptions.latents, - ) + latents: LatentsField = OutputField(description=FieldDescriptions.latents) width: int = OutputField(description=FieldDescriptions.width) height: int = OutputField(description=FieldDescriptions.height) @@ -346,7 +344,7 @@ class LatentsInvocation(BaseInvocation): latents: LatentsField = InputField(description="The latents tensor", input=Input.Connection) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.latents.get(self.latents.latents_name) + latents = context.tensors.get(self.latents.latents_name) return LatentsOutput.build(self.latents.latents_name, latents) diff --git a/invokeai/app/services/invocation_cache/invocation_cache_memory.py b/invokeai/app/services/invocation_cache/invocation_cache_memory.py index 4a503b3c6b..c700f81186 100644 --- a/invokeai/app/services/invocation_cache/invocation_cache_memory.py +++ b/invokeai/app/services/invocation_cache/invocation_cache_memory.py @@ -37,7 +37,8 @@ class MemoryInvocationCache(InvocationCacheBase): if self._max_cache_size == 0: return self._invoker.services.images.on_deleted(self._delete_by_match) - self._invoker.services.latents.on_deleted(self._delete_by_match) + self._invoker.services.tensors.on_deleted(self._delete_by_match) + self._invoker.services.conditioning.on_deleted(self._delete_by_match) def get(self, key: Union[int, str]) -> Optional[BaseInvocationOutput]: with self._lock: diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 51bfd5d77a..81885781ac 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -6,6 +6,10 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from logging import Logger + import torch + + from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData + from .board_image_records.board_image_records_base import BoardImageRecordStorageBase from .board_images.board_images_base import BoardImagesServiceABC from .board_records.board_records_base import BoardRecordStorageBase @@ -21,11 +25,11 @@ if TYPE_CHECKING: from .invocation_queue.invocation_queue_base import InvocationQueueABC from .invocation_stats.invocation_stats_base import InvocationStatsServiceBase from .item_storage.item_storage_base import ItemStorageABC - from .latents_storage.latents_storage_base import LatentsStorageBase from .model_install import ModelInstallServiceBase from .model_manager.model_manager_base import ModelManagerServiceBase from .model_records import ModelRecordServiceBase from .names.names_base import NameServiceBase + from .pickle_storage.pickle_storage_base import PickleStorageBase from .session_processor.session_processor_base import SessionProcessorBase from .session_queue.session_queue_base import SessionQueueBase from .shared.graph import GraphExecutionState @@ -48,7 +52,6 @@ class InvocationServices: images: "ImageServiceABC", image_files: "ImageFileStorageBase", image_records: "ImageRecordStorageBase", - latents: "LatentsStorageBase", logger: "Logger", model_manager: "ModelManagerServiceBase", model_records: "ModelRecordServiceBase", @@ -63,6 +66,8 @@ class InvocationServices: names: "NameServiceBase", urls: "UrlServiceBase", workflow_records: "WorkflowRecordsStorageBase", + tensors: "PickleStorageBase[torch.Tensor]", + conditioning: "PickleStorageBase[ConditioningFieldData]", ): self.board_images = board_images self.board_image_records = board_image_records @@ -74,7 +79,6 @@ class InvocationServices: self.images = images self.image_files = image_files self.image_records = image_records - self.latents = latents self.logger = logger self.model_manager = model_manager self.model_records = model_records @@ -89,3 +93,5 @@ class InvocationServices: self.names = names self.urls = urls self.workflow_records = workflow_records + self.tensors = tensors + self.conditioning = conditioning diff --git a/invokeai/app/services/latents_storage/__init__.py b/invokeai/app/services/latents_storage/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/invokeai/app/services/latents_storage/latents_storage_disk.py b/invokeai/app/services/latents_storage/latents_storage_disk.py deleted file mode 100644 index 9192b9147f..0000000000 --- a/invokeai/app/services/latents_storage/latents_storage_disk.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) - -from pathlib import Path -from typing import Union - -import torch - -from invokeai.app.services.invoker import Invoker - -from .latents_storage_base import LatentsStorageBase - - -class DiskLatentsStorage(LatentsStorageBase): - """Stores latents in a folder on disk without caching""" - - __output_folder: Path - - def __init__(self, output_folder: Union[str, Path]): - self.__output_folder = output_folder if isinstance(output_folder, Path) else Path(output_folder) - self.__output_folder.mkdir(parents=True, exist_ok=True) - - def start(self, invoker: Invoker) -> None: - self._invoker = invoker - self._delete_all_latents() - - def get(self, name: str) -> torch.Tensor: - latent_path = self.get_path(name) - return torch.load(latent_path) - - def save(self, name: str, data: torch.Tensor) -> None: - self.__output_folder.mkdir(parents=True, exist_ok=True) - latent_path = self.get_path(name) - torch.save(data, latent_path) - - def delete(self, name: str) -> None: - latent_path = self.get_path(name) - latent_path.unlink() - - def get_path(self, name: str) -> Path: - return self.__output_folder / name - - def _delete_all_latents(self) -> None: - """ - Deletes all latents from disk. - Must be called after we have access to `self._invoker` (e.g. in `start()`). - """ - deleted_latents_count = 0 - freed_space = 0 - for latents_file in Path(self.__output_folder).glob("*"): - if latents_file.is_file(): - freed_space += latents_file.stat().st_size - deleted_latents_count += 1 - latents_file.unlink() - if deleted_latents_count > 0: - freed_space_in_mb = round(freed_space / 1024 / 1024, 2) - self._invoker.services.logger.info( - f"Deleted {deleted_latents_count} latents files (freed {freed_space_in_mb}MB)" - ) diff --git a/invokeai/app/services/latents_storage/latents_storage_forward_cache.py b/invokeai/app/services/latents_storage/latents_storage_forward_cache.py deleted file mode 100644 index 6232b76a27..0000000000 --- a/invokeai/app/services/latents_storage/latents_storage_forward_cache.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) - -from queue import Queue -from typing import Dict, Optional - -import torch - -from invokeai.app.services.invoker import Invoker - -from .latents_storage_base import LatentsStorageBase - - -class ForwardCacheLatentsStorage(LatentsStorageBase): - """Caches the latest N latents in memory, writing-thorugh to and reading from underlying storage""" - - __cache: Dict[str, torch.Tensor] - __cache_ids: Queue - __max_cache_size: int - __underlying_storage: LatentsStorageBase - - def __init__(self, underlying_storage: LatentsStorageBase, max_cache_size: int = 20): - super().__init__() - self.__underlying_storage = underlying_storage - self.__cache = {} - self.__cache_ids = Queue() - self.__max_cache_size = max_cache_size - - def start(self, invoker: Invoker) -> None: - self._invoker = invoker - start_op = getattr(self.__underlying_storage, "start", None) - if callable(start_op): - start_op(invoker) - - def stop(self, invoker: Invoker) -> None: - self._invoker = invoker - stop_op = getattr(self.__underlying_storage, "stop", None) - if callable(stop_op): - stop_op(invoker) - - def get(self, name: str) -> torch.Tensor: - cache_item = self.__get_cache(name) - if cache_item is not None: - return cache_item - - latent = self.__underlying_storage.get(name) - self.__set_cache(name, latent) - return latent - - def save(self, name: str, data: torch.Tensor) -> None: - self.__underlying_storage.save(name, data) - self.__set_cache(name, data) - self._on_changed(data) - - def delete(self, name: str) -> None: - self.__underlying_storage.delete(name) - if name in self.__cache: - del self.__cache[name] - self._on_deleted(name) - - def __get_cache(self, name: str) -> Optional[torch.Tensor]: - return None if name not in self.__cache else self.__cache[name] - - def __set_cache(self, name: str, data: torch.Tensor): - if name not in self.__cache: - self.__cache[name] = data - self.__cache_ids.put(name) - if self.__cache_ids.qsize() > self.__max_cache_size: - self.__cache.pop(self.__cache_ids.get()) diff --git a/invokeai/app/services/latents_storage/latents_storage_base.py b/invokeai/app/services/pickle_storage/pickle_storage_base.py similarity index 68% rename from invokeai/app/services/latents_storage/latents_storage_base.py rename to invokeai/app/services/pickle_storage/pickle_storage_base.py index 9fa42b0ae6..558b97c0f1 100644 --- a/invokeai/app/services/latents_storage/latents_storage_base.py +++ b/invokeai/app/services/pickle_storage/pickle_storage_base.py @@ -1,15 +1,15 @@ # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) from abc import ABC, abstractmethod -from typing import Callable +from typing import Callable, Generic, TypeVar -import torch +T = TypeVar("T") -class LatentsStorageBase(ABC): - """Responsible for storing and retrieving latents.""" +class PickleStorageBase(ABC, Generic[T]): + """Responsible for storing and retrieving non-serializable data using a pickler.""" - _on_changed_callbacks: list[Callable[[torch.Tensor], None]] + _on_changed_callbacks: list[Callable[[T], None]] _on_deleted_callbacks: list[Callable[[str], None]] def __init__(self) -> None: @@ -17,18 +17,18 @@ class LatentsStorageBase(ABC): self._on_deleted_callbacks = [] @abstractmethod - def get(self, name: str) -> torch.Tensor: + def get(self, name: str) -> T: pass @abstractmethod - def save(self, name: str, data: torch.Tensor) -> None: + def save(self, name: str, data: T) -> None: pass @abstractmethod def delete(self, name: str) -> None: pass - def on_changed(self, on_changed: Callable[[torch.Tensor], None]) -> None: + def on_changed(self, on_changed: Callable[[T], None]) -> None: """Register a callback for when an item is changed""" self._on_changed_callbacks.append(on_changed) @@ -36,7 +36,7 @@ class LatentsStorageBase(ABC): """Register a callback for when an item is deleted""" self._on_deleted_callbacks.append(on_deleted) - def _on_changed(self, item: torch.Tensor) -> None: + def _on_changed(self, item: T) -> None: for callback in self._on_changed_callbacks: callback(item) diff --git a/invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py b/invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py new file mode 100644 index 0000000000..3002d9e045 --- /dev/null +++ b/invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py @@ -0,0 +1,58 @@ +from queue import Queue +from typing import Optional, TypeVar + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.pickle_storage.pickle_storage_base import PickleStorageBase + +T = TypeVar("T") + + +class PickleStorageForwardCache(PickleStorageBase[T]): + def __init__(self, underlying_storage: PickleStorageBase[T], max_cache_size: int = 20): + super().__init__() + self._underlying_storage = underlying_storage + self._cache: dict[str, T] = {} + self._cache_ids = Queue[str]() + self._max_cache_size = max_cache_size + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + start_op = getattr(self._underlying_storage, "start", None) + if callable(start_op): + start_op(invoker) + + def stop(self, invoker: Invoker) -> None: + self._invoker = invoker + stop_op = getattr(self._underlying_storage, "stop", None) + if callable(stop_op): + stop_op(invoker) + + def get(self, name: str) -> T: + cache_item = self._get_cache(name) + if cache_item is not None: + return cache_item + + latent = self._underlying_storage.get(name) + self._set_cache(name, latent) + return latent + + def save(self, name: str, data: T) -> None: + self._underlying_storage.save(name, data) + self._set_cache(name, data) + self._on_changed(data) + + def delete(self, name: str) -> None: + self._underlying_storage.delete(name) + if name in self._cache: + del self._cache[name] + self._on_deleted(name) + + def _get_cache(self, name: str) -> Optional[T]: + return None if name not in self._cache else self._cache[name] + + def _set_cache(self, name: str, data: T): + if name not in self._cache: + self._cache[name] = data + self._cache_ids.put(name) + if self._cache_ids.qsize() > self._max_cache_size: + self._cache.pop(self._cache_ids.get()) diff --git a/invokeai/app/services/pickle_storage/pickle_storage_torch.py b/invokeai/app/services/pickle_storage/pickle_storage_torch.py new file mode 100644 index 0000000000..0b3c9af7a3 --- /dev/null +++ b/invokeai/app/services/pickle_storage/pickle_storage_torch.py @@ -0,0 +1,62 @@ +# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) + +from pathlib import Path +from typing import TypeVar + +import torch + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.pickle_storage.pickle_storage_base import PickleStorageBase + +T = TypeVar("T") + + +class PickleStorageTorch(PickleStorageBase[T]): + """Responsible for storing and retrieving non-serializable data using `torch.save` and `torch.load`.""" + + def __init__(self, output_folder: Path, item_type_name: "str"): + self._output_folder = output_folder + self._output_folder.mkdir(parents=True, exist_ok=True) + self._item_type_name = item_type_name + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + self._delete_all_items() + + def get(self, name: str) -> T: + latent_path = self._get_path(name) + return torch.load(latent_path) + + def save(self, name: str, data: T) -> None: + self._output_folder.mkdir(parents=True, exist_ok=True) + latent_path = self._get_path(name) + torch.save(data, latent_path) + + def delete(self, name: str) -> None: + latent_path = self._get_path(name) + latent_path.unlink() + + def _get_path(self, name: str) -> Path: + return self._output_folder / name + + def _delete_all_items(self) -> None: + """ + Deletes all pickled items from disk. + Must be called after we have access to `self._invoker` (e.g. in `start()`). + """ + + if not self._invoker: + raise ValueError("Invoker is not set. Must call `start()` first.") + + deleted_latents_count = 0 + freed_space = 0 + for latents_file in Path(self._output_folder).glob("*"): + if latents_file.is_file(): + freed_space += latents_file.stat().st_size + deleted_latents_count += 1 + latents_file.unlink() + if deleted_latents_count > 0: + freed_space_in_mb = round(freed_space / 1024 / 1024, 2) + self._invoker.services.logger.info( + f"Deleted {deleted_latents_count} {self._item_type_name} files (freed {freed_space_in_mb}MB)" + ) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 97a62246fb..6756b1f5c6 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -216,48 +216,46 @@ class ImagesInterface(InvocationContextInterface): return self._services.images.get_dto(image_name) -class LatentsInterface(InvocationContextInterface): +class TensorsInterface(InvocationContextInterface): def save(self, tensor: Tensor) -> str: """ - Saves a latents tensor, returning its name. + Saves a tensor, returning its name. - :param tensor: The latents tensor to save. + :param tensor: The tensor to save. """ # Previously, we added a suffix indicating the type of Tensor we were saving, e.g. # "mask", "noise", "masked_latents", etc. # # Retaining that capability in this wrapper would require either many different methods - # to save latents, or extra args for this method. Instead of complicating the API, we - # will use the same naming scheme for all latents. + # to save tensors, or extra args for this method. Instead of complicating the API, we + # will use the same naming scheme for all tensors. # # This has a very minor impact as we don't use them after a session completes. - # Previously, invocations chose the name for their latents. This is a bit risky, so we + # Previously, invocations chose the name for their tensors. This is a bit risky, so we # will generate a name for them instead. We use a uuid to ensure the name is unique. # - # Because the name of the latents file will includes the session and invocation IDs, + # Because the name of the tensors file will includes the session and invocation IDs, # we don't need to worry about collisions. A truncated UUIDv4 is fine. name = f"{self._context_data.session_id}__{self._context_data.invocation.id}__{uuid_string()[:7]}" - self._services.latents.save( + self._services.tensors.save( name=name, data=tensor, ) return name - def get(self, latents_name: str) -> Tensor: + def get(self, tensor_name: str) -> Tensor: """ - Gets a latents tensor by name. + Gets a tensor by name. - :param latents_name: The name of the latents tensor to get. + :param tensor_name: The name of the tensor to get. """ - return self._services.latents.get(latents_name) + return self._services.tensors.get(tensor_name) class ConditioningInterface(InvocationContextInterface): - # TODO(psyche): We are (ab)using the latents storage service as a general pickle storage - # service, but it is typed to work with Tensors only. We have to fudge the types here. def save(self, conditioning_data: ConditioningFieldData) -> str: """ Saves a conditioning data object, returning its name. @@ -265,15 +263,12 @@ class ConditioningInterface(InvocationContextInterface): :param conditioning_context_data: The conditioning data to save. """ - # Conditioning data is *not* a Tensor, so we will suffix it to indicate this. - # - # See comment for `LatentsInterface.save` for more info about this method (it's very - # similar). + # See comment in TensorsInterface.save for why we generate the name here. - name = f"{self._context_data.session_id}__{self._context_data.invocation.id}__{uuid_string()[:7]}__conditioning" - self._services.latents.save( + name = f"{self._context_data.session_id}__{self._context_data.invocation.id}__{uuid_string()[:7]}" + self._services.conditioning.save( name=name, - data=conditioning_data, # type: ignore [arg-type] + data=conditioning_data, ) return name @@ -284,7 +279,7 @@ class ConditioningInterface(InvocationContextInterface): :param conditioning_name: The name of the conditioning data to get. """ - return self._services.latents.get(conditioning_name) # type: ignore [return-value] + return self._services.conditioning.get(conditioning_name) class ModelsInterface(InvocationContextInterface): @@ -400,7 +395,7 @@ class InvocationContext: def __init__( self, images: ImagesInterface, - latents: LatentsInterface, + tensors: TensorsInterface, conditioning: ConditioningInterface, models: ModelsInterface, logger: LoggerInterface, @@ -412,8 +407,8 @@ class InvocationContext: ) -> None: self.images = images """Provides methods to save, get and update images and their metadata.""" - self.latents = latents - """Provides methods to save and get latents tensors, including image, noise, masks, and masked images.""" + self.tensors = tensors + """Provides methods to save and get tensors, including image, noise, masks, and masked images.""" self.conditioning = conditioning """Provides methods to save and get conditioning data.""" self.models = models @@ -532,7 +527,7 @@ def build_invocation_context( logger = LoggerInterface(services=services, context_data=context_data) images = ImagesInterface(services=services, context_data=context_data) - latents = LatentsInterface(services=services, context_data=context_data) + tensors = TensorsInterface(services=services, context_data=context_data) models = ModelsInterface(services=services, context_data=context_data) config = ConfigInterface(services=services, context_data=context_data) util = UtilInterface(services=services, context_data=context_data) @@ -543,7 +538,7 @@ def build_invocation_context( images=images, logger=logger, config=config, - latents=latents, + tensors=tensors, models=models, context_data=context_data, util=util, From 5dd158a2d404adc3fade0a9916a18eebcc37ff6e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:17:23 +1100 Subject: [PATCH 073/411] tidy(nodes): do not refer to files as latents in `PickleStorageTorch` --- .../pickle_storage/pickle_storage_torch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/invokeai/app/services/pickle_storage/pickle_storage_torch.py b/invokeai/app/services/pickle_storage/pickle_storage_torch.py index 0b3c9af7a3..7b18dc0625 100644 --- a/invokeai/app/services/pickle_storage/pickle_storage_torch.py +++ b/invokeai/app/services/pickle_storage/pickle_storage_torch.py @@ -48,15 +48,15 @@ class PickleStorageTorch(PickleStorageBase[T]): if not self._invoker: raise ValueError("Invoker is not set. Must call `start()` first.") - deleted_latents_count = 0 + deleted_count = 0 freed_space = 0 - for latents_file in Path(self._output_folder).glob("*"): - if latents_file.is_file(): - freed_space += latents_file.stat().st_size - deleted_latents_count += 1 - latents_file.unlink() - if deleted_latents_count > 0: + for file in Path(self._output_folder).glob("*"): + if file.is_file(): + freed_space += file.stat().st_size + deleted_count += 1 + file.unlink() + if deleted_count > 0: freed_space_in_mb = round(freed_space / 1024 / 1024, 2) self._invoker.services.logger.info( - f"Deleted {deleted_latents_count} {self._item_type_name} files (freed {freed_space_in_mb}MB)" + f"Deleted {deleted_count} {self._item_type_name} files (freed {freed_space_in_mb}MB)" ) From de63e888d6df02b8ac21c0925abb59a2b14e7194 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:35:58 +1100 Subject: [PATCH 074/411] fix(nodes): add super init to `PickleStorageTorch` --- invokeai/app/services/pickle_storage/pickle_storage_torch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/app/services/pickle_storage/pickle_storage_torch.py b/invokeai/app/services/pickle_storage/pickle_storage_torch.py index 7b18dc0625..de411bbf47 100644 --- a/invokeai/app/services/pickle_storage/pickle_storage_torch.py +++ b/invokeai/app/services/pickle_storage/pickle_storage_torch.py @@ -15,6 +15,7 @@ class PickleStorageTorch(PickleStorageBase[T]): """Responsible for storing and retrieving non-serializable data using `torch.save` and `torch.load`.""" def __init__(self, output_folder: Path, item_type_name: "str"): + super().__init__() self._output_folder = output_folder self._output_folder.mkdir(parents=True, exist_ok=True) self._item_type_name = item_type_name From c96f50cc9adde22fcc51c6ad456f607146abb3f7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:43:33 +1100 Subject: [PATCH 075/411] feat(nodes): ItemStorageABC typevar no longer bound to pydantic.BaseModel This bound is totally unnecessary. There's no requirement for any implementation of `ItemStorageABC` to work only on pydantic models. --- invokeai/app/services/item_storage/item_storage_base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/invokeai/app/services/item_storage/item_storage_base.py b/invokeai/app/services/item_storage/item_storage_base.py index c93edf5188..d736679159 100644 --- a/invokeai/app/services/item_storage/item_storage_base.py +++ b/invokeai/app/services/item_storage/item_storage_base.py @@ -1,9 +1,7 @@ from abc import ABC, abstractmethod from typing import Callable, Generic, TypeVar -from pydantic import BaseModel - -T = TypeVar("T", bound=BaseModel) +T = TypeVar("T") class ItemStorageABC(ABC, Generic[T]): From ca09bd63a3184485b539b90519ce191fbc772869 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:44:30 +1100 Subject: [PATCH 076/411] tidy(nodes): do not refer to files as latents in `PickleStorageTorch` (again) --- .../services/pickle_storage/pickle_storage_torch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/invokeai/app/services/pickle_storage/pickle_storage_torch.py b/invokeai/app/services/pickle_storage/pickle_storage_torch.py index de411bbf47..16f0d7bb7a 100644 --- a/invokeai/app/services/pickle_storage/pickle_storage_torch.py +++ b/invokeai/app/services/pickle_storage/pickle_storage_torch.py @@ -25,17 +25,17 @@ class PickleStorageTorch(PickleStorageBase[T]): self._delete_all_items() def get(self, name: str) -> T: - latent_path = self._get_path(name) - return torch.load(latent_path) + file_path = self._get_path(name) + return torch.load(file_path) def save(self, name: str, data: T) -> None: self._output_folder.mkdir(parents=True, exist_ok=True) - latent_path = self._get_path(name) - torch.save(data, latent_path) + file_path = self._get_path(name) + torch.save(data, file_path) def delete(self, name: str) -> None: - latent_path = self._get_path(name) - latent_path.unlink() + file_path = self._get_path(name) + file_path.unlink() def _get_path(self, name: str) -> Path: return self._output_folder / name From a50c7c1cd7b457102f6e31a9fb3321e0f26f3b40 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:39:03 +1100 Subject: [PATCH 077/411] feat(nodes): use `ItemStorageABC` for tensors and conditioning Turns out `ItemStorageABC` was almost identical to `PickleStorageBase`. Instead of maintaining separate classes, we can use `ItemStorageABC` for both. There's only one change needed - the `ItemStorageABC.set` method must return the newly stored item's ID. This allows us to let the service handle the responsibility of naming the item, but still create the requisite output objects during node execution. The naming implementation is improved here. It extracts the name of the generic and appends a UUID to that string when saving items. --- invokeai/app/api/dependencies.py | 10 +-- invokeai/app/services/invocation_services.py | 5 +- .../item_storage/item_storage_base.py | 2 +- .../item_storage_ephemeral_disk.py | 72 +++++++++++++++++++ .../item_storage_forward_cache.py | 61 ++++++++++++++++ .../item_storage/item_storage_memory.py | 3 +- .../pickle_storage/pickle_storage_base.py | 45 ------------ .../pickle_storage_forward_cache.py | 58 --------------- .../pickle_storage/pickle_storage_torch.py | 63 ---------------- .../app/services/shared/invocation_context.py | 30 +------- 10 files changed, 145 insertions(+), 204 deletions(-) create mode 100644 invokeai/app/services/item_storage/item_storage_ephemeral_disk.py create mode 100644 invokeai/app/services/item_storage/item_storage_forward_cache.py delete mode 100644 invokeai/app/services/pickle_storage/pickle_storage_base.py delete mode 100644 invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py delete mode 100644 invokeai/app/services/pickle_storage/pickle_storage_torch.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 6bb0915cb6..d6fd970a22 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -4,9 +4,9 @@ from logging import Logger import torch +from invokeai.app.services.item_storage.item_storage_ephemeral_disk import ItemStorageEphemeralDisk +from invokeai.app.services.item_storage.item_storage_forward_cache import ItemStorageForwardCache from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory -from invokeai.app.services.pickle_storage.pickle_storage_forward_cache import PickleStorageForwardCache -from invokeai.app.services.pickle_storage.pickle_storage_torch import PickleStorageTorch from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData @@ -90,9 +90,9 @@ class ApiDependencies: image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) - tensors = PickleStorageForwardCache(PickleStorageTorch[torch.Tensor](output_folder / "tensors", "tensor")) - conditioning = PickleStorageForwardCache( - PickleStorageTorch[ConditioningFieldData](output_folder / "conditioning", "conditioning") + tensors = ItemStorageForwardCache(ItemStorageEphemeralDisk[torch.Tensor](output_folder / "tensors")) + conditioning = ItemStorageForwardCache( + ItemStorageEphemeralDisk[ConditioningFieldData](output_folder / "conditioning") ) model_manager = ModelManagerService(config, logger) model_record_service = ModelRecordServiceSQL(db=db) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 81885781ac..69599d83a4 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -29,7 +29,6 @@ if TYPE_CHECKING: from .model_manager.model_manager_base import ModelManagerServiceBase from .model_records import ModelRecordServiceBase from .names.names_base import NameServiceBase - from .pickle_storage.pickle_storage_base import PickleStorageBase from .session_processor.session_processor_base import SessionProcessorBase from .session_queue.session_queue_base import SessionQueueBase from .shared.graph import GraphExecutionState @@ -66,8 +65,8 @@ class InvocationServices: names: "NameServiceBase", urls: "UrlServiceBase", workflow_records: "WorkflowRecordsStorageBase", - tensors: "PickleStorageBase[torch.Tensor]", - conditioning: "PickleStorageBase[ConditioningFieldData]", + tensors: "ItemStorageABC[torch.Tensor]", + conditioning: "ItemStorageABC[ConditioningFieldData]", ): self.board_images = board_images self.board_image_records = board_image_records diff --git a/invokeai/app/services/item_storage/item_storage_base.py b/invokeai/app/services/item_storage/item_storage_base.py index d736679159..f2d62ea45f 100644 --- a/invokeai/app/services/item_storage/item_storage_base.py +++ b/invokeai/app/services/item_storage/item_storage_base.py @@ -26,7 +26,7 @@ class ItemStorageABC(ABC, Generic[T]): pass @abstractmethod - def set(self, item: T) -> None: + def set(self, item: T) -> str: """ Sets the item. The id will be extracted based on id_field. :param item: the item to set diff --git a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py new file mode 100644 index 0000000000..9843d1e54b --- /dev/null +++ b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py @@ -0,0 +1,72 @@ +import typing +from pathlib import Path +from typing import Optional, TypeVar + +import torch + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC +from invokeai.app.util.misc import uuid_string + +T = TypeVar("T") + + +class ItemStorageEphemeralDisk(ItemStorageABC[T]): + """Provides arbitrary item storage with a disk-backed ephemeral storage. The storage is cleared at startup.""" + + def __init__(self, output_folder: Path): + super().__init__() + self._output_folder = output_folder + self._output_folder.mkdir(parents=True, exist_ok=True) + self.__item_class_name: Optional[str] = None + + @property + def _item_class_name(self) -> str: + if not self.__item_class_name: + # `__orig_class__` is not available in the constructor for some technical, undoubtedly very pythonic reason + self.__item_class_name = typing.get_args(self.__orig_class__)[0].__name__ # pyright: ignore [reportUnknownMemberType, reportGeneralTypeIssues] + return self.__item_class_name + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + self._delete_all_items() + + def get(self, item_id: str) -> T: + file_path = self._get_path(item_id) + return torch.load(file_path) # pyright: ignore [reportUnknownMemberType] + + def set(self, item: T) -> str: + self._output_folder.mkdir(parents=True, exist_ok=True) + item_id = f"{self._item_class_name}_{uuid_string()}" + file_path = self._get_path(item_id) + torch.save(item, file_path) # pyright: ignore [reportUnknownMemberType] + return item_id + + def delete(self, item_id: str) -> None: + file_path = self._get_path(item_id) + file_path.unlink() + + def _get_path(self, item_id: str) -> Path: + return self._output_folder / item_id + + def _delete_all_items(self) -> None: + """ + Deletes all pickled items from disk. + Must be called after we have access to `self._invoker` (e.g. in `start()`). + """ + + if not self._invoker: + raise ValueError("Invoker is not set. Must call `start()` first.") + + deleted_count = 0 + freed_space = 0 + for file in Path(self._output_folder).glob("*"): + if file.is_file(): + freed_space += file.stat().st_size + deleted_count += 1 + file.unlink() + if deleted_count > 0: + freed_space_in_mb = round(freed_space / 1024 / 1024, 2) + self._invoker.services.logger.info( + f"Deleted {deleted_count} {self._item_class_name} files (freed {freed_space_in_mb}MB)" + ) diff --git a/invokeai/app/services/item_storage/item_storage_forward_cache.py b/invokeai/app/services/item_storage/item_storage_forward_cache.py new file mode 100644 index 0000000000..d1fe8e13fa --- /dev/null +++ b/invokeai/app/services/item_storage/item_storage_forward_cache.py @@ -0,0 +1,61 @@ +from queue import Queue +from typing import Optional, TypeVar + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC + +T = TypeVar("T") + + +class ItemStorageForwardCache(ItemStorageABC[T]): + """Provides a simple forward cache for an underlying storage. The cache is LRU and has a maximum size.""" + + def __init__(self, underlying_storage: ItemStorageABC[T], max_cache_size: int = 20): + super().__init__() + self._underlying_storage = underlying_storage + self._cache: dict[str, T] = {} + self._cache_ids = Queue[str]() + self._max_cache_size = max_cache_size + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + start_op = getattr(self._underlying_storage, "start", None) + if callable(start_op): + start_op(invoker) + + def stop(self, invoker: Invoker) -> None: + self._invoker = invoker + stop_op = getattr(self._underlying_storage, "stop", None) + if callable(stop_op): + stop_op(invoker) + + def get(self, item_id: str) -> T: + cache_item = self._get_cache(item_id) + if cache_item is not None: + return cache_item + + latent = self._underlying_storage.get(item_id) + self._set_cache(item_id, latent) + return latent + + def set(self, item: T) -> str: + item_id = self._underlying_storage.set(item) + self._set_cache(item_id, item) + self._on_changed(item) + return item_id + + def delete(self, item_id: str) -> None: + self._underlying_storage.delete(item_id) + if item_id in self._cache: + del self._cache[item_id] + self._on_deleted(item_id) + + def _get_cache(self, item_id: str) -> Optional[T]: + return None if item_id not in self._cache else self._cache[item_id] + + def _set_cache(self, item_id: str, data: T): + if item_id not in self._cache: + self._cache[item_id] = data + self._cache_ids.put(item_id) + if self._cache_ids.qsize() > self._max_cache_size: + self._cache.pop(self._cache_ids.get()) diff --git a/invokeai/app/services/item_storage/item_storage_memory.py b/invokeai/app/services/item_storage/item_storage_memory.py index d8dd0e0664..6d02874516 100644 --- a/invokeai/app/services/item_storage/item_storage_memory.py +++ b/invokeai/app/services/item_storage/item_storage_memory.py @@ -34,7 +34,7 @@ class ItemStorageMemory(ItemStorageABC[T], Generic[T]): self._items[item_id] = item return item - def set(self, item: T) -> None: + def set(self, item: T) -> str: item_id = getattr(item, self._id_field) if item_id in self._items: # If item already exists, remove it and add it to the end @@ -44,6 +44,7 @@ class ItemStorageMemory(ItemStorageABC[T], Generic[T]): self._items.popitem(last=False) self._items[item_id] = item self._on_changed(item) + return item_id def delete(self, item_id: str) -> None: # This is a no-op if the item doesn't exist. diff --git a/invokeai/app/services/pickle_storage/pickle_storage_base.py b/invokeai/app/services/pickle_storage/pickle_storage_base.py deleted file mode 100644 index 558b97c0f1..0000000000 --- a/invokeai/app/services/pickle_storage/pickle_storage_base.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) - -from abc import ABC, abstractmethod -from typing import Callable, Generic, TypeVar - -T = TypeVar("T") - - -class PickleStorageBase(ABC, Generic[T]): - """Responsible for storing and retrieving non-serializable data using a pickler.""" - - _on_changed_callbacks: list[Callable[[T], None]] - _on_deleted_callbacks: list[Callable[[str], None]] - - def __init__(self) -> None: - self._on_changed_callbacks = [] - self._on_deleted_callbacks = [] - - @abstractmethod - def get(self, name: str) -> T: - pass - - @abstractmethod - def save(self, name: str, data: T) -> None: - pass - - @abstractmethod - def delete(self, name: str) -> None: - pass - - def on_changed(self, on_changed: Callable[[T], None]) -> None: - """Register a callback for when an item is changed""" - self._on_changed_callbacks.append(on_changed) - - def on_deleted(self, on_deleted: Callable[[str], None]) -> None: - """Register a callback for when an item is deleted""" - self._on_deleted_callbacks.append(on_deleted) - - def _on_changed(self, item: T) -> None: - for callback in self._on_changed_callbacks: - callback(item) - - def _on_deleted(self, item_id: str) -> None: - for callback in self._on_deleted_callbacks: - callback(item_id) diff --git a/invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py b/invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py deleted file mode 100644 index 3002d9e045..0000000000 --- a/invokeai/app/services/pickle_storage/pickle_storage_forward_cache.py +++ /dev/null @@ -1,58 +0,0 @@ -from queue import Queue -from typing import Optional, TypeVar - -from invokeai.app.services.invoker import Invoker -from invokeai.app.services.pickle_storage.pickle_storage_base import PickleStorageBase - -T = TypeVar("T") - - -class PickleStorageForwardCache(PickleStorageBase[T]): - def __init__(self, underlying_storage: PickleStorageBase[T], max_cache_size: int = 20): - super().__init__() - self._underlying_storage = underlying_storage - self._cache: dict[str, T] = {} - self._cache_ids = Queue[str]() - self._max_cache_size = max_cache_size - - def start(self, invoker: Invoker) -> None: - self._invoker = invoker - start_op = getattr(self._underlying_storage, "start", None) - if callable(start_op): - start_op(invoker) - - def stop(self, invoker: Invoker) -> None: - self._invoker = invoker - stop_op = getattr(self._underlying_storage, "stop", None) - if callable(stop_op): - stop_op(invoker) - - def get(self, name: str) -> T: - cache_item = self._get_cache(name) - if cache_item is not None: - return cache_item - - latent = self._underlying_storage.get(name) - self._set_cache(name, latent) - return latent - - def save(self, name: str, data: T) -> None: - self._underlying_storage.save(name, data) - self._set_cache(name, data) - self._on_changed(data) - - def delete(self, name: str) -> None: - self._underlying_storage.delete(name) - if name in self._cache: - del self._cache[name] - self._on_deleted(name) - - def _get_cache(self, name: str) -> Optional[T]: - return None if name not in self._cache else self._cache[name] - - def _set_cache(self, name: str, data: T): - if name not in self._cache: - self._cache[name] = data - self._cache_ids.put(name) - if self._cache_ids.qsize() > self._max_cache_size: - self._cache.pop(self._cache_ids.get()) diff --git a/invokeai/app/services/pickle_storage/pickle_storage_torch.py b/invokeai/app/services/pickle_storage/pickle_storage_torch.py deleted file mode 100644 index 16f0d7bb7a..0000000000 --- a/invokeai/app/services/pickle_storage/pickle_storage_torch.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) - -from pathlib import Path -from typing import TypeVar - -import torch - -from invokeai.app.services.invoker import Invoker -from invokeai.app.services.pickle_storage.pickle_storage_base import PickleStorageBase - -T = TypeVar("T") - - -class PickleStorageTorch(PickleStorageBase[T]): - """Responsible for storing and retrieving non-serializable data using `torch.save` and `torch.load`.""" - - def __init__(self, output_folder: Path, item_type_name: "str"): - super().__init__() - self._output_folder = output_folder - self._output_folder.mkdir(parents=True, exist_ok=True) - self._item_type_name = item_type_name - - def start(self, invoker: Invoker) -> None: - self._invoker = invoker - self._delete_all_items() - - def get(self, name: str) -> T: - file_path = self._get_path(name) - return torch.load(file_path) - - def save(self, name: str, data: T) -> None: - self._output_folder.mkdir(parents=True, exist_ok=True) - file_path = self._get_path(name) - torch.save(data, file_path) - - def delete(self, name: str) -> None: - file_path = self._get_path(name) - file_path.unlink() - - def _get_path(self, name: str) -> Path: - return self._output_folder / name - - def _delete_all_items(self) -> None: - """ - Deletes all pickled items from disk. - Must be called after we have access to `self._invoker` (e.g. in `start()`). - """ - - if not self._invoker: - raise ValueError("Invoker is not set. Must call `start()` first.") - - deleted_count = 0 - freed_space = 0 - for file in Path(self._output_folder).glob("*"): - if file.is_file(): - freed_space += file.stat().st_size - deleted_count += 1 - file.unlink() - if deleted_count > 0: - freed_space_in_mb = round(freed_space / 1024 / 1024, 2) - self._invoker.services.logger.info( - f"Deleted {deleted_count} {self._item_type_name} files (freed {freed_space_in_mb}MB)" - ) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 6756b1f5c6..baff47a3df 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -12,7 +12,6 @@ from invokeai.app.services.image_records.image_records_common import ImageCatego from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID -from invokeai.app.util.misc import uuid_string from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.backend.model_management.model_manager import ModelInfo from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType @@ -224,26 +223,7 @@ class TensorsInterface(InvocationContextInterface): :param tensor: The tensor to save. """ - # Previously, we added a suffix indicating the type of Tensor we were saving, e.g. - # "mask", "noise", "masked_latents", etc. - # - # Retaining that capability in this wrapper would require either many different methods - # to save tensors, or extra args for this method. Instead of complicating the API, we - # will use the same naming scheme for all tensors. - # - # This has a very minor impact as we don't use them after a session completes. - - # Previously, invocations chose the name for their tensors. This is a bit risky, so we - # will generate a name for them instead. We use a uuid to ensure the name is unique. - # - # Because the name of the tensors file will includes the session and invocation IDs, - # we don't need to worry about collisions. A truncated UUIDv4 is fine. - - name = f"{self._context_data.session_id}__{self._context_data.invocation.id}__{uuid_string()[:7]}" - self._services.tensors.save( - name=name, - data=tensor, - ) + name = self._services.tensors.set(item=tensor) return name def get(self, tensor_name: str) -> Tensor: @@ -263,13 +243,7 @@ class ConditioningInterface(InvocationContextInterface): :param conditioning_context_data: The conditioning data to save. """ - # See comment in TensorsInterface.save for why we generate the name here. - - name = f"{self._context_data.session_id}__{self._context_data.invocation.id}__{uuid_string()[:7]}" - self._services.conditioning.save( - name=name, - data=conditioning_data, - ) + name = self._services.conditioning.set(item=conditioning_data) return name def get(self, conditioning_name: str) -> ConditioningFieldData: From 9cda62c2a7a3cf70b539a3e13f6496f5790da144 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:50:30 +1100 Subject: [PATCH 078/411] feat(nodes): create helper function to generate the item ID --- .../app/services/item_storage/item_storage_ephemeral_disk.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py index 9843d1e54b..377c9c39b3 100644 --- a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py +++ b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py @@ -37,7 +37,7 @@ class ItemStorageEphemeralDisk(ItemStorageABC[T]): def set(self, item: T) -> str: self._output_folder.mkdir(parents=True, exist_ok=True) - item_id = f"{self._item_class_name}_{uuid_string()}" + item_id = self._new_item_id() file_path = self._get_path(item_id) torch.save(item, file_path) # pyright: ignore [reportUnknownMemberType] return item_id @@ -49,6 +49,9 @@ class ItemStorageEphemeralDisk(ItemStorageABC[T]): def _get_path(self, item_id: str) -> Path: return self._output_folder / item_id + def _new_item_id(self) -> str: + return f"{self._item_class_name}_{uuid_string()}" + def _delete_all_items(self) -> None: """ Deletes all pickled items from disk. From ab58d34f9bb41a28a3597edc59941be8b4d8caa4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:51:04 +1100 Subject: [PATCH 079/411] feat(nodes): support custom save and load functions in `ItemStorageEphemeralDisk` --- .../item_storage/item_storage_common.py | 10 ++++++++ .../item_storage_ephemeral_disk.py | 24 +++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/item_storage/item_storage_common.py b/invokeai/app/services/item_storage/item_storage_common.py index 8fd677c71b..7f9bd7bd4e 100644 --- a/invokeai/app/services/item_storage/item_storage_common.py +++ b/invokeai/app/services/item_storage/item_storage_common.py @@ -1,5 +1,15 @@ +from pathlib import Path +from typing import Callable, TypeAlias, TypeVar + + class ItemNotFoundError(KeyError): """Raised when an item is not found in storage""" def __init__(self, item_id: str) -> None: super().__init__(f"Item with id {item_id} not found") + + +T = TypeVar("T") + +SaveFunc: TypeAlias = Callable[[T, Path], None] +LoadFunc: TypeAlias = Callable[[Path], T] diff --git a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py index 377c9c39b3..4dc67129da 100644 --- a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py +++ b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py @@ -6,18 +6,31 @@ import torch from invokeai.app.services.invoker import Invoker from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC +from invokeai.app.services.item_storage.item_storage_common import LoadFunc, SaveFunc from invokeai.app.util.misc import uuid_string T = TypeVar("T") class ItemStorageEphemeralDisk(ItemStorageABC[T]): - """Provides arbitrary item storage with a disk-backed ephemeral storage. The storage is cleared at startup.""" + """Provides a disk-backed ephemeral storage. The storage is cleared at startup. - def __init__(self, output_folder: Path): + :param output_folder: The folder where the items will be stored + :param save: The function to use to save the items to disk [torch.save] + :param load: The function to use to load the items from disk [torch.load] + """ + + def __init__( + self, + output_folder: Path, + save: SaveFunc[T] = torch.save, # pyright: ignore [reportUnknownMemberType] + load: LoadFunc[T] = torch.load, # pyright: ignore [reportUnknownMemberType] + ): super().__init__() self._output_folder = output_folder self._output_folder.mkdir(parents=True, exist_ok=True) + self._save = save + self._load = load self.__item_class_name: Optional[str] = None @property @@ -33,13 +46,13 @@ class ItemStorageEphemeralDisk(ItemStorageABC[T]): def get(self, item_id: str) -> T: file_path = self._get_path(item_id) - return torch.load(file_path) # pyright: ignore [reportUnknownMemberType] + return self._load(file_path) def set(self, item: T) -> str: self._output_folder.mkdir(parents=True, exist_ok=True) item_id = self._new_item_id() file_path = self._get_path(item_id) - torch.save(item, file_path) # pyright: ignore [reportUnknownMemberType] + self._save(item, file_path) return item_id def delete(self, item_id: str) -> None: @@ -58,6 +71,9 @@ class ItemStorageEphemeralDisk(ItemStorageABC[T]): Must be called after we have access to `self._invoker` (e.g. in `start()`). """ + # We could try using a temporary directory here, but they aren't cleared in the event of a crash, so we'd have + # to manually clear them on startup anyways. This is a bit simpler and more reliable. + if not self._invoker: raise ValueError("Invoker is not set. Must call `start()` first.") From 73d871116cc08ed8065e19bcfd1d9a10ed911c6d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 22:54:52 +1100 Subject: [PATCH 080/411] feat(nodes): support custom exception in ephemeral disk storage --- .../item_storage/item_storage_ephemeral_disk.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py index 4dc67129da..97c767c87d 100644 --- a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py +++ b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py @@ -1,12 +1,12 @@ import typing from pathlib import Path -from typing import Optional, TypeVar +from typing import Optional, Type, TypeVar import torch from invokeai.app.services.invoker import Invoker from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC -from invokeai.app.services.item_storage.item_storage_common import LoadFunc, SaveFunc +from invokeai.app.services.item_storage.item_storage_common import ItemNotFoundError, LoadFunc, SaveFunc from invokeai.app.util.misc import uuid_string T = TypeVar("T") @@ -18,6 +18,7 @@ class ItemStorageEphemeralDisk(ItemStorageABC[T]): :param output_folder: The folder where the items will be stored :param save: The function to use to save the items to disk [torch.save] :param load: The function to use to load the items from disk [torch.load] + :param load_exc: The exception that is raised when an item is not found [FileNotFoundError] """ def __init__( @@ -25,12 +26,14 @@ class ItemStorageEphemeralDisk(ItemStorageABC[T]): output_folder: Path, save: SaveFunc[T] = torch.save, # pyright: ignore [reportUnknownMemberType] load: LoadFunc[T] = torch.load, # pyright: ignore [reportUnknownMemberType] + load_exc: Type[Exception] = FileNotFoundError, ): super().__init__() self._output_folder = output_folder self._output_folder.mkdir(parents=True, exist_ok=True) self._save = save self._load = load + self._load_exc = load_exc self.__item_class_name: Optional[str] = None @property @@ -46,7 +49,10 @@ class ItemStorageEphemeralDisk(ItemStorageABC[T]): def get(self, item_id: str) -> T: file_path = self._get_path(item_id) - return self._load(file_path) + try: + return self._load(file_path) + except self._load_exc as e: + raise ItemNotFoundError(item_id) from e def set(self, item: T) -> str: self._output_folder.mkdir(parents=True, exist_ok=True) From 9f382419dce8a7d950c8f13bd55488a07f96048d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:30:46 +1100 Subject: [PATCH 081/411] revert(nodes): revert making tensors/conditioning use item storage Turns out they are just different enough in purpose that the implementations would be rather unintuitive. I've made a separate ObjectSerializer service to handle tensors and conditioning. Refined the class a bit too. --- invokeai/app/api/dependencies.py | 10 +- invokeai/app/invocations/latent.py | 24 ++--- invokeai/app/invocations/primitives.py | 2 +- invokeai/app/services/invocation_services.py | 6 +- .../item_storage/item_storage_base.py | 8 +- .../item_storage/item_storage_common.py | 10 -- .../item_storage_ephemeral_disk.py | 97 ------------------- .../item_storage_forward_cache.py | 61 ------------ .../item_storage/item_storage_memory.py | 3 +- .../object_serializer_base.py | 53 ++++++++++ .../object_serializer_common.py | 5 + .../object_serializer_ephemeral_disk.py | 84 ++++++++++++++++ .../object_serializer_forward_cache.py | 61 ++++++++++++ .../app/services/shared/invocation_context.py | 24 ++--- 14 files changed, 243 insertions(+), 205 deletions(-) delete mode 100644 invokeai/app/services/item_storage/item_storage_ephemeral_disk.py delete mode 100644 invokeai/app/services/item_storage/item_storage_forward_cache.py create mode 100644 invokeai/app/services/object_serializer/object_serializer_base.py create mode 100644 invokeai/app/services/object_serializer/object_serializer_common.py create mode 100644 invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py create mode 100644 invokeai/app/services/object_serializer/object_serializer_forward_cache.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index d6fd970a22..0c80494616 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -4,9 +4,9 @@ from logging import Logger import torch -from invokeai.app.services.item_storage.item_storage_ephemeral_disk import ItemStorageEphemeralDisk -from invokeai.app.services.item_storage.item_storage_forward_cache import ItemStorageForwardCache from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory +from invokeai.app.services.object_serializer.object_serializer_ephemeral_disk import ObjectSerializerEphemeralDisk +from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData @@ -90,9 +90,9 @@ class ApiDependencies: image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) - tensors = ItemStorageForwardCache(ItemStorageEphemeralDisk[torch.Tensor](output_folder / "tensors")) - conditioning = ItemStorageForwardCache( - ItemStorageEphemeralDisk[ConditioningFieldData](output_folder / "conditioning") + tensors = ObjectSerializerForwardCache(ObjectSerializerEphemeralDisk[torch.Tensor](output_folder / "tensors")) + conditioning = ObjectSerializerForwardCache( + ObjectSerializerEphemeralDisk[ConditioningFieldData](output_folder / "conditioning") ) model_manager = ModelManagerService(config, logger) model_record_service = ModelRecordServiceSQL(db=db) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 94440d3e2a..4137ab6e2f 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -304,11 +304,11 @@ class DenoiseLatentsInvocation(BaseInvocation): unet, seed, ) -> ConditioningData: - positive_cond_data = context.conditioning.get(self.positive_conditioning.conditioning_name) + positive_cond_data = context.conditioning.load(self.positive_conditioning.conditioning_name) c = positive_cond_data.conditionings[0].to(device=unet.device, dtype=unet.dtype) extra_conditioning_info = c.extra_conditioning - negative_cond_data = context.conditioning.get(self.negative_conditioning.conditioning_name) + negative_cond_data = context.conditioning.load(self.negative_conditioning.conditioning_name) uc = negative_cond_data.conditionings[0].to(device=unet.device, dtype=unet.dtype) conditioning_data = ConditioningData( @@ -621,10 +621,10 @@ class DenoiseLatentsInvocation(BaseInvocation): if self.denoise_mask is None: return None, None - mask = context.tensors.get(self.denoise_mask.mask_name) + mask = context.tensors.load(self.denoise_mask.mask_name) mask = tv_resize(mask, latents.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) if self.denoise_mask.masked_latents_name is not None: - masked_latents = context.tensors.get(self.denoise_mask.masked_latents_name) + masked_latents = context.tensors.load(self.denoise_mask.masked_latents_name) else: masked_latents = None @@ -636,11 +636,11 @@ class DenoiseLatentsInvocation(BaseInvocation): seed = None noise = None if self.noise is not None: - noise = context.tensors.get(self.noise.latents_name) + noise = context.tensors.load(self.noise.latents_name) seed = self.noise.seed if self.latents is not None: - latents = context.tensors.get(self.latents.latents_name) + latents = context.tensors.load(self.latents.latents_name) if seed is None: seed = self.latents.seed @@ -779,7 +779,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): @torch.no_grad() def invoke(self, context: InvocationContext) -> ImageOutput: - latents = context.tensors.get(self.latents.latents_name) + latents = context.tensors.load(self.latents.latents_name) vae_info = context.models.load(**self.vae.vae.model_dump()) @@ -870,7 +870,7 @@ class ResizeLatentsInvocation(BaseInvocation): antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.tensors.get(self.latents.latents_name) + latents = context.tensors.load(self.latents.latents_name) # TODO: device = choose_torch_device() @@ -911,7 +911,7 @@ class ScaleLatentsInvocation(BaseInvocation): antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.tensors.get(self.latents.latents_name) + latents = context.tensors.load(self.latents.latents_name) # TODO: device = choose_torch_device() @@ -1048,8 +1048,8 @@ class BlendLatentsInvocation(BaseInvocation): alpha: float = InputField(default=0.5, description=FieldDescriptions.blend_alpha) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents_a = context.tensors.get(self.latents_a.latents_name) - latents_b = context.tensors.get(self.latents_b.latents_name) + latents_a = context.tensors.load(self.latents_a.latents_name) + latents_b = context.tensors.load(self.latents_b.latents_name) if latents_a.shape != latents_b.shape: raise Exception("Latents to blend must be the same size.") @@ -1149,7 +1149,7 @@ class CropLatentsCoreInvocation(BaseInvocation): ) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.tensors.get(self.latents.latents_name) + latents = context.tensors.load(self.latents.latents_name) x1 = self.x // LATENT_SCALE_FACTOR y1 = self.y // LATENT_SCALE_FACTOR diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index 082d5432cc..d0f95c92d0 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -344,7 +344,7 @@ class LatentsInvocation(BaseInvocation): latents: LatentsField = InputField(description="The latents tensor", input=Input.Connection) def invoke(self, context: InvocationContext) -> LatentsOutput: - latents = context.tensors.get(self.latents.latents_name) + latents = context.tensors.load(self.latents.latents_name) return LatentsOutput.build(self.latents.latents_name, latents) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 69599d83a4..e893be8763 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import TYPE_CHECKING +from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase + if TYPE_CHECKING: from logging import Logger @@ -65,8 +67,8 @@ class InvocationServices: names: "NameServiceBase", urls: "UrlServiceBase", workflow_records: "WorkflowRecordsStorageBase", - tensors: "ItemStorageABC[torch.Tensor]", - conditioning: "ItemStorageABC[ConditioningFieldData]", + tensors: "ObjectSerializerBase[torch.Tensor]", + conditioning: "ObjectSerializerBase[ConditioningFieldData]", ): self.board_images = board_images self.board_image_records = board_image_records diff --git a/invokeai/app/services/item_storage/item_storage_base.py b/invokeai/app/services/item_storage/item_storage_base.py index f2d62ea45f..ef227ba241 100644 --- a/invokeai/app/services/item_storage/item_storage_base.py +++ b/invokeai/app/services/item_storage/item_storage_base.py @@ -1,7 +1,9 @@ from abc import ABC, abstractmethod from typing import Callable, Generic, TypeVar -T = TypeVar("T") +from pydantic import BaseModel + +T = TypeVar("T", bound=BaseModel) class ItemStorageABC(ABC, Generic[T]): @@ -26,9 +28,9 @@ class ItemStorageABC(ABC, Generic[T]): pass @abstractmethod - def set(self, item: T) -> str: + def set(self, item: T) -> None: """ - Sets the item. The id will be extracted based on id_field. + Sets the item. :param item: the item to set """ pass diff --git a/invokeai/app/services/item_storage/item_storage_common.py b/invokeai/app/services/item_storage/item_storage_common.py index 7f9bd7bd4e..8fd677c71b 100644 --- a/invokeai/app/services/item_storage/item_storage_common.py +++ b/invokeai/app/services/item_storage/item_storage_common.py @@ -1,15 +1,5 @@ -from pathlib import Path -from typing import Callable, TypeAlias, TypeVar - - class ItemNotFoundError(KeyError): """Raised when an item is not found in storage""" def __init__(self, item_id: str) -> None: super().__init__(f"Item with id {item_id} not found") - - -T = TypeVar("T") - -SaveFunc: TypeAlias = Callable[[T, Path], None] -LoadFunc: TypeAlias = Callable[[Path], T] diff --git a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py b/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py deleted file mode 100644 index 97c767c87d..0000000000 --- a/invokeai/app/services/item_storage/item_storage_ephemeral_disk.py +++ /dev/null @@ -1,97 +0,0 @@ -import typing -from pathlib import Path -from typing import Optional, Type, TypeVar - -import torch - -from invokeai.app.services.invoker import Invoker -from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC -from invokeai.app.services.item_storage.item_storage_common import ItemNotFoundError, LoadFunc, SaveFunc -from invokeai.app.util.misc import uuid_string - -T = TypeVar("T") - - -class ItemStorageEphemeralDisk(ItemStorageABC[T]): - """Provides a disk-backed ephemeral storage. The storage is cleared at startup. - - :param output_folder: The folder where the items will be stored - :param save: The function to use to save the items to disk [torch.save] - :param load: The function to use to load the items from disk [torch.load] - :param load_exc: The exception that is raised when an item is not found [FileNotFoundError] - """ - - def __init__( - self, - output_folder: Path, - save: SaveFunc[T] = torch.save, # pyright: ignore [reportUnknownMemberType] - load: LoadFunc[T] = torch.load, # pyright: ignore [reportUnknownMemberType] - load_exc: Type[Exception] = FileNotFoundError, - ): - super().__init__() - self._output_folder = output_folder - self._output_folder.mkdir(parents=True, exist_ok=True) - self._save = save - self._load = load - self._load_exc = load_exc - self.__item_class_name: Optional[str] = None - - @property - def _item_class_name(self) -> str: - if not self.__item_class_name: - # `__orig_class__` is not available in the constructor for some technical, undoubtedly very pythonic reason - self.__item_class_name = typing.get_args(self.__orig_class__)[0].__name__ # pyright: ignore [reportUnknownMemberType, reportGeneralTypeIssues] - return self.__item_class_name - - def start(self, invoker: Invoker) -> None: - self._invoker = invoker - self._delete_all_items() - - def get(self, item_id: str) -> T: - file_path = self._get_path(item_id) - try: - return self._load(file_path) - except self._load_exc as e: - raise ItemNotFoundError(item_id) from e - - def set(self, item: T) -> str: - self._output_folder.mkdir(parents=True, exist_ok=True) - item_id = self._new_item_id() - file_path = self._get_path(item_id) - self._save(item, file_path) - return item_id - - def delete(self, item_id: str) -> None: - file_path = self._get_path(item_id) - file_path.unlink() - - def _get_path(self, item_id: str) -> Path: - return self._output_folder / item_id - - def _new_item_id(self) -> str: - return f"{self._item_class_name}_{uuid_string()}" - - def _delete_all_items(self) -> None: - """ - Deletes all pickled items from disk. - Must be called after we have access to `self._invoker` (e.g. in `start()`). - """ - - # We could try using a temporary directory here, but they aren't cleared in the event of a crash, so we'd have - # to manually clear them on startup anyways. This is a bit simpler and more reliable. - - if not self._invoker: - raise ValueError("Invoker is not set. Must call `start()` first.") - - deleted_count = 0 - freed_space = 0 - for file in Path(self._output_folder).glob("*"): - if file.is_file(): - freed_space += file.stat().st_size - deleted_count += 1 - file.unlink() - if deleted_count > 0: - freed_space_in_mb = round(freed_space / 1024 / 1024, 2) - self._invoker.services.logger.info( - f"Deleted {deleted_count} {self._item_class_name} files (freed {freed_space_in_mb}MB)" - ) diff --git a/invokeai/app/services/item_storage/item_storage_forward_cache.py b/invokeai/app/services/item_storage/item_storage_forward_cache.py deleted file mode 100644 index d1fe8e13fa..0000000000 --- a/invokeai/app/services/item_storage/item_storage_forward_cache.py +++ /dev/null @@ -1,61 +0,0 @@ -from queue import Queue -from typing import Optional, TypeVar - -from invokeai.app.services.invoker import Invoker -from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC - -T = TypeVar("T") - - -class ItemStorageForwardCache(ItemStorageABC[T]): - """Provides a simple forward cache for an underlying storage. The cache is LRU and has a maximum size.""" - - def __init__(self, underlying_storage: ItemStorageABC[T], max_cache_size: int = 20): - super().__init__() - self._underlying_storage = underlying_storage - self._cache: dict[str, T] = {} - self._cache_ids = Queue[str]() - self._max_cache_size = max_cache_size - - def start(self, invoker: Invoker) -> None: - self._invoker = invoker - start_op = getattr(self._underlying_storage, "start", None) - if callable(start_op): - start_op(invoker) - - def stop(self, invoker: Invoker) -> None: - self._invoker = invoker - stop_op = getattr(self._underlying_storage, "stop", None) - if callable(stop_op): - stop_op(invoker) - - def get(self, item_id: str) -> T: - cache_item = self._get_cache(item_id) - if cache_item is not None: - return cache_item - - latent = self._underlying_storage.get(item_id) - self._set_cache(item_id, latent) - return latent - - def set(self, item: T) -> str: - item_id = self._underlying_storage.set(item) - self._set_cache(item_id, item) - self._on_changed(item) - return item_id - - def delete(self, item_id: str) -> None: - self._underlying_storage.delete(item_id) - if item_id in self._cache: - del self._cache[item_id] - self._on_deleted(item_id) - - def _get_cache(self, item_id: str) -> Optional[T]: - return None if item_id not in self._cache else self._cache[item_id] - - def _set_cache(self, item_id: str, data: T): - if item_id not in self._cache: - self._cache[item_id] = data - self._cache_ids.put(item_id) - if self._cache_ids.qsize() > self._max_cache_size: - self._cache.pop(self._cache_ids.get()) diff --git a/invokeai/app/services/item_storage/item_storage_memory.py b/invokeai/app/services/item_storage/item_storage_memory.py index 6d02874516..d8dd0e0664 100644 --- a/invokeai/app/services/item_storage/item_storage_memory.py +++ b/invokeai/app/services/item_storage/item_storage_memory.py @@ -34,7 +34,7 @@ class ItemStorageMemory(ItemStorageABC[T], Generic[T]): self._items[item_id] = item return item - def set(self, item: T) -> str: + def set(self, item: T) -> None: item_id = getattr(item, self._id_field) if item_id in self._items: # If item already exists, remove it and add it to the end @@ -44,7 +44,6 @@ class ItemStorageMemory(ItemStorageABC[T], Generic[T]): self._items.popitem(last=False) self._items[item_id] = item self._on_changed(item) - return item_id def delete(self, item_id: str) -> None: # This is a no-op if the item doesn't exist. diff --git a/invokeai/app/services/object_serializer/object_serializer_base.py b/invokeai/app/services/object_serializer/object_serializer_base.py new file mode 100644 index 0000000000..b01a641d8f --- /dev/null +++ b/invokeai/app/services/object_serializer/object_serializer_base.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod +from typing import Callable, Generic, TypeVar + +T = TypeVar("T") + + +class ObjectSerializerBase(ABC, Generic[T]): + """Saves and loads arbitrary python objects.""" + + def __init__(self) -> None: + self._on_saved_callbacks: list[Callable[[str, T], None]] = [] + self._on_deleted_callbacks: list[Callable[[str], None]] = [] + + @abstractmethod + def load(self, name: str) -> T: + """ + Loads the object. + :param name: The name of the object to load. + :raises ObjectNotFoundError: if the object is not found + """ + pass + + @abstractmethod + def save(self, obj: T) -> str: + """ + Saves the object, returning its name. + :param obj: The object to save. + """ + pass + + @abstractmethod + def delete(self, name: str) -> None: + """ + Deletes the object, if it exists. + :param name: The name of the object to delete. + """ + pass + + def on_saved(self, on_saved: Callable[[str, T], None]) -> None: + """Register a callback for when an object is saved""" + self._on_saved_callbacks.append(on_saved) + + def on_deleted(self, on_deleted: Callable[[str], None]) -> None: + """Register a callback for when an object is deleted""" + self._on_deleted_callbacks.append(on_deleted) + + def _on_saved(self, name: str, obj: T) -> None: + for callback in self._on_saved_callbacks: + callback(name, obj) + + def _on_deleted(self, name: str) -> None: + for callback in self._on_deleted_callbacks: + callback(name) diff --git a/invokeai/app/services/object_serializer/object_serializer_common.py b/invokeai/app/services/object_serializer/object_serializer_common.py new file mode 100644 index 0000000000..7057386541 --- /dev/null +++ b/invokeai/app/services/object_serializer/object_serializer_common.py @@ -0,0 +1,5 @@ +class ObjectNotFoundError(KeyError): + """Raised when an object is not found while loading""" + + def __init__(self, name: str) -> None: + super().__init__(f"Object with name {name} not found") diff --git a/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py b/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py new file mode 100644 index 0000000000..afa868b157 --- /dev/null +++ b/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py @@ -0,0 +1,84 @@ +import typing +from pathlib import Path +from typing import Optional, TypeVar + +import torch + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase +from invokeai.app.services.object_serializer.object_serializer_common import ObjectNotFoundError +from invokeai.app.util.misc import uuid_string + +T = TypeVar("T") + + +class ObjectSerializerEphemeralDisk(ObjectSerializerBase[T]): + """Provides a disk-backed ephemeral storage for arbitrary python objects. The storage is cleared at startup. + + :param output_folder: The folder where the objects will be stored + """ + + def __init__(self, output_dir: Path): + super().__init__() + self._output_dir = output_dir + self._output_dir.mkdir(parents=True, exist_ok=True) + self.__obj_class_name: Optional[str] = None + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + self._delete_all() + + def load(self, name: str) -> T: + file_path = self._get_path(name) + try: + return torch.load(file_path) # pyright: ignore [reportUnknownMemberType] + except FileNotFoundError as e: + raise ObjectNotFoundError(name) from e + + def save(self, obj: T) -> str: + name = self._new_name() + file_path = self._get_path(name) + torch.save(obj, file_path) # pyright: ignore [reportUnknownMemberType] + return name + + def delete(self, name: str) -> None: + file_path = self._get_path(name) + file_path.unlink() + + @property + def _obj_class_name(self) -> str: + if not self.__obj_class_name: + # `__orig_class__` is not available in the constructor for some technical, undoubtedly very pythonic reason + self.__obj_class_name = typing.get_args(self.__orig_class__)[0].__name__ # pyright: ignore [reportUnknownMemberType, reportGeneralTypeIssues] + return self.__obj_class_name + + def _get_path(self, name: str) -> Path: + return self._output_dir / name + + def _new_name(self) -> str: + return f"{self._obj_class_name}_{uuid_string()}" + + def _delete_all(self) -> None: + """ + Deletes all objects from disk. + Must be called after we have access to `self._invoker` (e.g. in `start()`). + """ + + # We could try using a temporary directory here, but they aren't cleared in the event of a crash, so we'd have + # to manually clear them on startup anyways. This is a bit simpler and more reliable. + + if not self._invoker: + raise ValueError("Invoker is not set. Must call `start()` first.") + + deleted_count = 0 + freed_space = 0 + for file in Path(self._output_dir).glob("*"): + if file.is_file(): + freed_space += file.stat().st_size + deleted_count += 1 + file.unlink() + if deleted_count > 0: + freed_space_in_mb = round(freed_space / 1024 / 1024, 2) + self._invoker.services.logger.info( + f"Deleted {deleted_count} {self._obj_class_name} files (freed {freed_space_in_mb}MB)" + ) diff --git a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py new file mode 100644 index 0000000000..40e34e6540 --- /dev/null +++ b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py @@ -0,0 +1,61 @@ +from queue import Queue +from typing import Optional, TypeVar + +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase + +T = TypeVar("T") + + +class ObjectSerializerForwardCache(ObjectSerializerBase[T]): + """Provides a simple forward cache for an underlying storage. The cache is LRU and has a maximum size.""" + + def __init__(self, underlying_storage: ObjectSerializerBase[T], max_cache_size: int = 20): + super().__init__() + self._underlying_storage = underlying_storage + self._cache: dict[str, T] = {} + self._cache_ids = Queue[str]() + self._max_cache_size = max_cache_size + + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + start_op = getattr(self._underlying_storage, "start", None) + if callable(start_op): + start_op(invoker) + + def stop(self, invoker: Invoker) -> None: + self._invoker = invoker + stop_op = getattr(self._underlying_storage, "stop", None) + if callable(stop_op): + stop_op(invoker) + + def load(self, name: str) -> T: + cache_item = self._get_cache(name) + if cache_item is not None: + return cache_item + + latent = self._underlying_storage.load(name) + self._set_cache(name, latent) + return latent + + def save(self, obj: T) -> str: + name = self._underlying_storage.save(obj) + self._set_cache(name, obj) + self._on_saved(name, obj) + return name + + def delete(self, name: str) -> None: + self._underlying_storage.delete(name) + if name in self._cache: + del self._cache[name] + self._on_deleted(name) + + def _get_cache(self, name: str) -> Optional[T]: + return None if name not in self._cache else self._cache[name] + + def _set_cache(self, name: str, data: T): + if name not in self._cache: + self._cache[name] = data + self._cache_ids.put(name) + if self._cache_ids.qsize() > self._max_cache_size: + self._cache.pop(self._cache_ids.get()) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index baff47a3df..8c5a821fd0 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -223,16 +223,16 @@ class TensorsInterface(InvocationContextInterface): :param tensor: The tensor to save. """ - name = self._services.tensors.set(item=tensor) - return name + tensor_id = self._services.tensors.save(obj=tensor) + return tensor_id - def get(self, tensor_name: str) -> Tensor: + def load(self, name: str) -> Tensor: """ - Gets a tensor by name. + Loads a tensor by name. - :param tensor_name: The name of the tensor to get. + :param name: The name of the tensor to load. """ - return self._services.tensors.get(tensor_name) + return self._services.tensors.load(name) class ConditioningInterface(InvocationContextInterface): @@ -243,17 +243,17 @@ class ConditioningInterface(InvocationContextInterface): :param conditioning_context_data: The conditioning data to save. """ - name = self._services.conditioning.set(item=conditioning_data) - return name + conditioning_id = self._services.conditioning.save(obj=conditioning_data) + return conditioning_id - def get(self, conditioning_name: str) -> ConditioningFieldData: + def load(self, name: str) -> ConditioningFieldData: """ - Gets conditioning data by name. + Loads conditioning data by name. - :param conditioning_name: The name of the conditioning data to get. + :param name: The name of the conditioning data to load. """ - return self._services.conditioning.get(conditioning_name) + return self._services.conditioning.load(name) class ModelsInterface(InvocationContextInterface): From 507aeac8a5be9891208569856e56cfc8fe007cdd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:43:22 +1100 Subject: [PATCH 082/411] tidy(nodes): remove object serializer on_saved It's unused. --- .../services/object_serializer/object_serializer_base.py | 9 --------- .../object_serializer/object_serializer_forward_cache.py | 1 - 2 files changed, 10 deletions(-) diff --git a/invokeai/app/services/object_serializer/object_serializer_base.py b/invokeai/app/services/object_serializer/object_serializer_base.py index b01a641d8f..ff19b4a039 100644 --- a/invokeai/app/services/object_serializer/object_serializer_base.py +++ b/invokeai/app/services/object_serializer/object_serializer_base.py @@ -8,7 +8,6 @@ class ObjectSerializerBase(ABC, Generic[T]): """Saves and loads arbitrary python objects.""" def __init__(self) -> None: - self._on_saved_callbacks: list[Callable[[str, T], None]] = [] self._on_deleted_callbacks: list[Callable[[str], None]] = [] @abstractmethod @@ -36,18 +35,10 @@ class ObjectSerializerBase(ABC, Generic[T]): """ pass - def on_saved(self, on_saved: Callable[[str, T], None]) -> None: - """Register a callback for when an object is saved""" - self._on_saved_callbacks.append(on_saved) - def on_deleted(self, on_deleted: Callable[[str], None]) -> None: """Register a callback for when an object is deleted""" self._on_deleted_callbacks.append(on_deleted) - def _on_saved(self, name: str, obj: T) -> None: - for callback in self._on_saved_callbacks: - callback(name, obj) - def _on_deleted(self, name: str) -> None: for callback in self._on_deleted_callbacks: callback(name) diff --git a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py index 40e34e6540..2a4ecdd844 100644 --- a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py +++ b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py @@ -41,7 +41,6 @@ class ObjectSerializerForwardCache(ObjectSerializerBase[T]): def save(self, obj: T) -> str: name = self._underlying_storage.save(obj) self._set_cache(name, obj) - self._on_saved(name, obj) return name def delete(self, name: str) -> None: From 23de78ec9f2ba8a700a7574853a7b4c5199ad4d7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 00:18:58 +1100 Subject: [PATCH 083/411] feat(nodes): allow `_delete_all` in obj serializer to be called at any time `_delete_all` logged how many items it deleted, and had to be called _after_ service start bc it needed access to logger. Move the logger call to the startup method and return the the deleted stats from `_delete_all`. This lets `_delete_all` be called at any time. --- .../object_serializer_ephemeral_disk.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py b/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py index afa868b157..9545d1714d 100644 --- a/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py +++ b/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py @@ -1,4 +1,5 @@ import typing +from dataclasses import dataclass from pathlib import Path from typing import Optional, TypeVar @@ -12,6 +13,12 @@ from invokeai.app.util.misc import uuid_string T = TypeVar("T") +@dataclass +class DeleteAllResult: + deleted_count: int + freed_space_bytes: float + + class ObjectSerializerEphemeralDisk(ObjectSerializerBase[T]): """Provides a disk-backed ephemeral storage for arbitrary python objects. The storage is cleared at startup. @@ -26,7 +33,12 @@ class ObjectSerializerEphemeralDisk(ObjectSerializerBase[T]): def start(self, invoker: Invoker) -> None: self._invoker = invoker - self._delete_all() + delete_all_result = self._delete_all() + if delete_all_result.deleted_count > 0: + freed_space_in_mb = round(delete_all_result.freed_space_bytes / 1024 / 1024, 2) + self._invoker.services.logger.info( + f"Deleted {delete_all_result.deleted_count} {self._obj_class_name} files (freed {freed_space_in_mb}MB)" + ) def load(self, name: str) -> T: file_path = self._get_path(name) @@ -58,18 +70,14 @@ class ObjectSerializerEphemeralDisk(ObjectSerializerBase[T]): def _new_name(self) -> str: return f"{self._obj_class_name}_{uuid_string()}" - def _delete_all(self) -> None: + def _delete_all(self) -> DeleteAllResult: """ Deletes all objects from disk. - Must be called after we have access to `self._invoker` (e.g. in `start()`). """ # We could try using a temporary directory here, but they aren't cleared in the event of a crash, so we'd have # to manually clear them on startup anyways. This is a bit simpler and more reliable. - if not self._invoker: - raise ValueError("Invoker is not set. Must call `start()` first.") - deleted_count = 0 freed_space = 0 for file in Path(self._output_dir).glob("*"): @@ -77,8 +85,4 @@ class ObjectSerializerEphemeralDisk(ObjectSerializerBase[T]): freed_space += file.stat().st_size deleted_count += 1 file.unlink() - if deleted_count > 0: - freed_space_in_mb = round(freed_space / 1024 / 1024, 2) - self._invoker.services.logger.info( - f"Deleted {deleted_count} {self._obj_class_name} files (freed {freed_space_in_mb}MB)" - ) + return DeleteAllResult(deleted_count, freed_space) From 34d23366f4d61032994d267c6be315c5c75a4b1a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 00:20:10 +1100 Subject: [PATCH 084/411] tests: add object serializer tests These test both object serializer and its forward cache implementation. --- .../test_object_serializer_ephemeral_disk.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 tests/test_object_serializer_ephemeral_disk.py diff --git a/tests/test_object_serializer_ephemeral_disk.py b/tests/test_object_serializer_ephemeral_disk.py new file mode 100644 index 0000000000..fffa65304f --- /dev/null +++ b/tests/test_object_serializer_ephemeral_disk.py @@ -0,0 +1,148 @@ +from dataclasses import dataclass +from pathlib import Path + +import pytest +import torch + +from invokeai.app.services.object_serializer.object_serializer_common import ObjectNotFoundError +from invokeai.app.services.object_serializer.object_serializer_ephemeral_disk import ObjectSerializerEphemeralDisk +from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache + + +@dataclass +class MockDataclass: + foo: str + + +@pytest.fixture +def obj_serializer(tmp_path: Path): + return ObjectSerializerEphemeralDisk[MockDataclass](tmp_path) + + +@pytest.fixture +def fwd_cache(tmp_path: Path): + return ObjectSerializerForwardCache(ObjectSerializerEphemeralDisk[MockDataclass](tmp_path), max_cache_size=2) + + +def test_obj_serializer_ephemeral_disk_initializes(tmp_path: Path): + obj_serializer = ObjectSerializerEphemeralDisk[MockDataclass](tmp_path) + assert obj_serializer._output_dir == tmp_path + + +def test_obj_serializer_ephemeral_disk_saves(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): + obj_1 = MockDataclass(foo="bar") + obj_1_name = obj_serializer.save(obj_1) + assert Path(obj_serializer._output_dir, obj_1_name).exists() + + obj_2 = MockDataclass(foo="baz") + obj_2_name = obj_serializer.save(obj_2) + assert Path(obj_serializer._output_dir, obj_2_name).exists() + + +def test_obj_serializer_ephemeral_disk_loads(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): + obj_1 = MockDataclass(foo="bar") + obj_1_name = obj_serializer.save(obj_1) + assert obj_serializer.load(obj_1_name).foo == "bar" + + obj_2 = MockDataclass(foo="baz") + obj_2_name = obj_serializer.save(obj_2) + assert obj_serializer.load(obj_2_name).foo == "baz" + + with pytest.raises(ObjectNotFoundError): + obj_serializer.load("nonexistent_object_name") + + +def test_obj_serializer_ephemeral_disk_deletes(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): + obj_1 = MockDataclass(foo="bar") + obj_1_name = obj_serializer.save(obj_1) + + obj_2 = MockDataclass(foo="bar") + obj_2_name = obj_serializer.save(obj_2) + + obj_serializer.delete(obj_1_name) + assert not Path(obj_serializer._output_dir, obj_1_name).exists() + assert Path(obj_serializer._output_dir, obj_2_name).exists() + + +def test_obj_serializer_ephemeral_disk_deletes_all(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): + obj_1 = MockDataclass(foo="bar") + obj_1_name = obj_serializer.save(obj_1) + + obj_2 = MockDataclass(foo="bar") + obj_2_name = obj_serializer.save(obj_2) + + delete_all_result = obj_serializer._delete_all() + + assert not Path(obj_serializer._output_dir, obj_1_name).exists() + assert not Path(obj_serializer._output_dir, obj_2_name).exists() + assert delete_all_result.deleted_count == 2 + + +def test_obj_serializer_ephemeral_disk_different_types(tmp_path: Path): + obj_serializer = ObjectSerializerEphemeralDisk[MockDataclass](tmp_path) + + obj_1 = MockDataclass(foo="bar") + obj_1_name = obj_serializer.save(obj_1) + obj_1_loaded = obj_serializer.load(obj_1_name) + assert isinstance(obj_1_loaded, MockDataclass) + assert obj_1_loaded.foo == "bar" + assert obj_1_name.startswith("MockDataclass_") + + obj_serializer = ObjectSerializerEphemeralDisk[int](tmp_path) + obj_2_name = obj_serializer.save(9001) + assert obj_serializer.load(obj_2_name) == 9001 + assert obj_2_name.startswith("int_") + + obj_serializer = ObjectSerializerEphemeralDisk[str](tmp_path) + obj_3_name = obj_serializer.save("foo") + assert obj_serializer.load(obj_3_name) == "foo" + assert obj_3_name.startswith("str_") + + obj_serializer = ObjectSerializerEphemeralDisk[torch.Tensor](tmp_path) + obj_4_name = obj_serializer.save(torch.tensor([1, 2, 3])) + obj_4_loaded = obj_serializer.load(obj_4_name) + assert isinstance(obj_4_loaded, torch.Tensor) + assert torch.equal(obj_4_loaded, torch.tensor([1, 2, 3])) + assert obj_4_name.startswith("Tensor_") + + +def test_obj_serializer_fwd_cache_initializes(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): + fwd_cache = ObjectSerializerForwardCache(obj_serializer) + assert fwd_cache._underlying_storage == obj_serializer + + +def test_obj_serializer_fwd_cache_saves_and_loads(fwd_cache: ObjectSerializerForwardCache[MockDataclass]): + obj = MockDataclass(foo="bar") + obj_name = fwd_cache.save(obj) + obj_loaded = fwd_cache.load(obj_name) + obj_underlying = fwd_cache._underlying_storage.load(obj_name) + assert obj_loaded == obj_underlying + assert obj_loaded.foo == "bar" + + +def test_obj_serializer_fwd_cache_respects_cache_size(fwd_cache: ObjectSerializerForwardCache[MockDataclass]): + obj_1 = MockDataclass(foo="bar") + obj_1_name = fwd_cache.save(obj_1) + obj_2 = MockDataclass(foo="baz") + obj_2_name = fwd_cache.save(obj_2) + obj_3 = MockDataclass(foo="qux") + obj_3_name = fwd_cache.save(obj_3) + assert obj_1_name not in fwd_cache._cache + assert obj_2_name in fwd_cache._cache + assert obj_3_name in fwd_cache._cache + # apparently qsize is "not reliable"? + assert fwd_cache._cache_ids.qsize() == 2 + + +def test_obj_serializer_fwd_cache_calls_delete_callback(fwd_cache: ObjectSerializerForwardCache[MockDataclass]): + called_name = None + obj_1 = MockDataclass(foo="bar") + + def on_deleted(name: str): + nonlocal called_name + called_name = name + + fwd_cache.on_deleted(on_deleted) + obj_1_name = fwd_cache.save(obj_1) + fwd_cache.delete(obj_1_name) + assert called_name == obj_1_name From aff44c0e58b027eecf1d6e06941f55b95761ee9e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 00:23:47 +1100 Subject: [PATCH 085/411] tidy(nodes): minor spelling correction --- invokeai/app/services/shared/invocation_context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 8c5a821fd0..828d3d8490 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -223,8 +223,8 @@ class TensorsInterface(InvocationContextInterface): :param tensor: The tensor to save. """ - tensor_id = self._services.tensors.save(obj=tensor) - return tensor_id + name = self._services.tensors.save(obj=tensor) + return name def load(self, name: str) -> Tensor: """ @@ -243,8 +243,8 @@ class ConditioningInterface(InvocationContextInterface): :param conditioning_context_data: The conditioning data to save. """ - conditioning_id = self._services.conditioning.save(obj=conditioning_data) - return conditioning_id + name = self._services.conditioning.save(obj=conditioning_data) + return name def load(self, name: str) -> ConditioningFieldData: """ From 6d25789705cdb8aa3b095be499e838539b33768b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 00:36:53 +1100 Subject: [PATCH 086/411] tests: fix broken tests --- .../object_serializer_ephemeral_disk.py | 9 ++++++--- .../object_serializer_forward_cache.py | 10 ++++++---- tests/aa_nodes/test_graph_execution_state.py | 5 +++-- tests/aa_nodes/test_invoker.py | 3 ++- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py b/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py index 9545d1714d..880848a142 100644 --- a/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py +++ b/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py @@ -1,15 +1,18 @@ import typing from dataclasses import dataclass from pathlib import Path -from typing import Optional, TypeVar +from typing import TYPE_CHECKING, Optional, TypeVar import torch -from invokeai.app.services.invoker import Invoker from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase from invokeai.app.services.object_serializer.object_serializer_common import ObjectNotFoundError from invokeai.app.util.misc import uuid_string +if TYPE_CHECKING: + from invokeai.app.services.invoker import Invoker + + T = TypeVar("T") @@ -31,7 +34,7 @@ class ObjectSerializerEphemeralDisk(ObjectSerializerBase[T]): self._output_dir.mkdir(parents=True, exist_ok=True) self.__obj_class_name: Optional[str] = None - def start(self, invoker: Invoker) -> None: + def start(self, invoker: "Invoker") -> None: self._invoker = invoker delete_all_result = self._delete_all() if delete_all_result.deleted_count > 0: diff --git a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py index 2a4ecdd844..c8ca13982c 100644 --- a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py +++ b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py @@ -1,11 +1,13 @@ from queue import Queue -from typing import Optional, TypeVar +from typing import TYPE_CHECKING, Optional, TypeVar -from invokeai.app.services.invoker import Invoker from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase T = TypeVar("T") +if TYPE_CHECKING: + from invokeai.app.services.invoker import Invoker + class ObjectSerializerForwardCache(ObjectSerializerBase[T]): """Provides a simple forward cache for an underlying storage. The cache is LRU and has a maximum size.""" @@ -17,13 +19,13 @@ class ObjectSerializerForwardCache(ObjectSerializerBase[T]): self._cache_ids = Queue[str]() self._max_cache_size = max_cache_size - def start(self, invoker: Invoker) -> None: + def start(self, invoker: "Invoker") -> None: self._invoker = invoker start_op = getattr(self._underlying_storage, "start", None) if callable(start_op): start_op(invoker) - def stop(self, invoker: Invoker) -> None: + def stop(self, invoker: "Invoker") -> None: self._invoker = invoker stop_op = getattr(self._underlying_storage, "stop", None) if callable(stop_op): diff --git a/tests/aa_nodes/test_graph_execution_state.py b/tests/aa_nodes/test_graph_execution_state.py index aba7c5694f..27d2d2230a 100644 --- a/tests/aa_nodes/test_graph_execution_state.py +++ b/tests/aa_nodes/test_graph_execution_state.py @@ -60,7 +60,6 @@ def mock_services() -> InvocationServices: image_records=None, # type: ignore images=None, # type: ignore invocation_cache=MemoryInvocationCache(max_cache_size=0), - latents=None, # type: ignore logger=logging, # type: ignore model_manager=None, # type: ignore model_records=None, # type: ignore @@ -74,6 +73,8 @@ def mock_services() -> InvocationServices: session_queue=None, # type: ignore urls=None, # type: ignore workflow_records=None, # type: ignore + tensors=None, + conditioning=None, ) @@ -89,7 +90,7 @@ def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[B config=None, context_data=None, images=None, - latents=None, + tensors=None, logger=None, models=None, util=None, diff --git a/tests/aa_nodes/test_invoker.py b/tests/aa_nodes/test_invoker.py index 2ae4eab58a..437ea0f00d 100644 --- a/tests/aa_nodes/test_invoker.py +++ b/tests/aa_nodes/test_invoker.py @@ -63,7 +63,6 @@ def mock_services() -> InvocationServices: image_records=None, # type: ignore images=None, # type: ignore invocation_cache=MemoryInvocationCache(max_cache_size=0), - latents=None, # type: ignore logger=logging, # type: ignore model_manager=None, # type: ignore model_records=None, # type: ignore @@ -77,6 +76,8 @@ def mock_services() -> InvocationServices: session_queue=None, # type: ignore urls=None, # type: ignore workflow_records=None, # type: ignore + tensors=None, + conditioning=None, ) From e08f16763b6938f249faf54ed44baf1c89b00265 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 07:55:36 +1100 Subject: [PATCH 087/411] feat(nodes): use LATENT_SCALE_FACTOR const in tensor output builders --- invokeai/app/invocations/noise.py | 5 +++-- invokeai/app/invocations/primitives.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index 74b3d6e4cb..4093030388 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -5,6 +5,7 @@ import torch from pydantic import field_validator from invokeai.app.invocations.fields import FieldDescriptions, InputField, LatentsField, OutputField +from invokeai.app.invocations.latent import LATENT_SCALE_FACTOR from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import SEED_MAX @@ -70,8 +71,8 @@ class NoiseOutput(BaseInvocationOutput): def build(cls, latents_name: str, latents: torch.Tensor, seed: int) -> "NoiseOutput": return cls( noise=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * 8, - height=latents.size()[2] * 8, + width=latents.size()[3] * LATENT_SCALE_FACTOR, + height=latents.size()[2] * LATENT_SCALE_FACTOR, ) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index d0f95c92d0..2a9cb8cf9b 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -16,6 +16,7 @@ from invokeai.app.invocations.fields import ( OutputField, UIComponent, ) +from invokeai.app.invocations.latent import LATENT_SCALE_FACTOR from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.shared.invocation_context import InvocationContext @@ -321,8 +322,8 @@ class LatentsOutput(BaseInvocationOutput): def build(cls, latents_name: str, latents: torch.Tensor, seed: Optional[int] = None) -> "LatentsOutput": return cls( latents=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * 8, - height=latents.size()[2] * 8, + width=latents.size()[3] * LATENT_SCALE_FACTOR, + height=latents.size()[2] * LATENT_SCALE_FACTOR, ) From 220baae7937063cf61314a7c37c909b8c983bf8b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 08:14:04 +1100 Subject: [PATCH 088/411] Revert "feat(nodes): use LATENT_SCALE_FACTOR const in tensor output builders" This reverts commit ef18fc546560277302f3886e456da9a47e8edce0. --- invokeai/app/invocations/noise.py | 5 ++--- invokeai/app/invocations/primitives.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index 4093030388..74b3d6e4cb 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -5,7 +5,6 @@ import torch from pydantic import field_validator from invokeai.app.invocations.fields import FieldDescriptions, InputField, LatentsField, OutputField -from invokeai.app.invocations.latent import LATENT_SCALE_FACTOR from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import SEED_MAX @@ -71,8 +70,8 @@ class NoiseOutput(BaseInvocationOutput): def build(cls, latents_name: str, latents: torch.Tensor, seed: int) -> "NoiseOutput": return cls( noise=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * LATENT_SCALE_FACTOR, - height=latents.size()[2] * LATENT_SCALE_FACTOR, + width=latents.size()[3] * 8, + height=latents.size()[2] * 8, ) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index 2a9cb8cf9b..d0f95c92d0 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -16,7 +16,6 @@ from invokeai.app.invocations.fields import ( OutputField, UIComponent, ) -from invokeai.app.invocations.latent import LATENT_SCALE_FACTOR from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.shared.invocation_context import InvocationContext @@ -322,8 +321,8 @@ class LatentsOutput(BaseInvocationOutput): def build(cls, latents_name: str, latents: torch.Tensor, seed: Optional[int] = None) -> "LatentsOutput": return cls( latents=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * LATENT_SCALE_FACTOR, - height=latents.size()[2] * LATENT_SCALE_FACTOR, + width=latents.size()[3] * 8, + height=latents.size()[2] * 8, ) From 1655061c968fa2b2d2b566c7dcbf471dca759db6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 10:57:01 +1100 Subject: [PATCH 089/411] tidy(nodes): clarify comment --- invokeai/app/services/shared/invocation_context.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 828d3d8490..3d06cf9272 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -279,15 +279,8 @@ class ModelsInterface(InvocationContextInterface): :param submodel: The submodel of the model to get. """ - # During this call, the model manager emits events with model loading status. The model - # manager itself has access to the events services, but does not have access to the - # required metadata for the events. - # - # For example, it needs access to the node's ID so that the events can be associated - # with the execution of a specific node. - # - # While this is available within the node, it's tedious to need to pass it in on every - # call. We can avoid that by wrapping the method here. + # The model manager emits events as it loads the model. It needs the context data to build + # the event payloads. return self._services.model_manager.get_model( model_name, base_model, model_type, submodel, context_data=self._context_data From 091f4cb58321001b63969d6af8608ec9f67c38c2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 11:05:33 +1100 Subject: [PATCH 090/411] fix(nodes): use `metadata`/`board_id` if provided by user, overriding `WithMetadata`/`WithBoard`-provided values --- .../app/services/shared/invocation_context.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 3d06cf9272..1ca44b7862 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -167,16 +167,19 @@ class ImagesInterface(InvocationContextInterface): **Use this only if you want to override or provide metadata manually!** """ - # If the invocation inherits metadata, use that. Else, use the metadata passed in. - metadata_ = ( - self._context_data.invocation.metadata - if isinstance(self._context_data.invocation, WithMetadata) - else metadata - ) + # If `metadata` is provided directly, use that. Else, use the metadata provided by `WithMetadata`, falling back to None. + metadata_ = None + if metadata: + metadata_ = metadata + elif isinstance(self._context_data.invocation, WithMetadata): + metadata_ = self._context_data.invocation.metadata - # If the invocation inherits WithBoard, use that. Else, use the board_id passed in. - board_ = self._context_data.invocation.board if isinstance(self._context_data.invocation, WithBoard) else None - board_id_ = board_.board_id if board_ is not None else board_id + # If `board_id` is provided directly, use that. Else, use the board provided by `WithBoard`, falling back to None. + board_id_ = None + if board_id: + board_id_ = board_id + elif isinstance(self._context_data.invocation, WithBoard) and self._context_data.invocation.board: + board_id_ = self._context_data.invocation.board.board_id return self._services.images.create( image=image, From 9edb99564738ceb9cf571610b0fa59ee16a8ba4d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:09:59 +1100 Subject: [PATCH 091/411] feat(nodes): make delete on startup configurable for obj serializer - The default is to not delete on startup - feels safer. - The two services using this class _do_ delete on startup. - The class has "ephemeral" removed from its name. - Tests & app updated for this change. --- invokeai/app/api/dependencies.py | 8 ++- ...eral_disk.py => object_serializer_disk.py} | 22 ++++--- ...disk.py => test_object_serializer_disk.py} | 64 ++++++++++++++----- 3 files changed, 67 insertions(+), 27 deletions(-) rename invokeai/app/services/object_serializer/{object_serializer_ephemeral_disk.py => object_serializer_disk.py} (77%) rename tests/{test_object_serializer_ephemeral_disk.py => test_object_serializer_disk.py} (65%) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 0c80494616..2acb961aa7 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -5,7 +5,7 @@ from logging import Logger import torch from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory -from invokeai.app.services.object_serializer.object_serializer_ephemeral_disk import ObjectSerializerEphemeralDisk +from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.backend.model_manager.metadata import ModelMetadataStore @@ -90,9 +90,11 @@ class ApiDependencies: image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) - tensors = ObjectSerializerForwardCache(ObjectSerializerEphemeralDisk[torch.Tensor](output_folder / "tensors")) + tensors = ObjectSerializerForwardCache( + ObjectSerializerDisk[torch.Tensor](output_folder / "tensors", delete_on_startup=True) + ) conditioning = ObjectSerializerForwardCache( - ObjectSerializerEphemeralDisk[ConditioningFieldData](output_folder / "conditioning") + ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", delete_on_startup=True) ) model_manager = ModelManagerService(config, logger) model_record_service = ModelRecordServiceSQL(db=db) diff --git a/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py b/invokeai/app/services/object_serializer/object_serializer_disk.py similarity index 77% rename from invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py rename to invokeai/app/services/object_serializer/object_serializer_disk.py index 880848a142..174ff15192 100644 --- a/invokeai/app/services/object_serializer/object_serializer_ephemeral_disk.py +++ b/invokeai/app/services/object_serializer/object_serializer_disk.py @@ -22,26 +22,30 @@ class DeleteAllResult: freed_space_bytes: float -class ObjectSerializerEphemeralDisk(ObjectSerializerBase[T]): - """Provides a disk-backed ephemeral storage for arbitrary python objects. The storage is cleared at startup. +class ObjectSerializerDisk(ObjectSerializerBase[T]): + """Provides a disk-backed storage for arbitrary python objects. :param output_folder: The folder where the objects will be stored + :param delete_on_startup: If True, all objects in the output folder will be deleted on startup """ - def __init__(self, output_dir: Path): + def __init__(self, output_dir: Path, delete_on_startup: bool = False): super().__init__() self._output_dir = output_dir self._output_dir.mkdir(parents=True, exist_ok=True) + self._delete_on_startup = delete_on_startup self.__obj_class_name: Optional[str] = None def start(self, invoker: "Invoker") -> None: self._invoker = invoker - delete_all_result = self._delete_all() - if delete_all_result.deleted_count > 0: - freed_space_in_mb = round(delete_all_result.freed_space_bytes / 1024 / 1024, 2) - self._invoker.services.logger.info( - f"Deleted {delete_all_result.deleted_count} {self._obj_class_name} files (freed {freed_space_in_mb}MB)" - ) + + if self._delete_on_startup: + delete_all_result = self._delete_all() + if delete_all_result.deleted_count > 0: + freed_space_in_mb = round(delete_all_result.freed_space_bytes / 1024 / 1024, 2) + self._invoker.services.logger.info( + f"Deleted {delete_all_result.deleted_count} {self._obj_class_name} files (freed {freed_space_in_mb}MB)" + ) def load(self, name: str) -> T: file_path = self._get_path(name) diff --git a/tests/test_object_serializer_ephemeral_disk.py b/tests/test_object_serializer_disk.py similarity index 65% rename from tests/test_object_serializer_ephemeral_disk.py rename to tests/test_object_serializer_disk.py index fffa65304f..5ce1e57901 100644 --- a/tests/test_object_serializer_ephemeral_disk.py +++ b/tests/test_object_serializer_disk.py @@ -1,11 +1,14 @@ from dataclasses import dataclass +from logging import Logger from pathlib import Path +from unittest.mock import Mock import pytest import torch +from invokeai.app.services.invoker import Invoker from invokeai.app.services.object_serializer.object_serializer_common import ObjectNotFoundError -from invokeai.app.services.object_serializer.object_serializer_ephemeral_disk import ObjectSerializerEphemeralDisk +from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache @@ -14,22 +17,31 @@ class MockDataclass: foo: str +def count_files(path: Path): + return len(list(path.iterdir())) + + @pytest.fixture def obj_serializer(tmp_path: Path): - return ObjectSerializerEphemeralDisk[MockDataclass](tmp_path) + return ObjectSerializerDisk[MockDataclass](tmp_path) @pytest.fixture def fwd_cache(tmp_path: Path): - return ObjectSerializerForwardCache(ObjectSerializerEphemeralDisk[MockDataclass](tmp_path), max_cache_size=2) + return ObjectSerializerForwardCache(ObjectSerializerDisk[MockDataclass](tmp_path), max_cache_size=2) -def test_obj_serializer_ephemeral_disk_initializes(tmp_path: Path): - obj_serializer = ObjectSerializerEphemeralDisk[MockDataclass](tmp_path) +@pytest.fixture +def mock_invoker_with_logger(): + return Mock(Invoker, services=Mock(logger=Mock(Logger))) + + +def test_obj_serializer_disk_initializes(tmp_path: Path): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path) assert obj_serializer._output_dir == tmp_path -def test_obj_serializer_ephemeral_disk_saves(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): +def test_obj_serializer_disk_saves(obj_serializer: ObjectSerializerDisk[MockDataclass]): obj_1 = MockDataclass(foo="bar") obj_1_name = obj_serializer.save(obj_1) assert Path(obj_serializer._output_dir, obj_1_name).exists() @@ -39,7 +51,7 @@ def test_obj_serializer_ephemeral_disk_saves(obj_serializer: ObjectSerializerEph assert Path(obj_serializer._output_dir, obj_2_name).exists() -def test_obj_serializer_ephemeral_disk_loads(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): +def test_obj_serializer_disk_loads(obj_serializer: ObjectSerializerDisk[MockDataclass]): obj_1 = MockDataclass(foo="bar") obj_1_name = obj_serializer.save(obj_1) assert obj_serializer.load(obj_1_name).foo == "bar" @@ -52,7 +64,7 @@ def test_obj_serializer_ephemeral_disk_loads(obj_serializer: ObjectSerializerEph obj_serializer.load("nonexistent_object_name") -def test_obj_serializer_ephemeral_disk_deletes(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): +def test_obj_serializer_disk_deletes(obj_serializer: ObjectSerializerDisk[MockDataclass]): obj_1 = MockDataclass(foo="bar") obj_1_name = obj_serializer.save(obj_1) @@ -64,7 +76,7 @@ def test_obj_serializer_ephemeral_disk_deletes(obj_serializer: ObjectSerializerE assert Path(obj_serializer._output_dir, obj_2_name).exists() -def test_obj_serializer_ephemeral_disk_deletes_all(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): +def test_obj_serializer_disk_deletes_all(obj_serializer: ObjectSerializerDisk[MockDataclass]): obj_1 = MockDataclass(foo="bar") obj_1_name = obj_serializer.save(obj_1) @@ -78,8 +90,30 @@ def test_obj_serializer_ephemeral_disk_deletes_all(obj_serializer: ObjectSeriali assert delete_all_result.deleted_count == 2 -def test_obj_serializer_ephemeral_disk_different_types(tmp_path: Path): - obj_serializer = ObjectSerializerEphemeralDisk[MockDataclass](tmp_path) +def test_obj_serializer_disk_default_no_delete_on_startup(tmp_path: Path, mock_invoker_with_logger: Invoker): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path) + assert obj_serializer._delete_on_startup is False + + obj_1 = MockDataclass(foo="bar") + obj_1_name = obj_serializer.save(obj_1) + + obj_serializer.start(mock_invoker_with_logger) + assert Path(tmp_path, obj_1_name).exists() + + +def test_obj_serializer_disk_delete_on_startup(tmp_path: Path, mock_invoker_with_logger: Invoker): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path, delete_on_startup=True) + assert obj_serializer._delete_on_startup is True + + obj_1 = MockDataclass(foo="bar") + obj_1_name = obj_serializer.save(obj_1) + + obj_serializer.start(mock_invoker_with_logger) + assert not Path(tmp_path, obj_1_name).exists() + + +def test_obj_serializer_disk_different_types(tmp_path: Path): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path) obj_1 = MockDataclass(foo="bar") obj_1_name = obj_serializer.save(obj_1) @@ -88,17 +122,17 @@ def test_obj_serializer_ephemeral_disk_different_types(tmp_path: Path): assert obj_1_loaded.foo == "bar" assert obj_1_name.startswith("MockDataclass_") - obj_serializer = ObjectSerializerEphemeralDisk[int](tmp_path) + obj_serializer = ObjectSerializerDisk[int](tmp_path) obj_2_name = obj_serializer.save(9001) assert obj_serializer.load(obj_2_name) == 9001 assert obj_2_name.startswith("int_") - obj_serializer = ObjectSerializerEphemeralDisk[str](tmp_path) + obj_serializer = ObjectSerializerDisk[str](tmp_path) obj_3_name = obj_serializer.save("foo") assert obj_serializer.load(obj_3_name) == "foo" assert obj_3_name.startswith("str_") - obj_serializer = ObjectSerializerEphemeralDisk[torch.Tensor](tmp_path) + obj_serializer = ObjectSerializerDisk[torch.Tensor](tmp_path) obj_4_name = obj_serializer.save(torch.tensor([1, 2, 3])) obj_4_loaded = obj_serializer.load(obj_4_name) assert isinstance(obj_4_loaded, torch.Tensor) @@ -106,7 +140,7 @@ def test_obj_serializer_ephemeral_disk_different_types(tmp_path: Path): assert obj_4_name.startswith("Tensor_") -def test_obj_serializer_fwd_cache_initializes(obj_serializer: ObjectSerializerEphemeralDisk[MockDataclass]): +def test_obj_serializer_fwd_cache_initializes(obj_serializer: ObjectSerializerDisk[MockDataclass]): fwd_cache = ObjectSerializerForwardCache(obj_serializer) assert fwd_cache._underlying_storage == obj_serializer From a9b1aad3d79d0c10a1d655c531b49ea87646d552 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 10 Feb 2024 09:41:23 +1100 Subject: [PATCH 092/411] tidy(nodes): do not store unnecessarily store invoker --- .../app/services/object_serializer/object_serializer_disk.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/invokeai/app/services/object_serializer/object_serializer_disk.py b/invokeai/app/services/object_serializer/object_serializer_disk.py index 174ff15192..b3827e16a9 100644 --- a/invokeai/app/services/object_serializer/object_serializer_disk.py +++ b/invokeai/app/services/object_serializer/object_serializer_disk.py @@ -37,13 +37,11 @@ class ObjectSerializerDisk(ObjectSerializerBase[T]): self.__obj_class_name: Optional[str] = None def start(self, invoker: "Invoker") -> None: - self._invoker = invoker - if self._delete_on_startup: delete_all_result = self._delete_all() if delete_all_result.deleted_count > 0: freed_space_in_mb = round(delete_all_result.freed_space_bytes / 1024 / 1024, 2) - self._invoker.services.logger.info( + invoker.services.logger.info( f"Deleted {delete_all_result.deleted_count} {self._obj_class_name} files (freed {freed_space_in_mb}MB)" ) From 6087ace4f10fb85815a398e8a01bb1287634b281 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 10 Feb 2024 10:06:50 +1100 Subject: [PATCH 093/411] tidy(nodes): "latents" -> "obj" --- .../object_serializer/object_serializer_forward_cache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py index c8ca13982c..812731f456 100644 --- a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py +++ b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py @@ -36,9 +36,9 @@ class ObjectSerializerForwardCache(ObjectSerializerBase[T]): if cache_item is not None: return cache_item - latent = self._underlying_storage.load(name) - self._set_cache(name, latent) - return latent + obj = self._underlying_storage.load(name) + self._set_cache(name, obj) + return obj def save(self, obj: T) -> str: name = self._underlying_storage.save(obj) From 66d0ec3f6ce770605ff78f848bb5dc9dc55a403f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 10 Feb 2024 10:10:48 +1100 Subject: [PATCH 094/411] chore(nodes): fix pyright ignore --- .../app/services/object_serializer/object_serializer_disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/object_serializer/object_serializer_disk.py b/invokeai/app/services/object_serializer/object_serializer_disk.py index b3827e16a9..06f86aa460 100644 --- a/invokeai/app/services/object_serializer/object_serializer_disk.py +++ b/invokeai/app/services/object_serializer/object_serializer_disk.py @@ -66,7 +66,7 @@ class ObjectSerializerDisk(ObjectSerializerBase[T]): def _obj_class_name(self) -> str: if not self.__obj_class_name: # `__orig_class__` is not available in the constructor for some technical, undoubtedly very pythonic reason - self.__obj_class_name = typing.get_args(self.__orig_class__)[0].__name__ # pyright: ignore [reportUnknownMemberType, reportGeneralTypeIssues] + self.__obj_class_name = typing.get_args(self.__orig_class__)[0].__name__ # pyright: ignore [reportUnknownMemberType, reportAttributeAccessIssue] return self.__obj_class_name def _get_path(self, name: str) -> Path: From 670f2f75e9afee842f61e591388247d578ab6730 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 10 Feb 2024 10:11:31 +1100 Subject: [PATCH 095/411] chore(nodes): update ObjectSerializerForwardCache docstring --- .../object_serializer/object_serializer_forward_cache.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py index 812731f456..b361259a4b 100644 --- a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py +++ b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py @@ -10,7 +10,10 @@ if TYPE_CHECKING: class ObjectSerializerForwardCache(ObjectSerializerBase[T]): - """Provides a simple forward cache for an underlying storage. The cache is LRU and has a maximum size.""" + """ + Provides a LRU cache for an instance of `ObjectSerializerBase`. + Saving an object to the cache always writes through to the underlying storage. + """ def __init__(self, underlying_storage: ObjectSerializerBase[T], max_cache_size: int = 20): super().__init__() From 11f64dab38f74e8a6f26d173fcf6f7b5e97c152f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 10 Feb 2024 18:46:51 +1100 Subject: [PATCH 096/411] tests: test ObjectSerializerDisk class name extraction --- tests/test_object_serializer_disk.py | 29 +++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_object_serializer_disk.py b/tests/test_object_serializer_disk.py index 5ce1e57901..2bc7e16937 100644 --- a/tests/test_object_serializer_disk.py +++ b/tests/test_object_serializer_disk.py @@ -113,28 +113,31 @@ def test_obj_serializer_disk_delete_on_startup(tmp_path: Path, mock_invoker_with def test_obj_serializer_disk_different_types(tmp_path: Path): - obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path) - + obj_serializer_1 = ObjectSerializerDisk[MockDataclass](tmp_path) obj_1 = MockDataclass(foo="bar") - obj_1_name = obj_serializer.save(obj_1) - obj_1_loaded = obj_serializer.load(obj_1_name) + obj_1_name = obj_serializer_1.save(obj_1) + obj_1_loaded = obj_serializer_1.load(obj_1_name) + assert obj_serializer_1._obj_class_name == "MockDataclass" assert isinstance(obj_1_loaded, MockDataclass) assert obj_1_loaded.foo == "bar" assert obj_1_name.startswith("MockDataclass_") - obj_serializer = ObjectSerializerDisk[int](tmp_path) - obj_2_name = obj_serializer.save(9001) - assert obj_serializer.load(obj_2_name) == 9001 + obj_serializer_2 = ObjectSerializerDisk[int](tmp_path) + obj_2_name = obj_serializer_2.save(9001) + assert obj_serializer_2._obj_class_name == "int" + assert obj_serializer_2.load(obj_2_name) == 9001 assert obj_2_name.startswith("int_") - obj_serializer = ObjectSerializerDisk[str](tmp_path) - obj_3_name = obj_serializer.save("foo") - assert obj_serializer.load(obj_3_name) == "foo" + obj_serializer_3 = ObjectSerializerDisk[str](tmp_path) + obj_3_name = obj_serializer_3.save("foo") + assert obj_serializer_3._obj_class_name == "str" + assert obj_serializer_3.load(obj_3_name) == "foo" assert obj_3_name.startswith("str_") - obj_serializer = ObjectSerializerDisk[torch.Tensor](tmp_path) - obj_4_name = obj_serializer.save(torch.tensor([1, 2, 3])) - obj_4_loaded = obj_serializer.load(obj_4_name) + obj_serializer_4 = ObjectSerializerDisk[torch.Tensor](tmp_path) + obj_4_name = obj_serializer_4.save(torch.tensor([1, 2, 3])) + obj_4_loaded = obj_serializer_4.load(obj_4_name) + assert obj_serializer_4._obj_class_name == "Tensor" assert isinstance(obj_4_loaded, torch.Tensor) assert torch.equal(obj_4_loaded, torch.tensor([1, 2, 3])) assert obj_4_name.startswith("Tensor_") From fece93543862a77a57a5363f46eab3088fb94a89 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 10 Feb 2024 19:11:28 +1100 Subject: [PATCH 097/411] feat(nodes): use TemporaryDirectory to handle ephemeral storage in ObjectSerializerDisk Replace `delete_on_startup: bool` & associated logic with `ephemeral: bool` and `TemporaryDirectory`. The temp dir is created inside of `output_dir`. For example, if `output_dir` is `invokeai/outputs/tensors/`, then the temp dir might be `invokeai/outputs/tensors/tmpvj35ht7b/`. The temp dir is cleaned up when the service is stopped, or when it is GC'd if not properly stopped. In the event of a catastrophic crash where the temp files are not cleaned up, the user can delete the tempdir themselves. This situation may not occur in normal use, but if you kill the process, python cannot clean up the temp dir itself. This includes running the app in a debugger and killing the debugger process - something I do relatively often. Tests updated. --- invokeai/app/api/dependencies.py | 4 +- .../object_serializer_disk.py | 52 +++++++-------- tests/test_object_serializer_disk.py | 65 ++++++++----------- 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 2acb961aa7..0f2a92b5c8 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -91,10 +91,10 @@ class ApiDependencies: images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) tensors = ObjectSerializerForwardCache( - ObjectSerializerDisk[torch.Tensor](output_folder / "tensors", delete_on_startup=True) + ObjectSerializerDisk[torch.Tensor](output_folder / "tensors", ephemeral=True) ) conditioning = ObjectSerializerForwardCache( - ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", delete_on_startup=True) + ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", ephemeral=True) ) model_manager = ModelManagerService(config, logger) model_record_service = ModelRecordServiceSQL(db=db) diff --git a/invokeai/app/services/object_serializer/object_serializer_disk.py b/invokeai/app/services/object_serializer/object_serializer_disk.py index 06f86aa460..935fec3060 100644 --- a/invokeai/app/services/object_serializer/object_serializer_disk.py +++ b/invokeai/app/services/object_serializer/object_serializer_disk.py @@ -1,3 +1,4 @@ +import tempfile import typing from dataclasses import dataclass from pathlib import Path @@ -23,28 +24,24 @@ class DeleteAllResult: class ObjectSerializerDisk(ObjectSerializerBase[T]): - """Provides a disk-backed storage for arbitrary python objects. + """Disk-backed storage for arbitrary python objects. Serialization is handled by `torch.save` and `torch.load`. - :param output_folder: The folder where the objects will be stored - :param delete_on_startup: If True, all objects in the output folder will be deleted on startup + :param output_dir: The folder where the serialized objects will be stored + :param ephemeral: If True, objects will be stored in a temporary directory inside the given output_dir and cleaned up on exit """ - def __init__(self, output_dir: Path, delete_on_startup: bool = False): + def __init__(self, output_dir: Path, ephemeral: bool = False): super().__init__() - self._output_dir = output_dir - self._output_dir.mkdir(parents=True, exist_ok=True) - self._delete_on_startup = delete_on_startup + self._ephemeral = ephemeral + self._base_output_dir = output_dir + self._base_output_dir.mkdir(parents=True, exist_ok=True) + # Must specify `ignore_cleanup_errors` to avoid fatal errors during cleanup on Windows + self._tempdir = ( + tempfile.TemporaryDirectory(dir=self._base_output_dir, ignore_cleanup_errors=True) if ephemeral else None + ) + self._output_dir = Path(self._tempdir.name) if self._tempdir else self._base_output_dir self.__obj_class_name: Optional[str] = None - def start(self, invoker: "Invoker") -> None: - if self._delete_on_startup: - delete_all_result = self._delete_all() - if delete_all_result.deleted_count > 0: - freed_space_in_mb = round(delete_all_result.freed_space_bytes / 1024 / 1024, 2) - invoker.services.logger.info( - f"Deleted {delete_all_result.deleted_count} {self._obj_class_name} files (freed {freed_space_in_mb}MB)" - ) - def load(self, name: str) -> T: file_path = self._get_path(name) try: @@ -75,19 +72,14 @@ class ObjectSerializerDisk(ObjectSerializerBase[T]): def _new_name(self) -> str: return f"{self._obj_class_name}_{uuid_string()}" - def _delete_all(self) -> DeleteAllResult: - """ - Deletes all objects from disk. - """ + def _tempdir_cleanup(self) -> None: + """Calls `cleanup` on the temporary directory, if it exists.""" + if self._tempdir: + self._tempdir.cleanup() - # We could try using a temporary directory here, but they aren't cleared in the event of a crash, so we'd have - # to manually clear them on startup anyways. This is a bit simpler and more reliable. + def __del__(self) -> None: + # In case the service is not properly stopped, clean up the temporary directory when the class instance is GC'd. + self._tempdir_cleanup() - deleted_count = 0 - freed_space = 0 - for file in Path(self._output_dir).glob("*"): - if file.is_file(): - freed_space += file.stat().st_size - deleted_count += 1 - file.unlink() - return DeleteAllResult(deleted_count, freed_space) + def stop(self, invoker: "Invoker") -> None: + self._tempdir_cleanup() diff --git a/tests/test_object_serializer_disk.py b/tests/test_object_serializer_disk.py index 2bc7e16937..125534c500 100644 --- a/tests/test_object_serializer_disk.py +++ b/tests/test_object_serializer_disk.py @@ -1,12 +1,10 @@ +import tempfile from dataclasses import dataclass -from logging import Logger from pathlib import Path -from unittest.mock import Mock import pytest import torch -from invokeai.app.services.invoker import Invoker from invokeai.app.services.object_serializer.object_serializer_common import ObjectNotFoundError from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache @@ -31,11 +29,6 @@ def fwd_cache(tmp_path: Path): return ObjectSerializerForwardCache(ObjectSerializerDisk[MockDataclass](tmp_path), max_cache_size=2) -@pytest.fixture -def mock_invoker_with_logger(): - return Mock(Invoker, services=Mock(logger=Mock(Logger))) - - def test_obj_serializer_disk_initializes(tmp_path: Path): obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path) assert obj_serializer._output_dir == tmp_path @@ -76,39 +69,33 @@ def test_obj_serializer_disk_deletes(obj_serializer: ObjectSerializerDisk[MockDa assert Path(obj_serializer._output_dir, obj_2_name).exists() -def test_obj_serializer_disk_deletes_all(obj_serializer: ObjectSerializerDisk[MockDataclass]): +def test_obj_serializer_ephemeral_creates_tempdir(tmp_path: Path): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path, ephemeral=True) + assert isinstance(obj_serializer._tempdir, tempfile.TemporaryDirectory) + assert obj_serializer._base_output_dir == tmp_path + assert obj_serializer._output_dir != tmp_path + assert obj_serializer._output_dir == Path(obj_serializer._tempdir.name) + + +def test_obj_serializer_ephemeral_deletes_tempdir(tmp_path: Path): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path, ephemeral=True) + tempdir_path = obj_serializer._output_dir + del obj_serializer + assert not tempdir_path.exists() + + +def test_obj_serializer_ephemeral_deletes_tempdir_on_stop(tmp_path: Path): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path, ephemeral=True) + tempdir_path = obj_serializer._output_dir + obj_serializer.stop(None) # pyright: ignore [reportArgumentType] + assert not tempdir_path.exists() + + +def test_obj_serializer_ephemeral_writes_to_tempdir(tmp_path: Path): + obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path, ephemeral=True) obj_1 = MockDataclass(foo="bar") obj_1_name = obj_serializer.save(obj_1) - - obj_2 = MockDataclass(foo="bar") - obj_2_name = obj_serializer.save(obj_2) - - delete_all_result = obj_serializer._delete_all() - - assert not Path(obj_serializer._output_dir, obj_1_name).exists() - assert not Path(obj_serializer._output_dir, obj_2_name).exists() - assert delete_all_result.deleted_count == 2 - - -def test_obj_serializer_disk_default_no_delete_on_startup(tmp_path: Path, mock_invoker_with_logger: Invoker): - obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path) - assert obj_serializer._delete_on_startup is False - - obj_1 = MockDataclass(foo="bar") - obj_1_name = obj_serializer.save(obj_1) - - obj_serializer.start(mock_invoker_with_logger) - assert Path(tmp_path, obj_1_name).exists() - - -def test_obj_serializer_disk_delete_on_startup(tmp_path: Path, mock_invoker_with_logger: Invoker): - obj_serializer = ObjectSerializerDisk[MockDataclass](tmp_path, delete_on_startup=True) - assert obj_serializer._delete_on_startup is True - - obj_1 = MockDataclass(foo="bar") - obj_1_name = obj_serializer.save(obj_1) - - obj_serializer.start(mock_invoker_with_logger) + assert Path(obj_serializer._output_dir, obj_1_name).exists() assert not Path(tmp_path, obj_1_name).exists() From e5d8921cf276ea977fa5fe822f40113e01ff88ac Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 08:52:07 +1100 Subject: [PATCH 098/411] feat(nodes): extract LATENT_SCALE_FACTOR to constants.py --- invokeai/app/invocations/constants.py | 7 +++++++ invokeai/app/invocations/latent.py | 7 +------ invokeai/backend/tiles/tiles.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 invokeai/app/invocations/constants.py diff --git a/invokeai/app/invocations/constants.py b/invokeai/app/invocations/constants.py new file mode 100644 index 0000000000..95b16f0d05 --- /dev/null +++ b/invokeai/app/invocations/constants.py @@ -0,0 +1,7 @@ +LATENT_SCALE_FACTOR = 8 +""" +HACK: Many nodes are currently hard-coded to use a fixed latent scale factor of 8. This is fragile, and will need to +be addressed if future models use a different latent scale factor. Also, note that there may be places where the scale +factor is hard-coded to a literal '8' rather than using this constant. +The ratio of image:latent dimensions is LATENT_SCALE_FACTOR:1, or 8:1. +""" diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 4137ab6e2f..fedfc38402 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -23,6 +23,7 @@ from diffusers.schedulers import SchedulerMixin as Scheduler from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR from invokeai.app.invocations.fields import ( ConditioningField, DenoiseMaskField, @@ -79,12 +80,6 @@ DEFAULT_PRECISION = choose_precision(choose_torch_device()) SAMPLER_NAME_VALUES = Literal[tuple(SCHEDULER_MAP.keys())] -# HACK: Many nodes are currently hard-coded to use a fixed latent scale factor of 8. This is fragile, and will need to -# be addressed if future models use a different latent scale factor. Also, note that there may be places where the scale -# factor is hard-coded to a literal '8' rather than using this constant. -# The ratio of image:latent dimensions is LATENT_SCALE_FACTOR:1, or 8:1. -LATENT_SCALE_FACTOR = 8 - @invocation_output("scheduler_output") class SchedulerOutput(BaseInvocationOutput): diff --git a/invokeai/backend/tiles/tiles.py b/invokeai/backend/tiles/tiles.py index 3c400fc87c..2757dadba2 100644 --- a/invokeai/backend/tiles/tiles.py +++ b/invokeai/backend/tiles/tiles.py @@ -3,7 +3,7 @@ from typing import Union import numpy as np -from invokeai.app.invocations.latent import LATENT_SCALE_FACTOR +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR from invokeai.backend.tiles.utils import TBLR, Tile, paste, seam_blend From e0694a28563c1ef483984f210ebc6cf7b12feedd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 08:54:01 +1100 Subject: [PATCH 099/411] feat(nodes): use LATENT_SCALE_FACTOR in primitives.py, noise.py - LatentsOutput.build - NoiseOutput.build - Noise.width, Noise.height multiple_of --- invokeai/app/invocations/noise.py | 9 +++++---- invokeai/app/invocations/primitives.py | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py index 74b3d6e4cb..335d3df292 100644 --- a/invokeai/app/invocations/noise.py +++ b/invokeai/app/invocations/noise.py @@ -4,6 +4,7 @@ import torch from pydantic import field_validator +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR from invokeai.app.invocations.fields import FieldDescriptions, InputField, LatentsField, OutputField from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.misc import SEED_MAX @@ -70,8 +71,8 @@ class NoiseOutput(BaseInvocationOutput): def build(cls, latents_name: str, latents: torch.Tensor, seed: int) -> "NoiseOutput": return cls( noise=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * 8, - height=latents.size()[2] * 8, + width=latents.size()[3] * LATENT_SCALE_FACTOR, + height=latents.size()[2] * LATENT_SCALE_FACTOR, ) @@ -93,13 +94,13 @@ class NoiseInvocation(BaseInvocation): ) width: int = InputField( default=512, - multiple_of=8, + multiple_of=LATENT_SCALE_FACTOR, gt=0, description=FieldDescriptions.width, ) height: int = InputField( default=512, - multiple_of=8, + multiple_of=LATENT_SCALE_FACTOR, gt=0, description=FieldDescriptions.height, ) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index d0f95c92d0..4342213482 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -4,6 +4,7 @@ from typing import Optional import torch +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR from invokeai.app.invocations.fields import ( ColorField, ConditioningField, @@ -321,8 +322,8 @@ class LatentsOutput(BaseInvocationOutput): def build(cls, latents_name: str, latents: torch.Tensor, seed: Optional[int] = None) -> "LatentsOutput": return cls( latents=LatentsField(latents_name=latents_name, seed=seed), - width=latents.size()[3] * 8, - height=latents.size()[2] * 8, + width=latents.size()[3] * LATENT_SCALE_FACTOR, + height=latents.size()[2] * LATENT_SCALE_FACTOR, ) From 0f8af643d153b1f2d13c6d6c888e6fc83664fdd8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 09:27:57 +1100 Subject: [PATCH 100/411] chore(backend): rename `ModelInfo` -> `LoadedModelInfo` We have two different classes named `ModelInfo` which might need to be used by API consumers. We need to export both but have to deal with this naming collision. The `ModelInfo` I've renamed here is the one that is returned when a model is loaded. It's the object least likely to be used by API consumers. --- invokeai/app/services/events/events_base.py | 10 +++++----- .../services/model_manager/model_manager_base.py | 4 ++-- .../model_manager/model_manager_default.py | 16 ++++++++-------- .../app/services/shared/invocation_context.py | 7 ++++--- invokeai/backend/__init__.py | 9 ++++++++- invokeai/backend/model_management/__init__.py | 2 +- .../backend/model_management/model_manager.py | 6 +++--- invokeai/backend/util/test_utils.py | 10 +++++----- invokeai/invocation_api/__init__.py | 4 ++-- 9 files changed, 38 insertions(+), 30 deletions(-) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index ad08ae0395..6b441efc2b 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -11,7 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( SessionQueueStatus, ) from invokeai.app.util.misc import get_timestamp -from invokeai.backend.model_management.model_manager import ModelInfo +from invokeai.backend.model_management.model_manager import LoadedModelInfo from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType @@ -201,7 +201,7 @@ class EventServiceBase: base_model: BaseModelType, model_type: ModelType, submodel: SubModelType, - model_info: ModelInfo, + loaded_model_info: LoadedModelInfo, ) -> None: """Emitted when a model is correctly loaded (returns model info)""" self.__emit_queue_event( @@ -215,9 +215,9 @@ class EventServiceBase: "base_model": base_model, "model_type": model_type, "submodel": submodel, - "hash": model_info.hash, - "location": str(model_info.location), - "precision": str(model_info.precision), + "hash": loaded_model_info.hash, + "location": str(loaded_model_info.location), + "precision": str(loaded_model_info.precision), }, ) diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index a9b53ae224..f888c0ec97 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -14,8 +14,8 @@ from invokeai.app.services.shared.invocation_context import InvocationContextDat from invokeai.backend.model_management import ( AddModelResult, BaseModelType, + LoadedModelInfo, MergeInterpolationMethod, - ModelInfo, ModelType, SchedulerPredictionType, SubModelType, @@ -48,7 +48,7 @@ class ModelManagerServiceBase(ABC): model_type: ModelType, submodel: Optional[SubModelType] = None, context_data: Optional[InvocationContextData] = None, - ) -> ModelInfo: + ) -> LoadedModelInfo: """Retrieve the indicated model with name and type. submodel can be used to get a part (such as the vae) of a diffusers pipeline.""" diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index b641dd3f1e..c3712abf8e 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -16,8 +16,8 @@ from invokeai.app.services.shared.invocation_context import InvocationContextDat from invokeai.backend.model_management import ( AddModelResult, BaseModelType, + LoadedModelInfo, MergeInterpolationMethod, - ModelInfo, ModelManager, ModelMerger, ModelNotFoundException, @@ -98,7 +98,7 @@ class ModelManagerService(ModelManagerServiceBase): model_type: ModelType, submodel: Optional[SubModelType] = None, context_data: Optional[InvocationContextData] = None, - ) -> ModelInfo: + ) -> LoadedModelInfo: """ Retrieve the indicated model. submodel can be used to get a part (such as the vae) of a diffusers mode. @@ -114,7 +114,7 @@ class ModelManagerService(ModelManagerServiceBase): submodel=submodel, ) - model_info = self.mgr.get_model( + loaded_model_info = self.mgr.get_model( model_name, base_model, model_type, @@ -128,10 +128,10 @@ class ModelManagerService(ModelManagerServiceBase): base_model=base_model, model_type=model_type, submodel=submodel, - model_info=model_info, + loaded_model_info=loaded_model_info, ) - return model_info + return loaded_model_info def model_exists( self, @@ -273,7 +273,7 @@ class ModelManagerService(ModelManagerServiceBase): base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None, - model_info: Optional[ModelInfo] = None, + loaded_model_info: Optional[LoadedModelInfo] = None, ): if self._invoker is None: return @@ -281,7 +281,7 @@ class ModelManagerService(ModelManagerServiceBase): if self._invoker.services.queue.is_canceled(context_data.session_id): raise CanceledException() - if model_info: + if loaded_model_info: self._invoker.services.events.emit_model_load_completed( queue_id=context_data.queue_id, queue_item_id=context_data.queue_item_id, @@ -291,7 +291,7 @@ class ModelManagerService(ModelManagerServiceBase): base_model=base_model, model_type=model_type, submodel=submodel, - model_info=model_info, + loaded_model_info=loaded_model_info, ) else: self._invoker.services.events.emit_model_load_started( diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 1ca44b7862..68fb78c143 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -13,7 +13,7 @@ from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.step_callback import stable_diffusion_step_callback -from invokeai.backend.model_management.model_manager import ModelInfo +from invokeai.backend.model_management.model_manager import LoadedModelInfo from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData @@ -272,14 +272,15 @@ class ModelsInterface(InvocationContextInterface): def load( self, model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None - ) -> ModelInfo: + ) -> LoadedModelInfo: """ - Loads a model, returning its `ModelInfo` object. + Loads a model. :param model_name: The name of the model to get. :param base_model: The base model of the model to get. :param model_type: The type of the model to get. :param submodel: The submodel of the model to get. + :returns: An object representing the loaded model. """ # The model manager emits events as it loads the model. It needs the context data to build diff --git a/invokeai/backend/__init__.py b/invokeai/backend/__init__.py index ae9a12edbe..54a1843d46 100644 --- a/invokeai/backend/__init__.py +++ b/invokeai/backend/__init__.py @@ -1,5 +1,12 @@ """ Initialization file for invokeai.backend """ -from .model_management import BaseModelType, ModelCache, ModelInfo, ModelManager, ModelType, SubModelType # noqa: F401 +from .model_management import ( # noqa: F401 + BaseModelType, + LoadedModelInfo, + ModelCache, + ModelManager, + ModelType, + SubModelType, +) from .model_management.models import SilenceWarnings # noqa: F401 diff --git a/invokeai/backend/model_management/__init__.py b/invokeai/backend/model_management/__init__.py index 03abf58eb4..d523a7a0c8 100644 --- a/invokeai/backend/model_management/__init__.py +++ b/invokeai/backend/model_management/__init__.py @@ -3,7 +3,7 @@ Initialization file for invokeai.backend.model_management """ # This import must be first -from .model_manager import AddModelResult, ModelInfo, ModelManager, SchedulerPredictionType +from .model_manager import AddModelResult, LoadedModelInfo, ModelManager, SchedulerPredictionType from .lora import ModelPatcher, ONNXModelPatcher from .model_cache import ModelCache diff --git a/invokeai/backend/model_management/model_manager.py b/invokeai/backend/model_management/model_manager.py index 362d8d3ff5..da74ca3fb5 100644 --- a/invokeai/backend/model_management/model_manager.py +++ b/invokeai/backend/model_management/model_manager.py @@ -271,7 +271,7 @@ CONFIG_FILE_VERSION = "3.0.0" @dataclass -class ModelInfo: +class LoadedModelInfo: context: ModelLocker name: str base_model: BaseModelType @@ -450,7 +450,7 @@ class ModelManager(object): base_model: BaseModelType, model_type: ModelType, submodel_type: Optional[SubModelType] = None, - ) -> ModelInfo: + ) -> LoadedModelInfo: """Given a model named identified in models.yaml, return an ModelInfo object describing it. :param model_name: symbolic name of the model in models.yaml @@ -508,7 +508,7 @@ class ModelManager(object): model_hash = "" # TODO: - return ModelInfo( + return LoadedModelInfo( context=model_context, name=model_name, base_model=base_model, diff --git a/invokeai/backend/util/test_utils.py b/invokeai/backend/util/test_utils.py index 09b9de9e98..685603cedc 100644 --- a/invokeai/backend/util/test_utils.py +++ b/invokeai/backend/util/test_utils.py @@ -7,7 +7,7 @@ import torch from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.backend.install.model_install_backend import ModelInstall -from invokeai.backend.model_management.model_manager import ModelInfo +from invokeai.backend.model_management.model_manager import LoadedModelInfo from invokeai.backend.model_management.models.base import BaseModelType, ModelNotFoundException, ModelType, SubModelType @@ -34,8 +34,8 @@ def install_and_load_model( base_model: BaseModelType, model_type: ModelType, submodel_type: Optional[SubModelType] = None, -) -> ModelInfo: - """Install a model if it is not already installed, then get the ModelInfo for that model. +) -> LoadedModelInfo: + """Install a model if it is not already installed, then get the LoadedModelInfo for that model. This is intended as a utility function for tests. @@ -49,9 +49,9 @@ def install_and_load_model( submodel_type (Optional[SubModelType]): The submodel type, forwarded to ModelManager.get_model(...). Returns: - ModelInfo + LoadedModelInfo """ - # If the requested model is already installed, return its ModelInfo. + # If the requested model is already installed, return its LoadedModelInfo. with contextlib.suppress(ModelNotFoundException): return model_installer.mgr.get_model(model_name, base_model, model_type, submodel_type) diff --git a/invokeai/invocation_api/__init__.py b/invokeai/invocation_api/__init__.py index e80bc26a00..2d3ceca11e 100644 --- a/invokeai/invocation_api/__init__.py +++ b/invokeai/invocation_api/__init__.py @@ -52,7 +52,7 @@ from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID -from invokeai.backend.model_management.model_manager import ModelInfo +from invokeai.backend.model_management.model_manager import LoadedModelInfo from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( @@ -121,7 +121,7 @@ __all__ = [ # invokeai.app.services.config.config_default "InvokeAIAppConfig", # invokeai.backend.model_management.model_manager - "ModelInfo", + "LoadedModelInfo", # invokeai.backend.model_management.models.base "BaseModelType", "ModelType", From 6d31bc53269c53506f05341a5c014a6d78681d8f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 09:39:36 +1100 Subject: [PATCH 101/411] chore(nodes): export model-related objects from invocation_api --- invokeai/invocation_api/__init__.py | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/invokeai/invocation_api/__init__.py b/invokeai/invocation_api/__init__.py index 2d3ceca11e..055dd12757 100644 --- a/invokeai/invocation_api/__init__.py +++ b/invokeai/invocation_api/__init__.py @@ -28,6 +28,22 @@ from invokeai.app.invocations.fields import ( WithMetadata, WithWorkflow, ) +from invokeai.app.invocations.model import ( + ClipField, + CLIPOutput, + LoraInfo, + LoraLoaderOutput, + LoRAModelField, + MainModelField, + ModelInfo, + ModelLoaderOutput, + SDXLLoraLoaderOutput, + UNetField, + UNetOutput, + VaeField, + VAEModelField, + VAEOutput, +) from invokeai.app.invocations.primitives import ( BooleanCollectionOutput, BooleanOutput, @@ -87,6 +103,21 @@ __all__ = [ "UIType", "WithMetadata", "WithWorkflow", + # invokeai.app.invocations.model + "ModelInfo", + "LoraInfo", + "UNetField", + "ClipField", + "VaeField", + "MainModelField", + "LoRAModelField", + "VAEModelField", + "UNetOutput", + "VAEOutput", + "CLIPOutput", + "ModelLoaderOutput", + "LoraLoaderOutput", + "SDXLLoraLoaderOutput", # invokeai.app.invocations.primitives "BooleanCollectionOutput", "BooleanOutput", From b845e890d151b4f7652d9131beb82870ff837605 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 09:43:36 +1100 Subject: [PATCH 102/411] chore(nodes): remove deprecation logic for nodes API --- .../app/services/shared/invocation_context.py | 112 ------------------ 1 file changed, 112 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 68fb78c143..c68dc1140b 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Optional -from deprecated import deprecated from PIL.Image import Image from torch import Tensor @@ -334,30 +333,6 @@ class UtilInterface(InvocationContextInterface): ) -deprecation_version = "3.7.0" -removed_version = "3.8.0" - - -def get_deprecation_reason(property_name: str, alternative: Optional[str] = None) -> str: - msg = f"{property_name} is deprecated as of v{deprecation_version}. It will be removed in v{removed_version}." - if alternative is not None: - msg += f" Use {alternative} instead." - msg += " See PLACEHOLDER_URL for details." - return msg - - -# Deprecation docstrings template. I don't think we can implement these programmatically with -# __doc__ because the IDE won't see them. - -""" -**DEPRECATED as of v3.7.0** - -PROPERTY_NAME will be removed in v3.8.0. Use ALTERNATIVE instead. See PLACEHOLDER_URL for details. - -OG_DOCSTRING -""" - - class InvocationContext: """ The `InvocationContext` provides access to various services and data for the current invocation. @@ -397,93 +372,6 @@ class InvocationContext: self._services = services """Provides access to the full application services. This is an internal API and may change without warning.""" - @property - @deprecated(version=deprecation_version, reason=get_deprecation_reason("`context.services`")) - def services(self) -> InvocationServices: - """ - **DEPRECATED as of v3.7.0** - - `context.services` will be removed in v3.8.0. See PLACEHOLDER_URL for details. - - The invocation services. - """ - return self._services - - @property - @deprecated( - version=deprecation_version, - reason=get_deprecation_reason("`context.graph_execution_state_id", "`context._data.session_id`"), - ) - def graph_execution_state_id(self) -> str: - """ - **DEPRECATED as of v3.7.0** - - `context.graph_execution_state_api` will be removed in v3.8.0. Use `context._data.session_id` instead. See PLACEHOLDER_URL for details. - - The ID of the session (aka graph execution state). - """ - return self._data.session_id - - @property - @deprecated( - version=deprecation_version, - reason=get_deprecation_reason("`context.queue_id`", "`context._data.queue_id`"), - ) - def queue_id(self) -> str: - """ - **DEPRECATED as of v3.7.0** - - `context.queue_id` will be removed in v3.8.0. Use `context._data.queue_id` instead. See PLACEHOLDER_URL for details. - - The ID of the queue. - """ - return self._data.queue_id - - @property - @deprecated( - version=deprecation_version, - reason=get_deprecation_reason("`context.queue_item_id`", "`context._data.queue_item_id`"), - ) - def queue_item_id(self) -> int: - """ - **DEPRECATED as of v3.7.0** - - `context.queue_item_id` will be removed in v3.8.0. Use `context._data.queue_item_id` instead. See PLACEHOLDER_URL for details. - - The ID of the queue item. - """ - return self._data.queue_item_id - - @property - @deprecated( - version=deprecation_version, - reason=get_deprecation_reason("`context.queue_batch_id`", "`context._data.batch_id`"), - ) - def queue_batch_id(self) -> str: - """ - **DEPRECATED as of v3.7.0** - - `context.queue_batch_id` will be removed in v3.8.0. Use `context._data.batch_id` instead. See PLACEHOLDER_URL for details. - - The ID of the batch. - """ - return self._data.batch_id - - @property - @deprecated( - version=deprecation_version, - reason=get_deprecation_reason("`context.workflow`", "`context._data.workflow`"), - ) - def workflow(self) -> Optional[WorkflowWithoutID]: - """ - **DEPRECATED as of v3.7.0** - - `context.workflow` will be removed in v3.8.0. Use `context._data.workflow` instead. See PLACEHOLDER_URL for details. - - The workflow associated with this queue item, if any. - """ - return self._data.workflow - def build_invocation_context( services: InvocationServices, From 25f64d5b19bbc18c185e7446b48fb40ae4ffce2a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 09:51:25 +1100 Subject: [PATCH 103/411] chore(nodes): "SAMPLER_NAME_VALUES" -> "SCHEDULER_NAME_VALUES" This was named inaccurately. --- invokeai/app/invocations/constants.py | 7 +++++++ invokeai/app/invocations/latent.py | 10 ++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/invokeai/app/invocations/constants.py b/invokeai/app/invocations/constants.py index 95b16f0d05..795e7a3b60 100644 --- a/invokeai/app/invocations/constants.py +++ b/invokeai/app/invocations/constants.py @@ -1,3 +1,7 @@ +from typing import Literal + +from invokeai.backend.stable_diffusion.schedulers import SCHEDULER_MAP + LATENT_SCALE_FACTOR = 8 """ HACK: Many nodes are currently hard-coded to use a fixed latent scale factor of 8. This is fragile, and will need to @@ -5,3 +9,6 @@ be addressed if future models use a different latent scale factor. Also, note th factor is hard-coded to a literal '8' rather than using this constant. The ratio of image:latent dimensions is LATENT_SCALE_FACTOR:1, or 8:1. """ + +SCHEDULER_NAME_VALUES = Literal[tuple(SCHEDULER_MAP.keys())] +"""A literal type representing the valid scheduler names.""" diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index fedfc38402..69e3f055ca 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -23,7 +23,7 @@ from diffusers.schedulers import SchedulerMixin as Scheduler from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize -from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR +from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR, SCHEDULER_NAME_VALUES from invokeai.app.invocations.fields import ( ConditioningField, DenoiseMaskField, @@ -78,12 +78,10 @@ if choose_torch_device() == torch.device("mps"): DEFAULT_PRECISION = choose_precision(choose_torch_device()) -SAMPLER_NAME_VALUES = Literal[tuple(SCHEDULER_MAP.keys())] - @invocation_output("scheduler_output") class SchedulerOutput(BaseInvocationOutput): - scheduler: SAMPLER_NAME_VALUES = OutputField(description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler) + scheduler: SCHEDULER_NAME_VALUES = OutputField(description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler) @invocation( @@ -96,7 +94,7 @@ class SchedulerOutput(BaseInvocationOutput): class SchedulerInvocation(BaseInvocation): """Selects a scheduler.""" - scheduler: SAMPLER_NAME_VALUES = InputField( + scheduler: SCHEDULER_NAME_VALUES = InputField( default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler, @@ -234,7 +232,7 @@ class DenoiseLatentsInvocation(BaseInvocation): description=FieldDescriptions.denoising_start, ) denoising_end: float = InputField(default=1.0, ge=0, le=1, description=FieldDescriptions.denoising_end) - scheduler: SAMPLER_NAME_VALUES = InputField( + scheduler: SCHEDULER_NAME_VALUES = InputField( default="euler", description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler, From 7a2159beebb25c861f0f499e6a2db5ee2231b27e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 10:06:53 +1100 Subject: [PATCH 104/411] feat(nodes): add more missing exports to invocation_api Crawled through a few custom nodes to figure out what I had missed. --- invokeai/invocation_api/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/invokeai/invocation_api/__init__.py b/invokeai/invocation_api/__init__.py index 055dd12757..e110b5a2db 100644 --- a/invokeai/invocation_api/__init__.py +++ b/invokeai/invocation_api/__init__.py @@ -7,9 +7,11 @@ TODO(psyche): Do we want to dogfood this? from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, + Classification, invocation, invocation_output, ) +from invokeai.app.invocations.constants import SCHEDULER_NAME_VALUES from invokeai.app.invocations.fields import ( BoardField, ColorField, @@ -28,6 +30,8 @@ from invokeai.app.invocations.fields import ( WithMetadata, WithWorkflow, ) +from invokeai.app.invocations.latent import SchedulerOutput +from invokeai.app.invocations.metadata import MetadataItemField, MetadataItemOutput, MetadataOutput from invokeai.app.invocations.model import ( ClipField, CLIPOutput, @@ -68,6 +72,7 @@ from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID +from invokeai.app.util.misc import SEED_MAX, get_random_seed from invokeai.backend.model_management.model_manager import LoadedModelInfo from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState @@ -77,11 +82,14 @@ from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( ExtraConditioningInfo, SDXLConditioningInfo, ) +from invokeai.backend.util.devices import CPU_DEVICE, CUDA_DEVICE, MPS_DEVICE, choose_precision, choose_torch_device +from invokeai.version import __version__ __all__ = [ # invokeai.app.invocations.baseinvocation "BaseInvocation", "BaseInvocationOutput", + "Classification", "invocation", "invocation_output", # invokeai.app.services.shared.invocation_context @@ -103,6 +111,12 @@ __all__ = [ "UIType", "WithMetadata", "WithWorkflow", + # invokeai.app.invocations.latent + "SchedulerOutput", + # invokeai.app.invocations.metadata + "MetadataItemField", + "MetadataItemOutput", + "MetadataOutput", # invokeai.app.invocations.model "ModelInfo", "LoraInfo", @@ -157,4 +171,17 @@ __all__ = [ "BaseModelType", "ModelType", "SubModelType", + # invokeai.app.invocations.constants + "SCHEDULER_NAME_VALUES", + # invokeai.version + "__version__", + # invokeai.backend.util.devices + "choose_precision", + "choose_torch_device", + "CPU_DEVICE", + "CUDA_DEVICE", + "MPS_DEVICE", + # invokeai.app.util.misc + "SEED_MAX", + "get_random_seed", ] From 5fbfed30acf1da1a2870730de9d9d64799c6bfdd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 13 Feb 2024 15:02:30 +1100 Subject: [PATCH 105/411] chore(ui): regen types --- .../frontend/web/src/services/api/schema.ts | 1802 +---------------- 1 file changed, 22 insertions(+), 1780 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 45358ed97d..1599b310c9 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -680,70 +680,6 @@ export type components = { */ type: "add"; }; - /** - * Adjust Image Hue Plus - * @description Adjusts the Hue of an image by rotating it in the selected color space - */ - AdjustImageHuePlusInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image to adjust */ - image?: components["schemas"]["ImageField"]; - /** - * Space - * @description Color space in which to rotate hue by polar coords (*: non-invertible) - * @default Okhsl - * @enum {string} - */ - space?: "HSV / HSL / RGB" | "Okhsl" | "Okhsv" | "*Oklch / Oklab" | "*LCh / CIELab" | "*UPLab (w/CIELab_to_UPLab.icc)"; - /** - * Degrees - * @description Degrees by which to rotate image hue - * @default 0 - */ - degrees?: number; - /** - * Preserve Lightness - * @description Whether to preserve CIELAB lightness values - * @default false - */ - preserve_lightness?: boolean; - /** - * Ok Adaptive Gamut - * @description Higher preserves chroma at the expense of lightness (Oklab) - * @default 0.05 - */ - ok_adaptive_gamut?: number; - /** - * Ok High Precision - * @description Use more steps in computing gamut (Oklab/Okhsv/Okhsl) - * @default true - */ - ok_high_precision?: boolean; - /** - * type - * @default img_hue_adjust_plus - * @constant - */ - type: "img_hue_adjust_plus"; - }; /** * AppConfig * @description App Config Response @@ -1454,39 +1390,6 @@ export type components = { */ type: "boolean_output"; }; - /** - * BRIA AI Background Removal - * @description Uses the new Bria 1.4 model to remove backgrounds from images. - */ - BriaRemoveBackgroundInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image to crop */ - image?: components["schemas"]["ImageField"]; - /** - * type - * @default bria_bg_remove - * @constant - */ - type: "bria_bg_remove"; - }; /** * CLIPOutput * @description Base class for invocations that output a CLIP field @@ -1581,282 +1484,6 @@ export type components = { /** @description Base model (usually 'Any') */ base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; }; - /** - * CMYK Color Separation - * @description Get color images from a base color and two others that subtractively mix to obtain it - */ - CMYKColorSeparationInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** - * Width - * @description Desired image width - * @default 512 - */ - width?: number; - /** - * Height - * @description Desired image height - * @default 512 - */ - height?: number; - /** - * C Value - * @description Desired final cyan value - * @default 0 - */ - c_value?: number; - /** - * M Value - * @description Desired final magenta value - * @default 25 - */ - m_value?: number; - /** - * Y Value - * @description Desired final yellow value - * @default 28 - */ - y_value?: number; - /** - * K Value - * @description Desired final black value - * @default 76 - */ - k_value?: number; - /** - * C Split - * @description Desired cyan split point % [0..1.0] - * @default 0.5 - */ - c_split?: number; - /** - * M Split - * @description Desired magenta split point % [0..1.0] - * @default 1 - */ - m_split?: number; - /** - * Y Split - * @description Desired yellow split point % [0..1.0] - * @default 0 - */ - y_split?: number; - /** - * K Split - * @description Desired black split point % [0..1.0] - * @default 0.5 - */ - k_split?: number; - /** - * Profile - * @description CMYK Color Profile - * @default Default - * @enum {string} - */ - profile?: "Default" | "PIL"; - /** - * type - * @default cmyk_separation - * @constant - */ - type: "cmyk_separation"; - }; - /** - * CMYK Merge - * @description Merge subtractive color channels (CMYK+alpha) - */ - CMYKMergeInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The c channel */ - c_channel?: components["schemas"]["ImageField"] | null; - /** @description The m channel */ - m_channel?: components["schemas"]["ImageField"] | null; - /** @description The y channel */ - y_channel?: components["schemas"]["ImageField"] | null; - /** @description The k channel */ - k_channel?: components["schemas"]["ImageField"] | null; - /** @description The alpha channel */ - alpha_channel?: components["schemas"]["ImageField"] | null; - /** - * Profile - * @description CMYK Color Profile - * @default Default - * @enum {string} - */ - profile?: "Default" | "PIL"; - /** - * type - * @default cmyk_merge - * @constant - */ - type: "cmyk_merge"; - }; - /** - * CMYKSeparationOutput - * @description Base class for invocations that output four L-mode images (C, M, Y, K) - */ - CMYKSeparationOutput: { - /** @description Blank image of the specified color */ - color_image: components["schemas"]["ImageField"]; - /** - * Width - * @description The width of the image in pixels - */ - width: number; - /** - * Height - * @description The height of the image in pixels - */ - height: number; - /** @description Blank image of the first separated color */ - part_a: components["schemas"]["ImageField"]; - /** - * Rgb Red A - * @description R value of color part A - */ - rgb_red_a: number; - /** - * Rgb Green A - * @description G value of color part A - */ - rgb_green_a: number; - /** - * Rgb Blue A - * @description B value of color part A - */ - rgb_blue_a: number; - /** @description Blank image of the second separated color */ - part_b: components["schemas"]["ImageField"]; - /** - * Rgb Red B - * @description R value of color part B - */ - rgb_red_b: number; - /** - * Rgb Green B - * @description G value of color part B - */ - rgb_green_b: number; - /** - * Rgb Blue B - * @description B value of color part B - */ - rgb_blue_b: number; - /** - * type - * @default cmyk_separation_output - * @constant - */ - type: "cmyk_separation_output"; - }; - /** - * CMYK Split - * @description Split an image into subtractive color channels (CMYK+alpha) - */ - CMYKSplitInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image to halftone */ - image?: components["schemas"]["ImageField"]; - /** - * Profile - * @description CMYK Color Profile - * @default Default - * @enum {string} - */ - profile?: "Default" | "PIL"; - /** - * type - * @default cmyk_split - * @constant - */ - type: "cmyk_split"; - }; - /** - * CMYKSplitOutput - * @description Base class for invocations that output four L-mode images (C, M, Y, K) - */ - CMYKSplitOutput: { - /** @description Grayscale image of the cyan channel */ - c_channel: components["schemas"]["ImageField"]; - /** @description Grayscale image of the magenta channel */ - m_channel: components["schemas"]["ImageField"]; - /** @description Grayscale image of the yellow channel */ - y_channel: components["schemas"]["ImageField"]; - /** @description Grayscale image of the k channel */ - k_channel: components["schemas"]["ImageField"]; - /** @description Grayscale image of the alpha channel */ - alpha_channel: components["schemas"]["ImageField"]; - /** - * Width - * @description The width of the image in pixels - */ - width: number; - /** - * Height - * @description The height of the image in pixels - */ - height: number; - /** - * type - * @default cmyk_split_output - * @constant - */ - type: "cmyk_split_output"; - }; /** * CV2 Infill * @description Infills transparent areas of an image using OpenCV Inpainting @@ -3491,6 +3118,8 @@ export type components = { * @description Generates an openpose pose from an image using DWPose */ DWOpenposeImageProcessorInvocation: { + /** @description The board to save the image to */ + board?: components["schemas"]["BoardField"] | null; /** @description Optional metadata to be saved with the image */ metadata?: components["schemas"]["MetadataField"] | null; /** @@ -4014,39 +3643,6 @@ export type components = { */ priority: number; }; - /** - * Equivalent Achromatic Lightness - * @description Calculate Equivalent Achromatic Lightness from image - */ - EquivalentAchromaticLightnessInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Image from which to get channel */ - image?: components["schemas"]["ImageField"]; - /** - * type - * @default ealightness - * @constant - */ - type: "ealightness"; - }; /** ExposedField */ ExposedField: { /** Nodeid */ @@ -4301,39 +3897,6 @@ export type components = { */ y: number; }; - /** - * Flatten Histogram (Grayscale) - * @description Scales the values of an L-mode image by scaling them to the full range 0..255 in equal proportions - */ - FlattenHistogramMono: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Single-channel image for which to flatten the histogram */ - image?: components["schemas"]["ImageField"]; - /** - * type - * @default flatten_histogram_mono - * @constant - */ - type: "flatten_histogram_mono"; - }; /** * Float Collection Primitive * @description A collection of float primitive values @@ -4683,7 +4246,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["ImageDilateOrErodeInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["HandDepthMeshGraphormerProcessor"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["TextToMaskClipsegInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["LinearUIOutputInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["OffsetLatentsInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["CMYKColorSeparationInvocation"] | components["schemas"]["NoiseImage2DInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["EquivalentAchromaticLightnessInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageBlendInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["ImageRotateInvocation"] | components["schemas"]["ShadowsHighlightsMidtonesMaskInvocation"] | components["schemas"]["ONNXLatentsToImageInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["BriaRemoveBackgroundInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ImageValueThresholdsInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ONNXPromptInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["NoiseSpectralInvocation"] | components["schemas"]["TextMaskInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["MaskedBlendLatentsInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CMYKMergeInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["TextToMaskClipsegAdvancedInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageCompositorInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["FlattenHistogramMono"] | components["schemas"]["AdjustImageHuePlusInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["CMYKSplitInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["ImageEnhanceInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["LatentConsistencyInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["OnnxModelLoaderInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ImageOffsetInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ONNXTextToLatentsInvocation"] | components["schemas"]["InfillColorInvocation"]; + [key: string]: components["schemas"]["ImageCropInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["IdealSizeInvocation"]; }; /** * Edges @@ -4720,7 +4283,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CMYKSplitOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["CMYKSeparationOutput"] | components["schemas"]["String2Output"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["ONNXModelLoaderOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["HandDepthOutput"] | components["schemas"]["ShadowsHighlightsMidtonesMasksOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ColorCollectionOutput"]; + [key: string]: components["schemas"]["MetadataOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ClipSkipInvocationOutput"]; }; /** * Errors @@ -4811,83 +4374,6 @@ export type components = { /** Detail */ detail?: components["schemas"]["ValidationError"][]; }; - /** - * Hand Depth w/ MeshGraphormer - * @description Generate hand depth maps to inpaint with using ControlNet - */ - HandDepthMeshGraphormerProcessor: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image to process */ - image?: components["schemas"]["ImageField"]; - /** - * Resolution - * @description Pixel resolution for output image - * @default 512 - */ - resolution?: number; - /** - * Mask Padding - * @description Amount to pad the hand mask by - * @default 30 - */ - mask_padding?: number; - /** - * Offload - * @description Offload model after usage - * @default false - */ - offload?: boolean; - /** - * type - * @default hand_depth_mesh_graphormer_image_processor - * @constant - */ - type: "hand_depth_mesh_graphormer_image_processor"; - }; - /** - * HandDepthOutput - * @description Base class for to output Meshgraphormer results - */ - HandDepthOutput: { - /** @description Improved hands depth map */ - image: components["schemas"]["ImageField"]; - /** @description Hands area mask */ - mask: components["schemas"]["ImageField"]; - /** - * Width - * @description The width of the depth map in pixels - */ - width: number; - /** - * Height - * @description The height of the depth map in pixels - */ - height: number; - /** - * type - * @default meshgraphormer_output - * @constant - */ - type: "meshgraphormer_output"; - }; /** * HED (softedge) Processor * @description Applies HED edge detection to image @@ -5260,87 +4746,6 @@ export type components = { */ type: "ideal_size_output"; }; - /** - * Image Layer Blend - * @description Blend two images together, with optional opacity, mask, and blend modes - */ - ImageBlendInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The top image to blend */ - layer_upper?: components["schemas"]["ImageField"]; - /** - * Blend Mode - * @description Available blend modes - * @default Normal - * @enum {string} - */ - blend_mode?: "Normal" | "Lighten Only" | "Darken Only" | "Lighten Only (EAL)" | "Darken Only (EAL)" | "Hue" | "Saturation" | "Color" | "Luminosity" | "Linear Dodge (Add)" | "Subtract" | "Multiply" | "Divide" | "Screen" | "Overlay" | "Linear Burn" | "Difference" | "Hard Light" | "Soft Light" | "Vivid Light" | "Linear Light" | "Color Burn" | "Color Dodge"; - /** - * Opacity - * @description Desired opacity of the upper layer - * @default 1 - */ - opacity?: number; - /** @description Optional mask, used to restrict areas from blending */ - mask?: components["schemas"]["ImageField"] | null; - /** - * Fit To Width - * @description Scale upper layer to fit base width - * @default false - */ - fit_to_width?: boolean; - /** - * Fit To Height - * @description Scale upper layer to fit base height - * @default true - */ - fit_to_height?: boolean; - /** @description The bottom image to blend */ - layer_base?: components["schemas"]["ImageField"]; - /** - * Color Space - * @description Available color spaces for blend computations - * @default Linear RGB - * @enum {string} - */ - color_space?: "RGB" | "Linear RGB" | "HSL (RGB)" | "HSV (RGB)" | "Okhsl" | "Okhsv" | "Oklch (Oklab)" | "LCh (CIELab)"; - /** - * Adaptive Gamut - * @description Adaptive gamut clipping (0=off). Higher prioritizes chroma over lightness - * @default 0 - */ - adaptive_gamut?: number; - /** - * High Precision - * @description Use more steps in computing gamut when possible - * @default true - */ - high_precision?: boolean; - /** - * type - * @default img_blend - * @constant - */ - type: "img_blend"; - }; /** * Blur Image * @description Blurs an image @@ -5594,77 +4999,6 @@ export type components = { */ type: "image_collection_output"; }; - /** - * Image Compositor - * @description Removes backdrop from subject image then overlays subject on background image - */ - ImageCompositorInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Image of the subject on a plain monochrome background */ - image_subject?: components["schemas"]["ImageField"]; - /** @description Image of a background scene */ - image_background?: components["schemas"]["ImageField"]; - /** - * Chroma Key - * @description Can be empty for corner flood select, or CSS-3 color or tuple - * @default - */ - chroma_key?: string; - /** - * Threshold - * @description Subject isolation flood-fill threshold - * @default 50 - */ - threshold?: number; - /** - * Fill X - * @description Scale base subject image to fit background width - * @default false - */ - fill_x?: boolean; - /** - * Fill Y - * @description Scale base subject image to fit background height - * @default true - */ - fill_y?: boolean; - /** - * X Offset - * @description x-offset for the subject - * @default 0 - */ - x_offset?: number; - /** - * Y Offset - * @description y-offset for the subject - * @default 0 - */ - y_offset?: number; - /** - * type - * @default img_composite - * @constant - */ - type: "img_composite"; - }; /** * Convert Image Mode * @description Converts an image to a different mode. @@ -5846,127 +5180,6 @@ export type components = { */ board_id?: string | null; }; - /** - * Image Dilate or Erode - * @description Dilate (expand) or erode (contract) an image - */ - ImageDilateOrErodeInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image from which to create a mask */ - image?: components["schemas"]["ImageField"]; - /** - * Lightness Only - * @description If true, only applies to image lightness (CIELa*b*) - * @default false - */ - lightness_only?: boolean; - /** - * Radius W - * @description Width (in pixels) by which to dilate(expand) or erode (contract) the image - * @default 4 - */ - radius_w?: number; - /** - * Radius H - * @description Height (in pixels) by which to dilate(expand) or erode (contract) the image - * @default 4 - */ - radius_h?: number; - /** - * Mode - * @description How to operate on the image - * @default Dilate - * @enum {string} - */ - mode?: "Dilate" | "Erode"; - /** - * type - * @default img_dilate_erode - * @constant - */ - type: "img_dilate_erode"; - }; - /** - * Enhance Image - * @description Applies processing from PIL's ImageEnhance module. - */ - ImageEnhanceInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image for which to apply processing */ - image?: components["schemas"]["ImageField"]; - /** - * Invert - * @description Whether to invert the image colors - * @default false - */ - invert?: boolean; - /** - * Color - * @description Color enhancement factor - * @default 1 - */ - color?: number; - /** - * Contrast - * @description Contrast enhancement factor - * @default 1 - */ - contrast?: number; - /** - * Brightness - * @description Brightness enhancement factor - * @default 1 - */ - brightness?: number; - /** - * Sharpness - * @description Sharpness enhancement factor - * @default 1 - */ - sharpness?: number; - /** - * type - * @default img_enhance - * @constant - */ - type: "img_enhance"; - }; /** * ImageField * @description An image primitive field @@ -6216,57 +5429,6 @@ export type components = { */ type: "img_nsfw"; }; - /** - * Offset Image - * @description Offsets an image by a given percentage (or pixel amount). - */ - ImageOffsetInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** - * As Pixels - * @description Interpret offsets as pixels rather than percentages - * @default false - */ - as_pixels?: boolean; - /** @description Image to be offset */ - image?: components["schemas"]["ImageField"]; - /** - * X Offset - * @description x-offset for the subject - * @default 0.5 - */ - x_offset?: number; - /** - * Y Offset - * @description y-offset for the subject - * @default 0.5 - */ - y_offset?: number; - /** - * type - * @default offset_image - * @constant - */ - type: "offset_image"; - }; /** * ImageOutput * @description Base class for nodes that output a single image @@ -6432,63 +5594,6 @@ export type components = { */ type: "img_resize"; }; - /** - * Rotate/Flip Image - * @description Rotates an image by a given angle (in degrees clockwise). - */ - ImageRotateInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Image to be rotated clockwise */ - image?: components["schemas"]["ImageField"]; - /** - * Degrees - * @description Angle (in degrees clockwise) by which to rotate - * @default 90 - */ - degrees?: number; - /** - * Expand To Fit - * @description If true, extends the image boundary to fit the rotated content - * @default true - */ - expand_to_fit?: boolean; - /** - * Flip Horizontal - * @description If true, flips the image horizontally - * @default false - */ - flip_horizontal?: boolean; - /** - * Flip Vertical - * @description If true, flips the image vertically - * @default false - */ - flip_vertical?: boolean; - /** - * type - * @default rotate_image - * @constant - */ - type: "rotate_image"; - }; /** * Scale Image * @description Scales an image by a factor @@ -6603,69 +5708,6 @@ export type components = { */ thumbnail_url: string; }; - /** - * Image Value Thresholds - * @description Clip image to pure black/white past specified thresholds - */ - ImageValueThresholdsInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image from which to create a mask */ - image?: components["schemas"]["ImageField"]; - /** - * Invert Output - * @description Make light areas dark and vice versa - * @default false - */ - invert_output?: boolean; - /** - * Renormalize Values - * @description Rescale remaining values from minimum to maximum - * @default false - */ - renormalize_values?: boolean; - /** - * Lightness Only - * @description If true, only applies to image lightness (CIELa*b*) - * @default false - */ - lightness_only?: boolean; - /** - * Threshold Upper - * @description Threshold above which will be set to full value - * @default 0.5 - */ - threshold_upper?: number; - /** - * Threshold Lower - * @description Threshold below which will be set to minimum value - * @default 0.5 - */ - threshold_lower?: number; - /** - * type - * @default img_val_thresholds - * @constant - */ - type: "img_val_thresholds"; - }; /** * Add Invisible Watermark * @description Add an invisible watermark to an image @@ -8025,47 +7067,6 @@ export type components = { */ type: "tomask"; }; - /** - * Blend Latents/Noise (Masked) - * @description Blend two latents using a given alpha and mask. Latents must have same size. - */ - MaskedBlendLatentsInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Latents tensor */ - latents_a?: components["schemas"]["LatentsField"]; - /** @description Latents tensor */ - latents_b?: components["schemas"]["LatentsField"]; - /** @description Mask for blending in latents B */ - mask?: components["schemas"]["ImageField"]; - /** - * Alpha - * @description Blending factor. 0.0 = use input A only, 1.0 = use input B only, 0.5 = 50% mix of input A and input B. - * @default 0.5 - */ - alpha?: number; - /** - * type - * @default lmblend - * @constant - */ - type: "lmblend"; - }; /** * Mediapipe Face Processor * @description Applies mediapipe face processing to image @@ -8678,86 +7679,6 @@ export type components = { */ value: string | number; }; - /** - * 2D Noise Image - * @description Creates an image of 2D Noise approximating the desired characteristics - */ - NoiseImage2DInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** - * Noise Type - * @description Desired noise spectral characteristics - * @default White - * @enum {string} - */ - noise_type?: "White" | "Red" | "Blue" | "Green"; - /** - * Width - * @description Desired image width - * @default 512 - */ - width?: number; - /** - * Height - * @description Desired image height - * @default 512 - */ - height?: number; - /** - * Seed - * @description Seed for noise generation - * @default 0 - */ - seed?: number; - /** - * Iterations - * @description Noise approx. iterations - * @default 15 - */ - iterations?: number; - /** - * Blur Threshold - * @description Threshold used in computing noise (lower is better/slower) - * @default 0.2 - */ - blur_threshold?: number; - /** - * Sigma Red - * @description Sigma for strong gaussian blur LPF for red/green - * @default 3 - */ - sigma_red?: number; - /** - * Sigma Blue - * @description Sigma for weak gaussian blur HPF for blue/green - * @default 1 - */ - sigma_blue?: number; - /** - * type - * @default noiseimg_2d - * @constant - */ - type: "noiseimg_2d"; - }; /** * Noise * @description Generates latent noise. @@ -8835,84 +7756,6 @@ export type components = { */ type: "noise_output"; }; - /** - * Noise (Spectral characteristics) - * @description Creates an image of 2D Noise approximating the desired characteristics - */ - NoiseSpectralInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** - * Noise Type - * @description Desired noise spectral characteristics - * @default White - * @enum {string} - */ - noise_type?: "White" | "Red" | "Blue" | "Green"; - /** - * Width - * @description Desired image width - * @default 512 - */ - width?: number; - /** - * Height - * @description Desired image height - * @default 512 - */ - height?: number; - /** - * Seed - * @description Seed for noise generation - * @default 0 - */ - seed?: number; - /** - * Iterations - * @description Noise approx. iterations - * @default 15 - */ - iterations?: number; - /** - * Blur Threshold - * @description Threshold used in computing noise (lower is better/slower) - * @default 0.2 - */ - blur_threshold?: number; - /** - * Sigma Red - * @description Sigma for strong gaussian blur LPF for red/green - * @default 3 - */ - sigma_red?: number; - /** - * Sigma Blue - * @description Sigma for weak gaussian blur HPF for blue/green - * @default 1 - */ - sigma_blue?: number; - /** - * type - * @default noise_spectral - * @constant - */ - type: "noise_spectral"; - }; /** * Normal BAE Processor * @description Applies NormalBae processing to image @@ -8960,107 +7803,6 @@ export type components = { */ type: "normalbae_image_processor"; }; - /** - * ONNX Latents to Image - * @description Generates an image from latents. - */ - ONNXLatentsToImageInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Denoised latents tensor */ - latents?: components["schemas"]["LatentsField"]; - /** @description VAE */ - vae?: components["schemas"]["VaeField"]; - /** - * type - * @default l2i_onnx - * @constant - */ - type: "l2i_onnx"; - }; - /** - * ONNXModelLoaderOutput - * @description Model loader output - */ - ONNXModelLoaderOutput: { - /** - * UNet - * @description UNet (scheduler, LoRAs) - */ - unet?: components["schemas"]["UNetField"]; - /** - * CLIP - * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count - */ - clip?: components["schemas"]["ClipField"]; - /** - * VAE Decoder - * @description VAE - */ - vae_decoder?: components["schemas"]["VaeField"]; - /** - * VAE Encoder - * @description VAE - */ - vae_encoder?: components["schemas"]["VaeField"]; - /** - * type - * @default model_loader_output_onnx - * @constant - */ - type: "model_loader_output_onnx"; - }; - /** ONNX Prompt (Raw) */ - ONNXPromptInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** - * Prompt - * @description Raw prompt text (no parsing) - * @default - */ - prompt?: string; - /** @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count */ - clip?: components["schemas"]["ClipField"]; - /** - * type - * @default prompt_onnx - * @constant - */ - type: "prompt_onnx"; - }; /** * ONNXSD1Config * @description Model config for ONNX format models based on sd-1. @@ -9242,117 +7984,6 @@ export type components = { /** Upcast Attention */ upcast_attention: boolean; }; - /** - * ONNX Text to Latents - * @description Generates latents from conditionings. - */ - ONNXTextToLatentsInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Positive conditioning tensor */ - positive_conditioning?: components["schemas"]["ConditioningField"]; - /** @description Negative conditioning tensor */ - negative_conditioning?: components["schemas"]["ConditioningField"]; - /** @description Noise tensor */ - noise?: components["schemas"]["LatentsField"]; - /** - * Steps - * @description Number of steps to run - * @default 10 - */ - steps?: number; - /** - * Cfg Scale - * @description Classifier-Free Guidance scale - * @default 7.5 - */ - cfg_scale?: number | number[]; - /** - * Scheduler - * @description Scheduler to use during inference - * @default euler - * @enum {string} - */ - 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"; - /** - * Precision - * @description Precision to use - * @default tensor(float16) - * @enum {string} - */ - precision?: "tensor(bool)" | "tensor(int8)" | "tensor(uint8)" | "tensor(int16)" | "tensor(uint16)" | "tensor(int32)" | "tensor(uint32)" | "tensor(int64)" | "tensor(uint64)" | "tensor(float16)" | "tensor(float)" | "tensor(double)"; - /** @description UNet (scheduler, LoRAs) */ - unet?: components["schemas"]["UNetField"]; - /** - * Control - * @description ControlNet(s) to apply - */ - control?: components["schemas"]["ControlField"] | components["schemas"]["ControlField"][]; - /** - * type - * @default t2l_onnx - * @constant - */ - type: "t2l_onnx"; - }; - /** - * Offset Latents - * @description Offsets a latents tensor by a given percentage of height/width. - */ - OffsetLatentsInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Latents tensor */ - latents?: components["schemas"]["LatentsField"]; - /** - * X Offset - * @description Approx percentage to offset (H) - * @default 0.5 - */ - x_offset?: number; - /** - * Y Offset - * @description Approx percentage to offset (V) - * @default 0.5 - */ - y_offset?: number; - /** - * type - * @default offset_latents - * @constant - */ - type: "offset_latents"; - }; /** OffsetPaginatedResults[BoardDTO] */ OffsetPaginatedResults_BoardDTO_: { /** @@ -9399,52 +8030,6 @@ export type components = { */ items: components["schemas"]["ImageDTO"][]; }; - /** - * OnnxModelField - * @description Onnx model field - */ - OnnxModelField: { - /** - * Model Name - * @description Name of the model - */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** @description Model Type */ - model_type: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"]; - }; - /** - * ONNX Main Model - * @description Loads a main model, outputting its submodels. - */ - OnnxModelLoaderInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description ONNX Main model (UNet, VAE, CLIP) to load */ - model: components["schemas"]["OnnxModelField"]; - /** - * type - * @default onnx_model_loader - * @constant - */ - type: "onnx_model_loader"; - }; /** PaginatedResults[ModelSummary] */ PaginatedResults_ModelSummary_: { /** @@ -10852,106 +9437,6 @@ export type components = { */ total: number; }; - /** - * Shadows/Highlights/Midtones - * @description Extract a Shadows/Highlights/Midtones mask from an image - */ - ShadowsHighlightsMidtonesMaskInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description Image from which to extract mask */ - image?: components["schemas"]["ImageField"]; - /** - * Invert Output - * @description Off: white on black / On: black on white - * @default true - */ - invert_output?: boolean; - /** - * Highlight Threshold - * @description Threshold beyond which mask values will be at extremum - * @default 0.75 - */ - highlight_threshold?: number; - /** - * Upper Mid Threshold - * @description Threshold to which to extend mask border by 0..1 gradient - * @default 0.7 - */ - upper_mid_threshold?: number; - /** - * Lower Mid Threshold - * @description Threshold to which to extend mask border by 0..1 gradient - * @default 0.3 - */ - lower_mid_threshold?: number; - /** - * Shadow Threshold - * @description Threshold beyond which mask values will be at extremum - * @default 0.25 - */ - shadow_threshold?: number; - /** - * Mask Expand Or Contract - * @description Pixels to grow (or shrink) the mask areas - * @default 0 - */ - mask_expand_or_contract?: number; - /** - * Mask Blur - * @description Gaussian blur radius to apply to the masks - * @default 0 - */ - mask_blur?: number; - /** - * type - * @default shmmask - * @constant - */ - type: "shmmask"; - }; - /** ShadowsHighlightsMidtonesMasksOutput */ - ShadowsHighlightsMidtonesMasksOutput: { - /** @description Soft-edged highlights mask */ - highlights_mask?: components["schemas"]["ImageField"]; - /** @description Soft-edged midtones mask */ - midtones_mask?: components["schemas"]["ImageField"]; - /** @description Soft-edged shadows mask */ - shadows_mask?: components["schemas"]["ImageField"]; - /** - * Width - * @description Width of the input/outputs - */ - width: number; - /** - * Height - * @description Height of the input/outputs - */ - height: number; - /** - * type - * @default shmmask_output - * @constant - */ - type: "shmmask_output"; - }; /** * Show Image * @description Displays a provided image using the OS image viewer, and passes it forward in the pipeline. @@ -11833,249 +10318,6 @@ export type components = { /** Right */ right: number; }; - /** - * Text Mask - * @description Creates a 2D rendering of a text mask from a given font - */ - TextMaskInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** - * Width - * @description The width of the desired mask - * @default 512 - */ - width?: number; - /** - * Height - * @description The height of the desired mask - * @default 512 - */ - height?: number; - /** - * Text - * @description The text to render - * @default - */ - text?: string; - /** - * Font - * @description Path to a FreeType-supported TTF/OTF font file - * @default - */ - font?: string; - /** - * Size - * @description Desired point size of text to use - * @default 64 - */ - size?: number; - /** - * Angle - * @description Angle of rotation to apply to the text - * @default 0 - */ - angle?: number; - /** - * X Offset - * @description x-offset for text rendering - * @default 24 - */ - x_offset?: number; - /** - * Y Offset - * @description y-offset for text rendering - * @default 36 - */ - y_offset?: number; - /** - * Invert - * @description Whether to invert color of the output - * @default false - */ - invert?: boolean; - /** - * type - * @default text_mask - * @constant - */ - type: "text_mask"; - }; - /** - * Text to Mask Advanced (Clipseg) - * @description Uses the Clipseg model to generate an image mask from a text prompt - */ - TextToMaskClipsegAdvancedInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image from which to create a mask */ - image?: components["schemas"]["ImageField"]; - /** - * Invert Output - * @description Off: white on black / On: black on white - * @default true - */ - invert_output?: boolean; - /** - * Prompt 1 - * @description First prompt with which to create a mask - */ - prompt_1?: string; - /** - * Prompt 2 - * @description Second prompt with which to create a mask (optional) - */ - prompt_2?: string; - /** - * Prompt 3 - * @description Third prompt with which to create a mask (optional) - */ - prompt_3?: string; - /** - * Prompt 4 - * @description Fourth prompt with which to create a mask (optional) - */ - prompt_4?: string; - /** - * Combine - * @description How to combine the results - * @default or - * @enum {string} - */ - combine?: "or" | "and" | "none (rgba multiplex)"; - /** - * Smoothing - * @description Radius of blur to apply before thresholding - * @default 4 - */ - smoothing?: number; - /** - * Subject Threshold - * @description Threshold above which is considered the subject - * @default 1 - */ - subject_threshold?: number; - /** - * Background Threshold - * @description Threshold below which is considered the background - * @default 0 - */ - background_threshold?: number; - /** - * type - * @default txt2mask_clipseg_adv - * @constant - */ - type: "txt2mask_clipseg_adv"; - }; - /** - * Text to Mask (Clipseg) - * @description Uses the Clipseg model to generate an image mask from a text prompt - */ - TextToMaskClipsegInvocation: { - /** @description Optional metadata to be saved with the image */ - metadata?: components["schemas"]["MetadataField"] | null; - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The image from which to create a mask */ - image?: components["schemas"]["ImageField"]; - /** - * Invert Output - * @description Off: white on black / On: black on white - * @default true - */ - invert_output?: boolean; - /** - * Prompt - * @description The prompt with which to create a mask - */ - prompt?: string; - /** - * Smoothing - * @description Radius of blur to apply before thresholding - * @default 4 - */ - smoothing?: number; - /** - * Subject Threshold - * @description Threshold above which is considered the subject - * @default 0.4 - */ - subject_threshold?: number; - /** - * Background Threshold - * @description Threshold below which is considered the background - * @default 0.4 - */ - background_threshold?: number; - /** - * Mask Expand Or Contract - * @description Pixels by which to grow (or shrink) mask after thresholding - * @default 0 - */ - mask_expand_or_contract?: number; - /** - * Mask Blur - * @description Radius of blur to apply after thresholding - * @default 0 - */ - mask_blur?: number; - /** - * type - * @default txt2mask_clipseg - * @constant - */ - type: "txt2mask_clipseg"; - }; /** * TextualInversionConfig * @description Model config for textual inversion embeddings. @@ -13066,18 +11308,6 @@ export type components = { * @enum {string} */ UIType: "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_MainModel" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; - /** - * T2IAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - T2IAdapterModelFormat: "diffusers"; - /** - * IPAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - IPAdapterModelFormat: "invokeai"; /** * StableDiffusionXLModelFormat * @description An enumeration. @@ -13085,11 +11315,11 @@ export type components = { */ StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; /** - * StableDiffusionOnnxModelFormat + * T2IAdapterModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; + T2IAdapterModelFormat: "diffusers"; /** * CLIPVisionModelFormat * @description An enumeration. @@ -13097,11 +11327,11 @@ export type components = { */ CLIPVisionModelFormat: "diffusers"; /** - * StableDiffusion2ModelFormat + * ControlNetModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusion1ModelFormat * @description An enumeration. @@ -13109,11 +11339,23 @@ export type components = { */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; /** - * ControlNetModelFormat + * IPAdapterModelFormat * @description An enumeration. * @enum {string} */ - ControlNetModelFormat: "checkpoint" | "diffusers"; + IPAdapterModelFormat: "invokeai"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusionOnnxModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionOnnxModelFormat: "olive" | "onnx"; }; responses: never; parameters: never; From f8525837b2fd6ce8a06d021c324e57ae5c7587aa Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:30:00 +1100 Subject: [PATCH 106/411] feat(ui): workflow schema v3 (WIP) The changes aim to deduplicate data between workflows and node templates, decoupling workflows from internal implementation details. A good amount of data that was needlessly duplicated from the node template to the workflow is removed. These changes substantially reduce the file size of workflows (and therefore the images with embedded workflows): - Default T2I SD1.5 workflow JSON is reduced from 23.7kb (798 lines) to 10.9kb (407 lines). - Default tiled upscale workflow JSON is reduced from 102.7kb (3341 lines) to 51.9kb (1774 lines). The trade-off is that we need to reference node templates to get things like the field type and other things. In practice, this is a non-issue, because we need a node template to do anything with a node anyways. - Field types are not included in the workflow. They are always pulled from the node templates. The field type is now properly an internal implementation detail and we can change it as needed. Previously this would require a migration for the workflow itself. With the v3 schema, the structure of a field type is an internal implementation detail that we are free to change as we see fit. - Workflow nodes no long have an `outputs` property and there is no longer such a thing as a `FieldOutputInstance`. These are only on the templates. These were never referenced at a time when we didn't also have the templates available, and there'd be no reason to do so. - Node width and height are no longer stored in the node. These weren't used. Also, per https://reactflow.dev/api-reference/types/node, we shouldn't be programmatically changing these properties. A future enhancement can properly add node resizing. - `nodeTemplates` slice is merged back into `nodesSlice` as `nodes.templates`. Turns out it's just a hassle having these separate in separate slices. - Workflow migration logic updated to support the new schema. V1 workflows migrate all the way to v3 now. - Changes throughout the nodes code to accommodate the above changes. --- .../middleware/devtools/actionSanitizer.ts | 2 +- .../listeners/getOpenAPISchema.ts | 2 +- .../listeners/updateAllNodesRequested.ts | 3 +- .../listeners/workflowLoadRequested.ts | 2 +- invokeai/frontend/web/src/app/store/store.ts | 2 - .../frontend/web/src/app/store/storeHooks.ts | 3 +- invokeai/frontend/web/src/app/store/util.ts | 2 + .../src/common/hooks/useIsReadyToEnqueue.ts | 6 +- .../flow/AddNodePopover/AddNodePopover.tsx | 14 +- .../flow/edges/util/makeEdgeSelector.ts | 18 +- .../InvocationNodeCollapsedHandles.tsx | 19 +- .../Invocation/InvocationNodeWrapper.tsx | 4 +- .../Invocation/fields/EditableFieldTitle.tsx | 4 +- .../nodes/Invocation/fields/FieldTitle.tsx | 2 +- .../Invocation/fields/FieldTooltipContent.tsx | 6 +- .../nodes/Invocation/fields/InputField.tsx | 6 +- .../Invocation/fields/InputFieldRenderer.tsx | 29 +- .../Invocation/fields/LinearViewField.tsx | 4 +- .../nodes/Invocation/fields/OutputField.tsx | 10 +- .../inspector/InspectorDetailsTab.tsx | 5 +- .../inspector/InspectorOutputsTab.tsx | 5 +- .../inspector/InspectorTemplateTab.tsx | 5 +- .../hooks/useAnyOrDirectInputFieldNames.ts | 20 +- .../src/features/nodes/hooks/useBuildNode.ts | 2 +- .../hooks/useConnectionInputFieldNames.ts | 20 +- .../nodes/hooks/useConnectionState.ts | 10 +- .../nodes/hooks/useDoNodeVersionsMatch.ts | 18 +- .../nodes/hooks/useDoesInputHaveValue.ts | 12 +- .../src/features/nodes/hooks/useFieldData.ts | 23 - .../nodes/hooks/useFieldInputInstance.ts | 15 +- .../features/nodes/hooks/useFieldInputKind.ts | 15 +- .../nodes/hooks/useFieldInputTemplate.ts | 15 +- .../src/features/nodes/hooks/useFieldLabel.ts | 10 +- .../nodes/hooks/useFieldOutputInstance.ts | 23 - .../nodes/hooks/useFieldOutputTemplate.ts | 15 +- .../features/nodes/hooks/useFieldTemplate.ts | 21 +- .../nodes/hooks/useFieldTemplateTitle.ts | 16 +- .../features/nodes/hooks/useFieldType.ts.ts | 14 +- .../nodes/hooks/useGetNodesNeedUpdate.ts | 5 +- .../features/nodes/hooks/useHasImageOutput.ts | 13 +- .../features/nodes/hooks/useIsIntermediate.ts | 10 +- .../nodes/hooks/useIsValidConnection.ts | 44 +- .../nodes/hooks/useNodeClassification.ts | 17 +- .../src/features/nodes/hooks/useNodeData.ts | 7 +- .../src/features/nodes/hooks/useNodeLabel.ts | 9 +- .../nodes/hooks/useNodeNeedsUpdate.ts | 15 +- .../src/features/nodes/hooks/useNodePack.ts | 10 +- .../features/nodes/hooks/useNodeTemplate.ts | 13 +- .../nodes/hooks/useNodeTemplateByType.ts | 10 +- .../nodes/hooks/useNodeTemplateTitle.ts | 15 +- .../nodes/hooks/useOutputFieldNames.ts | 20 +- .../src/features/nodes/hooks/useUseCache.ts | 8 +- .../nodes/hooks/useWorkflowWatcher.ts | 4 +- .../web/src/features/nodes/store/actions.ts | 4 +- .../nodes/store/nodeTemplatesSlice.ts | 24 - .../src/features/nodes/store/nodesSlice.ts | 15 +- .../web/src/features/nodes/store/selectors.ts | 51 + .../web/src/features/nodes/store/types.ts | 5 +- .../store/util/findConnectionToValidHandle.ts | 30 +- .../util/makeIsConnectionValidSelector.ts | 2 +- .../src/features/nodes/store/workflowSlice.ts | 6 +- .../web/src/features/nodes/types/field.ts | 130 +-- .../src/features/nodes/types/invocation.ts | 24 +- .../web/src/features/nodes/types/v2/common.ts | 188 ++++ .../src/features/nodes/types/v2/constants.ts | 80 ++ .../web/src/features/nodes/types/v2/error.ts | 58 ++ .../web/src/features/nodes/types/v2/field.ts | 875 ++++++++++++++++++ .../src/features/nodes/types/v2/invocation.ts | 93 ++ .../src/features/nodes/types/v2/metadata.ts | 77 ++ .../src/features/nodes/types/v2/openapi.ts | 86 ++ .../web/src/features/nodes/types/v2/semver.ts | 21 + .../src/features/nodes/types/v2/workflow.ts | 89 ++ .../web/src/features/nodes/types/workflow.ts | 10 +- .../nodes/util/node/buildInvocationNode.ts | 22 +- .../features/nodes/util/node/nodeUpdate.ts | 1 - .../util/schema/buildFieldInputInstance.ts | 3 - .../nodes/util/workflow/buildWorkflow.ts | 20 +- .../nodes/util/workflow/migrations.ts | 32 +- .../nodes/util/workflow/validateWorkflow.ts | 4 +- .../workflowLibrary/hooks/useSaveWorkflow.ts | 4 +- 80 files changed, 1940 insertions(+), 616 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/util.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/store/nodeTemplatesSlice.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/selectors.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/common.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/constants.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/error.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/field.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/invocation.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/metadata.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/openapi.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/semver.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/v2/workflow.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts index 2e2d2014b2..ed8c82d91c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -1,6 +1,6 @@ import type { UnknownAction } from '@reduxjs/toolkit'; import { isAnyGraphBuilt } from 'features/nodes/store/actions'; -import { nodeTemplatesBuilt } from 'features/nodes/store/nodeTemplatesSlice'; +import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; import { cloneDeep } from 'lodash-es'; import { appInfoApi } from 'services/api/endpoints/appInfo'; import type { Graph } from 'services/api/types'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts index b2d3615909..88518e2c0b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema.ts @@ -1,6 +1,6 @@ import { logger } from 'app/logging/logger'; import { parseify } from 'common/util/serialize'; -import { nodeTemplatesBuilt } from 'features/nodes/store/nodeTemplatesSlice'; +import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; import { parseSchema } from 'features/nodes/util/schema/parseSchema'; import { size } from 'lodash-es'; import { appInfoApi } from 'services/api/endpoints/appInfo'; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts index 752c3b09df..ac1298da5b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts @@ -15,8 +15,7 @@ export const addUpdateAllNodesRequestedListener = () => { actionCreator: updateAllNodesRequested, effect: (action, { dispatch, getState }) => { const log = logger('nodes'); - const nodes = getState().nodes.nodes; - const templates = getState().nodeTemplates.templates; + const { nodes, templates } = getState().nodes; let unableToUpdateCount = 0; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts index 9307031e6d..ad41dc2654 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested.ts @@ -18,7 +18,7 @@ export const addWorkflowLoadRequestedListener = () => { effect: (action, { dispatch, getState }) => { const log = logger('nodes'); const { workflow, asCopy } = action.payload; - const nodeTemplates = getState().nodeTemplates.templates; + const nodeTemplates = getState().nodes.templates; try { const { workflow: validatedWorkflow, warnings } = validateWorkflow(workflow, nodeTemplates); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index e25e1351eb..270662c3d2 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -16,7 +16,6 @@ import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice'; import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice'; import { modelManagerPersistConfig, modelManagerSlice } from 'features/modelManager/store/modelManagerSlice'; import { nodesPersistConfig, nodesSlice } from 'features/nodes/store/nodesSlice'; -import { nodesTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice'; @@ -46,7 +45,6 @@ const allReducers = { [gallerySlice.name]: gallerySlice.reducer, [generationSlice.name]: generationSlice.reducer, [nodesSlice.name]: nodesSlice.reducer, - [nodesTemplatesSlice.name]: nodesTemplatesSlice.reducer, [postprocessingSlice.name]: postprocessingSlice.reducer, [systemSlice.name]: systemSlice.reducer, [configSlice.name]: configSlice.reducer, diff --git a/invokeai/frontend/web/src/app/store/storeHooks.ts b/invokeai/frontend/web/src/app/store/storeHooks.ts index f1a9aa979c..6bc904acb3 100644 --- a/invokeai/frontend/web/src/app/store/storeHooks.ts +++ b/invokeai/frontend/web/src/app/store/storeHooks.ts @@ -1,7 +1,8 @@ import type { AppThunkDispatch, RootState } from 'app/store/store'; import type { TypedUseSelectorHook } from 'react-redux'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector, useStore } from 'react-redux'; // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppStore = () => useStore(); diff --git a/invokeai/frontend/web/src/app/store/util.ts b/invokeai/frontend/web/src/app/store/util.ts new file mode 100644 index 0000000000..381f7f85d2 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/util.ts @@ -0,0 +1,2 @@ +export const EMPTY_ARRAY = []; +export const EMPTY_OBJECT = {}; diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 4952fa1c47..baa704e75c 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -8,7 +8,6 @@ import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { selectSystemSlice } from 'features/system/store/systemSlice'; @@ -23,11 +22,10 @@ const selector = createMemoizedSelector( selectGenerationSlice, selectSystemSlice, selectNodesSlice, - selectNodeTemplatesSlice, selectDynamicPromptsSlice, activeTabNameSelector, ], - (controlAdapters, generation, system, nodes, nodeTemplates, dynamicPrompts, activeTabName) => { + (controlAdapters, generation, system, nodes, dynamicPrompts, activeTabName) => { const { initialImage, model, positivePrompt } = generation; const { isConnected } = system; @@ -54,7 +52,7 @@ const selector = createMemoizedSelector( return; } - const nodeTemplate = nodeTemplates.templates[node.data.type]; + const nodeTemplate = nodes.templates[node.data.type]; if (!nodeTemplate) { // Node type not found diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index b24b52c6ab..061209cafc 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -7,8 +7,12 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { SelectInstance } from 'chakra-react-select'; import { useBuildNode } from 'features/nodes/hooks/useBuildNode'; -import { addNodePopoverClosed, addNodePopoverOpened, nodeAdded } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; +import { + addNodePopoverClosed, + addNodePopoverOpened, + nodeAdded, + selectNodesSlice, +} from 'features/nodes/store/nodesSlice'; import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; import { filter, map, memoize, some } from 'lodash-es'; import type { KeyboardEventHandler } from 'react'; @@ -54,10 +58,10 @@ const AddNodePopover = () => { const fieldFilter = useAppSelector((s) => s.nodes.connectionStartFieldType); const handleFilter = useAppSelector((s) => s.nodes.connectionStartParams?.handleType); - const selector = createMemoizedSelector(selectNodeTemplatesSlice, (nodeTemplates) => { + const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { // If we have a connection in progress, we need to filter the node choices const filteredNodeTemplates = fieldFilter - ? filter(nodeTemplates.templates, (template) => { + ? filter(nodes.templates, (template) => { const handles = handleFilter === 'source' ? template.inputs : template.outputs; return some(handles, (handle) => { @@ -67,7 +71,7 @@ const AddNodePopover = () => { return validateSourceAndTargetTypes(sourceType, targetType); }); }) - : map(nodeTemplates.templates); + : map(nodes.templates); const options: ComboboxOption[] = map(filteredNodeTemplates, (template) => { return { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index 4bfc588e67..ba40b4984c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -1,10 +1,17 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectFieldOutputTemplate } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getFieldColor } from './getEdgeColor'; +const defaultReturnValue = { + isSelected: false, + shouldAnimate: false, + stroke: colorTokenToCssVar('base.500'), +}; + export const makeEdgeSelector = ( source: string, sourceHandleId: string | null | undefined, @@ -12,14 +19,19 @@ export const makeEdgeSelector = ( targetHandleId: string | null | undefined, selected?: boolean ) => - createMemoizedSelector(selectNodesSlice, (nodes) => { + createMemoizedSelector(selectNodesSlice, (nodes): { isSelected: boolean; shouldAnimate: boolean; stroke: string } => { const sourceNode = nodes.nodes.find((node) => node.id === source); const targetNode = nodes.nodes.find((node) => node.id === target); const isInvocationToInvocationEdge = isInvocationNode(sourceNode) && isInvocationNode(targetNode); - const isSelected = sourceNode?.selected || targetNode?.selected || selected; - const sourceType = isInvocationToInvocationEdge ? sourceNode?.data?.outputs[sourceHandleId || '']?.type : undefined; + const isSelected = Boolean(sourceNode?.selected || targetNode?.selected || selected); + if (!sourceNode || !sourceHandleId) { + return defaultReturnValue; + } + + const outputFieldTemplate = selectFieldOutputTemplate(nodes, sourceNode.id, sourceHandleId); + const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined; const stroke = sourceType && nodes.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx index c287842f6e..b888e8a516 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx @@ -1,6 +1,5 @@ import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; -import { useNodeData } from 'features/nodes/hooks/useNodeData'; -import { isInvocationNodeData } from 'features/nodes/types/invocation'; +import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate'; import { map } from 'lodash-es'; import type { CSSProperties } from 'react'; import { memo, useMemo } from 'react'; @@ -13,7 +12,7 @@ interface Props { const hiddenHandleStyles: CSSProperties = { visibility: 'hidden' }; const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => { - const data = useNodeData(nodeId); + const template = useNodeTemplate(nodeId); const { base600 } = useChakraThemeTokens(); const dummyHandleStyles: CSSProperties = useMemo( @@ -37,7 +36,7 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => { [dummyHandleStyles] ); - if (!isInvocationNodeData(data)) { + if (!template) { return null; } @@ -45,14 +44,14 @@ const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => { <> - {map(data.inputs, (input) => ( + {map(template.inputs, (input) => ( { ))} - {map(data.outputs, (output) => ( + {map(template.outputs, (output) => ( ) => { const { id: nodeId, type, isOpen, label } = data; const hasTemplateSelector = useMemo( - () => createSelector(selectNodeTemplatesSlice, (nodeTemplates) => Boolean(nodeTemplates.templates[type])), + () => createSelector(selectNodesSlice, (nodes) => Boolean(nodes.templates[type])), [type] ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx index c2231f703a..e02b1a1474 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/EditableFieldTitle.tsx @@ -22,7 +22,7 @@ import FieldTooltipContent from './FieldTooltipContent'; interface Props { nodeId: string; fieldName: string; - kind: 'input' | 'output'; + kind: 'inputs' | 'outputs'; isMissingInput?: boolean; withTooltip?: boolean; } @@ -58,7 +58,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => { return ( : undefined} + label={withTooltip ? : undefined} openDelay={HANDLE_TOOLTIP_OPEN_DELAY} > { - const field = useFieldInstance(nodeId, fieldName); + const field = useFieldInputInstance(nodeId, fieldName); const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); const isInputTemplate = isFieldInputTemplate(fieldTemplate); const fieldTypeName = useFieldTypeName(fieldTemplate?.type); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx index 2b9f7960e4..66b0d3f755 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx @@ -25,7 +25,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { const [isHovered, setIsHovered] = useState(false); const { isConnected, isConnectionInProgress, isConnectionStartField, connectionError, shouldDim } = - useConnectionState({ nodeId, fieldName, kind: 'input' }); + useConnectionState({ nodeId, fieldName, kind: 'inputs' }); const isMissingInput = useMemo(() => { if (!fieldTemplate) { @@ -76,7 +76,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { @@ -101,7 +101,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index c1d52c1d4f..b6e331c114 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -1,6 +1,5 @@ -import { Box, Text } from '@invoke-ai/ui-library'; -import { useFieldInstance } from 'features/nodes/hooks/useFieldData'; -import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate'; +import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance'; +import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate'; import { isBoardFieldInputInstance, isBoardFieldInputTemplate, @@ -38,7 +37,6 @@ import { isVAEModelFieldInputTemplate, } from 'features/nodes/types/field'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; import BoardFieldInputComponent from './inputs/BoardFieldInputComponent'; import BooleanFieldInputComponent from './inputs/BooleanFieldInputComponent'; @@ -63,17 +61,8 @@ type InputFieldProps = { }; const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { - const { t } = useTranslation(); - const fieldInstance = useFieldInstance(nodeId, fieldName); - const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input'); - - if (fieldTemplate?.fieldKind === 'output') { - return ( - - {t('nodes.outputFieldInInput')}: {fieldInstance?.type.name} - - ); - } + const fieldInstance = useFieldInputInstance(nodeId, fieldName); + const fieldTemplate = useFieldInputTemplate(nodeId, fieldName); if (isStringFieldInputInstance(fieldInstance) && isStringFieldInputTemplate(fieldTemplate)) { return ; @@ -141,18 +130,10 @@ const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { return ; } - if (fieldInstance && fieldTemplate) { + if (fieldTemplate) { // Fallback for when there is no component for the type return null; } - - return ( - - - {t('nodes.unknownFieldType', { type: fieldInstance?.type.name })} - - - ); }; export default memo(InputFieldRenderer); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index d0a30ecc3c..0cd199f7a4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -62,7 +62,7 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { /> - + {isValueChanged && ( { /> )} } + label={} openDelay={HANDLE_TOOLTIP_OPEN_DELAY} placement="top" > diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx index 48c4c0d740..f2d776a2da 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx @@ -1,6 +1,5 @@ import { Flex, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; -import { useFieldOutputInstance } from 'features/nodes/hooks/useFieldOutputInstance'; import { useFieldOutputTemplate } from 'features/nodes/hooks/useFieldOutputTemplate'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import type { PropsWithChildren } from 'react'; @@ -18,18 +17,17 @@ interface Props { const OutputField = ({ nodeId, fieldName }: Props) => { const { t } = useTranslation(); const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName); - const fieldInstance = useFieldOutputInstance(nodeId, fieldName); const { isConnected, isConnectionInProgress, isConnectionStartField, connectionError, shouldDim } = - useConnectionState({ nodeId, fieldName, kind: 'output' }); + useConnectionState({ nodeId, fieldName, kind: 'outputs' }); - if (!fieldTemplate || !fieldInstance) { + if (!fieldTemplate) { return ( {t('nodes.unknownOutput', { - name: fieldTemplate?.title ?? fieldName, + name: fieldName, })} @@ -40,7 +38,7 @@ const OutputField = ({ nodeId, fieldName }: Props) => { return ( } + label={} openDelay={HANDLE_TOOLTIP_OPEN_DELAY} placement="top" shouldWrapChildren diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx index b7c9033d6b..d72d2f5aa8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx @@ -6,19 +6,18 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea'; import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import EditableNodeTitle from './details/EditableNodeTitle'; -const selector = createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { +const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - const lastSelectedNodeTemplate = lastSelectedNode ? nodeTemplates.templates[lastSelectedNode.data.type] : undefined; + const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined; if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) { return; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index ee7dfaa693..978eeddd24 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -5,7 +5,6 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,12 +13,12 @@ import type { AnyResult } from 'services/events/types'; import ImageOutputPreview from './outputs/ImageOutputPreview'; -const selector = createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { +const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - const lastSelectedNodeTemplate = lastSelectedNode ? nodeTemplates.templates[lastSelectedNode.data.type] : undefined; + const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined; const nes = nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__']; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx index 28f0e82d68..ea6e8ed704 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx @@ -3,16 +3,15 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selector = createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { +const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - const lastSelectedNodeTemplate = lastSelectedNode ? nodeTemplates.templates[lastSelectedNode.data.type] : undefined; + const lastSelectedNodeTemplate = lastSelectedNode ? nodes.templates[lastSelectedNode.data.type] : undefined; return { template: lastSelectedNodeTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts index d0263a8bda..c882924e24 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts @@ -1,26 +1,22 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { EMPTY_ARRAY } from 'app/store/util'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeTemplate } from 'features/nodes/store/selectors'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; import { useMemo } from 'react'; -export const useAnyOrDirectInputFieldNames = (nodeId: string) => { +export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return []; + createMemoizedSelector(selectNodesSlice, (nodes) => { + const template = selectNodeTemplate(nodes, nodeId); + if (!template) { + return EMPTY_ARRAY; } - const nodeTemplate = nodeTemplates.templates[node.data.type]; - if (!nodeTemplate) { - return []; - } - const fields = map(nodeTemplate.inputs).filter( + const fields = map(template.inputs).filter( (field) => (['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) && keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts index aecc931893..b19edf3c85 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildNode.ts @@ -13,7 +13,7 @@ export const SHARED_NODE_PROPERTIES: Partial = { }; export const useBuildNode = () => { - const nodeTemplates = useAppSelector((s) => s.nodeTemplates.templates); + const nodeTemplates = useAppSelector((s) => s.nodes.templates); const flow = useReactFlow(); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts index 23f318517b..dc8a05b88c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts @@ -1,28 +1,24 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { EMPTY_ARRAY } from 'app/store/util'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeTemplate } from 'features/nodes/store/selectors'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { keys, map } from 'lodash-es'; import { useMemo } from 'react'; -export const useConnectionInputFieldNames = (nodeId: string) => { +export const useConnectionInputFieldNames = (nodeId: string): string[] => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return []; - } - const nodeTemplate = nodeTemplates.templates[node.data.type]; - if (!nodeTemplate) { - return []; + createMemoizedSelector(selectNodesSlice, (nodes) => { + const template = selectNodeTemplate(nodes, nodeId); + if (!template) { + return EMPTY_ARRAY; } // get the visible fields - const fields = map(nodeTemplate.inputs).filter( + const fields = map(template.inputs).filter( (field) => (field.input === 'connection' && !field.type.isCollectionOrScalar) || !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name) diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index a6f8b663f6..97b96f323a 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -14,7 +14,7 @@ const selectIsConnectionInProgress = createSelector( export type UseConnectionStateProps = { nodeId: string; fieldName: string; - kind: 'input' | 'output'; + kind: 'inputs' | 'outputs'; }; export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => { @@ -26,8 +26,8 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta Boolean( nodes.edges.filter((edge) => { return ( - (kind === 'input' ? edge.target : edge.source) === nodeId && - (kind === 'input' ? edge.targetHandle : edge.sourceHandle) === fieldName + (kind === 'inputs' ? edge.target : edge.source) === nodeId && + (kind === 'inputs' ? edge.targetHandle : edge.sourceHandle) === fieldName ); }).length ) @@ -36,7 +36,7 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta ); const selectConnectionError = useMemo( - () => makeConnectionErrorSelector(nodeId, fieldName, kind === 'input' ? 'target' : 'source', fieldType), + () => makeConnectionErrorSelector(nodeId, fieldName, kind === 'inputs' ? 'target' : 'source', fieldType), [nodeId, fieldName, kind, fieldType] ); @@ -46,7 +46,7 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta Boolean( nodes.connectionStartParams?.nodeId === nodeId && nodes.connectionStartParams?.handleId === fieldName && - nodes.connectionStartParams?.handleType === { input: 'target', output: 'source' }[kind] + nodes.connectionStartParams?.handleType === { inputs: 'target', outputs: 'source' }[kind] ) ), [fieldName, kind, nodeId] diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useDoNodeVersionsMatch.ts b/invokeai/frontend/web/src/features/nodes/hooks/useDoNodeVersionsMatch.ts index bfbf0a3b2d..91994cf752 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useDoNodeVersionsMatch.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useDoNodeVersionsMatch.ts @@ -2,23 +2,19 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { compareVersions } from 'compare-versions'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeData, selectNodeTemplate } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; -export const useDoNodeVersionsMatch = (nodeId: string) => { +export const useDoNodeVersionsMatch = (nodeId: string): boolean => { const selector = useMemo( () => - createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { + createSelector(selectNodesSlice, (nodes) => { + const data = selectNodeData(nodes, nodeId); + const template = selectNodeTemplate(nodes, nodeId); + if (!template?.version || !data?.version) { return false; } - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - if (!nodeTemplate?.version || !node.data?.version) { - return false; - } - return compareVersions(nodeTemplate.version, node.data.version) === 0; + return compareVersions(template.version, data.version) === 0; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts index cfe5c90d9c..5051eaa55b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts @@ -1,18 +1,18 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeData } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; -export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => { +export const useDoesInputHaveValue = (nodeId: string, fieldName: string): boolean => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; + const data = selectNodeData(nodes, nodeId); + if (!data) { + return false; } - return node?.data.inputs[fieldName]?.value !== undefined; + return data.inputs[fieldName]?.value !== undefined; }), [fieldName, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts deleted file mode 100644 index 8b35a2d44b..0000000000 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldData.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; -import { useMemo } from 'react'; - -export const useFieldInstance = (nodeId: string, fieldName: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return node?.data.inputs[fieldName]; - }), - [fieldName, nodeId] - ); - - const fieldData = useAppSelector(selector); - - return fieldData; -}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts index 0793f1f952..25065e7aba 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts @@ -1,23 +1,20 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldInputInstance } from 'features/nodes/store/selectors'; +import type { FieldInputInstance } from 'features/nodes/types/field'; import { useMemo } from 'react'; -export const useFieldInputInstance = (nodeId: string, fieldName: string) => { +export const useFieldInputInstance = (nodeId: string, fieldName: string): FieldInputInstance | null => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return node.data.inputs[fieldName]; + return selectFieldInputInstance(nodes, nodeId, fieldName); }), [fieldName, nodeId] ); - const fieldTemplate = useAppSelector(selector); + const fieldData = useAppSelector(selector); - return fieldTemplate; + return fieldData; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts index 11d44dbde2..08de3d9b20 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputKind.ts @@ -1,21 +1,16 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldInputTemplate } from 'features/nodes/store/selectors'; +import type { FieldInput } from 'features/nodes/types/field'; import { useMemo } from 'react'; export const useFieldInputKind = (nodeId: string, fieldName: string) => { const selector = useMemo( () => - createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - const fieldTemplate = nodeTemplate?.inputs[fieldName]; - return fieldTemplate?.input; + createSelector(selectNodesSlice, (nodes): FieldInput | null => { + const template = selectFieldInputTemplate(nodes, nodeId, fieldName); + return template?.input ?? null; }), [fieldName, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts index 8533d2be8d..e8289d7e07 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts @@ -1,20 +1,15 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldInputTemplate } from 'features/nodes/store/selectors'; +import type { FieldInputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; -export const useFieldInputTemplate = (nodeId: string, fieldName: string) => { +export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - return nodeTemplate?.inputs[fieldName]; + createMemoizedSelector(selectNodesSlice, (nodes) => { + return selectFieldInputTemplate(nodes, nodeId, fieldName); }), [fieldName, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts index ef57956047..92eab8d1b1 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts @@ -1,18 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldInputInstance } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; -export const useFieldLabel = (nodeId: string, fieldName: string) => { +export const useFieldLabel = (nodeId: string, fieldName: string): string | null => { const selector = useMemo( () => createSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return node?.data.inputs[fieldName]?.label; + return selectFieldInputInstance(nodes, nodeId, fieldName)?.label ?? null; }), [fieldName, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts deleted file mode 100644 index 8b71f1ea01..0000000000 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; -import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; -import { useMemo } from 'react'; - -export const useFieldOutputInstance = (nodeId: string, fieldName: string) => { - const selector = useMemo( - () => - createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - return node.data.outputs[fieldName]; - }), - [fieldName, nodeId] - ); - - const fieldTemplate = useAppSelector(selector); - - return fieldTemplate; -}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts index 11f592b399..cb154071e9 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts @@ -1,20 +1,15 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldOutputTemplate } from 'features/nodes/store/selectors'; +import type { FieldOutputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; -export const useFieldOutputTemplate = (nodeId: string, fieldName: string) => { +export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate | null => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; - } - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - return nodeTemplate?.outputs[fieldName]; + createMemoizedSelector(selectNodesSlice, (nodes) => { + return selectFieldOutputTemplate(nodes, nodeId, fieldName); }), [fieldName, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts index 663821da81..7be4ecfd4d 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts @@ -1,21 +1,22 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { KIND_MAP } from 'features/nodes/types/constants'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors'; +import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; -export const useFieldTemplate = (nodeId: string, fieldName: string, kind: 'input' | 'output') => { +export const useFieldTemplate = ( + nodeId: string, + fieldName: string, + kind: 'inputs' | 'outputs' +): FieldInputTemplate | FieldOutputTemplate | null => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; + createMemoizedSelector(selectNodesSlice, (nodes) => { + if (kind === 'inputs') { + return selectFieldInputTemplate(nodes, nodeId, fieldName); } - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - return nodeTemplate?.[KIND_MAP[kind]][fieldName]; + return selectFieldOutputTemplate(nodes, nodeId, fieldName); }), [fieldName, kind, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts index cfdcda6efa..e41e019572 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplateTitle.ts @@ -1,21 +1,17 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { KIND_MAP } from 'features/nodes/types/constants'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; -export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'input' | 'output') => { +export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): string | null => { const selector = useMemo( () => - createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; + createSelector(selectNodesSlice, (nodes) => { + if (kind === 'inputs') { + return selectFieldInputTemplate(nodes, nodeId, fieldName)?.title ?? null; } - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - return nodeTemplate?.[KIND_MAP[kind]][fieldName]?.title; + return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.title ?? null; }), [fieldName, kind, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts index a834726a13..a71a4d044e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldType.ts.ts @@ -1,20 +1,18 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { KIND_MAP } from 'features/nodes/types/constants'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors'; +import type { FieldType } from 'features/nodes/types/field'; import { useMemo } from 'react'; -export const useFieldType = (nodeId: string, fieldName: string, kind: 'input' | 'output') => { +export const useFieldType = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): FieldType | null => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; + if (kind === 'inputs') { + return selectFieldInputTemplate(nodes, nodeId, fieldName)?.type ?? null; } - const field = node.data[KIND_MAP[kind]][fieldName]; - return field?.type; + return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.type ?? null; }), [fieldName, kind, nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts index a8019c92d6..71344197d5 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts @@ -1,13 +1,12 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; -const selector = createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => +const selector = createSelector(selectNodesSlice, (nodes) => nodes.nodes.filter(isInvocationNode).some((node) => { - const template = nodeTemplates.templates[node.data.type]; + const template = nodes.templates[node.data.type]; if (!template) { return false; } diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts index 617e713c7c..3ac3cabb22 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useHasImageOutput.ts @@ -1,24 +1,21 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeTemplate } from 'features/nodes/store/selectors'; import { some } from 'lodash-es'; import { useMemo } from 'react'; -export const useHasImageOutput = (nodeId: string) => { +export const useHasImageOutput = (nodeId: string): boolean => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } + const template = selectNodeTemplate(nodes, nodeId); return some( - node.data.outputs, + template?.outputs, (output) => output.type.name === 'ImageField' && // the image primitive node (node type "image") does not actually save the image, do not show the image-saving checkboxes - node.data.type !== 'image' + template?.type !== 'image' ); }), [nodeId] diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts index 729bfa0cea..3fad0a2a86 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts @@ -1,18 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeData } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; -export const useIsIntermediate = (nodeId: string) => { +export const useIsIntermediate = (nodeId: string): boolean => { const selector = useMemo( () => createSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - return node.data.isIntermediate; + return selectNodeData(nodes, nodeId)?.isIntermediate ?? false; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 39a8abbe7a..ded05c7b9b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -1,11 +1,10 @@ // TODO: enable this at some point -import { useAppSelector } from 'app/store/storeHooks'; +import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic'; import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { useCallback } from 'react'; import type { Connection, Node } from 'reactflow'; -import { useReactFlow } from 'reactflow'; /** * NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts` @@ -13,39 +12,34 @@ import { useReactFlow } from 'reactflow'; */ export const useIsValidConnection = () => { - const flow = useReactFlow(); + const store = useAppStore(); const shouldValidateGraph = useAppSelector((s) => s.nodes.shouldValidateGraph); const isValidConnection = useCallback( ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { - const edges = flow.getEdges(); - const nodes = flow.getNodes(); // Connection must have valid targets if (!(source && sourceHandle && target && targetHandle)) { return false; } - // Find the source and target nodes - const sourceNode = flow.getNode(source) as Node; - const targetNode = flow.getNode(target) as Node; - - // Conditional guards against undefined nodes/handles - if (!(sourceNode && targetNode && sourceNode.data && targetNode.data)) { - return false; - } - - const sourceField = sourceNode.data.outputs[sourceHandle]; - const targetField = targetNode.data.inputs[targetHandle]; - - if (!sourceField || !targetField) { - // something has gone terribly awry - return false; - } - if (source === target) { // Don't allow nodes to connect to themselves, even if validation is disabled return false; } + const state = store.getState(); + const { nodes, edges, templates } = state.nodes; + + // Find the source and target nodes + const sourceNode = nodes.find((node) => node.id === source) as Node; + const targetNode = nodes.find((node) => node.id === target) as Node; + const sourceFieldTemplate = templates[sourceNode.data.type]?.outputs[sourceHandle]; + const targetFieldTemplate = templates[targetNode.data.type]?.inputs[targetHandle]; + + // Conditional guards against undefined nodes/handles + if (!(sourceFieldTemplate && targetFieldTemplate)) { + return false; + } + if (!shouldValidateGraph) { // manual override! return true; @@ -69,20 +63,20 @@ export const useIsValidConnection = () => { return edge.target === target && edge.targetHandle === targetHandle; }) && // except CollectionItem inputs can have multiples - targetField.type.name !== 'CollectionItemField' + targetFieldTemplate.type.name !== 'CollectionItemField' ) { return false; } // Must use the originalType here if it exists - if (!validateSourceAndTargetTypes(sourceField.type, targetField.type)) { + if (!validateSourceAndTargetTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) { return false; } // Graphs much be acyclic (no loops!) return getIsGraphAcyclic(source, target, nodes, edges); }, - [flow, shouldValidateGraph] + [shouldValidateGraph, store] ); return isValidConnection; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts index c61721030e..bab8ff3f19 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts @@ -1,20 +1,15 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import type { Classification } from 'features/nodes/types/common'; import { useMemo } from 'react'; -export const useNodeClassification = (nodeId: string) => { +export const useNodeClassification = (nodeId: string): Classification | null => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - return nodeTemplate?.classification; + createSelector(selectNodesSlice, (nodes) => { + return selectNodeTemplate(nodes, nodeId)?.classification ?? null; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index c507def5ee..fa21008ff8 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -1,14 +1,15 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodeData } from 'features/nodes/store/selectors'; +import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; -export const useNodeData = (nodeId: string) => { +export const useNodeData = (nodeId: string): InvocationNodeData | null => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - return node?.data; + return selectNodeData(nodes, nodeId); }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts index c5fc43742a..31dcb9c466 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts @@ -1,19 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeData } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useNodeLabel = (nodeId: string) => { const selector = useMemo( () => createSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - - return node.data.label; + return selectNodeData(nodes, nodeId)?.label ?? null; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts index e6efa667f1..aa0294f70f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts @@ -1,21 +1,20 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectInvocationNode, selectNodeTemplate } from 'features/nodes/store/selectors'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import { useMemo } from 'react'; export const useNodeNeedsUpdate = (nodeId: string) => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - const template = nodeTemplates.templates[node?.data.type ?? '']; - if (isInvocationNode(node) && template) { - return getNeedsUpdate(node, template); + createMemoizedSelector(selectNodesSlice, (nodes) => { + const node = selectInvocationNode(nodes, nodeId); + const template = selectNodeTemplate(nodes, nodeId); + if (!node || !template) { + return false; } - return false; + return getNeedsUpdate(node, template); }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts index ca3dd5cfdf..5c920866e9 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts @@ -1,18 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeData } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; -export const useNodePack = (nodeId: string) => { +export const useNodePack = (nodeId: string): string | null => { const selector = useMemo( () => createSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - return node.data.nodePack; + return selectNodeData(nodes, nodeId)?.nodePack ?? null; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts index 7544cbff46..866c9275fb 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts @@ -1,16 +1,15 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; +import { selectNodeTemplate } from 'features/nodes/store/selectors'; +import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; -export const useNodeTemplate = (nodeId: string) => { +export const useNodeTemplate = (nodeId: string): InvocationTemplate | null => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - const nodeTemplate = nodeTemplates.templates[node?.data.type ?? '']; - return nodeTemplate; + createSelector(selectNodesSlice, (nodes) => { + return selectNodeTemplate(nodes, nodeId); }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateByType.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateByType.ts index 8fd1345f6f..a0c870f694 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateByType.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateByType.ts @@ -1,14 +1,14 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; +import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; -export const useNodeTemplateByType = (type: string) => { +export const useNodeTemplateByType = (type: string): InvocationTemplate | null => { const selector = useMemo( () => - createMemoizedSelector(selectNodeTemplatesSlice, (nodeTemplates): InvocationTemplate | undefined => { - return nodeTemplates.templates[type]; + createSelector(selectNodesSlice, (nodes) => { + return nodes.templates[type] ?? null; }), [type] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts index 15d2ec38c3..120b8c758b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts @@ -1,21 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeTemplate } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; -export const useNodeTemplateTitle = (nodeId: string) => { +export const useNodeTemplateTitle = (nodeId: string): string | null => { const selector = useMemo( () => - createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - const nodeTemplate = node ? nodeTemplates.templates[node.data.type] : undefined; - - return nodeTemplate?.title; + createSelector(selectNodesSlice, (nodes) => { + return selectNodeTemplate(nodes, nodeId)?.title ?? null; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts index e352bd8b90..24863080a7 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts @@ -1,8 +1,8 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; +import { EMPTY_ARRAY } from 'app/store/util'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeTemplate } from 'features/nodes/store/selectors'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { map } from 'lodash-es'; import { useMemo } from 'react'; @@ -10,17 +10,13 @@ import { useMemo } from 'react'; export const useOutputFieldNames = (nodeId: string) => { const selector = useMemo( () => - createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return []; - } - const nodeTemplate = nodeTemplates.templates[node.data.type]; - if (!nodeTemplate) { - return []; + createSelector(selectNodesSlice, (nodes) => { + const template = selectNodeTemplate(nodes, nodeId); + if (!template) { + return EMPTY_ARRAY; } - return getSortedFilteredFieldNames(map(nodeTemplate.outputs)); + return getSortedFilteredFieldNames(map(template.outputs)); }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts b/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts index edfc990882..aaca80039b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts @@ -1,18 +1,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { selectNodeData } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useUseCache = (nodeId: string) => { const selector = useMemo( () => createSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return false; - } - return node.data.useCache; + return selectNodeData(nodes, nodeId)?.useCache ?? false; }), [nodeId] ); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts index 0e4806d81b..5d79c15442 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts @@ -2,14 +2,14 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; import type { BuildWorkflowArg } from 'features/nodes/util/workflow/buildWorkflow'; import { buildWorkflowFast } from 'features/nodes/util/workflow/buildWorkflow'; import { debounce } from 'lodash-es'; import { atom } from 'nanostores'; import { useEffect } from 'react'; -export const $builtWorkflow = atom(null); +export const $builtWorkflow = atom(null); const debouncedBuildWorkflow = debounce((arg: BuildWorkflowArg) => { $builtWorkflow.set(buildWorkflowFast(arg)); diff --git a/invokeai/frontend/web/src/features/nodes/store/actions.ts b/invokeai/frontend/web/src/features/nodes/store/actions.ts index 00457494bf..b32a3ba997 100644 --- a/invokeai/frontend/web/src/features/nodes/store/actions.ts +++ b/invokeai/frontend/web/src/features/nodes/store/actions.ts @@ -1,5 +1,5 @@ import { createAction, isAnyOf } from '@reduxjs/toolkit'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; import type { Graph } from 'services/api/types'; export const textToImageGraphBuilt = createAction('nodes/textToImageGraphBuilt'); @@ -21,4 +21,4 @@ export const workflowLoadRequested = createAction<{ export const updateAllNodesRequested = createAction('nodes/updateAllNodesRequested'); -export const workflowLoaded = createAction('workflow/workflowLoaded'); +export const workflowLoaded = createAction('workflow/workflowLoaded'); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodeTemplatesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodeTemplatesSlice.ts deleted file mode 100644 index c211131aab..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/nodeTemplatesSlice.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; -import type { RootState } from 'app/store/store'; -import type { InvocationTemplate } from 'features/nodes/types/invocation'; - -import type { NodeTemplatesState } from './types'; - -export const initialNodeTemplatesState: NodeTemplatesState = { - templates: {}, -}; - -export const nodesTemplatesSlice = createSlice({ - name: 'nodeTemplates', - initialState: initialNodeTemplatesState, - reducers: { - nodeTemplatesBuilt: (state, action: PayloadAction>) => { - state.templates = action.payload; - }, - }, -}); - -export const { nodeTemplatesBuilt } = nodesTemplatesSlice.actions; - -export const selectNodeTemplatesSlice = (state: RootState) => state.nodeTemplates; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index aee01b381b..6b596da063 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -42,7 +42,7 @@ import { zT2IAdapterModelFieldValue, zVAEModelFieldValue, } from 'features/nodes/types/field'; -import type { AnyNode, NodeExecutionState } from 'features/nodes/types/invocation'; +import type { AnyNode, InvocationTemplate, NodeExecutionState } from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation'; import { cloneDeep, forEach } from 'lodash-es'; import type { @@ -92,6 +92,7 @@ export const initialNodesState: NodesState = { _version: 1, nodes: [], edges: [], + templates: {}, connectionStartParams: null, connectionStartFieldType: null, connectionMade: false, @@ -190,6 +191,7 @@ export const nodesSlice = createSlice({ node, state.nodes, state.edges, + state.templates, nodeId, handleId, handleType, @@ -224,12 +226,12 @@ export const nodesSlice = createSlice({ if (!nodeId || !handleId) { return; } - const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId); - const node = state.nodes?.[nodeIndex]; + const node = state.nodes.find((n) => n.id === nodeId); if (!isInvocationNode(node)) { return; } - const field = handleType === 'source' ? node.data.outputs[handleId] : node.data.inputs[handleId]; + const template = state.templates[node.data.type]; + const field = handleType === 'source' ? template?.outputs[handleId] : template?.inputs[handleId]; state.connectionStartFieldType = field?.type ?? null; }, connectionMade: (state, action: PayloadAction) => { @@ -260,6 +262,7 @@ export const nodesSlice = createSlice({ mouseOverNode, state.nodes, state.edges, + state.templates, nodeId, handleId, handleType, @@ -677,6 +680,9 @@ export const nodesSlice = createSlice({ selectionModeChanged: (state, action: PayloadAction) => { state.selectionMode = action.payload ? SelectionMode.Full : SelectionMode.Partial; }, + nodeTemplatesBuilt: (state, action: PayloadAction>) => { + state.templates = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(workflowLoaded, (state, action) => { @@ -808,6 +814,7 @@ export const { shouldValidateGraphChanged, viewportChanged, edgeAdded, + nodeTemplatesBuilt, } = nodesSlice.actions; // This is used for tracking `state.workflow.isTouched` diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts new file mode 100644 index 0000000000..90675d6270 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -0,0 +1,51 @@ +import type { NodesState } from 'features/nodes/store/types'; +import type { FieldInputInstance, FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; +import type { InvocationNode, InvocationNodeData, InvocationTemplate } from 'features/nodes/types/invocation'; +import { isInvocationNode } from 'features/nodes/types/invocation'; + +export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode | null => { + const node = nodesSlice.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return null; + } + return node; +}; + +export const selectNodeData = (nodesSlice: NodesState, nodeId: string): InvocationNodeData | null => { + return selectInvocationNode(nodesSlice, nodeId)?.data ?? null; +}; + +export const selectNodeTemplate = (nodesSlice: NodesState, nodeId: string): InvocationTemplate | null => { + const node = selectInvocationNode(nodesSlice, nodeId); + if (!node) { + return null; + } + return nodesSlice.templates[node.data.type] ?? null; +}; + +export const selectFieldInputInstance = ( + nodesSlice: NodesState, + nodeId: string, + fieldName: string +): FieldInputInstance | null => { + const data = selectNodeData(nodesSlice, nodeId); + return data?.inputs[fieldName] ?? null; +}; + +export const selectFieldInputTemplate = ( + nodesSlice: NodesState, + nodeId: string, + fieldName: string +): FieldInputTemplate | null => { + const template = selectNodeTemplate(nodesSlice, nodeId); + return template?.inputs[fieldName] ?? null; +}; + +export const selectFieldOutputTemplate = ( + nodesSlice: NodesState, + nodeId: string, + fieldName: string +): FieldOutputTemplate | null => { + const template = selectNodeTemplate(nodesSlice, nodeId); + return template?.outputs[fieldName] ?? null; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index 8b0de447e4..1a040d2c70 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -5,13 +5,14 @@ import type { InvocationTemplate, NodeExecutionState, } from 'features/nodes/types/invocation'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; import type { OnConnectStartParams, SelectionMode, Viewport, XYPosition } from 'reactflow'; export type NodesState = { _version: 1; nodes: AnyNode[]; edges: InvocationNodeEdge[]; + templates: Record; connectionStartParams: OnConnectStartParams | null; connectionStartFieldType: FieldType | null; connectionMade: boolean; @@ -38,7 +39,7 @@ export type FieldIdentifierWithValue = FieldIdentifier & { value: StatefulFieldValue; }; -export type WorkflowsState = Omit & { +export type WorkflowsState = Omit & { _version: 1; isTouched: boolean; mode: WorkflowMode; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts index 9f2c37a2ad..ef899c5f41 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/findConnectionToValidHandle.ts @@ -1,4 +1,6 @@ -import type { FieldInputInstance, FieldOutputInstance, FieldType } from 'features/nodes/types/field'; +import type { FieldInputTemplate, FieldOutputTemplate, FieldType } from 'features/nodes/types/field'; +import type { AnyNode, InvocationNodeEdge, InvocationTemplate } from 'features/nodes/types/invocation'; +import { isInvocationNode } from 'features/nodes/types/invocation'; import type { Connection, Edge, HandleType, Node } from 'reactflow'; import { getIsGraphAcyclic } from './getIsGraphAcyclic'; @@ -9,7 +11,7 @@ const isValidConnection = ( handleCurrentType: HandleType, handleCurrentFieldType: FieldType, node: Node, - handle: FieldInputInstance | FieldOutputInstance + handle: FieldInputTemplate | FieldOutputTemplate ) => { let isValidConnection = true; if (handleCurrentType === 'source') { @@ -38,24 +40,31 @@ const isValidConnection = ( }; export const findConnectionToValidHandle = ( - node: Node, - nodes: Node[], - edges: Edge[], + node: AnyNode, + nodes: AnyNode[], + edges: InvocationNodeEdge[], + templates: Record, handleCurrentNodeId: string, handleCurrentName: string, handleCurrentType: HandleType, handleCurrentFieldType: FieldType ): Connection | null => { - if (node.id === handleCurrentNodeId) { + if (node.id === handleCurrentNodeId || !isInvocationNode(node)) { return null; } - const handles = handleCurrentType === 'source' ? node.data.inputs : node.data.outputs; + const template = templates[node.data.type]; + + if (!template) { + return null; + } + + const handles = handleCurrentType === 'source' ? template.inputs : template.outputs; //Prioritize handles whos name matches the node we're coming from - if (handles[handleCurrentName]) { - const handle = handles[handleCurrentName]; + const handle = handles[handleCurrentName]; + if (handle) { const sourceID = handleCurrentType === 'source' ? handleCurrentNodeId : node.id; const targetID = handleCurrentType === 'source' ? node.id : handleCurrentNodeId; const sourceHandle = handleCurrentType === 'source' ? handleCurrentName : handle.name; @@ -77,6 +86,9 @@ export const findConnectionToValidHandle = ( for (const handleName in handles) { const handle = handles[handleName]; + if (!handle) { + continue; + } const sourceID = handleCurrentType === 'source' ? handleCurrentNodeId : node.id; const targetID = handleCurrentType === 'source' ? node.id : handleCurrentNodeId; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index 8575932cbd..d6ea0d9c86 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -16,7 +16,7 @@ export const makeConnectionErrorSelector = ( nodeId: string, fieldName: string, handleType: HandleType, - fieldType?: FieldType + fieldType?: FieldType | null ) => { return createSelector(selectNodesSlice, (nodesSlice) => { if (!fieldType) { diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 2978f25138..4f40a68e1f 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -10,10 +10,10 @@ import type { } from 'features/nodes/store/types'; import type { FieldIdentifier } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; -import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowCategory, WorkflowV3 } from 'features/nodes/types/workflow'; import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es'; -export const blankWorkflow: Omit = { +export const blankWorkflow: Omit = { name: '', author: '', description: '', @@ -22,7 +22,7 @@ export const blankWorkflow: Omit = { tags: '', notes: '', exposedFields: [], - meta: { version: '2.0.0', category: 'user' }, + meta: { version: '3.0.0', category: 'user' }, id: undefined, }; diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 38f1af55dd..aa6164d6e5 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -46,20 +46,11 @@ export type FieldInput = z.infer; export const zFieldUIComponent = z.enum(['none', 'textarea', 'slider']); export type FieldUIComponent = z.infer; -export const zFieldInstanceBase = z.object({ - id: z.string().trim().min(1), +export const zFieldInputInstanceBase = z.object({ name: z.string().trim().min(1), -}); -export const zFieldInputInstanceBase = zFieldInstanceBase.extend({ - fieldKind: z.literal('input'), label: z.string().nullish(), }); -export const zFieldOutputInstanceBase = zFieldInstanceBase.extend({ - fieldKind: z.literal('output'), -}); -export type FieldInstanceBase = z.infer; export type FieldInputInstanceBase = z.infer; -export type FieldOutputInstanceBase = z.infer; export const zFieldTemplateBase = z.object({ name: z.string().min(1), @@ -102,12 +93,8 @@ export const zIntegerFieldType = zFieldTypeBase.extend({ }); export const zIntegerFieldValue = z.number().int(); export const zIntegerFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zIntegerFieldType, value: zIntegerFieldValue, }); -export const zIntegerFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zIntegerFieldType, -}); export const zIntegerFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zIntegerFieldType, default: zIntegerFieldValue, @@ -136,12 +123,8 @@ export const zFloatFieldType = zFieldTypeBase.extend({ }); export const zFloatFieldValue = z.number(); export const zFloatFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zFloatFieldType, value: zFloatFieldValue, }); -export const zFloatFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zFloatFieldType, -}); export const zFloatFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zFloatFieldType, default: zFloatFieldValue, @@ -157,7 +140,6 @@ export const zFloatFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type FloatFieldType = z.infer; export type FloatFieldValue = z.infer; export type FloatFieldInputInstance = z.infer; -export type FloatFieldOutputInstance = z.infer; export type FloatFieldInputTemplate = z.infer; export type FloatFieldOutputTemplate = z.infer; export const isFloatFieldInputInstance = (val: unknown): val is FloatFieldInputInstance => @@ -172,12 +154,8 @@ export const zStringFieldType = zFieldTypeBase.extend({ }); export const zStringFieldValue = z.string(); export const zStringFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zStringFieldType, value: zStringFieldValue, }); -export const zStringFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zStringFieldType, -}); export const zStringFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zStringFieldType, default: zStringFieldValue, @@ -191,7 +169,6 @@ export const zStringFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type StringFieldType = z.infer; export type StringFieldValue = z.infer; export type StringFieldInputInstance = z.infer; -export type StringFieldOutputInstance = z.infer; export type StringFieldInputTemplate = z.infer; export type StringFieldOutputTemplate = z.infer; export const isStringFieldInputInstance = (val: unknown): val is StringFieldInputInstance => @@ -206,12 +183,8 @@ export const zBooleanFieldType = zFieldTypeBase.extend({ }); export const zBooleanFieldValue = z.boolean(); export const zBooleanFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zBooleanFieldType, value: zBooleanFieldValue, }); -export const zBooleanFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zBooleanFieldType, -}); export const zBooleanFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zBooleanFieldType, default: zBooleanFieldValue, @@ -222,7 +195,6 @@ export const zBooleanFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type BooleanFieldType = z.infer; export type BooleanFieldValue = z.infer; export type BooleanFieldInputInstance = z.infer; -export type BooleanFieldOutputInstance = z.infer; export type BooleanFieldInputTemplate = z.infer; export type BooleanFieldOutputTemplate = z.infer; export const isBooleanFieldInputInstance = (val: unknown): val is BooleanFieldInputInstance => @@ -237,12 +209,8 @@ export const zEnumFieldType = zFieldTypeBase.extend({ }); export const zEnumFieldValue = z.string(); export const zEnumFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zEnumFieldType, value: zEnumFieldValue, }); -export const zEnumFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zEnumFieldType, -}); export const zEnumFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zEnumFieldType, default: zEnumFieldValue, @@ -255,7 +223,6 @@ export const zEnumFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type EnumFieldType = z.infer; export type EnumFieldValue = z.infer; export type EnumFieldInputInstance = z.infer; -export type EnumFieldOutputInstance = z.infer; export type EnumFieldInputTemplate = z.infer; export type EnumFieldOutputTemplate = z.infer; export const isEnumFieldInputInstance = (val: unknown): val is EnumFieldInputInstance => @@ -270,12 +237,8 @@ export const zImageFieldType = zFieldTypeBase.extend({ }); export const zImageFieldValue = zImageField.optional(); export const zImageFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zImageFieldType, value: zImageFieldValue, }); -export const zImageFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zImageFieldType, -}); export const zImageFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zImageFieldType, default: zImageFieldValue, @@ -286,7 +249,6 @@ export const zImageFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type ImageFieldType = z.infer; export type ImageFieldValue = z.infer; export type ImageFieldInputInstance = z.infer; -export type ImageFieldOutputInstance = z.infer; export type ImageFieldInputTemplate = z.infer; export type ImageFieldOutputTemplate = z.infer; export const isImageFieldInputInstance = (val: unknown): val is ImageFieldInputInstance => @@ -301,12 +263,8 @@ export const zBoardFieldType = zFieldTypeBase.extend({ }); export const zBoardFieldValue = zBoardField.optional(); export const zBoardFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zBoardFieldType, value: zBoardFieldValue, }); -export const zBoardFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zBoardFieldType, -}); export const zBoardFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zBoardFieldType, default: zBoardFieldValue, @@ -317,7 +275,6 @@ export const zBoardFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type BoardFieldType = z.infer; export type BoardFieldValue = z.infer; export type BoardFieldInputInstance = z.infer; -export type BoardFieldOutputInstance = z.infer; export type BoardFieldInputTemplate = z.infer; export type BoardFieldOutputTemplate = z.infer; export const isBoardFieldInputInstance = (val: unknown): val is BoardFieldInputInstance => @@ -332,12 +289,8 @@ export const zColorFieldType = zFieldTypeBase.extend({ }); export const zColorFieldValue = zColorField.optional(); export const zColorFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zColorFieldType, value: zColorFieldValue, }); -export const zColorFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zColorFieldType, -}); export const zColorFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zColorFieldType, default: zColorFieldValue, @@ -348,7 +301,6 @@ export const zColorFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type ColorFieldType = z.infer; export type ColorFieldValue = z.infer; export type ColorFieldInputInstance = z.infer; -export type ColorFieldOutputInstance = z.infer; export type ColorFieldInputTemplate = z.infer; export type ColorFieldOutputTemplate = z.infer; export const isColorFieldInputInstance = (val: unknown): val is ColorFieldInputInstance => @@ -363,12 +315,8 @@ export const zMainModelFieldType = zFieldTypeBase.extend({ }); export const zMainModelFieldValue = zMainModelField.optional(); export const zMainModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zMainModelFieldType, value: zMainModelFieldValue, }); -export const zMainModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zMainModelFieldType, -}); export const zMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zMainModelFieldType, default: zMainModelFieldValue, @@ -379,7 +327,6 @@ export const zMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type MainModelFieldType = z.infer; export type MainModelFieldValue = z.infer; export type MainModelFieldInputInstance = z.infer; -export type MainModelFieldOutputInstance = z.infer; export type MainModelFieldInputTemplate = z.infer; export type MainModelFieldOutputTemplate = z.infer; export const isMainModelFieldInputInstance = (val: unknown): val is MainModelFieldInputInstance => @@ -394,12 +341,8 @@ export const zSDXLMainModelFieldType = zFieldTypeBase.extend({ }); export const zSDXLMainModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL models only. export const zSDXLMainModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zSDXLMainModelFieldType, value: zSDXLMainModelFieldValue, }); -export const zSDXLMainModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zSDXLMainModelFieldType, -}); export const zSDXLMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zSDXLMainModelFieldType, default: zSDXLMainModelFieldValue, @@ -410,7 +353,6 @@ export const zSDXLMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend export type SDXLMainModelFieldType = z.infer; export type SDXLMainModelFieldValue = z.infer; export type SDXLMainModelFieldInputInstance = z.infer; -export type SDXLMainModelFieldOutputInstance = z.infer; export type SDXLMainModelFieldInputTemplate = z.infer; export type SDXLMainModelFieldOutputTemplate = z.infer; export const isSDXLMainModelFieldInputInstance = (val: unknown): val is SDXLMainModelFieldInputInstance => @@ -425,12 +367,8 @@ export const zSDXLRefinerModelFieldType = zFieldTypeBase.extend({ }); export const zSDXLRefinerModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL Refiner models only. export const zSDXLRefinerModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zSDXLRefinerModelFieldType, value: zSDXLRefinerModelFieldValue, }); -export const zSDXLRefinerModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zSDXLRefinerModelFieldType, -}); export const zSDXLRefinerModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zSDXLRefinerModelFieldType, default: zSDXLRefinerModelFieldValue, @@ -441,7 +379,6 @@ export const zSDXLRefinerModelFieldOutputTemplate = zFieldOutputTemplateBase.ext export type SDXLRefinerModelFieldType = z.infer; export type SDXLRefinerModelFieldValue = z.infer; export type SDXLRefinerModelFieldInputInstance = z.infer; -export type SDXLRefinerModelFieldOutputInstance = z.infer; export type SDXLRefinerModelFieldInputTemplate = z.infer; export type SDXLRefinerModelFieldOutputTemplate = z.infer; export const isSDXLRefinerModelFieldInputInstance = (val: unknown): val is SDXLRefinerModelFieldInputInstance => @@ -456,12 +393,8 @@ export const zVAEModelFieldType = zFieldTypeBase.extend({ }); export const zVAEModelFieldValue = zVAEModelField.optional(); export const zVAEModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zVAEModelFieldType, value: zVAEModelFieldValue, }); -export const zVAEModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zVAEModelFieldType, -}); export const zVAEModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zVAEModelFieldType, default: zVAEModelFieldValue, @@ -472,7 +405,6 @@ export const zVAEModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type VAEModelFieldType = z.infer; export type VAEModelFieldValue = z.infer; export type VAEModelFieldInputInstance = z.infer; -export type VAEModelFieldOutputInstance = z.infer; export type VAEModelFieldInputTemplate = z.infer; export type VAEModelFieldOutputTemplate = z.infer; export const isVAEModelFieldInputInstance = (val: unknown): val is VAEModelFieldInputInstance => @@ -487,12 +419,8 @@ export const zLoRAModelFieldType = zFieldTypeBase.extend({ }); export const zLoRAModelFieldValue = zLoRAModelField.optional(); export const zLoRAModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zLoRAModelFieldType, value: zLoRAModelFieldValue, }); -export const zLoRAModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zLoRAModelFieldType, -}); export const zLoRAModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zLoRAModelFieldType, default: zLoRAModelFieldValue, @@ -503,7 +431,6 @@ export const zLoRAModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type LoRAModelFieldType = z.infer; export type LoRAModelFieldValue = z.infer; export type LoRAModelFieldInputInstance = z.infer; -export type LoRAModelFieldOutputInstance = z.infer; export type LoRAModelFieldInputTemplate = z.infer; export type LoRAModelFieldOutputTemplate = z.infer; export const isLoRAModelFieldInputInstance = (val: unknown): val is LoRAModelFieldInputInstance => @@ -518,12 +445,8 @@ export const zControlNetModelFieldType = zFieldTypeBase.extend({ }); export const zControlNetModelFieldValue = zControlNetModelField.optional(); export const zControlNetModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zControlNetModelFieldType, value: zControlNetModelFieldValue, }); -export const zControlNetModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zControlNetModelFieldType, -}); export const zControlNetModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zControlNetModelFieldType, default: zControlNetModelFieldValue, @@ -534,7 +457,6 @@ export const zControlNetModelFieldOutputTemplate = zFieldOutputTemplateBase.exte export type ControlNetModelFieldType = z.infer; export type ControlNetModelFieldValue = z.infer; export type ControlNetModelFieldInputInstance = z.infer; -export type ControlNetModelFieldOutputInstance = z.infer; export type ControlNetModelFieldInputTemplate = z.infer; export type ControlNetModelFieldOutputTemplate = z.infer; export const isControlNetModelFieldInputInstance = (val: unknown): val is ControlNetModelFieldInputInstance => @@ -551,12 +473,8 @@ export const zIPAdapterModelFieldType = zFieldTypeBase.extend({ }); export const zIPAdapterModelFieldValue = zIPAdapterModelField.optional(); export const zIPAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zIPAdapterModelFieldType, value: zIPAdapterModelFieldValue, }); -export const zIPAdapterModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zIPAdapterModelFieldType, -}); export const zIPAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zIPAdapterModelFieldType, default: zIPAdapterModelFieldValue, @@ -567,7 +485,6 @@ export const zIPAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.exten export type IPAdapterModelFieldType = z.infer; export type IPAdapterModelFieldValue = z.infer; export type IPAdapterModelFieldInputInstance = z.infer; -export type IPAdapterModelFieldOutputInstance = z.infer; export type IPAdapterModelFieldInputTemplate = z.infer; export type IPAdapterModelFieldOutputTemplate = z.infer; export const isIPAdapterModelFieldInputInstance = (val: unknown): val is IPAdapterModelFieldInputInstance => @@ -584,12 +501,8 @@ export const zT2IAdapterModelFieldType = zFieldTypeBase.extend({ }); export const zT2IAdapterModelFieldValue = zT2IAdapterModelField.optional(); export const zT2IAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zT2IAdapterModelFieldType, value: zT2IAdapterModelFieldValue, }); -export const zT2IAdapterModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zT2IAdapterModelFieldType, -}); export const zT2IAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zT2IAdapterModelFieldType, default: zT2IAdapterModelFieldValue, @@ -600,7 +513,6 @@ export const zT2IAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.exte export type T2IAdapterModelFieldType = z.infer; export type T2IAdapterModelFieldValue = z.infer; export type T2IAdapterModelFieldInputInstance = z.infer; -export type T2IAdapterModelFieldOutputInstance = z.infer; export type T2IAdapterModelFieldInputTemplate = z.infer; export type T2IAdapterModelFieldOutputTemplate = z.infer; export const isT2IAdapterModelFieldInputInstance = (val: unknown): val is T2IAdapterModelFieldInputInstance => @@ -615,12 +527,8 @@ export const zSchedulerFieldType = zFieldTypeBase.extend({ }); export const zSchedulerFieldValue = zSchedulerField.optional(); export const zSchedulerFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zSchedulerFieldType, value: zSchedulerFieldValue, }); -export const zSchedulerFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zSchedulerFieldType, -}); export const zSchedulerFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zSchedulerFieldType, default: zSchedulerFieldValue, @@ -631,7 +539,6 @@ export const zSchedulerFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type SchedulerFieldType = z.infer; export type SchedulerFieldValue = z.infer; export type SchedulerFieldInputInstance = z.infer; -export type SchedulerFieldOutputInstance = z.infer; export type SchedulerFieldInputTemplate = z.infer; export type SchedulerFieldOutputTemplate = z.infer; export const isSchedulerFieldInputInstance = (val: unknown): val is SchedulerFieldInputInstance => @@ -657,12 +564,8 @@ export const zStatelessFieldType = zFieldTypeBase.extend({ }); export const zStatelessFieldValue = z.undefined().catch(undefined); // stateless --> no value, but making this z.never() introduces a lot of extra TS fanagling export const zStatelessFieldInputInstance = zFieldInputInstanceBase.extend({ - type: zStatelessFieldType, value: zStatelessFieldValue, }); -export const zStatelessFieldOutputInstance = zFieldOutputInstanceBase.extend({ - type: zStatelessFieldType, -}); export const zStatelessFieldInputTemplate = zFieldInputTemplateBase.extend({ type: zStatelessFieldType, default: zStatelessFieldValue, @@ -675,7 +578,6 @@ export const zStatelessFieldOutputTemplate = zFieldOutputTemplateBase.extend({ export type StatelessFieldType = z.infer; export type StatelessFieldValue = z.infer; export type StatelessFieldInputInstance = z.infer; -export type StatelessFieldOutputInstance = z.infer; export type StatelessFieldInputTemplate = z.infer; export type StatelessFieldOutputTemplate = z.infer; // #endregion @@ -783,36 +685,6 @@ export const isFieldInputInstance = (val: unknown): val is FieldInputInstance => zFieldInputInstance.safeParse(val).success; // #endregion -// #region StatefulFieldOutputInstance & FieldOutputInstance -export const zStatefulFieldOutputInstance = z.union([ - zIntegerFieldOutputInstance, - zFloatFieldOutputInstance, - zStringFieldOutputInstance, - zBooleanFieldOutputInstance, - zEnumFieldOutputInstance, - zImageFieldOutputInstance, - zBoardFieldOutputInstance, - zMainModelFieldOutputInstance, - zSDXLMainModelFieldOutputInstance, - zSDXLRefinerModelFieldOutputInstance, - zVAEModelFieldOutputInstance, - zLoRAModelFieldOutputInstance, - zControlNetModelFieldOutputInstance, - zIPAdapterModelFieldOutputInstance, - zT2IAdapterModelFieldOutputInstance, - zColorFieldOutputInstance, - zSchedulerFieldOutputInstance, -]); -export type StatefulFieldOutputInstance = z.infer; -export const isStatefulFieldOutputInstance = (val: unknown): val is StatefulFieldOutputInstance => - zStatefulFieldOutputInstance.safeParse(val).success; - -export const zFieldOutputInstance = z.union([zStatefulFieldOutputInstance, zStatelessFieldOutputInstance]); -export type FieldOutputInstance = z.infer; -export const isFieldOutputInstance = (val: unknown): val is FieldOutputInstance => - zFieldOutputInstance.safeParse(val).success; -// #endregion - // #region StatefulFieldInputTemplate & FieldInputTemplate export const zStatefulFieldInputTemplate = z.union([ zIntegerFieldInputTemplate, diff --git a/invokeai/frontend/web/src/features/nodes/types/invocation.ts b/invokeai/frontend/web/src/features/nodes/types/invocation.ts index 86ec70fd9b..5ccb19430d 100644 --- a/invokeai/frontend/web/src/features/nodes/types/invocation.ts +++ b/invokeai/frontend/web/src/features/nodes/types/invocation.ts @@ -2,7 +2,7 @@ import type { Edge, Node } from 'reactflow'; import { z } from 'zod'; import { zClassification, zProgressImage } from './common'; -import { zFieldInputInstance, zFieldInputTemplate, zFieldOutputInstance, zFieldOutputTemplate } from './field'; +import { zFieldInputInstance, zFieldInputTemplate, zFieldOutputTemplate } from './field'; import { zSemVer } from './semver'; // #region InvocationTemplate @@ -25,16 +25,15 @@ export type InvocationTemplate = z.infer; // #region NodeData export const zInvocationNodeData = z.object({ id: z.string().trim().min(1), - type: z.string().trim().min(1), - label: z.string(), - isOpen: z.boolean(), - notes: z.string(), - isIntermediate: z.boolean(), - useCache: z.boolean(), version: zSemVer, nodePack: z.string().min(1).nullish(), + label: z.string(), + notes: z.string(), + type: z.string().trim().min(1), inputs: z.record(zFieldInputInstance), - outputs: z.record(zFieldOutputInstance), + isOpen: z.boolean(), + isIntermediate: z.boolean(), + useCache: z.boolean(), }); export const zNotesNodeData = z.object({ @@ -62,11 +61,12 @@ export type NotesNode = Node; export type CurrentImageNode = Node; export type AnyNode = Node; -export const isInvocationNode = (node?: AnyNode): node is InvocationNode => Boolean(node && node.type === 'invocation'); -export const isNotesNode = (node?: AnyNode): node is NotesNode => Boolean(node && node.type === 'notes'); -export const isCurrentImageNode = (node?: AnyNode): node is CurrentImageNode => +export const isInvocationNode = (node?: AnyNode | null): node is InvocationNode => + Boolean(node && node.type === 'invocation'); +export const isNotesNode = (node?: AnyNode | null): node is NotesNode => Boolean(node && node.type === 'notes'); +export const isCurrentImageNode = (node?: AnyNode | null): node is CurrentImageNode => Boolean(node && node.type === 'current_image'); -export const isInvocationNodeData = (node?: AnyNodeData): node is InvocationNodeData => +export const isInvocationNodeData = (node?: AnyNodeData | null): node is InvocationNodeData => Boolean(node && !['notes', 'current_image'].includes(node.type)); // node.type may be 'notes', 'current_image', or any invocation type // #endregion diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/common.ts b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts new file mode 100644 index 0000000000..b524474379 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/common.ts @@ -0,0 +1,188 @@ +import { z } from 'zod'; + +// #region Field data schemas +export const zImageField = z.object({ + image_name: z.string().trim().min(1), +}); +export type ImageField = z.infer; + +export const zBoardField = z.object({ + board_id: z.string().trim().min(1), +}); +export type BoardField = z.infer; + +export const zColorField = z.object({ + r: z.number().int().min(0).max(255), + g: z.number().int().min(0).max(255), + b: z.number().int().min(0).max(255), + a: z.number().int().min(0).max(255), +}); +export type ColorField = z.infer; + +export const zClassification = z.enum(['stable', 'beta', 'prototype']); +export type Classification = z.infer; + +export const zSchedulerField = z.enum([ + 'euler', + 'deis', + 'ddim', + 'ddpm', + 'dpmpp_2s', + 'dpmpp_2m', + 'dpmpp_2m_sde', + 'dpmpp_sde', + 'heun', + 'kdpm_2', + 'lms', + 'pndm', + 'unipc', + 'euler_k', + 'dpmpp_2s_k', + 'dpmpp_2m_k', + 'dpmpp_2m_sde_k', + 'dpmpp_sde_k', + 'heun_k', + 'lms_k', + 'euler_a', + 'kdpm_2_a', + 'lcm', +]); +export type SchedulerField = z.infer; +// #endregion + +// #region Model-related schemas +export const zBaseModel = z.enum(['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']); +export const zModelType = z.enum(['main', 'vae', 'lora', 'controlnet', 'embedding']); +export const zModelName = z.string().min(3); +export const zModelIdentifier = z.object({ + model_name: zModelName, + base_model: zBaseModel, +}); +export type BaseModel = z.infer; +export type ModelType = z.infer; +export type ModelIdentifier = z.infer; + +export const zMainModelField = z.object({ + model_name: zModelName, + base_model: zBaseModel, + model_type: z.literal('main'), +}); +export const zSDXLRefinerModelField = z.object({ + model_name: z.string().min(1), + base_model: z.literal('sdxl-refiner'), + model_type: z.literal('main'), +}); +export type MainModelField = z.infer; +export type SDXLRefinerModelField = z.infer; + +export const zSubModelType = z.enum([ + 'unet', + 'text_encoder', + 'text_encoder_2', + 'tokenizer', + 'tokenizer_2', + 'vae', + 'vae_decoder', + 'vae_encoder', + 'scheduler', + 'safety_checker', +]); +export type SubModelType = z.infer; + +export const zVAEModelField = zModelIdentifier; + +export const zModelInfo = zModelIdentifier.extend({ + model_type: zModelType, + submodel: zSubModelType.optional(), +}); +export type ModelInfo = z.infer; + +export const zLoRAModelField = zModelIdentifier; +export type LoRAModelField = z.infer; + +export const zControlNetModelField = zModelIdentifier; +export type ControlNetModelField = z.infer; + +export const zIPAdapterModelField = zModelIdentifier; +export type IPAdapterModelField = z.infer; + +export const zT2IAdapterModelField = zModelIdentifier; +export type T2IAdapterModelField = z.infer; + +export const zLoraInfo = zModelInfo.extend({ + weight: z.number().optional(), +}); +export type LoraInfo = z.infer; + +export const zUNetField = z.object({ + unet: zModelInfo, + scheduler: zModelInfo, + loras: z.array(zLoraInfo), +}); +export type UNetField = z.infer; + +export const zCLIPField = z.object({ + tokenizer: zModelInfo, + text_encoder: zModelInfo, + skipped_layers: z.number(), + loras: z.array(zLoraInfo), +}); +export type CLIPField = z.infer; + +export const zVAEField = z.object({ + vae: zModelInfo, +}); +export type VAEField = z.infer; +// #endregion + +// #region Control Adapters +export const zControlField = z.object({ + image: zImageField, + control_model: zControlNetModelField, + control_weight: z.union([z.number(), z.array(z.number())]).optional(), + begin_step_percent: z.number().optional(), + end_step_percent: z.number().optional(), + control_mode: z.enum(['balanced', 'more_prompt', 'more_control', 'unbalanced']).optional(), + resize_mode: z.enum(['just_resize', 'crop_resize', 'fill_resize', 'just_resize_simple']).optional(), +}); +export type ControlField = z.infer; + +export const zIPAdapterField = z.object({ + image: zImageField, + ip_adapter_model: zIPAdapterModelField, + weight: z.number(), + begin_step_percent: z.number().optional(), + end_step_percent: z.number().optional(), +}); +export type IPAdapterField = z.infer; + +export const zT2IAdapterField = z.object({ + image: zImageField, + t2i_adapter_model: zT2IAdapterModelField, + weight: z.union([z.number(), z.array(z.number())]).optional(), + begin_step_percent: z.number().optional(), + end_step_percent: z.number().optional(), + resize_mode: z.enum(['just_resize', 'crop_resize', 'fill_resize', 'just_resize_simple']).optional(), +}); +export type T2IAdapterField = z.infer; +// #endregion + +// #region ProgressImage +export const zProgressImage = z.object({ + dataURL: z.string(), + width: z.number().int(), + height: z.number().int(), +}); +export type ProgressImage = z.infer; +// #endregion + +// #region ImageOutput +export const zImageOutput = z.object({ + image: zImageField, + width: z.number().int().gt(0), + height: z.number().int().gt(0), + type: z.literal('image_output'), +}); +export type ImageOutput = z.infer; +export const isImageOutput = (output: unknown): output is ImageOutput => zImageOutput.safeParse(output).success; +// #endregion diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/constants.ts b/invokeai/frontend/web/src/features/nodes/types/v2/constants.ts new file mode 100644 index 0000000000..35ef9e9fd2 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/constants.ts @@ -0,0 +1,80 @@ +import type { Node } from 'reactflow'; + +/** + * How long to wait before showing a tooltip when hovering a field handle. + */ +export const HANDLE_TOOLTIP_OPEN_DELAY = 500; + +/** + * The width of a node in the UI in pixels. + */ +export const NODE_WIDTH = 320; + +/** + * This class name is special - reactflow uses it to identify the drag handle of a node, + * applying the appropriate listeners to it. + */ +export const DRAG_HANDLE_CLASSNAME = 'node-drag-handle'; + +/** + * reactflow-specifc properties shared between all node types. + */ +export const SHARED_NODE_PROPERTIES: Partial = { + dragHandle: `.${DRAG_HANDLE_CLASSNAME}`, +}; + +/** + * Helper for getting the kind of a field. + */ +export const KIND_MAP = { + input: 'inputs' as const, + output: 'outputs' as const, +}; + +/** + * Model types' handles are rendered as squares in the UI. + */ +export const MODEL_TYPES = [ + 'IPAdapterModelField', + 'ControlNetModelField', + 'LoRAModelField', + 'MainModelField', + 'SDXLMainModelField', + 'SDXLRefinerModelField', + 'VaeModelField', + 'UNetField', + 'VaeField', + 'ClipField', + 'T2IAdapterModelField', + 'IPAdapterModelField', +]; + +/** + * Colors for each field type - applies to their handles and edges. + */ +export const FIELD_COLORS: { [key: string]: string } = { + BoardField: 'purple.500', + BooleanField: 'green.500', + ClipField: 'green.500', + ColorField: 'pink.300', + ConditioningField: 'cyan.500', + ControlField: 'teal.500', + ControlNetModelField: 'teal.500', + EnumField: 'blue.500', + FloatField: 'orange.500', + ImageField: 'purple.500', + IntegerField: 'red.500', + IPAdapterField: 'teal.500', + IPAdapterModelField: 'teal.500', + LatentsField: 'pink.500', + LoRAModelField: 'teal.500', + MainModelField: 'teal.500', + SDXLMainModelField: 'teal.500', + SDXLRefinerModelField: 'teal.500', + StringField: 'yellow.500', + T2IAdapterField: 'teal.500', + T2IAdapterModelField: 'teal.500', + UNetField: 'red.500', + VaeField: 'blue.500', + VaeModelField: 'teal.500', +}; diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/error.ts b/invokeai/frontend/web/src/features/nodes/types/v2/error.ts new file mode 100644 index 0000000000..905b487fb0 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/error.ts @@ -0,0 +1,58 @@ +/** + * Invalid Workflow Version Error + * Raised when a workflow version is not recognized. + */ +export class WorkflowVersionError extends Error { + /** + * Create WorkflowVersionError + * @param {String} message + */ + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} +/** + * Workflow Migration Error + * Raised when a workflow migration fails. + */ +export class WorkflowMigrationError extends Error { + /** + * Create WorkflowMigrationError + * @param {String} message + */ + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +/** + * Unable to Update Node Error + * Raised when a node cannot be updated. + */ +export class NodeUpdateError extends Error { + /** + * Create NodeUpdateError + * @param {String} message + */ + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +/** + * FieldParseError + * Raised when a field cannot be parsed from a field schema. + */ +export class FieldParseError extends Error { + /** + * Create FieldTypeParseError + * @param {String} message + */ + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/field.ts b/invokeai/frontend/web/src/features/nodes/types/v2/field.ts new file mode 100644 index 0000000000..38f1af55dd --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/field.ts @@ -0,0 +1,875 @@ +import { z } from 'zod'; + +import { + zBoardField, + zColorField, + zControlNetModelField, + zImageField, + zIPAdapterModelField, + zLoRAModelField, + zMainModelField, + zSchedulerField, + zT2IAdapterModelField, + zVAEModelField, +} from './common'; + +/** + * zod schemas & inferred types for fields. + * + * These schemas and types are only required for stateful field - fields that have UI components + * and allow the user to directly provide values. + * + * This includes primitive values (numbers, strings, booleans), models, scheduler, etc. + * + * If a field type does not have a UI component, then it does not need to be included here, because + * we never store its value. Such field types will be handled via the "StatelessField" logic. + * + * Fields require: + * - zFieldType - zod schema for the field type + * - zFieldValue - zod schema for the field value + * - zFieldInputInstance - zod schema for the field's input instance + * - zFieldOutputInstance - zod schema for the field's output instance + * - zFieldInputTemplate - zod schema for the field's input template + * - zFieldOutputTemplate - zod schema for the field's output template + * - inferred types for each schema + * - type guards for InputInstance and InputTemplate + * + * These then must be added to the unions at the bottom of this file. + */ + +/** */ + +// #region Base schemas & misc +export const zFieldInput = z.enum(['connection', 'direct', 'any']); +export type FieldInput = z.infer; + +export const zFieldUIComponent = z.enum(['none', 'textarea', 'slider']); +export type FieldUIComponent = z.infer; + +export const zFieldInstanceBase = z.object({ + id: z.string().trim().min(1), + name: z.string().trim().min(1), +}); +export const zFieldInputInstanceBase = zFieldInstanceBase.extend({ + fieldKind: z.literal('input'), + label: z.string().nullish(), +}); +export const zFieldOutputInstanceBase = zFieldInstanceBase.extend({ + fieldKind: z.literal('output'), +}); +export type FieldInstanceBase = z.infer; +export type FieldInputInstanceBase = z.infer; +export type FieldOutputInstanceBase = z.infer; + +export const zFieldTemplateBase = z.object({ + name: z.string().min(1), + title: z.string().min(1), + description: z.string().nullish(), + ui_hidden: z.boolean(), + ui_type: z.string().nullish(), + ui_order: z.number().int().nullish(), +}); +export const zFieldInputTemplateBase = zFieldTemplateBase.extend({ + fieldKind: z.literal('input'), + input: zFieldInput, + required: z.boolean(), + ui_component: zFieldUIComponent.nullish(), + ui_choice_labels: z.record(z.string()).nullish(), +}); +export const zFieldOutputTemplateBase = zFieldTemplateBase.extend({ + fieldKind: z.literal('output'), +}); +export type FieldTemplateBase = z.infer; +export type FieldInputTemplateBase = z.infer; +export type FieldOutputTemplateBase = z.infer; + +export const zFieldTypeBase = z.object({ + isCollection: z.boolean(), + isCollectionOrScalar: z.boolean(), +}); + +export const zFieldIdentifier = z.object({ + nodeId: z.string().trim().min(1), + fieldName: z.string().trim().min(1), +}); +export type FieldIdentifier = z.infer; +export const isFieldIdentifier = (val: unknown): val is FieldIdentifier => zFieldIdentifier.safeParse(val).success; +// #endregion + +// #region IntegerField +export const zIntegerFieldType = zFieldTypeBase.extend({ + name: z.literal('IntegerField'), +}); +export const zIntegerFieldValue = z.number().int(); +export const zIntegerFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zIntegerFieldType, + value: zIntegerFieldValue, +}); +export const zIntegerFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zIntegerFieldType, +}); +export const zIntegerFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zIntegerFieldType, + default: zIntegerFieldValue, + multipleOf: z.number().int().optional(), + maximum: z.number().int().optional(), + exclusiveMaximum: z.number().int().optional(), + minimum: z.number().int().optional(), + exclusiveMinimum: z.number().int().optional(), +}); +export const zIntegerFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zIntegerFieldType, +}); +export type IntegerFieldType = z.infer; +export type IntegerFieldValue = z.infer; +export type IntegerFieldInputInstance = z.infer; +export type IntegerFieldInputTemplate = z.infer; +export const isIntegerFieldInputInstance = (val: unknown): val is IntegerFieldInputInstance => + zIntegerFieldInputInstance.safeParse(val).success; +export const isIntegerFieldInputTemplate = (val: unknown): val is IntegerFieldInputTemplate => + zIntegerFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region FloatField +export const zFloatFieldType = zFieldTypeBase.extend({ + name: z.literal('FloatField'), +}); +export const zFloatFieldValue = z.number(); +export const zFloatFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zFloatFieldType, + value: zFloatFieldValue, +}); +export const zFloatFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zFloatFieldType, +}); +export const zFloatFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zFloatFieldType, + default: zFloatFieldValue, + multipleOf: z.number().optional(), + maximum: z.number().optional(), + exclusiveMaximum: z.number().optional(), + minimum: z.number().optional(), + exclusiveMinimum: z.number().optional(), +}); +export const zFloatFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zFloatFieldType, +}); +export type FloatFieldType = z.infer; +export type FloatFieldValue = z.infer; +export type FloatFieldInputInstance = z.infer; +export type FloatFieldOutputInstance = z.infer; +export type FloatFieldInputTemplate = z.infer; +export type FloatFieldOutputTemplate = z.infer; +export const isFloatFieldInputInstance = (val: unknown): val is FloatFieldInputInstance => + zFloatFieldInputInstance.safeParse(val).success; +export const isFloatFieldInputTemplate = (val: unknown): val is FloatFieldInputTemplate => + zFloatFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region StringField +export const zStringFieldType = zFieldTypeBase.extend({ + name: z.literal('StringField'), +}); +export const zStringFieldValue = z.string(); +export const zStringFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zStringFieldType, + value: zStringFieldValue, +}); +export const zStringFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zStringFieldType, +}); +export const zStringFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zStringFieldType, + default: zStringFieldValue, + maxLength: z.number().int().optional(), + minLength: z.number().int().optional(), +}); +export const zStringFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zStringFieldType, +}); + +export type StringFieldType = z.infer; +export type StringFieldValue = z.infer; +export type StringFieldInputInstance = z.infer; +export type StringFieldOutputInstance = z.infer; +export type StringFieldInputTemplate = z.infer; +export type StringFieldOutputTemplate = z.infer; +export const isStringFieldInputInstance = (val: unknown): val is StringFieldInputInstance => + zStringFieldInputInstance.safeParse(val).success; +export const isStringFieldInputTemplate = (val: unknown): val is StringFieldInputTemplate => + zStringFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region BooleanField +export const zBooleanFieldType = zFieldTypeBase.extend({ + name: z.literal('BooleanField'), +}); +export const zBooleanFieldValue = z.boolean(); +export const zBooleanFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zBooleanFieldType, + value: zBooleanFieldValue, +}); +export const zBooleanFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zBooleanFieldType, +}); +export const zBooleanFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zBooleanFieldType, + default: zBooleanFieldValue, +}); +export const zBooleanFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zBooleanFieldType, +}); +export type BooleanFieldType = z.infer; +export type BooleanFieldValue = z.infer; +export type BooleanFieldInputInstance = z.infer; +export type BooleanFieldOutputInstance = z.infer; +export type BooleanFieldInputTemplate = z.infer; +export type BooleanFieldOutputTemplate = z.infer; +export const isBooleanFieldInputInstance = (val: unknown): val is BooleanFieldInputInstance => + zBooleanFieldInputInstance.safeParse(val).success; +export const isBooleanFieldInputTemplate = (val: unknown): val is BooleanFieldInputTemplate => + zBooleanFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region EnumField +export const zEnumFieldType = zFieldTypeBase.extend({ + name: z.literal('EnumField'), +}); +export const zEnumFieldValue = z.string(); +export const zEnumFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zEnumFieldType, + value: zEnumFieldValue, +}); +export const zEnumFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zEnumFieldType, +}); +export const zEnumFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zEnumFieldType, + default: zEnumFieldValue, + options: z.array(z.string()), + labels: z.record(z.string()).optional(), +}); +export const zEnumFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zEnumFieldType, +}); +export type EnumFieldType = z.infer; +export type EnumFieldValue = z.infer; +export type EnumFieldInputInstance = z.infer; +export type EnumFieldOutputInstance = z.infer; +export type EnumFieldInputTemplate = z.infer; +export type EnumFieldOutputTemplate = z.infer; +export const isEnumFieldInputInstance = (val: unknown): val is EnumFieldInputInstance => + zEnumFieldInputInstance.safeParse(val).success; +export const isEnumFieldInputTemplate = (val: unknown): val is EnumFieldInputTemplate => + zEnumFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region ImageField +export const zImageFieldType = zFieldTypeBase.extend({ + name: z.literal('ImageField'), +}); +export const zImageFieldValue = zImageField.optional(); +export const zImageFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zImageFieldType, + value: zImageFieldValue, +}); +export const zImageFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zImageFieldType, +}); +export const zImageFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zImageFieldType, + default: zImageFieldValue, +}); +export const zImageFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zImageFieldType, +}); +export type ImageFieldType = z.infer; +export type ImageFieldValue = z.infer; +export type ImageFieldInputInstance = z.infer; +export type ImageFieldOutputInstance = z.infer; +export type ImageFieldInputTemplate = z.infer; +export type ImageFieldOutputTemplate = z.infer; +export const isImageFieldInputInstance = (val: unknown): val is ImageFieldInputInstance => + zImageFieldInputInstance.safeParse(val).success; +export const isImageFieldInputTemplate = (val: unknown): val is ImageFieldInputTemplate => + zImageFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region BoardField +export const zBoardFieldType = zFieldTypeBase.extend({ + name: z.literal('BoardField'), +}); +export const zBoardFieldValue = zBoardField.optional(); +export const zBoardFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zBoardFieldType, + value: zBoardFieldValue, +}); +export const zBoardFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zBoardFieldType, +}); +export const zBoardFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zBoardFieldType, + default: zBoardFieldValue, +}); +export const zBoardFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zBoardFieldType, +}); +export type BoardFieldType = z.infer; +export type BoardFieldValue = z.infer; +export type BoardFieldInputInstance = z.infer; +export type BoardFieldOutputInstance = z.infer; +export type BoardFieldInputTemplate = z.infer; +export type BoardFieldOutputTemplate = z.infer; +export const isBoardFieldInputInstance = (val: unknown): val is BoardFieldInputInstance => + zBoardFieldInputInstance.safeParse(val).success; +export const isBoardFieldInputTemplate = (val: unknown): val is BoardFieldInputTemplate => + zBoardFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region ColorField +export const zColorFieldType = zFieldTypeBase.extend({ + name: z.literal('ColorField'), +}); +export const zColorFieldValue = zColorField.optional(); +export const zColorFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zColorFieldType, + value: zColorFieldValue, +}); +export const zColorFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zColorFieldType, +}); +export const zColorFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zColorFieldType, + default: zColorFieldValue, +}); +export const zColorFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zColorFieldType, +}); +export type ColorFieldType = z.infer; +export type ColorFieldValue = z.infer; +export type ColorFieldInputInstance = z.infer; +export type ColorFieldOutputInstance = z.infer; +export type ColorFieldInputTemplate = z.infer; +export type ColorFieldOutputTemplate = z.infer; +export const isColorFieldInputInstance = (val: unknown): val is ColorFieldInputInstance => + zColorFieldInputInstance.safeParse(val).success; +export const isColorFieldInputTemplate = (val: unknown): val is ColorFieldInputTemplate => + zColorFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region MainModelField +export const zMainModelFieldType = zFieldTypeBase.extend({ + name: z.literal('MainModelField'), +}); +export const zMainModelFieldValue = zMainModelField.optional(); +export const zMainModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zMainModelFieldType, + value: zMainModelFieldValue, +}); +export const zMainModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zMainModelFieldType, +}); +export const zMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zMainModelFieldType, + default: zMainModelFieldValue, +}); +export const zMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zMainModelFieldType, +}); +export type MainModelFieldType = z.infer; +export type MainModelFieldValue = z.infer; +export type MainModelFieldInputInstance = z.infer; +export type MainModelFieldOutputInstance = z.infer; +export type MainModelFieldInputTemplate = z.infer; +export type MainModelFieldOutputTemplate = z.infer; +export const isMainModelFieldInputInstance = (val: unknown): val is MainModelFieldInputInstance => + zMainModelFieldInputInstance.safeParse(val).success; +export const isMainModelFieldInputTemplate = (val: unknown): val is MainModelFieldInputTemplate => + zMainModelFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region SDXLMainModelField +export const zSDXLMainModelFieldType = zFieldTypeBase.extend({ + name: z.literal('SDXLMainModelField'), +}); +export const zSDXLMainModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL models only. +export const zSDXLMainModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zSDXLMainModelFieldType, + value: zSDXLMainModelFieldValue, +}); +export const zSDXLMainModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zSDXLMainModelFieldType, +}); +export const zSDXLMainModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zSDXLMainModelFieldType, + default: zSDXLMainModelFieldValue, +}); +export const zSDXLMainModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zSDXLMainModelFieldType, +}); +export type SDXLMainModelFieldType = z.infer; +export type SDXLMainModelFieldValue = z.infer; +export type SDXLMainModelFieldInputInstance = z.infer; +export type SDXLMainModelFieldOutputInstance = z.infer; +export type SDXLMainModelFieldInputTemplate = z.infer; +export type SDXLMainModelFieldOutputTemplate = z.infer; +export const isSDXLMainModelFieldInputInstance = (val: unknown): val is SDXLMainModelFieldInputInstance => + zSDXLMainModelFieldInputInstance.safeParse(val).success; +export const isSDXLMainModelFieldInputTemplate = (val: unknown): val is SDXLMainModelFieldInputTemplate => + zSDXLMainModelFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region SDXLRefinerModelField +export const zSDXLRefinerModelFieldType = zFieldTypeBase.extend({ + name: z.literal('SDXLRefinerModelField'), +}); +export const zSDXLRefinerModelFieldValue = zMainModelFieldValue; // TODO: Narrow to SDXL Refiner models only. +export const zSDXLRefinerModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zSDXLRefinerModelFieldType, + value: zSDXLRefinerModelFieldValue, +}); +export const zSDXLRefinerModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zSDXLRefinerModelFieldType, +}); +export const zSDXLRefinerModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zSDXLRefinerModelFieldType, + default: zSDXLRefinerModelFieldValue, +}); +export const zSDXLRefinerModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zSDXLRefinerModelFieldType, +}); +export type SDXLRefinerModelFieldType = z.infer; +export type SDXLRefinerModelFieldValue = z.infer; +export type SDXLRefinerModelFieldInputInstance = z.infer; +export type SDXLRefinerModelFieldOutputInstance = z.infer; +export type SDXLRefinerModelFieldInputTemplate = z.infer; +export type SDXLRefinerModelFieldOutputTemplate = z.infer; +export const isSDXLRefinerModelFieldInputInstance = (val: unknown): val is SDXLRefinerModelFieldInputInstance => + zSDXLRefinerModelFieldInputInstance.safeParse(val).success; +export const isSDXLRefinerModelFieldInputTemplate = (val: unknown): val is SDXLRefinerModelFieldInputTemplate => + zSDXLRefinerModelFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region VAEModelField +export const zVAEModelFieldType = zFieldTypeBase.extend({ + name: z.literal('VAEModelField'), +}); +export const zVAEModelFieldValue = zVAEModelField.optional(); +export const zVAEModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zVAEModelFieldType, + value: zVAEModelFieldValue, +}); +export const zVAEModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zVAEModelFieldType, +}); +export const zVAEModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zVAEModelFieldType, + default: zVAEModelFieldValue, +}); +export const zVAEModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zVAEModelFieldType, +}); +export type VAEModelFieldType = z.infer; +export type VAEModelFieldValue = z.infer; +export type VAEModelFieldInputInstance = z.infer; +export type VAEModelFieldOutputInstance = z.infer; +export type VAEModelFieldInputTemplate = z.infer; +export type VAEModelFieldOutputTemplate = z.infer; +export const isVAEModelFieldInputInstance = (val: unknown): val is VAEModelFieldInputInstance => + zVAEModelFieldInputInstance.safeParse(val).success; +export const isVAEModelFieldInputTemplate = (val: unknown): val is VAEModelFieldInputTemplate => + zVAEModelFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region LoRAModelField +export const zLoRAModelFieldType = zFieldTypeBase.extend({ + name: z.literal('LoRAModelField'), +}); +export const zLoRAModelFieldValue = zLoRAModelField.optional(); +export const zLoRAModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zLoRAModelFieldType, + value: zLoRAModelFieldValue, +}); +export const zLoRAModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zLoRAModelFieldType, +}); +export const zLoRAModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zLoRAModelFieldType, + default: zLoRAModelFieldValue, +}); +export const zLoRAModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zLoRAModelFieldType, +}); +export type LoRAModelFieldType = z.infer; +export type LoRAModelFieldValue = z.infer; +export type LoRAModelFieldInputInstance = z.infer; +export type LoRAModelFieldOutputInstance = z.infer; +export type LoRAModelFieldInputTemplate = z.infer; +export type LoRAModelFieldOutputTemplate = z.infer; +export const isLoRAModelFieldInputInstance = (val: unknown): val is LoRAModelFieldInputInstance => + zLoRAModelFieldInputInstance.safeParse(val).success; +export const isLoRAModelFieldInputTemplate = (val: unknown): val is LoRAModelFieldInputTemplate => + zLoRAModelFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region ControlNetModelField +export const zControlNetModelFieldType = zFieldTypeBase.extend({ + name: z.literal('ControlNetModelField'), +}); +export const zControlNetModelFieldValue = zControlNetModelField.optional(); +export const zControlNetModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zControlNetModelFieldType, + value: zControlNetModelFieldValue, +}); +export const zControlNetModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zControlNetModelFieldType, +}); +export const zControlNetModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zControlNetModelFieldType, + default: zControlNetModelFieldValue, +}); +export const zControlNetModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zControlNetModelFieldType, +}); +export type ControlNetModelFieldType = z.infer; +export type ControlNetModelFieldValue = z.infer; +export type ControlNetModelFieldInputInstance = z.infer; +export type ControlNetModelFieldOutputInstance = z.infer; +export type ControlNetModelFieldInputTemplate = z.infer; +export type ControlNetModelFieldOutputTemplate = z.infer; +export const isControlNetModelFieldInputInstance = (val: unknown): val is ControlNetModelFieldInputInstance => + zControlNetModelFieldInputInstance.safeParse(val).success; +export const isControlNetModelFieldInputTemplate = (val: unknown): val is ControlNetModelFieldInputTemplate => + zControlNetModelFieldInputTemplate.safeParse(val).success; +export const isControlNetModelFieldValue = (v: unknown): v is ControlNetModelFieldValue => + zControlNetModelFieldValue.safeParse(v).success; +// #endregion + +// #region IPAdapterModelField +export const zIPAdapterModelFieldType = zFieldTypeBase.extend({ + name: z.literal('IPAdapterModelField'), +}); +export const zIPAdapterModelFieldValue = zIPAdapterModelField.optional(); +export const zIPAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zIPAdapterModelFieldType, + value: zIPAdapterModelFieldValue, +}); +export const zIPAdapterModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zIPAdapterModelFieldType, +}); +export const zIPAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zIPAdapterModelFieldType, + default: zIPAdapterModelFieldValue, +}); +export const zIPAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zIPAdapterModelFieldType, +}); +export type IPAdapterModelFieldType = z.infer; +export type IPAdapterModelFieldValue = z.infer; +export type IPAdapterModelFieldInputInstance = z.infer; +export type IPAdapterModelFieldOutputInstance = z.infer; +export type IPAdapterModelFieldInputTemplate = z.infer; +export type IPAdapterModelFieldOutputTemplate = z.infer; +export const isIPAdapterModelFieldInputInstance = (val: unknown): val is IPAdapterModelFieldInputInstance => + zIPAdapterModelFieldInputInstance.safeParse(val).success; +export const isIPAdapterModelFieldInputTemplate = (val: unknown): val is IPAdapterModelFieldInputTemplate => + zIPAdapterModelFieldInputTemplate.safeParse(val).success; +export const isIPAdapterModelFieldValue = (val: unknown): val is IPAdapterModelFieldValue => + zIPAdapterModelFieldValue.safeParse(val).success; +// #endregion + +// #region T2IAdapterField +export const zT2IAdapterModelFieldType = zFieldTypeBase.extend({ + name: z.literal('T2IAdapterModelField'), +}); +export const zT2IAdapterModelFieldValue = zT2IAdapterModelField.optional(); +export const zT2IAdapterModelFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zT2IAdapterModelFieldType, + value: zT2IAdapterModelFieldValue, +}); +export const zT2IAdapterModelFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zT2IAdapterModelFieldType, +}); +export const zT2IAdapterModelFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zT2IAdapterModelFieldType, + default: zT2IAdapterModelFieldValue, +}); +export const zT2IAdapterModelFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zT2IAdapterModelFieldType, +}); +export type T2IAdapterModelFieldType = z.infer; +export type T2IAdapterModelFieldValue = z.infer; +export type T2IAdapterModelFieldInputInstance = z.infer; +export type T2IAdapterModelFieldOutputInstance = z.infer; +export type T2IAdapterModelFieldInputTemplate = z.infer; +export type T2IAdapterModelFieldOutputTemplate = z.infer; +export const isT2IAdapterModelFieldInputInstance = (val: unknown): val is T2IAdapterModelFieldInputInstance => + zT2IAdapterModelFieldInputInstance.safeParse(val).success; +export const isT2IAdapterModelFieldInputTemplate = (val: unknown): val is T2IAdapterModelFieldInputTemplate => + zT2IAdapterModelFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region SchedulerField +export const zSchedulerFieldType = zFieldTypeBase.extend({ + name: z.literal('SchedulerField'), +}); +export const zSchedulerFieldValue = zSchedulerField.optional(); +export const zSchedulerFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zSchedulerFieldType, + value: zSchedulerFieldValue, +}); +export const zSchedulerFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zSchedulerFieldType, +}); +export const zSchedulerFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zSchedulerFieldType, + default: zSchedulerFieldValue, +}); +export const zSchedulerFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zSchedulerFieldType, +}); +export type SchedulerFieldType = z.infer; +export type SchedulerFieldValue = z.infer; +export type SchedulerFieldInputInstance = z.infer; +export type SchedulerFieldOutputInstance = z.infer; +export type SchedulerFieldInputTemplate = z.infer; +export type SchedulerFieldOutputTemplate = z.infer; +export const isSchedulerFieldInputInstance = (val: unknown): val is SchedulerFieldInputInstance => + zSchedulerFieldInputInstance.safeParse(val).success; +export const isSchedulerFieldInputTemplate = (val: unknown): val is SchedulerFieldInputTemplate => + zSchedulerFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region StatelessField +/** + * StatelessField is a catchall for stateless fields with no UI input components. They do not + * do not support "direct" input, instead only accepting connections from other fields. + * + * This field type serves as a "generic" field type. + * + * Examples include: + * - Fields like UNetField or LatentsField where we do not allow direct UI input + * - Reserved fields like IsIntermediate + * - Any other field we don't have full-on schemas for + */ +export const zStatelessFieldType = zFieldTypeBase.extend({ + name: z.string().min(1), // stateless --> we accept the field's name as the type +}); +export const zStatelessFieldValue = z.undefined().catch(undefined); // stateless --> no value, but making this z.never() introduces a lot of extra TS fanagling +export const zStatelessFieldInputInstance = zFieldInputInstanceBase.extend({ + type: zStatelessFieldType, + value: zStatelessFieldValue, +}); +export const zStatelessFieldOutputInstance = zFieldOutputInstanceBase.extend({ + type: zStatelessFieldType, +}); +export const zStatelessFieldInputTemplate = zFieldInputTemplateBase.extend({ + type: zStatelessFieldType, + default: zStatelessFieldValue, + input: z.literal('connection'), // stateless --> only accepts connection inputs +}); +export const zStatelessFieldOutputTemplate = zFieldOutputTemplateBase.extend({ + type: zStatelessFieldType, +}); + +export type StatelessFieldType = z.infer; +export type StatelessFieldValue = z.infer; +export type StatelessFieldInputInstance = z.infer; +export type StatelessFieldOutputInstance = z.infer; +export type StatelessFieldInputTemplate = z.infer; +export type StatelessFieldOutputTemplate = z.infer; +// #endregion + +/** + * Here we define the main field unions: + * - FieldType + * - FieldValue + * - FieldInputInstance + * - FieldOutputInstance + * - FieldInputTemplate + * - FieldOutputTemplate + * + * All stateful fields are unioned together, and then that union is unioned with StatelessField. + * + * This allows us to interact with stateful fields without needing to worry about "generic" handling + * for all other StatelessFields. + */ + +// #region StatefulFieldType & FieldType +export const zStatefulFieldType = z.union([ + zIntegerFieldType, + zFloatFieldType, + zStringFieldType, + zBooleanFieldType, + zEnumFieldType, + zImageFieldType, + zBoardFieldType, + zMainModelFieldType, + zSDXLMainModelFieldType, + zSDXLRefinerModelFieldType, + zVAEModelFieldType, + zLoRAModelFieldType, + zControlNetModelFieldType, + zIPAdapterModelFieldType, + zT2IAdapterModelFieldType, + zColorFieldType, + zSchedulerFieldType, +]); +export type StatefulFieldType = z.infer; +export const isStatefulFieldType = (val: unknown): val is StatefulFieldType => + zStatefulFieldType.safeParse(val).success; + +export const zFieldType = z.union([zStatefulFieldType, zStatelessFieldType]); +export type FieldType = z.infer; +export const isFieldType = (val: unknown): val is FieldType => zFieldType.safeParse(val).success; +// #endregion + +// #region StatefulFieldValue & FieldValue +export const zStatefulFieldValue = z.union([ + zIntegerFieldValue, + zFloatFieldValue, + zStringFieldValue, + zBooleanFieldValue, + zEnumFieldValue, + zImageFieldValue, + zBoardFieldValue, + zMainModelFieldValue, + zSDXLMainModelFieldValue, + zSDXLRefinerModelFieldValue, + zVAEModelFieldValue, + zLoRAModelFieldValue, + zControlNetModelFieldValue, + zIPAdapterModelFieldValue, + zT2IAdapterModelFieldValue, + zColorFieldValue, + zSchedulerFieldValue, +]); +export type StatefulFieldValue = z.infer; +export const isStatefulFieldValue = (val: unknown): val is StatefulFieldValue => + zStatefulFieldValue.safeParse(val).success; + +export const zFieldValue = z.union([zStatefulFieldValue, zStatelessFieldValue]); +export type FieldValue = z.infer; +export const isFieldValue = (val: unknown): val is FieldValue => zFieldValue.safeParse(val).success; +// #endregion + +// #region StatefulFieldInputInstance & FieldInputInstance +export const zStatefulFieldInputInstance = z.union([ + zIntegerFieldInputInstance, + zFloatFieldInputInstance, + zStringFieldInputInstance, + zBooleanFieldInputInstance, + zEnumFieldInputInstance, + zImageFieldInputInstance, + zBoardFieldInputInstance, + zMainModelFieldInputInstance, + zSDXLMainModelFieldInputInstance, + zSDXLRefinerModelFieldInputInstance, + zVAEModelFieldInputInstance, + zLoRAModelFieldInputInstance, + zControlNetModelFieldInputInstance, + zIPAdapterModelFieldInputInstance, + zT2IAdapterModelFieldInputInstance, + zColorFieldInputInstance, + zSchedulerFieldInputInstance, +]); +export type StatefulFieldInputInstance = z.infer; +export const isStatefulFieldInputInstance = (val: unknown): val is StatefulFieldInputInstance => + zStatefulFieldInputInstance.safeParse(val).success; + +export const zFieldInputInstance = z.union([zStatefulFieldInputInstance, zStatelessFieldInputInstance]); +export type FieldInputInstance = z.infer; +export const isFieldInputInstance = (val: unknown): val is FieldInputInstance => + zFieldInputInstance.safeParse(val).success; +// #endregion + +// #region StatefulFieldOutputInstance & FieldOutputInstance +export const zStatefulFieldOutputInstance = z.union([ + zIntegerFieldOutputInstance, + zFloatFieldOutputInstance, + zStringFieldOutputInstance, + zBooleanFieldOutputInstance, + zEnumFieldOutputInstance, + zImageFieldOutputInstance, + zBoardFieldOutputInstance, + zMainModelFieldOutputInstance, + zSDXLMainModelFieldOutputInstance, + zSDXLRefinerModelFieldOutputInstance, + zVAEModelFieldOutputInstance, + zLoRAModelFieldOutputInstance, + zControlNetModelFieldOutputInstance, + zIPAdapterModelFieldOutputInstance, + zT2IAdapterModelFieldOutputInstance, + zColorFieldOutputInstance, + zSchedulerFieldOutputInstance, +]); +export type StatefulFieldOutputInstance = z.infer; +export const isStatefulFieldOutputInstance = (val: unknown): val is StatefulFieldOutputInstance => + zStatefulFieldOutputInstance.safeParse(val).success; + +export const zFieldOutputInstance = z.union([zStatefulFieldOutputInstance, zStatelessFieldOutputInstance]); +export type FieldOutputInstance = z.infer; +export const isFieldOutputInstance = (val: unknown): val is FieldOutputInstance => + zFieldOutputInstance.safeParse(val).success; +// #endregion + +// #region StatefulFieldInputTemplate & FieldInputTemplate +export const zStatefulFieldInputTemplate = z.union([ + zIntegerFieldInputTemplate, + zFloatFieldInputTemplate, + zStringFieldInputTemplate, + zBooleanFieldInputTemplate, + zEnumFieldInputTemplate, + zImageFieldInputTemplate, + zBoardFieldInputTemplate, + zMainModelFieldInputTemplate, + zSDXLMainModelFieldInputTemplate, + zSDXLRefinerModelFieldInputTemplate, + zVAEModelFieldInputTemplate, + zLoRAModelFieldInputTemplate, + zControlNetModelFieldInputTemplate, + zIPAdapterModelFieldInputTemplate, + zT2IAdapterModelFieldInputTemplate, + zColorFieldInputTemplate, + zSchedulerFieldInputTemplate, + zStatelessFieldInputTemplate, +]); +export type StatefulFieldInputTemplate = z.infer; +export const isStatefulFieldInputTemplate = (val: unknown): val is StatefulFieldInputTemplate => + zStatefulFieldInputTemplate.safeParse(val).success; + +export const zFieldInputTemplate = z.union([zStatefulFieldInputTemplate, zStatelessFieldInputTemplate]); +export type FieldInputTemplate = z.infer; +export const isFieldInputTemplate = (val: unknown): val is FieldInputTemplate => + zFieldInputTemplate.safeParse(val).success; +// #endregion + +// #region StatefulFieldOutputTemplate & FieldOutputTemplate +export const zStatefulFieldOutputTemplate = z.union([ + zIntegerFieldOutputTemplate, + zFloatFieldOutputTemplate, + zStringFieldOutputTemplate, + zBooleanFieldOutputTemplate, + zEnumFieldOutputTemplate, + zImageFieldOutputTemplate, + zBoardFieldOutputTemplate, + zMainModelFieldOutputTemplate, + zSDXLMainModelFieldOutputTemplate, + zSDXLRefinerModelFieldOutputTemplate, + zVAEModelFieldOutputTemplate, + zLoRAModelFieldOutputTemplate, + zControlNetModelFieldOutputTemplate, + zIPAdapterModelFieldOutputTemplate, + zT2IAdapterModelFieldOutputTemplate, + zColorFieldOutputTemplate, + zSchedulerFieldOutputTemplate, +]); +export type StatefulFieldOutputTemplate = z.infer; +export const isStatefulFieldOutputTemplate = (val: unknown): val is StatefulFieldOutputTemplate => + zStatefulFieldOutputTemplate.safeParse(val).success; + +export const zFieldOutputTemplate = z.union([zStatefulFieldOutputTemplate, zStatelessFieldOutputTemplate]); +export type FieldOutputTemplate = z.infer; +export const isFieldOutputTemplate = (val: unknown): val is FieldOutputTemplate => + zFieldOutputTemplate.safeParse(val).success; +// #endregion diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/invocation.ts b/invokeai/frontend/web/src/features/nodes/types/v2/invocation.ts new file mode 100644 index 0000000000..86ec70fd9b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/invocation.ts @@ -0,0 +1,93 @@ +import type { Edge, Node } from 'reactflow'; +import { z } from 'zod'; + +import { zClassification, zProgressImage } from './common'; +import { zFieldInputInstance, zFieldInputTemplate, zFieldOutputInstance, zFieldOutputTemplate } from './field'; +import { zSemVer } from './semver'; + +// #region InvocationTemplate +export const zInvocationTemplate = z.object({ + type: z.string(), + title: z.string(), + description: z.string(), + tags: z.array(z.string().min(1)), + inputs: z.record(zFieldInputTemplate), + outputs: z.record(zFieldOutputTemplate), + outputType: z.string().min(1), + version: zSemVer, + useCache: z.boolean(), + nodePack: z.string().min(1).nullish(), + classification: zClassification, +}); +export type InvocationTemplate = z.infer; +// #endregion + +// #region NodeData +export const zInvocationNodeData = z.object({ + id: z.string().trim().min(1), + type: z.string().trim().min(1), + label: z.string(), + isOpen: z.boolean(), + notes: z.string(), + isIntermediate: z.boolean(), + useCache: z.boolean(), + version: zSemVer, + nodePack: z.string().min(1).nullish(), + inputs: z.record(zFieldInputInstance), + outputs: z.record(zFieldOutputInstance), +}); + +export const zNotesNodeData = z.object({ + id: z.string().trim().min(1), + type: z.literal('notes'), + label: z.string(), + isOpen: z.boolean(), + notes: z.string(), +}); +export const zCurrentImageNodeData = z.object({ + id: z.string().trim().min(1), + type: z.literal('current_image'), + label: z.string(), + isOpen: z.boolean(), +}); +export const zAnyNodeData = z.union([zInvocationNodeData, zNotesNodeData, zCurrentImageNodeData]); + +export type NotesNodeData = z.infer; +export type InvocationNodeData = z.infer; +export type CurrentImageNodeData = z.infer; +export type AnyNodeData = z.infer; + +export type InvocationNode = Node; +export type NotesNode = Node; +export type CurrentImageNode = Node; +export type AnyNode = Node; + +export const isInvocationNode = (node?: AnyNode): node is InvocationNode => Boolean(node && node.type === 'invocation'); +export const isNotesNode = (node?: AnyNode): node is NotesNode => Boolean(node && node.type === 'notes'); +export const isCurrentImageNode = (node?: AnyNode): node is CurrentImageNode => + Boolean(node && node.type === 'current_image'); +export const isInvocationNodeData = (node?: AnyNodeData): node is InvocationNodeData => + Boolean(node && !['notes', 'current_image'].includes(node.type)); // node.type may be 'notes', 'current_image', or any invocation type +// #endregion + +// #region NodeExecutionState +export const zNodeStatus = z.enum(['PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED']); +export const zNodeExecutionState = z.object({ + nodeId: z.string().trim().min(1), + status: zNodeStatus, + progress: z.number().nullable(), + progressImage: zProgressImage.nullable(), + error: z.string().nullable(), + outputs: z.array(z.any()), +}); +export type NodeExecutionState = z.infer; +export type NodeStatus = z.infer; +// #endregion + +// #region Edges +export const zInvocationNodeEdgeExtra = z.object({ + type: z.union([z.literal('default'), z.literal('collapsed')]), +}); +export type InvocationNodeEdgeExtra = z.infer; +export type InvocationNodeEdge = Edge; +// #endregion diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/metadata.ts b/invokeai/frontend/web/src/features/nodes/types/v2/metadata.ts new file mode 100644 index 0000000000..0cc30499e3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/metadata.ts @@ -0,0 +1,77 @@ +import { z } from 'zod'; + +import { + zControlField, + zIPAdapterField, + zLoRAModelField, + zMainModelField, + zSDXLRefinerModelField, + zT2IAdapterField, + zVAEModelField, +} from './common'; + +// #region Metadata-optimized versions of schemas +// TODO: It's possible that `deepPartial` will be deprecated: +// - https://github.com/colinhacks/zod/issues/2106 +// - https://github.com/colinhacks/zod/issues/2854 +export const zLoRAMetadataItem = z.object({ + lora: zLoRAModelField.deepPartial(), + weight: z.number(), +}); +const zControlNetMetadataItem = zControlField.deepPartial(); +const zIPAdapterMetadataItem = zIPAdapterField.deepPartial(); +const zT2IAdapterMetadataItem = zT2IAdapterField.deepPartial(); +const zSDXLRefinerModelMetadataItem = zSDXLRefinerModelField.deepPartial(); +const zModelMetadataItem = zMainModelField.deepPartial(); +const zVAEModelMetadataItem = zVAEModelField.deepPartial(); +export type LoRAMetadataItem = z.infer; +export type ControlNetMetadataItem = z.infer; +export type IPAdapterMetadataItem = z.infer; +export type T2IAdapterMetadataItem = z.infer; +export type SDXLRefinerModelMetadataItem = z.infer; +export type ModelMetadataItem = z.infer; +export type VAEModelMetadataItem = z.infer; +// #endregion + +// #region CoreMetadata +export const zCoreMetadata = z + .object({ + app_version: z.string().nullish().catch(null), + generation_mode: z.string().nullish().catch(null), + created_by: z.string().nullish().catch(null), + positive_prompt: z.string().nullish().catch(null), + negative_prompt: z.string().nullish().catch(null), + width: z.number().int().nullish().catch(null), + height: z.number().int().nullish().catch(null), + seed: z.number().int().nullish().catch(null), + rand_device: z.string().nullish().catch(null), + cfg_scale: z.number().nullish().catch(null), + cfg_rescale_multiplier: z.number().nullish().catch(null), + steps: z.number().int().nullish().catch(null), + scheduler: z.string().nullish().catch(null), + clip_skip: z.number().int().nullish().catch(null), + model: zModelMetadataItem.nullish().catch(null), + controlnets: z.array(zControlNetMetadataItem).nullish().catch(null), + ipAdapters: z.array(zIPAdapterMetadataItem).nullish().catch(null), + t2iAdapters: z.array(zT2IAdapterMetadataItem).nullish().catch(null), + loras: z.array(zLoRAMetadataItem).nullish().catch(null), + vae: zVAEModelMetadataItem.nullish().catch(null), + strength: z.number().nullish().catch(null), + hrf_enabled: z.boolean().nullish().catch(null), + hrf_strength: z.number().nullish().catch(null), + hrf_method: z.string().nullish().catch(null), + init_image: z.string().nullish().catch(null), + positive_style_prompt: z.string().nullish().catch(null), + negative_style_prompt: z.string().nullish().catch(null), + refiner_model: zSDXLRefinerModelMetadataItem.nullish().catch(null), + refiner_cfg_scale: z.number().nullish().catch(null), + refiner_steps: z.number().int().nullish().catch(null), + refiner_scheduler: z.string().nullish().catch(null), + refiner_positive_aesthetic_score: z.number().nullish().catch(null), + refiner_negative_aesthetic_score: z.number().nullish().catch(null), + refiner_start: z.number().nullish().catch(null), + }) + .passthrough(); +export type CoreMetadata = z.infer; + +// #endregion diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/openapi.ts b/invokeai/frontend/web/src/features/nodes/types/v2/openapi.ts new file mode 100644 index 0000000000..83d774439a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/openapi.ts @@ -0,0 +1,86 @@ +import type { OpenAPIV3_1 } from 'openapi-types'; +import type { + InputFieldJSONSchemaExtra, + InvocationJSONSchemaExtra, + OutputFieldJSONSchemaExtra, +} from 'services/api/types'; + +// Janky customization of OpenAPI Schema :/ + +export type InvocationSchemaExtra = InvocationJSONSchemaExtra & { + output: OpenAPIV3_1.ReferenceObject; // the output of the invocation + title: string; + category?: string; + tags?: string[]; + version: string; + properties: Omit< + NonNullable & (InputFieldJSONSchemaExtra | OutputFieldJSONSchemaExtra), + 'type' + > & { + type: Omit & { + default: string; + }; + use_cache: Omit & { + default: boolean; + }; + }; +}; + +export type InvocationSchemaType = { + default: string; // the type of the invocation +}; + +export type InvocationBaseSchemaObject = Omit & + InvocationSchemaExtra; + +export type InvocationOutputSchemaObject = Omit & { + properties: OpenAPIV3_1.SchemaObject['properties'] & { + type: Omit & { + default: string; + }; + } & { + class: 'output'; + }; +}; + +export type InvocationFieldSchema = OpenAPIV3_1.SchemaObject & InputFieldJSONSchemaExtra; + +export type OpenAPIV3_1SchemaOrRef = OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject; + +export interface ArraySchemaObject extends InvocationBaseSchemaObject { + type: OpenAPIV3_1.ArraySchemaObjectType; + items: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject; +} +export interface NonArraySchemaObject extends InvocationBaseSchemaObject { + type?: OpenAPIV3_1.NonArraySchemaObjectType; +} + +export type InvocationSchemaObject = (ArraySchemaObject | NonArraySchemaObject) & { class: 'invocation' }; + +export const isSchemaObject = ( + obj: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | undefined +): obj is OpenAPIV3_1.SchemaObject => Boolean(obj && !('$ref' in obj)); + +export const isArraySchemaObject = ( + obj: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | undefined +): obj is OpenAPIV3_1.ArraySchemaObject => Boolean(obj && !('$ref' in obj) && obj.type === 'array'); + +export const isNonArraySchemaObject = ( + obj: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | undefined +): obj is OpenAPIV3_1.NonArraySchemaObject => Boolean(obj && !('$ref' in obj) && obj.type !== 'array'); + +export const isRefObject = ( + obj: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | undefined +): obj is OpenAPIV3_1.ReferenceObject => Boolean(obj && '$ref' in obj); + +export const isInvocationSchemaObject = ( + obj: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | InvocationSchemaObject +): obj is InvocationSchemaObject => 'class' in obj && obj.class === 'invocation'; + +export const isInvocationOutputSchemaObject = ( + obj: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject | InvocationOutputSchemaObject +): obj is InvocationOutputSchemaObject => 'class' in obj && obj.class === 'output'; + +export const isInvocationFieldSchema = ( + obj: OpenAPIV3_1.ReferenceObject | OpenAPIV3_1.SchemaObject +): obj is InvocationFieldSchema => !('$ref' in obj); diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/semver.ts b/invokeai/frontend/web/src/features/nodes/types/v2/semver.ts new file mode 100644 index 0000000000..3ba330eac4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/semver.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +// Schemas and types for working with semver + +const zVersionInt = z.coerce.number().int().min(0); + +export const zSemVer = z.string().refine((val) => { + const [major, minor, patch] = val.split('.'); + return ( + zVersionInt.safeParse(major).success && zVersionInt.safeParse(minor).success && zVersionInt.safeParse(patch).success + ); +}); + +export const zParsedSemver = zSemVer.transform((val) => { + const [major, minor, patch] = val.split('.'); + return { + major: Number(major), + minor: Number(minor), + patch: Number(patch), + }; +}); diff --git a/invokeai/frontend/web/src/features/nodes/types/v2/workflow.ts b/invokeai/frontend/web/src/features/nodes/types/v2/workflow.ts new file mode 100644 index 0000000000..723a354013 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/v2/workflow.ts @@ -0,0 +1,89 @@ +import { z } from 'zod'; + +import { zFieldIdentifier } from './field'; +import { zInvocationNodeData, zNotesNodeData } from './invocation'; + +// #region Workflow misc +export const zXYPosition = z + .object({ + x: z.number(), + y: z.number(), + }) + .default({ x: 0, y: 0 }); +export type XYPosition = z.infer; + +export const zDimension = z.number().gt(0).nullish(); +export type Dimension = z.infer; + +export const zWorkflowCategory = z.enum(['user', 'default', 'project']); +export type WorkflowCategory = z.infer; +// #endregion + +// #region Workflow Nodes +export const zWorkflowInvocationNode = z.object({ + id: z.string().trim().min(1), + type: z.literal('invocation'), + data: zInvocationNodeData, + width: zDimension, + height: zDimension, + position: zXYPosition, +}); +export const zWorkflowNotesNode = z.object({ + id: z.string().trim().min(1), + type: z.literal('notes'), + data: zNotesNodeData, + width: zDimension, + height: zDimension, + position: zXYPosition, +}); +export const zWorkflowNode = z.union([zWorkflowInvocationNode, zWorkflowNotesNode]); + +export type WorkflowInvocationNode = z.infer; +export type WorkflowNotesNode = z.infer; +export type WorkflowNode = z.infer; + +export const isWorkflowInvocationNode = (val: unknown): val is WorkflowInvocationNode => + zWorkflowInvocationNode.safeParse(val).success; +// #endregion + +// #region Workflow Edges +export const zWorkflowEdgeBase = z.object({ + id: z.string().trim().min(1), + source: z.string().trim().min(1), + target: z.string().trim().min(1), +}); +export const zWorkflowEdgeDefault = zWorkflowEdgeBase.extend({ + type: z.literal('default'), + sourceHandle: z.string().trim().min(1), + targetHandle: z.string().trim().min(1), +}); +export const zWorkflowEdgeCollapsed = zWorkflowEdgeBase.extend({ + type: z.literal('collapsed'), +}); +export const zWorkflowEdge = z.union([zWorkflowEdgeDefault, zWorkflowEdgeCollapsed]); + +export type WorkflowEdgeDefault = z.infer; +export type WorkflowEdgeCollapsed = z.infer; +export type WorkflowEdge = z.infer; +// #endregion + +// #region Workflow +export const zWorkflowV2 = z.object({ + id: z.string().min(1).optional(), + name: z.string(), + author: z.string(), + description: z.string(), + version: z.string(), + contact: z.string(), + tags: z.string(), + notes: z.string(), + nodes: z.array(zWorkflowNode), + edges: z.array(zWorkflowEdge), + exposedFields: z.array(zFieldIdentifier), + meta: z.object({ + category: zWorkflowCategory.default('user'), + version: z.literal('2.0.0'), + }), +}); +export type WorkflowV2 = z.infer; +// #endregion diff --git a/invokeai/frontend/web/src/features/nodes/types/workflow.ts b/invokeai/frontend/web/src/features/nodes/types/workflow.ts index 723a354013..adad7c0f21 100644 --- a/invokeai/frontend/web/src/features/nodes/types/workflow.ts +++ b/invokeai/frontend/web/src/features/nodes/types/workflow.ts @@ -24,16 +24,12 @@ export const zWorkflowInvocationNode = z.object({ id: z.string().trim().min(1), type: z.literal('invocation'), data: zInvocationNodeData, - width: zDimension, - height: zDimension, position: zXYPosition, }); export const zWorkflowNotesNode = z.object({ id: z.string().trim().min(1), type: z.literal('notes'), data: zNotesNodeData, - width: zDimension, - height: zDimension, position: zXYPosition, }); export const zWorkflowNode = z.union([zWorkflowInvocationNode, zWorkflowNotesNode]); @@ -68,7 +64,7 @@ export type WorkflowEdge = z.infer; // #endregion // #region Workflow -export const zWorkflowV2 = z.object({ +export const zWorkflowV3 = z.object({ id: z.string().min(1).optional(), name: z.string(), author: z.string(), @@ -82,8 +78,8 @@ export const zWorkflowV2 = z.object({ exposedFields: z.array(zFieldIdentifier), meta: z.object({ category: zWorkflowCategory.default('user'), - version: z.literal('2.0.0'), + version: z.literal('3.0.0'), }), }); -export type WorkflowV2 = z.infer; +export type WorkflowV3 = z.infer; // #endregion diff --git a/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts b/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts index ea40bd4660..af19aa86ea 100644 --- a/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts @@ -1,5 +1,5 @@ import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; -import type { FieldInputInstance, FieldOutputInstance } from 'features/nodes/types/field'; +import type { FieldInputInstance } from 'features/nodes/types/field'; import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation'; import { buildFieldInputInstance } from 'features/nodes/util/schema/buildFieldInputInstance'; import { reduce } from 'lodash-es'; @@ -24,25 +24,6 @@ export const buildInvocationNode = (position: XYPosition, template: InvocationTe {} as Record ); - const outputs = reduce( - template.outputs, - (outputsAccumulator, outputTemplate, outputName) => { - const fieldId = uuidv4(); - - const outputFieldValue: FieldOutputInstance = { - id: fieldId, - name: outputName, - type: outputTemplate.type, - fieldKind: 'output', - }; - - outputsAccumulator[outputName] = outputFieldValue; - - return outputsAccumulator; - }, - {} as Record - ); - const node: InvocationNode = { ...SHARED_NODE_PROPERTIES, id: nodeId, @@ -58,7 +39,6 @@ export const buildInvocationNode = (position: XYPosition, template: InvocationTe isIntermediate: type === 'save_image' ? false : true, useCache: template.useCache, inputs, - outputs, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts b/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts index f195c49d30..5ece51d0f3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts @@ -54,6 +54,5 @@ export const updateNode = (node: InvocationNode, template: InvocationTemplate): // Remove any fields that are not in the template clone.data.inputs = pick(clone.data.inputs, keys(defaults.data.inputs)); - clone.data.outputs = pick(clone.data.outputs, keys(defaults.data.outputs)); return clone; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts index dd3cf0ad7b..f8097566c9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputInstance.ts @@ -23,11 +23,8 @@ const FIELD_VALUE_FALLBACK_MAP: Record = export const buildFieldInputInstance = (id: string, template: FieldInputTemplate): FieldInputInstance => { const fieldInstance: FieldInputInstance = { - id, name: template.name, - type: template.type, label: '', - fieldKind: 'input' as const, value: template.default ?? get(FIELD_VALUE_FALLBACK_MAP, template.type.name), }; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts index 70775a9882..720da16464 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts @@ -2,8 +2,8 @@ import { logger } from 'app/logging/logger'; import { parseify } from 'common/util/serialize'; import type { NodesState, WorkflowsState } from 'features/nodes/store/types'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; -import { zWorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; +import { zWorkflowV3 } from 'features/nodes/types/workflow'; import i18n from 'i18n'; import { cloneDeep, pick } from 'lodash-es'; import { fromZodError } from 'zod-validation-error'; @@ -25,14 +25,14 @@ const workflowKeys = [ 'exposedFields', 'meta', 'id', -] satisfies (keyof WorkflowV2)[]; +] satisfies (keyof WorkflowV3)[]; -export type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2; +export type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV3; -export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV2 => { +export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 => { const clonedWorkflow = pick(cloneDeep(workflow), workflowKeys); - const newWorkflow: WorkflowV2 = { + const newWorkflow: WorkflowV3 = { ...clonedWorkflow, nodes: [], edges: [], @@ -45,8 +45,6 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo type: node.type, data: cloneDeep(node.data), position: { ...node.position }, - width: node.width, - height: node.height, }); } else if (isNotesNode(node) && node.type) { newWorkflow.nodes.push({ @@ -54,8 +52,6 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo type: node.type, data: cloneDeep(node.data), position: { ...node.position }, - width: node.width, - height: node.height, }); } }); @@ -83,12 +79,12 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo return newWorkflow; }; -export const buildWorkflowWithValidation = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV2 | null => { +export const buildWorkflowWithValidation = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 | null => { // builds what really, really should be a valid workflow const workflowToValidate = buildWorkflowFast({ nodes, edges, workflow }); // but bc we are storing this in the DB, let's be extra sure - const result = zWorkflowV2.safeParse(workflowToValidate); + const result = zWorkflowV3.safeParse(workflowToValidate); if (!result.success) { const { message } = fromZodError(result.error, { diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index a2677f3d17..a023c96ba9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -6,8 +6,10 @@ import { zSemVer } from 'features/nodes/types/semver'; import { FIELD_TYPE_V1_TO_FIELD_TYPE_V2_MAPPING } from 'features/nodes/types/v1/fieldTypeMap'; import type { WorkflowV1 } from 'features/nodes/types/v1/workflowV1'; import { zWorkflowV1 } from 'features/nodes/types/v1/workflowV1'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; -import { zWorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowV2 } from 'features/nodes/types/v2/workflow'; +import { zWorkflowV2 } from 'features/nodes/types/v2/workflow'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; +import { zWorkflowV3 } from 'features/nodes/types/workflow'; import { t } from 'i18next'; import { forEach } from 'lodash-es'; import { z } from 'zod'; @@ -30,7 +32,7 @@ const zWorkflowMetaVersion = z.object({ * - Workflow schema version bumped to 2.0.0 */ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { - const invocationTemplates = $store.get()?.getState().nodeTemplates.templates; + const invocationTemplates = $store.get()?.getState().nodes.templates; if (!invocationTemplates) { throw new Error(t('app.storeNotInitialized')); @@ -70,26 +72,34 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { return zWorkflowV2.parse(workflowToMigrate); }; +const migrateV2toV3 = (workflowToMigrate: WorkflowV2): WorkflowV3 => { + // Bump version + (workflowToMigrate as unknown as WorkflowV3).meta.version = '3.0.0'; + // Parsing strips out any extra properties not in the latest version + return zWorkflowV3.parse(workflowToMigrate); +}; + /** * Parses a workflow and migrates it to the latest version if necessary. */ -export const parseAndMigrateWorkflow = (data: unknown): WorkflowV2 => { +export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => { const workflowVersionResult = zWorkflowMetaVersion.safeParse(data); if (!workflowVersionResult.success) { throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion')); } - const { version } = workflowVersionResult.data.meta; + let workflow = data as WorkflowV1 | WorkflowV2 | WorkflowV3; - if (version === '1.0.0') { - const v1 = zWorkflowV1.parse(data); - return migrateV1toV2(v1); + if (workflow.meta.version === '1.0.0') { + const v1 = zWorkflowV1.parse(workflow); + workflow = migrateV1toV2(v1); } - if (version === '2.0.0') { - return zWorkflowV2.parse(data); + if (workflow.meta.version === '2.0.0') { + const v2 = zWorkflowV2.parse(workflow); + workflow = migrateV2toV3(v2); } - throw new WorkflowVersionError(t('nodes.unrecognizedWorkflowVersion', { version })); + return workflow as WorkflowV3; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts index 848d2aee77..5096e588b0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts @@ -1,6 +1,6 @@ import { parseify } from 'common/util/serialize'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; import { isWorkflowInvocationNode } from 'features/nodes/types/workflow'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import { t } from 'i18next'; @@ -16,7 +16,7 @@ type WorkflowWarning = { }; type ValidateWorkflowResult = { - workflow: WorkflowV2; + workflow: WorkflowV3; warnings: WorkflowWarning[]; }; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts index 5d484b6897..7b49d70213 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -3,7 +3,7 @@ import { useToast } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; import { workflowIDChanged, workflowSaved } from 'features/nodes/store/workflowSlice'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowV3 } from 'features/nodes/types/workflow'; import { workflowUpdated } from 'features/workflowLibrary/store/actions'; import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,7 +18,7 @@ type UseSaveLibraryWorkflowReturn = { type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn; -export const isWorkflowWithID = (workflow: WorkflowV2): workflow is O.Required => +export const isWorkflowWithID = (workflow: WorkflowV3): workflow is O.Required => Boolean(workflow.id); export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { From fe27af461a999ad9de51fcede354a2f6d8e431f3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:51:44 +1100 Subject: [PATCH 107/411] feat(ui): add vitest - Add vitest. - Consolidate vite configs into single file (easier to config everything based on env for testing) --- invokeai/frontend/web/config/common.mts | 12 - .../frontend/web/config/vite.app.config.mts | 33 --- .../web/config/vite.package.config.mts | 46 ---- invokeai/frontend/web/package.json | 7 +- invokeai/frontend/web/pnpm-lock.yaml | 222 +++++++++++++++++- invokeai/frontend/web/vite.config.mts | 88 ++++++- 6 files changed, 306 insertions(+), 102 deletions(-) delete mode 100644 invokeai/frontend/web/config/common.mts delete mode 100644 invokeai/frontend/web/config/vite.app.config.mts delete mode 100644 invokeai/frontend/web/config/vite.package.config.mts diff --git a/invokeai/frontend/web/config/common.mts b/invokeai/frontend/web/config/common.mts deleted file mode 100644 index fd559cabd1..0000000000 --- a/invokeai/frontend/web/config/common.mts +++ /dev/null @@ -1,12 +0,0 @@ -import react from '@vitejs/plugin-react-swc'; -import { visualizer } from 'rollup-plugin-visualizer'; -import type { PluginOption, UserConfig } from 'vite'; -import eslint from 'vite-plugin-eslint'; -import tsconfigPaths from 'vite-tsconfig-paths'; - -export const commonPlugins: UserConfig['plugins'] = [ - react(), - eslint(), - tsconfigPaths(), - visualizer() as unknown as PluginOption, -]; diff --git a/invokeai/frontend/web/config/vite.app.config.mts b/invokeai/frontend/web/config/vite.app.config.mts deleted file mode 100644 index 9683ed26a4..0000000000 --- a/invokeai/frontend/web/config/vite.app.config.mts +++ /dev/null @@ -1,33 +0,0 @@ -import type { UserConfig } from 'vite'; - -import { commonPlugins } from './common.mjs'; - -export const appConfig: UserConfig = { - base: './', - plugins: [...commonPlugins], - build: { - chunkSizeWarningLimit: 1500, - }, - server: { - // Proxy HTTP requests to the flask server - proxy: { - // Proxy socket.io to the nodes socketio server - '/ws/socket.io': { - target: 'ws://127.0.0.1:9090', - ws: true, - }, - // Proxy openapi schema definiton - '/openapi.json': { - target: 'http://127.0.0.1:9090/openapi.json', - rewrite: (path) => path.replace(/^\/openapi.json/, ''), - changeOrigin: true, - }, - // proxy nodes api - '/api/v1': { - target: 'http://127.0.0.1:9090/api/v1', - rewrite: (path) => path.replace(/^\/api\/v1/, ''), - changeOrigin: true, - }, - }, - }, -}; diff --git a/invokeai/frontend/web/config/vite.package.config.mts b/invokeai/frontend/web/config/vite.package.config.mts deleted file mode 100644 index 3c05d52e00..0000000000 --- a/invokeai/frontend/web/config/vite.package.config.mts +++ /dev/null @@ -1,46 +0,0 @@ -import path from 'path'; -import type { UserConfig } from 'vite'; -import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; -import dts from 'vite-plugin-dts'; - -import { commonPlugins } from './common.mjs'; - -export const packageConfig: UserConfig = { - base: './', - plugins: [ - ...commonPlugins, - dts({ - insertTypesEntry: true, - }), - cssInjectedByJsPlugin(), - ], - build: { - cssCodeSplit: true, - lib: { - entry: path.resolve(__dirname, '../src/index.ts'), - name: 'InvokeAIUI', - fileName: (format) => `invoke-ai-ui.${format}.js`, - }, - rollupOptions: { - external: ['react', 'react-dom', '@emotion/react', '@chakra-ui/react', '@invoke-ai/ui-library'], - output: { - globals: { - react: 'React', - 'react-dom': 'ReactDOM', - '@emotion/react': 'EmotionReact', - '@invoke-ai/ui-library': 'UiLibrary', - }, - }, - }, - }, - resolve: { - alias: { - app: path.resolve(__dirname, '../src/app'), - assets: path.resolve(__dirname, '../src/assets'), - common: path.resolve(__dirname, '../src/common'), - features: path.resolve(__dirname, '../src/features'), - services: path.resolve(__dirname, '../src/services'), - theme: path.resolve(__dirname, '../src/theme'), - }, - }, -}; diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index cd95183c7a..b2838e538c 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -33,7 +33,9 @@ "preinstall": "npx only-allow pnpm", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "unimported": "npx unimported" + "unimported": "npx unimported", + "test": "vitest", + "test:no-watch": "vitest --no-watch" }, "madge": { "excludeRegExp": [ @@ -157,7 +159,8 @@ "vite-plugin-css-injected-by-js": "^3.3.1", "vite-plugin-dts": "^3.7.1", "vite-plugin-eslint": "^1.8.1", - "vite-tsconfig-paths": "^4.3.1" + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.2.2" }, "pnpm": { "patchedDependencies": { diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 1d9083d1b4..f3bf68cf1d 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -215,7 +215,7 @@ devDependencies: version: 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.0.12) '@storybook/test': specifier: ^7.6.10 - version: 7.6.10 + version: 7.6.10(vitest@1.2.2) '@storybook/theming': specifier: ^7.6.10 version: 7.6.10(react-dom@18.2.0)(react@18.2.0) @@ -318,6 +318,9 @@ devDependencies: vite-tsconfig-paths: specifier: ^4.3.1 version: 4.3.1(typescript@5.3.3)(vite@5.0.12) + vitest: + specifier: ^1.2.2 + version: 1.2.2(@types/node@20.11.5) packages: @@ -5464,7 +5467,7 @@ packages: - supports-color dev: true - /@storybook/test@7.6.10: + /@storybook/test@7.6.10(vitest@1.2.2): resolution: {integrity: sha512-dn/T+HcWOBlVh3c74BHurp++BaqBoQgNbSIaXlYDpJoZ+DzNIoEQVsWFYm5gCbtKK27iFd4n52RiQI3f6Vblqw==} dependencies: '@storybook/client-logger': 7.6.10 @@ -5472,7 +5475,7 @@ packages: '@storybook/instrumenter': 7.6.10 '@storybook/preview-api': 7.6.10 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.2.0 + '@testing-library/jest-dom': 6.2.0(vitest@1.2.2) '@testing-library/user-event': 14.3.0(@testing-library/dom@9.3.4) '@types/chai': 4.3.11 '@vitest/expect': 0.34.7 @@ -5652,7 +5655,7 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.2.0: + /@testing-library/jest-dom@6.2.0(vitest@1.2.2): resolution: {integrity: sha512-+BVQlJ9cmEn5RDMUS8c2+TU6giLvzaHZ8sU/x0Jj7fk+6/46wPdwlgOPcpxS17CjcanBi/3VmGMqVr2rmbUmNw==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: @@ -5678,6 +5681,7 @@ packages: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 + vitest: 1.2.2(@types/node@20.11.5) dev: true /@testing-library/user-event@14.3.0(@testing-library/dom@9.3.4): @@ -6490,12 +6494,42 @@ packages: chai: 4.4.1 dev: true + /@vitest/expect@1.2.2: + resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==} + dependencies: + '@vitest/spy': 1.2.2 + '@vitest/utils': 1.2.2 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.2.2: + resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==} + dependencies: + '@vitest/utils': 1.2.2 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.2.2: + resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==} + dependencies: + magic-string: 0.30.5 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + /@vitest/spy@0.34.7: resolution: {integrity: sha512-NMMSzOY2d8L0mcOt4XcliDOS1ISyGlAXuQtERWVOoVHnKwmG+kKhinAiGw3dTtMQWybfa89FG8Ucg9tiC/FhTQ==} dependencies: tinyspy: 2.2.0 dev: true + /@vitest/spy@1.2.2: + resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==} + dependencies: + tinyspy: 2.2.0 + dev: true + /@vitest/utils@0.34.7: resolution: {integrity: sha512-ziAavQLpCYS9sLOorGrFFKmy2gnfiNU0ZJ15TsMz/K92NAPS/rp9K4z6AJQQk5Y8adCy4Iwpxy7pQumQ/psnRg==} dependencies: @@ -6504,6 +6538,15 @@ packages: pretty-format: 29.7.0 dev: true + /@vitest/utils@1.2.2: + resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /@volar/language-core@1.11.1: resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} dependencies: @@ -7184,6 +7227,11 @@ packages: engines: {node: '>=0.4.0'} dev: true + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} @@ -7661,6 +7709,11 @@ packages: engines: {node: '>= 0.8'} dev: true + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: @@ -9173,6 +9226,12 @@ packages: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -10547,6 +10606,10 @@ packages: hasBin: true dev: true + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + /jsondiffpatch@0.6.0: resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -10648,6 +10711,14 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.5.0 + pkg-types: 1.0.3 + dev: true + /locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -10986,6 +11057,15 @@ packages: hasBin: true dev: true + /mlly@1.5.0: + resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==} + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + ufo: 1.3.2 + dev: true + /module-definition@3.4.0: resolution: {integrity: sha512-XxJ88R1v458pifaSkPNLUTdSPNVGMP2SXVncVmApGO+gAfrLANiYe6JofymCzVceGOMwQE2xogxBSc8uB7XegA==} engines: {node: '>=6.0'} @@ -11380,6 +11460,13 @@ packages: yocto-queue: 0.1.0 dev: true + /p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + dependencies: + yocto-queue: 1.0.0 + dev: true + /p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} @@ -11550,6 +11637,14 @@ packages: find-up: 5.0.0 dev: true + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.1 + mlly: 1.5.0 + pathe: 1.1.2 + dev: true + /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -12850,6 +12945,10 @@ packages: object-inspect: 1.13.1 dev: true + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true @@ -12968,6 +13067,10 @@ packages: stackframe: 1.3.4 dev: false + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} dev: false @@ -12992,6 +13095,10 @@ packages: engines: {node: '>= 0.8'} dev: true + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: true + /stop-iteration-iterator@1.0.0: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} @@ -13161,6 +13268,12 @@ packages: engines: {node: '>=8'} dev: true + /strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + dependencies: + acorn: 8.11.3 + dev: true + /stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false @@ -13311,6 +13424,15 @@ packages: /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + engines: {node: '>=14.0.0'} + dev: true + /tinyspy@2.2.0: resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} engines: {node: '>=14.0.0'} @@ -13828,6 +13950,27 @@ packages: engines: {node: '>= 0.8'} dev: true + /vite-node@1.2.2(@types/node@20.11.5): + resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.0.12(@types/node@20.11.5) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-plugin-css-injected-by-js@3.3.1(vite@5.0.12): resolution: {integrity: sha512-PjM/X45DR3/V1K1fTRs8HtZHEQ55kIfdrn+dzaqNBFrOYO073SeSNCxp4j7gSYhV9NffVHaEnOL4myoko0ePAg==} peerDependencies: @@ -13926,6 +14069,63 @@ packages: fsevents: 2.3.3 dev: true + /vitest@1.2.2(@types/node@20.11.5): + resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': ^1.0.0 + '@vitest/ui': ^1.0.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.11.5 + '@vitest/expect': 1.2.2 + '@vitest/runner': 1.2.2 + '@vitest/snapshot': 1.2.2 + '@vitest/spy': 1.2.2 + '@vitest/utils': 1.2.2 + acorn-walk: 8.3.2 + cac: 6.7.14 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.5 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 1.3.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.0.12(@types/node@20.11.5) + vite-node: 1.2.2(@types/node@20.11.5) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -14049,6 +14249,15 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: true @@ -14189,6 +14398,11 @@ packages: engines: {node: '>=10'} dev: true + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + /z-schema@5.0.5: resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} engines: {node: '>=8.0.0'} diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index b76dd24b62..325c6467de 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -1,12 +1,90 @@ +/// +import react from '@vitejs/plugin-react-swc'; +import path from 'path'; +import { visualizer } from 'rollup-plugin-visualizer'; +import type { PluginOption } from 'vite'; import { defineConfig } from 'vite'; - -import { appConfig } from './config/vite.app.config.mjs'; -import { packageConfig } from './config/vite.package.config.mjs'; +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import dts from 'vite-plugin-dts'; +import eslint from 'vite-plugin-eslint'; +import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig(({ mode }) => { if (mode === 'package') { - return packageConfig; + return { + base: './', + plugins: [ + react(), + eslint(), + tsconfigPaths(), + visualizer() as unknown as PluginOption, + dts({ + insertTypesEntry: true, + }), + cssInjectedByJsPlugin(), + ], + build: { + cssCodeSplit: true, + lib: { + entry: path.resolve(__dirname, '../src/index.ts'), + name: 'InvokeAIUI', + fileName: (format) => `invoke-ai-ui.${format}.js`, + }, + rollupOptions: { + external: ['react', 'react-dom', '@emotion/react', '@chakra-ui/react', '@invoke-ai/ui-library'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + '@emotion/react': 'EmotionReact', + '@invoke-ai/ui-library': 'UiLibrary', + }, + }, + }, + }, + resolve: { + alias: { + app: path.resolve(__dirname, '../src/app'), + assets: path.resolve(__dirname, '../src/assets'), + common: path.resolve(__dirname, '../src/common'), + features: path.resolve(__dirname, '../src/features'), + services: path.resolve(__dirname, '../src/services'), + theme: path.resolve(__dirname, '../src/theme'), + }, + }, + }; } - return appConfig; + return { + base: './', + plugins: [react(), mode !== 'test' && eslint(), tsconfigPaths(), visualizer() as unknown as PluginOption], + build: { + chunkSizeWarningLimit: 1500, + }, + server: { + // Proxy HTTP requests to the flask server + proxy: { + // Proxy socket.io to the nodes socketio server + '/ws/socket.io': { + target: 'ws://127.0.0.1:9090', + ws: true, + }, + // Proxy openapi schema definiton + '/openapi.json': { + target: 'http://127.0.0.1:9090/openapi.json', + rewrite: (path) => path.replace(/^\/openapi.json/, ''), + changeOrigin: true, + }, + // proxy nodes api + '/api/v1': { + target: 'http://127.0.0.1:9090/api/v1', + rewrite: (path) => path.replace(/^\/api\/v1/, ''), + changeOrigin: true, + }, + }, + }, + test: { + // + }, + }; }); From 30db708c4fa058837faeda7eb20fcab1fb60af85 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:53:30 +1100 Subject: [PATCH 108/411] feat(ui): add more types of FieldParseError Unfortunately you cannot test for both a specific type of error and match its message. Splitting the error classes makes it easier to test expected error conditions. --- .../web/src/features/nodes/types/error.ts | 5 ++++ .../nodes/util/schema/parseFieldType.ts | 30 +++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/error.ts b/invokeai/frontend/web/src/features/nodes/types/error.ts index 905b487fb0..c3da136c7a 100644 --- a/invokeai/frontend/web/src/features/nodes/types/error.ts +++ b/invokeai/frontend/web/src/features/nodes/types/error.ts @@ -56,3 +56,8 @@ export class FieldParseError extends Error { this.name = this.constructor.name; } } + +export class UnableToExtractSchemaNameFromRefError extends FieldParseError {} +export class UnsupportedArrayItemType extends FieldParseError {} +export class UnsupportedUnionError extends FieldParseError {} +export class UnsupportedPrimitiveTypeError extends FieldParseError {} \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts index 14b1aefd6d..13da6b3831 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.ts @@ -1,6 +1,12 @@ -import { FieldParseError } from 'features/nodes/types/error'; +import { + FieldParseError, + UnableToExtractSchemaNameFromRefError, + UnsupportedArrayItemType, + UnsupportedPrimitiveTypeError, + UnsupportedUnionError, +} from 'features/nodes/types/error'; import type { FieldType } from 'features/nodes/types/field'; -import type { OpenAPIV3_1SchemaOrRef } from 'features/nodes/types/openapi'; +import type { InvocationFieldSchema, OpenAPIV3_1SchemaOrRef } from 'features/nodes/types/openapi'; import { isArraySchemaObject, isInvocationFieldSchema, @@ -42,7 +48,7 @@ const isCollectionFieldType = (fieldType: string) => { return false; }; -export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType => { +export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef | InvocationFieldSchema): FieldType => { if (isInvocationFieldSchema(schemaObject)) { // Check if this field has an explicit type provided by the node schema const { ui_type } = schemaObject; @@ -72,7 +78,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType // This is a single ref type const name = refObjectToSchemaName(allOf[0]); if (!name) { - throw new FieldParseError(t('nodes.unableToExtractSchemaNameFromRef')); + throw new UnableToExtractSchemaNameFromRefError(t('nodes.unableToExtractSchemaNameFromRef')); } return { name, @@ -95,7 +101,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType if (isRefObject(filteredAnyOf[0])) { const name = refObjectToSchemaName(filteredAnyOf[0]); if (!name) { - throw new FieldParseError(t('nodes.unableToExtractSchemaNameFromRef')); + throw new UnableToExtractSchemaNameFromRefError(t('nodes.unableToExtractSchemaNameFromRef')); } return { @@ -118,7 +124,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType if (filteredAnyOf.length !== 2) { // This is a union of more than 2 types, which we don't support - throw new FieldParseError( + throw new UnsupportedUnionError( t('nodes.unsupportedAnyOfLength', { count: filteredAnyOf.length, }) @@ -159,7 +165,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType }; } - throw new FieldParseError( + throw new UnsupportedUnionError( t('nodes.unsupportedMismatchedUnion', { firstType, secondType, @@ -178,7 +184,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType if (isSchemaObject(schemaObject.items)) { const itemType = schemaObject.items.type; if (!itemType || isArray(itemType)) { - throw new FieldParseError( + throw new UnsupportedArrayItemType( t('nodes.unsupportedArrayItemType', { type: itemType, }) @@ -188,7 +194,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType const name = OPENAPI_TO_FIELD_TYPE_MAP[itemType]; if (!name) { // it's 'null', 'object', or 'array' - skip - throw new FieldParseError( + throw new UnsupportedArrayItemType( t('nodes.unsupportedArrayItemType', { type: itemType, }) @@ -204,7 +210,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType // This is a ref object, extract the type name const name = refObjectToSchemaName(schemaObject.items); if (!name) { - throw new FieldParseError(t('nodes.unableToExtractSchemaNameFromRef')); + throw new UnableToExtractSchemaNameFromRefError(t('nodes.unableToExtractSchemaNameFromRef')); } return { name, @@ -216,7 +222,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType const name = OPENAPI_TO_FIELD_TYPE_MAP[schemaObject.type]; if (!name) { // it's 'null', 'object', or 'array' - skip - throw new FieldParseError( + throw new UnsupportedPrimitiveTypeError( t('nodes.unsupportedArrayItemType', { type: schemaObject.type, }) @@ -232,7 +238,7 @@ export const parseFieldType = (schemaObject: OpenAPIV3_1SchemaOrRef): FieldType } else if (isRefObject(schemaObject)) { const name = refObjectToSchemaName(schemaObject); if (!name) { - throw new FieldParseError(t('nodes.unableToExtractSchemaNameFromRef')); + throw new UnableToExtractSchemaNameFromRefError(t('nodes.unableToExtractSchemaNameFromRef')); } return { name, From 95453a22b1f991b29fd99d40d1cff2f924330272 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:53:52 +1100 Subject: [PATCH 109/411] tests(ui): add `parseFieldType.test.ts` --- .../nodes/util/schema/parseFieldType.test.ts | 379 ++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts new file mode 100644 index 0000000000..2f4ce48a32 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts @@ -0,0 +1,379 @@ +import { + UnableToExtractSchemaNameFromRefError, + UnsupportedArrayItemType, + UnsupportedPrimitiveTypeError, + UnsupportedUnionError, +} from 'features/nodes/types/error'; +import type { InvocationFieldSchema, OpenAPIV3_1SchemaOrRef } from 'features/nodes/types/openapi'; +import { parseFieldType, refObjectToSchemaName } from 'features/nodes/util/schema/parseFieldType'; +import { describe, expect, it } from 'vitest'; + +type ParseFieldTypeTestCase = { + name: string; + schema: OpenAPIV3_1SchemaOrRef | InvocationFieldSchema; + expected: { name: string; isCollection: boolean; isCollectionOrScalar: boolean }; +}; + +const primitiveTypes: ParseFieldTypeTestCase[] = [ + { + name: 'Scalar IntegerField', + schema: { type: 'integer' }, + expected: { name: 'IntegerField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Scalar FloatField', + schema: { type: 'number' }, + expected: { name: 'FloatField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Scalar StringField', + schema: { type: 'string' }, + expected: { name: 'StringField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Scalar BooleanField', + schema: { type: 'boolean' }, + expected: { name: 'BooleanField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Collection IntegerField', + schema: { items: { type: 'integer' }, type: 'array' }, + expected: { name: 'IntegerField', isCollection: true, isCollectionOrScalar: false }, + }, + { + name: 'Collection FloatField', + schema: { items: { type: 'number' }, type: 'array' }, + expected: { name: 'FloatField', isCollection: true, isCollectionOrScalar: false }, + }, + { + name: 'Collection StringField', + schema: { items: { type: 'string' }, type: 'array' }, + expected: { name: 'StringField', isCollection: true, isCollectionOrScalar: false }, + }, + { + name: 'Collection BooleanField', + schema: { items: { type: 'boolean' }, type: 'array' }, + expected: { name: 'BooleanField', isCollection: true, isCollectionOrScalar: false }, + }, + { + name: 'CollectionOrScalar IntegerField', + schema: { + anyOf: [ + { + type: 'integer', + }, + { + items: { + type: 'integer', + }, + type: 'array', + }, + ], + }, + expected: { name: 'IntegerField', isCollection: false, isCollectionOrScalar: true }, + }, + { + name: 'CollectionOrScalar FloatField', + schema: { + anyOf: [ + { + type: 'number', + }, + { + items: { + type: 'number', + }, + type: 'array', + }, + ], + }, + expected: { name: 'FloatField', isCollection: false, isCollectionOrScalar: true }, + }, + { + name: 'CollectionOrScalar StringField', + schema: { + anyOf: [ + { + type: 'string', + }, + { + items: { + type: 'string', + }, + type: 'array', + }, + ], + }, + expected: { name: 'StringField', isCollection: false, isCollectionOrScalar: true }, + }, + { + name: 'CollectionOrScalar BooleanField', + schema: { + anyOf: [ + { + type: 'boolean', + }, + { + items: { + type: 'boolean', + }, + type: 'array', + }, + ], + }, + expected: { name: 'BooleanField', isCollection: false, isCollectionOrScalar: true }, + }, +]; + +const complexTypes: ParseFieldTypeTestCase[] = [ + { + name: 'Scalar ConditioningField', + schema: { + allOf: [ + { + $ref: '#/components/schemas/ConditioningField', + }, + ], + }, + expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Nullable Scalar ConditioningField', + schema: { + anyOf: [ + { + $ref: '#/components/schemas/ConditioningField', + }, + { + type: 'null', + }, + ], + }, + expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Collection ConditioningField', + schema: { + anyOf: [ + { + items: { + $ref: '#/components/schemas/ConditioningField', + }, + type: 'array', + }, + ], + }, + expected: { name: 'ConditioningField', isCollection: true, isCollectionOrScalar: false }, + }, + { + name: 'Nullable Collection ConditioningField', + schema: { + anyOf: [ + { + items: { + $ref: '#/components/schemas/ConditioningField', + }, + type: 'array', + }, + { + type: 'null', + }, + ], + }, + expected: { name: 'ConditioningField', isCollection: true, isCollectionOrScalar: false }, + }, + { + name: 'CollectionOrScalar ConditioningField', + schema: { + anyOf: [ + { + items: { + $ref: '#/components/schemas/ConditioningField', + }, + type: 'array', + }, + { + $ref: '#/components/schemas/ConditioningField', + }, + ], + }, + expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: true }, + }, + { + name: 'Nullable CollectionOrScalar ConditioningField', + schema: { + anyOf: [ + { + items: { + $ref: '#/components/schemas/ConditioningField', + }, + type: 'array', + }, + { + $ref: '#/components/schemas/ConditioningField', + }, + { + type: 'null', + }, + ], + }, + expected: { name: 'ConditioningField', isCollection: false, isCollectionOrScalar: true }, + }, +]; + +const specialCases: ParseFieldTypeTestCase[] = [ + { + name: 'String EnumField', + schema: { + type: 'string', + enum: ['large', 'base', 'small'], + }, + expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'String EnumField with one value', + schema: { + const: 'Some Value', + }, + expected: { name: 'EnumField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Explicit ui_type (SchedulerField)', + schema: { + type: 'string', + enum: ['ddim', 'ddpm', 'deis'], + ui_type: 'SchedulerField', + }, + expected: { name: 'SchedulerField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Explicit ui_type (AnyField)', + schema: { + type: 'string', + enum: ['ddim', 'ddpm', 'deis'], + ui_type: 'AnyField', + }, + expected: { name: 'AnyField', isCollection: false, isCollectionOrScalar: false }, + }, + { + name: 'Explicit ui_type (CollectionField)', + schema: { + type: 'string', + enum: ['ddim', 'ddpm', 'deis'], + ui_type: 'CollectionField', + }, + expected: { name: 'CollectionField', isCollection: true, isCollectionOrScalar: false }, + }, +]; + +describe('refObjectToSchemaName', async () => { + it('parses ref object 1', () => { + expect( + refObjectToSchemaName({ + $ref: '#/components/schemas/ImageField', + }) + ).toEqual('ImageField'); + }); + it('parses ref object 2', () => { + expect( + refObjectToSchemaName({ + $ref: '#/components/schemas/T2IAdapterModelField', + }) + ).toEqual('T2IAdapterModelField'); + }); +}); + +describe.concurrent('parseFieldType', async () => { + it.each(primitiveTypes)('parses primitive types ($name)', ({ schema, expected }) => { + expect(parseFieldType(schema)).toEqual(expected); + }); + it.each(complexTypes)('parses complex types ($name)', ({ schema, expected }) => { + expect(parseFieldType(schema)).toEqual(expected); + }); + it.each(specialCases)('parses special case types ($name)', ({ schema, expected }) => { + expect(parseFieldType(schema)).toEqual(expected); + }); + + it('raises if it cannot extract a schema name from a ref', () => { + expect(() => + parseFieldType({ + allOf: [ + { + $ref: '#/components/schemas/', + }, + ], + }) + ).toThrowError(UnableToExtractSchemaNameFromRefError); + }); + + it('raises if it receives a union of mismatched types', () => { + expect(() => + parseFieldType({ + anyOf: [ + { + type: 'string', + }, + { + type: 'integer', + }, + ], + }) + ).toThrowError(UnsupportedUnionError); + }); + + it('raises if it receives a union of mismatched types (excluding null)', () => { + expect(() => + parseFieldType({ + anyOf: [ + { + type: 'string', + }, + { + type: 'integer', + }, + { + type: 'null', + }, + ], + }) + ).toThrowError(UnsupportedUnionError); + }); + + it('raises if it received an unsupported primitive type (object)', () => { + expect(() => + parseFieldType({ + type: 'object', + }) + ).toThrowError(UnsupportedPrimitiveTypeError); + }); + + it('raises if it received an unsupported primitive type (null)', () => { + expect(() => + parseFieldType({ + type: 'null', + }) + ).toThrowError(UnsupportedPrimitiveTypeError); + }); + + it('raises if it received an unsupported array item type (object)', () => { + expect(() => + parseFieldType({ + items: { + type: 'object', + }, + type: 'array', + }) + ).toThrowError(UnsupportedArrayItemType); + }); + + it('raises if it received an unsupported array item type (null)', () => { + expect(() => + parseFieldType({ + items: { + type: 'null', + }, + type: 'array', + }) + ).toThrowError(UnsupportedArrayItemType); + }); +}); From 010c4eae65b2001d1ff6882128417f1238bafabf Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 22 Jan 2024 14:37:23 -0500 Subject: [PATCH 110/411] add concept of repo variant --- invokeai/backend/model_manager/config.py | 4 +- invokeai/backend/model_manager/probe.py | 19 ++++++++++ tests/test_model_probe.py | 9 ++++- .../vae/taesdxl-fp16/config.json | 37 +++++++++++++++++++ .../diffusion_pytorch_model.fp16.safetensors | 0 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 tests/test_model_probe/vae/taesdxl-fp16/config.json create mode 100644 tests/test_model_probe/vae/taesdxl-fp16/diffusion_pytorch_model.fp16.safetensors diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 964cc19f19..b4685caf10 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -150,7 +150,7 @@ class _DiffusersConfig(ModelConfigBase): """Model config for diffusers-style models.""" format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers - + repo_variant: Optional[ModelRepoVariant] = ModelRepoVariant.DEFAULT class LoRAConfig(ModelConfigBase): """Model config for LoRA/Lycoris models.""" @@ -179,7 +179,6 @@ class ControlNetDiffusersConfig(_DiffusersConfig): type: Literal[ModelType.ControlNet] = ModelType.ControlNet format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers - class ControlNetCheckpointConfig(_CheckpointConfig): """Model config for ControlNet models (diffusers version).""" @@ -215,7 +214,6 @@ class MainDiffusersConfig(_DiffusersConfig, _MainConfig): prediction_type: SchedulerPredictionType = SchedulerPredictionType.Epsilon upcast_attention: bool = False - class ONNXSD1Config(_MainConfig): """Model config for ONNX format models based on sd-1.""" diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index cd048d2fe7..ba3ac3dd0c 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -20,6 +20,7 @@ from .config import ( ModelFormat, ModelType, ModelVariantType, + ModelRepoVariant, SchedulerPredictionType, ) from .hash import FastModelHash @@ -155,6 +156,9 @@ class ModelProbe(object): fields["original_hash"] = fields.get("original_hash") or hash fields["current_hash"] = fields.get("current_hash") or hash + if format_type == ModelFormat.Diffusers: + fields["repo_variant"] = fields.get("repo_variant") or probe.get_repo_variant() + # additional fields needed for main and controlnet models if fields["type"] in [ModelType.Main, ModelType.ControlNet] and fields["format"] == ModelFormat.Checkpoint: fields["config"] = cls._get_checkpoint_config_path( @@ -477,6 +481,20 @@ class FolderProbeBase(ProbeBase): def get_format(self) -> ModelFormat: return ModelFormat("diffusers") + def get_repo_variant(self) -> ModelRepoVariant: + # get all files ending in .bin or .safetensors + weight_files = list(self.model_path.glob('**/*.safetensors')) + weight_files.extend(list(self.model_path.glob('**/*.bin'))) + for x in weight_files: + if ".fp16" in x.suffixes: + return ModelRepoVariant.FP16 + if "openvino_model" in x.name: + return ModelRepoVariant.OPENVINO + if "flax_model" in x.name: + return ModelRepoVariant.FLAX + if x.suffix == ".onnx": + return ModelRepoVariant.ONNX + return ModelRepoVariant.DEFAULT class PipelineFolderProbe(FolderProbeBase): def get_base_type(self) -> BaseModelType: @@ -522,6 +540,7 @@ class PipelineFolderProbe(FolderProbeBase): except Exception: pass return ModelVariantType.Normal + class VaeFolderProbe(FolderProbeBase): diff --git a/tests/test_model_probe.py b/tests/test_model_probe.py index 248b7d602f..415559a64c 100644 --- a/tests/test_model_probe.py +++ b/tests/test_model_probe.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest from invokeai.backend import BaseModelType -from invokeai.backend.model_management.model_probe import VaeFolderProbe +from invokeai.backend.model_manager.probe import VaeFolderProbe @pytest.mark.parametrize( @@ -20,3 +20,10 @@ def test_get_base_type(vae_path: str, expected_type: BaseModelType, datadir: Pat probe = VaeFolderProbe(sd1_vae_path) base_type = probe.get_base_type() assert base_type == expected_type + repo_variant = probe.get_repo_variant() + assert repo_variant == 'default' + +def test_repo_variant(datadir: Path): + probe = VaeFolderProbe(datadir / "vae" / "taesdxl-fp16") + repo_variant = probe.get_repo_variant() + assert repo_variant == 'fp16' diff --git a/tests/test_model_probe/vae/taesdxl-fp16/config.json b/tests/test_model_probe/vae/taesdxl-fp16/config.json new file mode 100644 index 0000000000..62f01c3eb4 --- /dev/null +++ b/tests/test_model_probe/vae/taesdxl-fp16/config.json @@ -0,0 +1,37 @@ +{ + "_class_name": "AutoencoderTiny", + "_diffusers_version": "0.20.0.dev0", + "act_fn": "relu", + "decoder_block_out_channels": [ + 64, + 64, + 64, + 64 + ], + "encoder_block_out_channels": [ + 64, + 64, + 64, + 64 + ], + "force_upcast": false, + "in_channels": 3, + "latent_channels": 4, + "latent_magnitude": 3, + "latent_shift": 0.5, + "num_decoder_blocks": [ + 3, + 3, + 3, + 1 + ], + "num_encoder_blocks": [ + 1, + 3, + 3, + 3 + ], + "out_channels": 3, + "scaling_factor": 1.0, + "upsampling_scaling_factor": 2 +} diff --git a/tests/test_model_probe/vae/taesdxl-fp16/diffusion_pytorch_model.fp16.safetensors b/tests/test_model_probe/vae/taesdxl-fp16/diffusion_pytorch_model.fp16.safetensors new file mode 100644 index 0000000000..e69de29bb2 From b8e875bb7359957ed542561589ac65e5facf6465 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 31 Jan 2024 23:37:59 -0500 Subject: [PATCH 111/411] add ram cache module and support files --- invokeai/backend/model_manager/config.py | 3 + .../backend/model_manager/load/__init__.py | 0 .../backend/model_manager/load/load_base.py | 193 ++++++++++ .../model_manager/load/load_default.py | 168 +++++++++ .../model_manager/load/memory_snapshot.py | 100 ++++++ .../backend/model_manager/load/model_util.py | 109 ++++++ .../model_manager/load/optimizations.py | 30 ++ .../model_manager/load/ram_cache/__init__.py | 0 .../load/ram_cache/ram_cache_base.py | 145 ++++++++ .../load/ram_cache/ram_cache_default.py | 332 ++++++++++++++++++ invokeai/backend/model_manager/load/vae.py | 31 ++ .../backend/model_manager/onnx_runtime.py | 216 ++++++++++++ invokeai/backend/model_manager/probe.py | 8 +- tests/test_model_probe.py | 5 +- 14 files changed, 1334 insertions(+), 6 deletions(-) create mode 100644 invokeai/backend/model_manager/load/__init__.py create mode 100644 invokeai/backend/model_manager/load/load_base.py create mode 100644 invokeai/backend/model_manager/load/load_default.py create mode 100644 invokeai/backend/model_manager/load/memory_snapshot.py create mode 100644 invokeai/backend/model_manager/load/model_util.py create mode 100644 invokeai/backend/model_manager/load/optimizations.py create mode 100644 invokeai/backend/model_manager/load/ram_cache/__init__.py create mode 100644 invokeai/backend/model_manager/load/ram_cache/ram_cache_base.py create mode 100644 invokeai/backend/model_manager/load/ram_cache/ram_cache_default.py create mode 100644 invokeai/backend/model_manager/load/vae.py create mode 100644 invokeai/backend/model_manager/onnx_runtime.py diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index b4685caf10..338669c873 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -152,6 +152,7 @@ class _DiffusersConfig(ModelConfigBase): format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers repo_variant: Optional[ModelRepoVariant] = ModelRepoVariant.DEFAULT + class LoRAConfig(ModelConfigBase): """Model config for LoRA/Lycoris models.""" @@ -179,6 +180,7 @@ class ControlNetDiffusersConfig(_DiffusersConfig): type: Literal[ModelType.ControlNet] = ModelType.ControlNet format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers + class ControlNetCheckpointConfig(_CheckpointConfig): """Model config for ControlNet models (diffusers version).""" @@ -214,6 +216,7 @@ class MainDiffusersConfig(_DiffusersConfig, _MainConfig): prediction_type: SchedulerPredictionType = SchedulerPredictionType.Epsilon upcast_attention: bool = False + class ONNXSD1Config(_MainConfig): """Model config for ONNX format models based on sd-1.""" diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py new file mode 100644 index 0000000000..7cb7222b71 --- /dev/null +++ b/invokeai/backend/model_manager/load/load_base.py @@ -0,0 +1,193 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +""" +Base class for model loading in InvokeAI. + +Use like this: + + loader = AnyModelLoader(...) + loaded_model = loader.get_model('019ab39adfa1840455') + with loaded_model as model: # context manager moves model into VRAM + # do something with loaded_model +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from logging import Logger +from pathlib import Path +from typing import Any, Callable, Dict, Optional, Type, Union + +import torch +from diffusers import DiffusionPipeline +from injector import inject + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.app.services.model_records import ModelRecordServiceBase +from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType +from invokeai.backend.model_manager.convert_cache import ModelConvertCacheBase +from invokeai.backend.model_manager.onnx_runtime import IAIOnnxRuntimeModel +from invokeai.backend.model_manager.ram_cache import ModelCacheBase + +AnyModel = Union[DiffusionPipeline, torch.nn.Module, IAIOnnxRuntimeModel] + + +class ModelLockerBase(ABC): + """Base class for the model locker used by the loader.""" + + @abstractmethod + def lock(self) -> None: + """Lock the contained model and move it into VRAM.""" + pass + + @abstractmethod + def unlock(self) -> None: + """Unlock the contained model, and remove it from VRAM.""" + pass + + @property + @abstractmethod + def model(self) -> AnyModel: + """Return the model.""" + pass + + +@dataclass +class LoadedModel: + """Context manager object that mediates transfer from RAM<->VRAM.""" + + config: AnyModelConfig + locker: ModelLockerBase + + def __enter__(self) -> AnyModel: # I think load_file() always returns a dict + """Context entry.""" + self.locker.lock() + return self.model + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Context exit.""" + self.locker.unlock() + + @property + def model(self) -> AnyModel: + """Return the model without locking it.""" + return self.locker.model() + + +class ModelLoaderBase(ABC): + """Abstract base class for loading models into RAM/VRAM.""" + + @abstractmethod + def __init__( + self, + app_config: InvokeAIAppConfig, + logger: Logger, + ram_cache: ModelCacheBase, + convert_cache: ModelConvertCacheBase, + ): + """Initialize the loader.""" + pass + + @abstractmethod + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """ + Return a model given its key. + + Given a model key identified in the model configuration backend, + return a ModelInfo object that can be used to retrieve the model. + + :param model_config: Model configuration, as returned by ModelConfigRecordStore + :param submodel_type: an ModelType enum indicating the portion of + the model to retrieve (e.g. ModelType.Vae) + """ + pass + + @abstractmethod + def get_size_fs( + self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None + ) -> int: + """Return size in bytes of the model, calculated before loading.""" + pass + + +# TO DO: Better name? +class AnyModelLoader: + """This class manages the model loaders and invokes the correct one to load a model of given base and type.""" + + # this tracks the loader subclasses + _registry: Dict[str, Type[ModelLoaderBase]] = {} + + @inject + def __init__( + self, + store: ModelRecordServiceBase, + app_config: InvokeAIAppConfig, + logger: Logger, + ram_cache: ModelCacheBase, + convert_cache: ModelConvertCacheBase, + ): + """Store the provided ModelRecordServiceBase and empty the registry.""" + self._store = store + self._app_config = app_config + self._logger = logger + self._ram_cache = ram_cache + self._convert_cache = convert_cache + + def get_model(self, key: str, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """ + Return a model given its key. + + Given a model key identified in the model configuration backend, + return a ModelInfo object that can be used to retrieve the model. + + :param key: model key, as known to the config backend + :param submodel_type: an ModelType enum indicating the portion of + the model to retrieve (e.g. ModelType.Vae) + """ + model_config = self._store.get_model(key) + implementation = self.__class__.get_implementation( + base=model_config.base, type=model_config.type, format=model_config.format + ) + return implementation( + app_config=self._app_config, + logger=self._logger, + ram_cache=self._ram_cache, + convert_cache=self._convert_cache, + ).load_model(model_config, submodel_type) + + @staticmethod + def _to_registry_key(base: BaseModelType, type: ModelType, format: ModelFormat) -> str: + return "-".join([base.value, type.value, format.value]) + + @classmethod + def get_implementation(cls, base: BaseModelType, type: ModelType, format: ModelFormat) -> Type[ModelLoaderBase]: + """Get subclass of ModelLoaderBase registered to handle base and type.""" + key1 = cls._to_registry_key(base, type, format) # for a specific base type + key2 = cls._to_registry_key(BaseModelType.Any, type, format) # with wildcard Any + implementation = cls._registry.get(key1) or cls._registry.get(key2) + if not implementation: + raise NotImplementedError( + "No subclass of LoadedModel is registered for base={base}, type={type}, format={format}" + ) + return implementation + + @classmethod + def register( + cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any + ) -> Callable[[Type[ModelLoaderBase]], Type[ModelLoaderBase]]: + """Define a decorator which registers the subclass of loader.""" + + def decorator(subclass: Type[ModelLoaderBase]) -> Type[ModelLoaderBase]: + print("Registering class", subclass.__name__) + key = cls._to_registry_key(base, type, format) + cls._registry[key] = subclass + return subclass + + return decorator + + +# in _init__.py will call something like +# def configure_loader_dependencies(binder): +# binder.bind(ModelRecordServiceBase, ApiDependencies.invoker.services.model_records, scope=singleton) +# binder.bind(InvokeAIAppConfig, ApiDependencies.invoker.services.configuration, scope=singleton) +# etc +# injector = Injector(configure_loader_dependencies) +# loader = injector.get(ModelFactory) diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py new file mode 100644 index 0000000000..eb2d432aaa --- /dev/null +++ b/invokeai/backend/model_manager/load/load_default.py @@ -0,0 +1,168 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Default implementation of model loading in InvokeAI.""" + +import sys +from logging import Logger +from pathlib import Path +from typing import Any, Dict, Optional, Tuple + +from diffusers import ModelMixin +from diffusers.configuration_utils import ConfigMixin +from injector import inject + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.model_manager import AnyModelConfig, InvalidModelConfigException, ModelRepoVariant, SubModelType +from invokeai.backend.model_manager.convert_cache import ModelConvertCacheBase +from invokeai.backend.model_manager.load.load_base import AnyModel, LoadedModel, ModelLoaderBase +from invokeai.backend.model_manager.load.model_util import calc_model_size_by_fs +from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init +from invokeai.backend.model_manager.ram_cache import ModelCacheBase, ModelLockerBase +from invokeai.backend.util.devices import choose_torch_device, torch_dtype + + +class ConfigLoader(ConfigMixin): + """Subclass of ConfigMixin for loading diffusers configuration files.""" + + @classmethod + def load_config(cls, *args: Any, **kwargs: Any) -> Dict[str, Any]: + """Load a diffusrs ConfigMixin configuration.""" + cls.config_name = kwargs.pop("config_name") + # Diffusers doesn't provide typing info + return super().load_config(*args, **kwargs) # type: ignore + + +# TO DO: The loader is not thread safe! +class ModelLoader(ModelLoaderBase): + """Default implementation of ModelLoaderBase.""" + + @inject # can inject instances of each of the classes in the call signature + def __init__( + self, + app_config: InvokeAIAppConfig, + logger: Logger, + ram_cache: ModelCacheBase, + convert_cache: ModelConvertCacheBase, + ): + """Initialize the loader.""" + self._app_config = app_config + self._logger = logger + self._ram_cache = ram_cache + self._convert_cache = convert_cache + self._torch_dtype = torch_dtype(choose_torch_device()) + self._size: Optional[int] = None # model size + + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """ + Return a model given its configuration. + + Given a model's configuration as returned by the ModelRecordConfigStore service, + return a LoadedModel object that can be used for inference. + + :param model config: Configuration record for this model + :param submodel_type: an ModelType enum indicating the portion of + the model to retrieve (e.g. ModelType.Vae) + """ + if model_config.type == "main" and not submodel_type: + raise InvalidModelConfigException("submodel_type is required when loading a main model") + + model_path, is_submodel_override = self._get_model_path(model_config, submodel_type) + if is_submodel_override: + submodel_type = None + + if not model_path.exists(): + raise InvalidModelConfigException(f"Files for model 'model_config.name' not found at {model_path}") + + model_path = self._convert_if_needed(model_config, model_path, submodel_type) + locker = self._load_if_needed(model_config, model_path, submodel_type) + return LoadedModel(config=model_config, locker=locker) + + # IMPORTANT: This needs to be overridden in the StableDiffusion subclass so as to handle vae overrides + # and submodels!!!! + def _get_model_path( + self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None + ) -> Tuple[Path, bool]: + model_base = self._app_config.models_path + return ((model_base / config.path).resolve(), False) + + def _convert_if_needed( + self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None + ) -> Path: + if not self._needs_conversion(config): + return model_path + + self._convert_cache.make_room(self._size or self.get_size_fs(config, model_path, submodel_type)) + cache_path: Path = self._convert_cache.cache_path(config.key) + if cache_path.exists(): + return cache_path + + self._convert_model(model_path, cache_path) + return cache_path + + def _needs_conversion(self, config: AnyModelConfig) -> bool: + return False + + def _load_if_needed( + self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None + ) -> ModelLockerBase: + # TO DO: This is not thread safe! + if self._ram_cache.exists(config.key, submodel_type): + return self._ram_cache.get(config.key, submodel_type) + + model_variant = getattr(config, "repo_variant", None) + self._ram_cache.make_room(self.get_size_fs(config, model_path, submodel_type)) + + # This is where the model is actually loaded! + with skip_torch_weight_init(): + loaded_model = self._load_model(model_path, model_variant=model_variant, submodel_type=submodel_type) + + self._ram_cache.put( + config.key, + submodel_type=submodel_type, + model=loaded_model, + ) + + return self._ram_cache.get(config.key, submodel_type) + + def get_size_fs( + self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None + ) -> int: + """Get the size of the model on disk.""" + return calc_model_size_by_fs( + model_path=model_path, + subfolder=submodel_type.value if submodel_type else None, + variant=config.repo_variant if hasattr(config, "repo_variant") else None, + ) + + def _convert_model(self, model_path: Path, cache_path: Path) -> None: + raise NotImplementedError + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + raise NotImplementedError + + def _load_diffusers_config(self, model_path: Path, config_name: str = "config.json") -> Dict[str, Any]: + return ConfigLoader.load_config(model_path, config_name=config_name) + + # TO DO: Add exception handling + def _hf_definition_to_type(self, module: str, class_name: str) -> ModelMixin: # fix with correct type + if module in ["diffusers", "transformers"]: + res_type = sys.modules[module] + else: + res_type = sys.modules["diffusers"].pipelines + result: ModelMixin = getattr(res_type, class_name) + return result + + # TO DO: Add exception handling + def _get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelType] = None) -> ModelMixin: + if submodel_type: + config = self._load_diffusers_config(model_path, config_name="model_index.json") + module, class_name = config[submodel_type.value] + return self._hf_definition_to_type(module=module, class_name=class_name) + else: + config = self._load_diffusers_config(model_path, config_name="config.json") + class_name = config["_class_name"] + return self._hf_definition_to_type(module="diffusers", class_name=class_name) diff --git a/invokeai/backend/model_manager/load/memory_snapshot.py b/invokeai/backend/model_manager/load/memory_snapshot.py new file mode 100644 index 0000000000..504829a427 --- /dev/null +++ b/invokeai/backend/model_manager/load/memory_snapshot.py @@ -0,0 +1,100 @@ +import gc +from typing import Optional + +import psutil +import torch +from typing_extensions import Self + +from invokeai.backend.model_management.libc_util import LibcUtil, Struct_mallinfo2 + +GB = 2**30 # 1 GB + + +class MemorySnapshot: + """A snapshot of RAM and VRAM usage. All values are in bytes.""" + + def __init__(self, process_ram: int, vram: Optional[int], malloc_info: Optional[Struct_mallinfo2]): + """Initialize a MemorySnapshot. + + Most of the time, `MemorySnapshot` will be constructed with `MemorySnapshot.capture()`. + + Args: + process_ram (int): CPU RAM used by the current process. + vram (Optional[int]): VRAM used by torch. + malloc_info (Optional[Struct_mallinfo2]): Malloc info obtained from LibcUtil. + """ + self.process_ram = process_ram + self.vram = vram + self.malloc_info = malloc_info + + @classmethod + def capture(cls, run_garbage_collector: bool = True) -> Self: + """Capture and return a MemorySnapshot. + + Note: This function has significant overhead, particularly if `run_garbage_collector == True`. + + Args: + run_garbage_collector (bool, optional): If true, gc.collect() will be run before checking the process RAM + usage. Defaults to True. + + Returns: + MemorySnapshot + """ + if run_garbage_collector: + gc.collect() + + # According to the psutil docs (https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info), rss is + # supported on all platforms. + process_ram = psutil.Process().memory_info().rss + + if torch.cuda.is_available(): + vram = torch.cuda.memory_allocated() + else: + # TODO: We could add support for mps.current_allocated_memory() as well. Leaving out for now until we have + # time to test it properly. + vram = None + + try: + malloc_info = LibcUtil().mallinfo2() # type: ignore + except (OSError, AttributeError): + # OSError: This is expected in environments that do not have the 'libc.so.6' shared library. + # AttributeError: This is expected in environments that have `libc.so.6` but do not have the `mallinfo2` (e.g. glibc < 2.33) + # TODO: Does `mallinfo` work? + malloc_info = None + + return cls(process_ram, vram, malloc_info) + + +def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: Optional[MemorySnapshot]) -> str: + """Get a pretty string describing the difference between two `MemorySnapshot`s.""" + + def get_msg_line(prefix: str, val1: int, val2: int) -> str: + diff = val2 - val1 + return f"{prefix: <30} ({(diff/GB):+5.3f}): {(val1/GB):5.3f}GB -> {(val2/GB):5.3f}GB\n" + + msg = "" + + if snapshot_1 is None or snapshot_2 is None: + return msg + + msg += get_msg_line("Process RAM", snapshot_1.process_ram, snapshot_2.process_ram) + + if snapshot_1.malloc_info is not None and snapshot_2.malloc_info is not None: + msg += get_msg_line("libc mmap allocated", snapshot_1.malloc_info.hblkhd, snapshot_2.malloc_info.hblkhd) + + msg += get_msg_line("libc arena used", snapshot_1.malloc_info.uordblks, snapshot_2.malloc_info.uordblks) + + msg += get_msg_line("libc arena free", snapshot_1.malloc_info.fordblks, snapshot_2.malloc_info.fordblks) + + libc_total_allocated_1 = snapshot_1.malloc_info.arena + snapshot_1.malloc_info.hblkhd + libc_total_allocated_2 = snapshot_2.malloc_info.arena + snapshot_2.malloc_info.hblkhd + msg += get_msg_line("libc total allocated", libc_total_allocated_1, libc_total_allocated_2) + + libc_total_used_1 = snapshot_1.malloc_info.uordblks + snapshot_1.malloc_info.hblkhd + libc_total_used_2 = snapshot_2.malloc_info.uordblks + snapshot_2.malloc_info.hblkhd + msg += get_msg_line("libc total used", libc_total_used_1, libc_total_used_2) + + if snapshot_1.vram is not None and snapshot_2.vram is not None: + msg += get_msg_line("VRAM", snapshot_1.vram, snapshot_2.vram) + + return msg diff --git a/invokeai/backend/model_manager/load/model_util.py b/invokeai/backend/model_manager/load/model_util.py new file mode 100644 index 0000000000..18407cbca2 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_util.py @@ -0,0 +1,109 @@ +# Copyright (c) 2024 The InvokeAI Development Team +"""Various utility functions needed by the loader and caching system.""" + +import json +from pathlib import Path +from typing import Optional, Union + +import torch +from diffusers import DiffusionPipeline + +from invokeai.backend.model_manager.onnx_runtime import IAIOnnxRuntimeModel + + +def calc_model_size_by_data(model: Union[DiffusionPipeline, torch.nn.Module, IAIOnnxRuntimeModel]) -> int: + """Get size of a model in memory in bytes.""" + if isinstance(model, DiffusionPipeline): + return _calc_pipeline_by_data(model) + elif isinstance(model, torch.nn.Module): + return _calc_model_by_data(model) + elif isinstance(model, IAIOnnxRuntimeModel): + return _calc_onnx_model_by_data(model) + else: + return 0 + + +def _calc_pipeline_by_data(pipeline: DiffusionPipeline) -> int: + res = 0 + assert hasattr(pipeline, "components") + for submodel_key in pipeline.components.keys(): + submodel = getattr(pipeline, submodel_key) + if submodel is not None and isinstance(submodel, torch.nn.Module): + res += _calc_model_by_data(submodel) + return res + + +def _calc_model_by_data(model: torch.nn.Module) -> int: + mem_params = sum([param.nelement() * param.element_size() for param in model.parameters()]) + mem_bufs = sum([buf.nelement() * buf.element_size() for buf in model.buffers()]) + mem: int = mem_params + mem_bufs # in bytes + return mem + + +def _calc_onnx_model_by_data(model: IAIOnnxRuntimeModel) -> int: + tensor_size = model.tensors.size() * 2 # The session doubles this + mem = tensor_size # in bytes + return mem + + +def calc_model_size_by_fs(model_path: Path, subfolder: Optional[str] = None, variant: Optional[str] = None) -> int: + """Estimate the size of a model on disk in bytes.""" + if subfolder is not None: + model_path = model_path / subfolder + + # this can happen when, for example, the safety checker is not downloaded. + if not model_path.exists(): + return 0 + + all_files = [f for f in model_path.iterdir() if (model_path / f).is_file()] + + fp16_files = {f for f in all_files if ".fp16." in f.name or ".fp16-" in f.name} + bit8_files = {f for f in all_files if ".8bit." in f.name or ".8bit-" in f.name} + other_files = set(all_files) - fp16_files - bit8_files + + if variant is None: + files = other_files + elif variant == "fp16": + files = fp16_files + elif variant == "8bit": + files = bit8_files + else: + raise NotImplementedError(f"Unknown variant: {variant}") + + # try read from index if exists + index_postfix = ".index.json" + if variant is not None: + index_postfix = f".index.{variant}.json" + + for file in files: + if not file.name.endswith(index_postfix): + continue + try: + with open(model_path / file, "r") as f: + index_data = json.loads(f.read()) + return int(index_data["metadata"]["total_size"]) + except Exception: + pass + + # calculate files size if there is no index file + formats = [ + (".safetensors",), # safetensors + (".bin",), # torch + (".onnx", ".pb"), # onnx + (".msgpack",), # flax + (".ckpt",), # tf + (".h5",), # tf2 + ] + + for file_format in formats: + model_files = [f for f in files if f.suffix in file_format] + if len(model_files) == 0: + continue + + model_size = 0 + for model_file in model_files: + file_stats = (model_path / model_file).stat() + model_size += file_stats.st_size + return model_size + + return 0 # scheduler/feature_extractor/tokenizer - models without loading to gpu diff --git a/invokeai/backend/model_manager/load/optimizations.py b/invokeai/backend/model_manager/load/optimizations.py new file mode 100644 index 0000000000..a46d262175 --- /dev/null +++ b/invokeai/backend/model_manager/load/optimizations.py @@ -0,0 +1,30 @@ +from contextlib import contextmanager + +import torch + + +def _no_op(*args, **kwargs): + pass + + +@contextmanager +def skip_torch_weight_init(): + """A context manager that monkey-patches several of the common torch layers (torch.nn.Linear, torch.nn.Conv1d, etc.) + to skip weight initialization. + + By default, `torch.nn.Linear` and `torch.nn.ConvNd` layers initialize their weights (according to a particular + distribution) when __init__ is called. This weight initialization step can take a significant amount of time, and is + completely unnecessary if the intent is to load checkpoint weights from disk for the layer. This context manager + monkey-patches common torch layers to skip the weight initialization step. + """ + torch_modules = [torch.nn.Linear, torch.nn.modules.conv._ConvNd, torch.nn.Embedding] + saved_functions = [m.reset_parameters for m in torch_modules] + + try: + for torch_module in torch_modules: + torch_module.reset_parameters = _no_op + + yield None + finally: + for torch_module, saved_function in zip(torch_modules, saved_functions, strict=True): + torch_module.reset_parameters = saved_function diff --git a/invokeai/backend/model_manager/load/ram_cache/__init__.py b/invokeai/backend/model_manager/load/ram_cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/backend/model_manager/load/ram_cache/ram_cache_base.py b/invokeai/backend/model_manager/load/ram_cache/ram_cache_base.py new file mode 100644 index 0000000000..cd80d1e78b --- /dev/null +++ b/invokeai/backend/model_manager/load/ram_cache/ram_cache_base.py @@ -0,0 +1,145 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team +# TODO: Add Stalker's proper name to copyright +""" +Manage a RAM cache of diffusion/transformer models for fast switching. +They are moved between GPU VRAM and CPU RAM as necessary. If the cache +grows larger than a preset maximum, then the least recently used +model will be cleared and (re)loaded from disk when next needed. +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from logging import Logger +from typing import Dict, Optional + +import torch + +from invokeai.backend.model_manager import SubModelType +from invokeai.backend.model_manager.load.load_base import AnyModel, ModelLockerBase + + +@dataclass +class CacheStats(object): + """Data object to record statistics on cache hits/misses.""" + + hits: int = 0 # cache hits + misses: int = 0 # cache misses + high_watermark: int = 0 # amount of cache used + in_cache: int = 0 # number of models in cache + cleared: int = 0 # number of models cleared to make space + cache_size: int = 0 # total size of cache + loaded_model_sizes: Dict[str, int] = field(default_factory=dict) + + +@dataclass +class CacheRecord: + """Elements of the cache.""" + + key: str + model: AnyModel + size: int + _locks: int = 0 + + def lock(self) -> None: + """Lock this record.""" + self._locks += 1 + + def unlock(self) -> None: + """Unlock this record.""" + self._locks -= 1 + assert self._locks >= 0 + + @property + def locked(self) -> bool: + """Return true if record is locked.""" + return self._locks > 0 + + +class ModelCacheBase(ABC): + """Virtual base class for RAM model cache.""" + + @property + @abstractmethod + def storage_device(self) -> torch.device: + """Return the storage device (e.g. "CPU" for RAM).""" + pass + + @property + @abstractmethod + def execution_device(self) -> torch.device: + """Return the exection device (e.g. "cuda" for VRAM).""" + pass + + @property + @abstractmethod + def lazy_offloading(self) -> bool: + """Return true if the cache is configured to lazily offload models in VRAM.""" + pass + + @abstractmethod + def offload_unlocked_models(self) -> None: + """Offload from VRAM any models not actively in use.""" + pass + + @abstractmethod + def move_model_to_device(self, cache_entry: CacheRecord, device: torch.device) -> None: + """Move model into the indicated device.""" + pass + + @property + @abstractmethod + def logger(self) -> Logger: + """Return the logger used by the cache.""" + pass + + @abstractmethod + def make_room(self, size: int) -> None: + """Make enough room in the cache to accommodate a new model of indicated size.""" + pass + + @abstractmethod + def put( + self, + key: str, + model: AnyModel, + submodel_type: Optional[SubModelType] = None, + ) -> None: + """Store model under key and optional submodel_type.""" + pass + + @abstractmethod + def get( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + ) -> ModelLockerBase: + """ + Retrieve model locker object using key and optional submodel_type. + + This may return an UnknownModelException if the model is not in the cache. + """ + pass + + @abstractmethod + def exists( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + ) -> bool: + """Return true if the model identified by key and submodel_type is in the cache.""" + pass + + @abstractmethod + def cache_size(self) -> int: + """Get the total size of the models currently cached.""" + pass + + @abstractmethod + def get_stats(self) -> CacheStats: + """Return cache hit/miss/size statistics.""" + pass + + @abstractmethod + def print_cuda_stats(self) -> None: + """Log debugging information on CUDA usage.""" + pass diff --git a/invokeai/backend/model_manager/load/ram_cache/ram_cache_default.py b/invokeai/backend/model_manager/load/ram_cache/ram_cache_default.py new file mode 100644 index 0000000000..bd43e978c8 --- /dev/null +++ b/invokeai/backend/model_manager/load/ram_cache/ram_cache_default.py @@ -0,0 +1,332 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team +# TODO: Add Stalker's proper name to copyright +""" +Manage a RAM cache of diffusion/transformer models for fast switching. +They are moved between GPU VRAM and CPU RAM as necessary. If the cache +grows larger than a preset maximum, then the least recently used +model will be cleared and (re)loaded from disk when next needed. + +The cache returns context manager generators designed to load the +model into the GPU within the context, and unload outside the +context. Use like this: + + cache = ModelCache(max_cache_size=7.5) + with cache.get_model('runwayml/stable-diffusion-1-5') as SD1, + cache.get_model('stabilityai/stable-diffusion-2') as SD2: + do_something_in_GPU(SD1,SD2) + + +""" + +import math +import time +from contextlib import suppress +from logging import Logger +from typing import Any, Dict, List, Optional + +import torch + +from invokeai.app.services.model_records import UnknownModelException +from invokeai.backend.model_manager import SubModelType +from invokeai.backend.model_manager.load.load_base import AnyModel, ModelLockerBase +from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff +from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data +from invokeai.backend.model_manager.load.ram_cache.ram_cache_base import CacheRecord, CacheStats, ModelCacheBase +from invokeai.backend.util.devices import choose_torch_device +from invokeai.backend.util.logging import InvokeAILogger + +if choose_torch_device() == torch.device("mps"): + from torch import mps + +# Maximum size of the cache, in gigs +# Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously +DEFAULT_MAX_CACHE_SIZE = 6.0 + +# amount of GPU memory to hold in reserve for use by generations (GB) +DEFAULT_MAX_VRAM_CACHE_SIZE = 2.75 + +# actual size of a gig +GIG = 1073741824 + +# Size of a MB in bytes. +MB = 2**20 + + +class ModelCache(ModelCacheBase): + """Implementation of ModelCacheBase.""" + + def __init__( + self, + max_cache_size: float = DEFAULT_MAX_CACHE_SIZE, + max_vram_cache_size: float = DEFAULT_MAX_VRAM_CACHE_SIZE, + execution_device: torch.device = torch.device("cuda"), + storage_device: torch.device = torch.device("cpu"), + precision: torch.dtype = torch.float16, + sequential_offload: bool = False, + lazy_offloading: bool = True, + sha_chunksize: int = 16777216, + log_memory_usage: bool = False, + logger: Optional[Logger] = None, + ): + """ + Initialize the model RAM cache. + + :param max_cache_size: Maximum size of the RAM cache [6.0 GB] + :param execution_device: Torch device to load active model into [torch.device('cuda')] + :param storage_device: Torch device to save inactive model in [torch.device('cpu')] + :param precision: Precision for loaded models [torch.float16] + :param lazy_offloading: Keep model in VRAM until another model needs to be loaded + :param sequential_offload: Conserve VRAM by loading and unloading each stage of the pipeline sequentially + :param log_memory_usage: If True, a memory snapshot will be captured before and after every model cache + operation, and the result will be logged (at debug level). There is a time cost to capturing the memory + snapshots, so it is recommended to disable this feature unless you are actively inspecting the model cache's + behaviour. + """ + # allow lazy offloading only when vram cache enabled + self._lazy_offloading = lazy_offloading and max_vram_cache_size > 0 + self._precision: torch.dtype = precision + self._max_cache_size: float = max_cache_size + self._max_vram_cache_size: float = max_vram_cache_size + self._execution_device: torch.device = execution_device + self._storage_device: torch.device = storage_device + self._logger = logger or InvokeAILogger.get_logger(self.__class__.__name__) + self._log_memory_usage = log_memory_usage + + # used for stats collection + self.stats = None + + self._cached_models: Dict[str, CacheRecord] = {} + self._cache_stack: List[str] = [] + + class ModelLocker(ModelLockerBase): + """Internal class that mediates movement in and out of GPU.""" + + def __init__(self, cache: ModelCacheBase, cache_entry: CacheRecord): + """ + Initialize the model locker. + + :param cache: The ModelCache object + :param cache_entry: The entry in the model cache + """ + self._cache = cache + self._cache_entry = cache_entry + + @property + def model(self) -> AnyModel: + """Return the model without moving it around.""" + return self._cache_entry.model + + def lock(self) -> Any: + """Move the model into the execution device (GPU) and lock it.""" + if not hasattr(self.model, "to"): + return self.model + + # NOTE that the model has to have the to() method in order for this code to move it into GPU! + self._cache_entry.lock() + + try: + if self._cache.lazy_offloading: + self._cache.offload_unlocked_models() + + self._cache.move_model_to_device(self._cache_entry, self._cache.execution_device) + + self._cache.logger.debug(f"Locking {self._cache_entry.key} in {self._cache.execution_device}") + self._cache.print_cuda_stats() + + except Exception: + self._cache_entry.unlock() + raise + return self.model + + def unlock(self) -> None: + """Call upon exit from context.""" + if not hasattr(self.model, "to"): + return + + self._cache_entry.unlock() + if not self._cache.lazy_offloading: + self._cache.offload_unlocked_models() + self._cache.print_cuda_stats() + + @property + def logger(self) -> Logger: + """Return the logger used by the cache.""" + return self._logger + + @property + def lazy_offloading(self) -> bool: + """Return true if the cache is configured to lazily offload models in VRAM.""" + return self._lazy_offloading + + @property + def storage_device(self) -> torch.device: + """Return the storage device (e.g. "CPU" for RAM).""" + return self._storage_device + + @property + def execution_device(self) -> torch.device: + """Return the exection device (e.g. "cuda" for VRAM).""" + return self._execution_device + + def cache_size(self) -> int: + """Get the total size of the models currently cached.""" + total = 0 + for cache_record in self._cached_models.values(): + total += cache_record.size + return total + + def exists( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + ) -> bool: + """Return true if the model identified by key and submodel_type is in the cache.""" + key = self._make_cache_key(key, submodel_type) + return key in self._cached_models + + def put( + self, + key: str, + model: AnyModel, + submodel_type: Optional[SubModelType] = None, + ) -> None: + """Store model under key and optional submodel_type.""" + key = self._make_cache_key(key, submodel_type) + assert key not in self._cached_models + + loaded_model_size = calc_model_size_by_data(model) + cache_record = CacheRecord(key, model, loaded_model_size) + self._cached_models[key] = cache_record + self._cache_stack.append(key) + + def get( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + ) -> ModelLockerBase: + """ + Retrieve model using key and optional submodel_type. + + This may return an UnknownModelException if the model is not in the cache. + """ + key = self._make_cache_key(key, submodel_type) + if key not in self._cached_models: + raise UnknownModelException + + # this moves the entry to the top (right end) of the stack + with suppress(Exception): + self._cache_stack.remove(key) + self._cache_stack.append(key) + cache_entry = self._cached_models[key] + return self.ModelLocker( + cache=self, + cache_entry=cache_entry, + ) + + def _capture_memory_snapshot(self) -> Optional[MemorySnapshot]: + if self._log_memory_usage: + return MemorySnapshot.capture() + return None + + def _make_cache_key(self, model_key: str, submodel_type: Optional[SubModelType] = None) -> str: + if submodel_type: + return f"{model_key}:{submodel_type.value}" + else: + return model_key + + def offload_unlocked_models(self) -> None: + """Move any unused models from VRAM.""" + reserved = self._max_vram_cache_size * GIG + vram_in_use = torch.cuda.memory_allocated() + self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM used for models; max allowed={(reserved/GIG):.2f}GB") + for _, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size): + if vram_in_use <= reserved: + break + if not cache_entry.locked: + self.move_model_to_device(cache_entry, self.storage_device) + + vram_in_use = torch.cuda.memory_allocated() + self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM used for models; max allowed={(reserved/GIG):.2f}GB") + + torch.cuda.empty_cache() + if choose_torch_device() == torch.device("mps"): + mps.empty_cache() + + # TO DO: Only reason to pass the CacheRecord rather than the model is to get the key and size + # for printing debugging messages. Revisit whether this is necessary + def move_model_to_device(self, cache_entry: CacheRecord, target_device: torch.device) -> None: + """Move model into the indicated device.""" + # These attributes are not in the base class but in derived classes + assert hasattr(cache_entry.model, "device") + assert hasattr(cache_entry.model, "to") + + source_device = cache_entry.model.device + + # Note: We compare device types only so that 'cuda' == 'cuda:0'. This would need to be revised to support + # multi-GPU. + if torch.device(source_device).type == torch.device(target_device).type: + return + + start_model_to_time = time.time() + snapshot_before = self._capture_memory_snapshot() + cache_entry.model.to(target_device) + snapshot_after = self._capture_memory_snapshot() + end_model_to_time = time.time() + self.logger.debug( + f"Moved model '{cache_entry.key}' from {source_device} to" + f" {target_device} in {(end_model_to_time-start_model_to_time):.2f}s.\n" + f"Estimated model size: {(cache_entry.size/GIG):.3f} GB.\n" + f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" + ) + + if ( + snapshot_before is not None + and snapshot_after is not None + and snapshot_before.vram is not None + and snapshot_after.vram is not None + ): + vram_change = abs(snapshot_before.vram - snapshot_after.vram) + + # If the estimated model size does not match the change in VRAM, log a warning. + if not math.isclose( + vram_change, + cache_entry.size, + rel_tol=0.1, + abs_tol=10 * MB, + ): + self.logger.debug( + f"Moving model '{cache_entry.key}' from {source_device} to" + f" {target_device} caused an unexpected change in VRAM usage. The model's" + " estimated size may be incorrect. Estimated model size:" + f" {(cache_entry.size/GIG):.3f} GB.\n" + f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" + ) + + def print_cuda_stats(self) -> None: + """Log CUDA diagnostics.""" + vram = "%4.2fG" % (torch.cuda.memory_allocated() / GIG) + ram = "%4.2fG" % self.cache_size() + + cached_models = 0 + loaded_models = 0 + locked_models = 0 + for cache_record in self._cached_models.values(): + cached_models += 1 + assert hasattr(cache_record.model, "device") + if cache_record.model.device is self.storage_device: + loaded_models += 1 + if cache_record.locked: + locked_models += 1 + + self.logger.debug( + f"Current VRAM/RAM usage: {vram}/{ram}; cached_models/loaded_models/locked_models/ =" + f" {cached_models}/{loaded_models}/{locked_models}" + ) + + def get_stats(self) -> CacheStats: + """Return cache hit/miss/size statistics.""" + raise NotImplementedError + + def make_room(self, size: int) -> None: + """Make enough room in the cache to accommodate a new model of indicated size.""" + raise NotImplementedError diff --git a/invokeai/backend/model_manager/load/vae.py b/invokeai/backend/model_manager/load/vae.py new file mode 100644 index 0000000000..a6cbe241e1 --- /dev/null +++ b/invokeai/backend/model_manager/load/vae.py @@ -0,0 +1,31 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for VAE model loading in InvokeAI.""" + +from pathlib import Path +from typing import Dict, Optional + +import torch + +from invokeai.backend.model_manager import BaseModelType, ModelFormat, ModelRepoVariant, ModelType, SubModelType +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader + + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Vae, format=ModelFormat.Diffusers) +class VaeDiffusersModel(ModelLoader): + """Class to load VAE models.""" + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> Dict[str, torch.Tensor]: + if submodel_type is not None: + raise Exception("There are no submodels in VAEs") + vae_class = self._get_hf_load_class(model_path) + variant = model_variant.value if model_variant else "" + result: Dict[str, torch.Tensor] = vae_class.from_pretrained( + model_path, torch_dtype=self._torch_dtype, variant=variant + ) # type: ignore + return result diff --git a/invokeai/backend/model_manager/onnx_runtime.py b/invokeai/backend/model_manager/onnx_runtime.py new file mode 100644 index 0000000000..f79fa01569 --- /dev/null +++ b/invokeai/backend/model_manager/onnx_runtime.py @@ -0,0 +1,216 @@ +# Copyright (c) 2024 The InvokeAI Development Team +import os +import sys +from pathlib import Path +from typing import Any, List, Optional, Tuple, Union + +import numpy as np +import onnx +from onnx import numpy_helper +from onnxruntime import InferenceSession, SessionOptions, get_available_providers + +ONNX_WEIGHTS_NAME = "model.onnx" + + +# NOTE FROM LS: This was copied from Stalker's original implementation. +# I have not yet gone through and fixed all the type hints +class IAIOnnxRuntimeModel: + class _tensor_access: + def __init__(self, model): # type: ignore + self.model = model + self.indexes = {} + for idx, obj in enumerate(self.model.proto.graph.initializer): + self.indexes[obj.name] = idx + + def __getitem__(self, key: str): # type: ignore + value = self.model.proto.graph.initializer[self.indexes[key]] + return numpy_helper.to_array(value) + + def __setitem__(self, key: str, value: np.ndarray): # type: ignore + new_node = numpy_helper.from_array(value) + # set_external_data(new_node, location="in-memory-location") + new_node.name = key + # new_node.ClearField("raw_data") + del self.model.proto.graph.initializer[self.indexes[key]] + self.model.proto.graph.initializer.insert(self.indexes[key], new_node) + # self.model.data[key] = OrtValue.ortvalue_from_numpy(value) + + # __delitem__ + + def __contains__(self, key: str) -> bool: + return self.indexes[key] in self.model.proto.graph.initializer + + def items(self) -> List[Tuple[str, Any]]: # fixme + raise NotImplementedError("tensor.items") + # return [(obj.name, obj) for obj in self.raw_proto] + + def keys(self) -> List[str]: + return list(self.indexes.keys()) + + def values(self) -> List[Any]: # fixme + raise NotImplementedError("tensor.values") + # return [obj for obj in self.raw_proto] + + def size(self) -> int: + bytesSum = 0 + for node in self.model.proto.graph.initializer: + bytesSum += sys.getsizeof(node.raw_data) + return bytesSum + + class _access_helper: + def __init__(self, raw_proto): # type: ignore + self.indexes = {} + self.raw_proto = raw_proto + for idx, obj in enumerate(raw_proto): + self.indexes[obj.name] = idx + + def __getitem__(self, key: str): # type: ignore + return self.raw_proto[self.indexes[key]] + + def __setitem__(self, key: str, value): # type: ignore + index = self.indexes[key] + del self.raw_proto[index] + self.raw_proto.insert(index, value) + + # __delitem__ + + def __contains__(self, key: str) -> bool: + return key in self.indexes + + def items(self) -> List[Tuple[str, Any]]: + return [(obj.name, obj) for obj in self.raw_proto] + + def keys(self) -> List[str]: + return list(self.indexes.keys()) + + def values(self) -> List[Any]: # fixme + return list(self.raw_proto) + + def __init__(self, model_path: str, provider: Optional[str]): + self.path = model_path + self.session = None + self.provider = provider + """ + self.data_path = self.path + "_data" + if not os.path.exists(self.data_path): + print(f"Moving model tensors to separate file: {self.data_path}") + tmp_proto = onnx.load(model_path, load_external_data=True) + onnx.save_model(tmp_proto, self.path, save_as_external_data=True, all_tensors_to_one_file=True, location=os.path.basename(self.data_path), size_threshold=1024, convert_attribute=False) + del tmp_proto + gc.collect() + + self.proto = onnx.load(model_path, load_external_data=False) + """ + + self.proto = onnx.load(model_path, load_external_data=True) + # self.data = dict() + # for tensor in self.proto.graph.initializer: + # name = tensor.name + + # if tensor.HasField("raw_data"): + # npt = numpy_helper.to_array(tensor) + # orv = OrtValue.ortvalue_from_numpy(npt) + # # self.data[name] = orv + # # set_external_data(tensor, location="in-memory-location") + # tensor.name = name + # # tensor.ClearField("raw_data") + + self.nodes = self._access_helper(self.proto.graph.node) # type: ignore + # self.initializers = self._access_helper(self.proto.graph.initializer) + # print(self.proto.graph.input) + # print(self.proto.graph.initializer) + + self.tensors = self._tensor_access(self) # type: ignore + + # TODO: integrate with model manager/cache + def create_session(self, height=None, width=None): + if self.session is None or self.session_width != width or self.session_height != height: + # onnx.save(self.proto, "tmp.onnx") + # onnx.save_model(self.proto, "tmp.onnx", save_as_external_data=True, all_tensors_to_one_file=True, location="tmp.onnx_data", size_threshold=1024, convert_attribute=False) + # TODO: something to be able to get weight when they already moved outside of model proto + # (trimmed_model, external_data) = buffer_external_data_tensors(self.proto) + sess = SessionOptions() + # self._external_data.update(**external_data) + # sess.add_external_initializers(list(self.data.keys()), list(self.data.values())) + # sess.enable_profiling = True + + # sess.intra_op_num_threads = 1 + # sess.inter_op_num_threads = 1 + # sess.execution_mode = ExecutionMode.ORT_SEQUENTIAL + # sess.graph_optimization_level = GraphOptimizationLevel.ORT_ENABLE_ALL + # sess.enable_cpu_mem_arena = True + # sess.enable_mem_pattern = True + # sess.add_session_config_entry("session.intra_op.use_xnnpack_threadpool", "1") ########### It's the key code + self.session_height = height + self.session_width = width + if height and width: + sess.add_free_dimension_override_by_name("unet_sample_batch", 2) + sess.add_free_dimension_override_by_name("unet_sample_channels", 4) + sess.add_free_dimension_override_by_name("unet_hidden_batch", 2) + sess.add_free_dimension_override_by_name("unet_hidden_sequence", 77) + sess.add_free_dimension_override_by_name("unet_sample_height", self.session_height) + sess.add_free_dimension_override_by_name("unet_sample_width", self.session_width) + sess.add_free_dimension_override_by_name("unet_time_batch", 1) + providers = [] + if self.provider: + providers.append(self.provider) + else: + providers = get_available_providers() + if "TensorrtExecutionProvider" in providers: + providers.remove("TensorrtExecutionProvider") + try: + self.session = InferenceSession(self.proto.SerializeToString(), providers=providers, sess_options=sess) + except Exception as e: + raise e + # self.session = InferenceSession("tmp.onnx", providers=[self.provider], sess_options=self.sess_options) + # self.io_binding = self.session.io_binding() + + def release_session(self): + self.session = None + import gc + + gc.collect() + return + + def __call__(self, **kwargs): + if self.session is None: + raise Exception("You should call create_session before running model") + + inputs = {k: np.array(v) for k, v in kwargs.items()} + # output_names = self.session.get_outputs() + # for k in inputs: + # self.io_binding.bind_cpu_input(k, inputs[k]) + # for name in output_names: + # self.io_binding.bind_output(name.name) + # self.session.run_with_iobinding(self.io_binding, None) + # return self.io_binding.copy_outputs_to_cpu() + return self.session.run(None, inputs) + + # compatability with diffusers load code + @classmethod + def from_pretrained( + cls, + model_id: Union[str, Path], + subfolder: Optional[Union[str, Path]] = None, + file_name: Optional[str] = None, + provider: Optional[str] = None, + sess_options: Optional["SessionOptions"] = None, + **kwargs: Any, + ) -> Any: # fixme + file_name = file_name or ONNX_WEIGHTS_NAME + + if os.path.isdir(model_id): + model_path = model_id + if subfolder is not None: + model_path = os.path.join(model_path, subfolder) + model_path = os.path.join(model_path, file_name) + + else: + model_path = model_id + + # load model from local directory + if not os.path.isfile(model_path): + raise Exception(f"Model not found: {model_path}") + + # TODO: session options + return cls(str(model_path), provider=provider) diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index ba3ac3dd0c..9fd118b782 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -18,9 +18,9 @@ from .config import ( InvalidModelConfigException, ModelConfigFactory, ModelFormat, + ModelRepoVariant, ModelType, ModelVariantType, - ModelRepoVariant, SchedulerPredictionType, ) from .hash import FastModelHash @@ -483,8 +483,8 @@ class FolderProbeBase(ProbeBase): def get_repo_variant(self) -> ModelRepoVariant: # get all files ending in .bin or .safetensors - weight_files = list(self.model_path.glob('**/*.safetensors')) - weight_files.extend(list(self.model_path.glob('**/*.bin'))) + weight_files = list(self.model_path.glob("**/*.safetensors")) + weight_files.extend(list(self.model_path.glob("**/*.bin"))) for x in weight_files: if ".fp16" in x.suffixes: return ModelRepoVariant.FP16 @@ -496,6 +496,7 @@ class FolderProbeBase(ProbeBase): return ModelRepoVariant.ONNX return ModelRepoVariant.DEFAULT + class PipelineFolderProbe(FolderProbeBase): def get_base_type(self) -> BaseModelType: with open(self.model_path / "unet" / "config.json", "r") as file: @@ -540,7 +541,6 @@ class PipelineFolderProbe(FolderProbeBase): except Exception: pass return ModelVariantType.Normal - class VaeFolderProbe(FolderProbeBase): diff --git a/tests/test_model_probe.py b/tests/test_model_probe.py index 415559a64c..aacae06a8b 100644 --- a/tests/test_model_probe.py +++ b/tests/test_model_probe.py @@ -21,9 +21,10 @@ def test_get_base_type(vae_path: str, expected_type: BaseModelType, datadir: Pat base_type = probe.get_base_type() assert base_type == expected_type repo_variant = probe.get_repo_variant() - assert repo_variant == 'default' + assert repo_variant == "default" + def test_repo_variant(datadir: Path): probe = VaeFolderProbe(datadir / "vae" / "taesdxl-fp16") repo_variant = probe.get_repo_variant() - assert repo_variant == 'fp16' + assert repo_variant == "fp16" From 8ba5360269ce075e330abeb01b7a862373886a50 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 3 Feb 2024 22:55:09 -0500 Subject: [PATCH 112/411] model loading and conversion implemented for vaes --- invokeai/app/api/dependencies.py | 17 +- .../app/services/config/config_default.py | 21 +- .../model_install/model_install_default.py | 5 +- .../model_records/model_records_base.py | 15 +- .../model_records/model_records_sql.py | 43 +- .../app/services/shared/sqlite/sqlite_util.py | 2 + .../sqlite_migrator/migrations/migration_6.py | 44 + invokeai/backend/install/install_helper.py | 11 +- invokeai/backend/model_manager/__init__.py | 4 + invokeai/backend/model_manager/config.py | 10 +- .../convert_ckpt_to_diffusers.py | 1744 +++++++++++++++++ .../backend/model_manager/load/__init__.py | 35 + .../load/convert_cache/__init__.py | 4 + .../load/convert_cache/convert_cache_base.py | 28 + .../convert_cache/convert_cache_default.py | 64 + .../backend/model_manager/load/load_base.py | 72 +- .../model_manager/load/load_default.py | 23 +- .../load/model_cache/__init__.py | 5 + .../model_cache_base.py} | 60 +- .../model_cache_default.py} | 202 +- .../load/model_cache/model_locker.py | 59 + .../load/model_loaders/__init__.py | 3 + .../model_manager/load/model_loaders/vae.py | 83 + .../backend/model_manager/load/model_util.py | 3 + .../model_manager/load/ram_cache/__init__.py | 0 invokeai/backend/model_manager/load/vae.py | 31 - invokeai/backend/util/__init__.py | 12 +- invokeai/backend/util/devices.py | 5 +- invokeai/backend/util/util.py | 14 + 29 files changed, 2382 insertions(+), 237 deletions(-) create mode 100644 invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py create mode 100644 invokeai/backend/model_manager/convert_ckpt_to_diffusers.py create mode 100644 invokeai/backend/model_manager/load/convert_cache/__init__.py create mode 100644 invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py create mode 100644 invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py create mode 100644 invokeai/backend/model_manager/load/model_cache/__init__.py rename invokeai/backend/model_manager/load/{ram_cache/ram_cache_base.py => model_cache/model_cache_base.py} (77%) rename invokeai/backend/model_manager/load/{ram_cache/ram_cache_default.py => model_cache/model_cache_default.py} (63%) create mode 100644 invokeai/backend/model_manager/load/model_cache/model_locker.py create mode 100644 invokeai/backend/model_manager/load/model_loaders/__init__.py create mode 100644 invokeai/backend/model_manager/load/model_loaders/vae.py delete mode 100644 invokeai/backend/model_manager/load/ram_cache/__init__.py delete mode 100644 invokeai/backend/model_manager/load/vae.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 0f2a92b5c8..dcb8d21997 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -8,6 +8,8 @@ from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMe from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache from invokeai.app.services.shared.sqlite.sqlite_util import init_db +from invokeai.backend.model_manager.load import AnyModelLoader, ModelConvertCache +from invokeai.backend.model_manager.load.model_cache import ModelCache from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData from invokeai.backend.util.logging import InvokeAILogger @@ -98,15 +100,26 @@ class ApiDependencies: ) model_manager = ModelManagerService(config, logger) model_record_service = ModelRecordServiceSQL(db=db) + model_loader = AnyModelLoader( + app_config=config, + logger=logger, + ram_cache=ModelCache( + max_cache_size=config.ram_cache_size, max_vram_cache_size=config.vram_cache_size, logger=logger + ), + convert_cache=ModelConvertCache( + cache_path=config.models_convert_cache_path, max_size=config.convert_cache_size + ), + ) + model_record_service = ModelRecordServiceSQL(db=db, loader=model_loader) download_queue_service = DownloadQueueService(event_bus=events) - metadata_store = ModelMetadataStore(db=db) model_install_service = ModelInstallService( app_config=config, record_store=model_record_service, download_queue=download_queue_service, - metadata_store=metadata_store, + metadata_store=ModelMetadataStore(db=db), event_bus=events, ) + model_manager = ModelManagerService(config, logger) # TO DO: legacy model manager v1. Remove names = SimpleNameService() performance_statistics = InvocationStatsService() processor = DefaultInvocationProcessor() diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 132afc2272..b161ea18d6 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -237,6 +237,7 @@ class InvokeAIAppConfig(InvokeAISettings): autoimport_dir : Path = Field(default=Path('autoimport'), description='Path to a directory of models files to be imported on startup.', json_schema_extra=Categories.Paths) conf_path : Path = Field(default=Path('configs/models.yaml'), description='Path to models definition file', json_schema_extra=Categories.Paths) models_dir : Path = Field(default=Path('models'), description='Path to the models directory', json_schema_extra=Categories.Paths) + convert_cache_dir : Path = Field(default=Path('models/.cache'), description='Path to the converted models cache directory', json_schema_extra=Categories.Paths) legacy_conf_dir : Path = Field(default=Path('configs/stable-diffusion'), description='Path to directory of legacy checkpoint config files', json_schema_extra=Categories.Paths) db_dir : Path = Field(default=Path('databases'), description='Path to InvokeAI databases directory', json_schema_extra=Categories.Paths) outdir : Path = Field(default=Path('outputs'), description='Default folder for output images', json_schema_extra=Categories.Paths) @@ -262,6 +263,8 @@ class InvokeAIAppConfig(InvokeAISettings): # CACHE ram : float = Field(default=7.5, gt=0, description="Maximum memory amount used by model cache for rapid switching (floating point number, GB)", json_schema_extra=Categories.ModelCache, ) vram : float = Field(default=0.25, ge=0, description="Amount of VRAM reserved for model storage (floating point number, GB)", json_schema_extra=Categories.ModelCache, ) + convert_cache : float = Field(default=10.0, ge=0, description="Maximum size of on-disk converted models cache (GB)", json_schema_extra=Categories.ModelCache) + lazy_offload : bool = Field(default=True, description="Keep models in VRAM until their space is needed", json_schema_extra=Categories.ModelCache, ) log_memory_usage : bool = Field(default=False, description="If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.", json_schema_extra=Categories.ModelCache) @@ -404,6 +407,11 @@ class InvokeAIAppConfig(InvokeAISettings): """Path to the models directory.""" return self._resolve(self.models_dir) + @property + def models_convert_cache_path(self) -> Path: + """Path to the converted cache models directory.""" + return self._resolve(self.convert_cache_dir) + @property def custom_nodes_path(self) -> Path: """Path to the custom nodes directory.""" @@ -433,15 +441,20 @@ class InvokeAIAppConfig(InvokeAISettings): return True @property - def ram_cache_size(self) -> Union[Literal["auto"], float]: - """Return the ram cache size using the legacy or modern setting.""" + def ram_cache_size(self) -> float: + """Return the ram cache size using the legacy or modern setting (GB).""" return self.max_cache_size or self.ram @property - def vram_cache_size(self) -> Union[Literal["auto"], float]: - """Return the vram cache size using the legacy or modern setting.""" + def vram_cache_size(self) -> float: + """Return the vram cache size using the legacy or modern setting (GB).""" return self.max_vram_cache_size or self.vram + @property + def convert_cache_size(self) -> float: + """Return the convert cache size on disk (GB).""" + return self.convert_cache + @property def use_cpu(self) -> bool: """Return true if the device is set to CPU or the always_use_cpu flag is set.""" diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 82c667f584..2b2294bfce 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -145,7 +145,7 @@ class ModelInstallService(ModelInstallServiceBase): ) -> str: # noqa D102 model_path = Path(model_path) config = config or {} - if config.get("source") is None: + if not config.get("source"): config["source"] = model_path.resolve().as_posix() return self._register(model_path, config) @@ -156,7 +156,7 @@ class ModelInstallService(ModelInstallServiceBase): ) -> str: # noqa D102 model_path = Path(model_path) config = config or {} - if config.get("source") is None: + if not config.get("source"): config["source"] = model_path.resolve().as_posix() info: AnyModelConfig = self._probe_model(Path(model_path), config) @@ -300,6 +300,7 @@ class ModelInstallService(ModelInstallServiceBase): job.total_bytes = self._stat_size(job.local_path) job.bytes = job.total_bytes self._signal_job_running(job) + job.config_in["source"] = str(job.source) if job.inplace: key = self.register_path(job.local_path, job.config_in) else: diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py index 57597570cd..31cfecb4ec 100644 --- a/invokeai/app/services/model_records/model_records_base.py +++ b/invokeai/app/services/model_records/model_records_base.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union from pydantic import BaseModel, Field from invokeai.app.services.shared.pagination import PaginatedResults -from invokeai.backend.model_manager.config import AnyModelConfig, BaseModelType, ModelFormat, ModelType +from invokeai.backend.model_manager import LoadedModel, AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore @@ -102,6 +102,19 @@ class ModelRecordServiceBase(ABC): """ pass + @abstractmethod + def load_model(self, key: str, submodel_type: Optional[SubModelType]) -> LoadedModel: + """ + Load the indicated model into memory and return a LoadedModel object. + + :param key: Key of model config to be fetched. + :param submodel_type: For main (pipeline models), the submodel to fetch + + Exceptions: UnknownModelException -- model with this key not known + NotImplementedException -- a model loader was not provided at initialization time + """ + pass + @property @abstractmethod def metadata_store(self) -> ModelMetadataStore: diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index 4512da5d41..eee867ccb4 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -42,6 +42,7 @@ Typical usage: import json import sqlite3 +import time from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple, Union @@ -53,8 +54,10 @@ from invokeai.backend.model_manager.config import ( ModelConfigFactory, ModelFormat, ModelType, + SubModelType, ) from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore, UnknownMetadataException +from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel from ..shared.sqlite.sqlite_database import SqliteDatabase from .model_records_base import ( @@ -69,16 +72,17 @@ from .model_records_base import ( class ModelRecordServiceSQL(ModelRecordServiceBase): """Implementation of the ModelConfigStore ABC using a SQL database.""" - def __init__(self, db: SqliteDatabase): + def __init__(self, db: SqliteDatabase, loader: Optional[AnyModelLoader]=None): """ Initialize a new object from preexisting sqlite3 connection and threading lock objects. - :param conn: sqlite3 connection object - :param lock: threading Lock object + :param db: Sqlite connection object + :param loader: Initialized model loader object (optional) """ super().__init__() self._db = db - self._cursor = self._db.conn.cursor() + self._cursor = db.conn.cursor() + self._loader = loader @property def db(self) -> SqliteDatabase: @@ -199,7 +203,7 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): with self._db.lock: self._cursor.execute( """--sql - SELECT config FROM model_config + SELECT config, strftime('%s',updated_at) FROM model_config WHERE id=?; """, (key,), @@ -207,9 +211,24 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): rows = self._cursor.fetchone() if not rows: raise UnknownModelException("model not found") - model = ModelConfigFactory.make_config(json.loads(rows[0])) + model = ModelConfigFactory.make_config(json.loads(rows[0]), timestamp=rows[1]) return model + def load_model(self, key: str, submodel_type: Optional[SubModelType]) -> LoadedModel: + """ + Load the indicated model into memory and return a LoadedModel object. + + :param key: Key of model config to be fetched. + :param submodel_type: For main (pipeline models), the submodel to fetch. + + Exceptions: UnknownModelException -- model with this key not known + NotImplementedException -- a model loader was not provided at initialization time + """ + if not self._loader: + raise NotImplementedError(f"Class {self.__class__} was not initialized with a model loader") + model_config = self.get_model(key) + return self._loader.load_model(model_config, submodel_type) + def exists(self, key: str) -> bool: """ Return True if a model with the indicated key exists in the databse. @@ -265,12 +284,12 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): with self._db.lock: self._cursor.execute( f"""--sql - select config FROM model_config + select config, strftime('%s',updated_at) FROM model_config {where}; """, tuple(bindings), ) - results = [ModelConfigFactory.make_config(json.loads(x[0])) for x in self._cursor.fetchall()] + results = [ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall()] return results def search_by_path(self, path: Union[str, Path]) -> List[AnyModelConfig]: @@ -279,12 +298,12 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): with self._db.lock: self._cursor.execute( """--sql - SELECT config FROM model_config + SELECT config, strftime('%s',updated_at) FROM model_config WHERE path=?; """, (str(path),), ) - results = [ModelConfigFactory.make_config(json.loads(x[0])) for x in self._cursor.fetchall()] + results = [ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall()] return results def search_by_hash(self, hash: str) -> List[AnyModelConfig]: @@ -293,12 +312,12 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): with self._db.lock: self._cursor.execute( """--sql - SELECT config FROM model_config + SELECT config, strftime('%s',updated_at) FROM model_config WHERE original_hash=?; """, (hash,), ) - results = [ModelConfigFactory.make_config(json.loads(x[0])) for x in self._cursor.fetchall()] + results = [ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall()] return results @property diff --git a/invokeai/app/services/shared/sqlite/sqlite_util.py b/invokeai/app/services/shared/sqlite/sqlite_util.py index 6079b3f08d..681886eacd 100644 --- a/invokeai/app/services/shared/sqlite/sqlite_util.py +++ b/invokeai/app/services/shared/sqlite/sqlite_util.py @@ -8,6 +8,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_2 import from invokeai.app.services.shared.sqlite_migrator.migrations.migration_3 import build_migration_3 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_4 import build_migration_4 from invokeai.app.services.shared.sqlite_migrator.migrations.migration_5 import build_migration_5 +from invokeai.app.services.shared.sqlite_migrator.migrations.migration_6 import build_migration_6 from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator @@ -33,6 +34,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto migrator.register_migration(build_migration_3(app_config=config, logger=logger)) migrator.register_migration(build_migration_4()) migrator.register_migration(build_migration_5()) + migrator.register_migration(build_migration_6()) migrator.run_migrations() return db diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py new file mode 100644 index 0000000000..e72878f726 --- /dev/null +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py @@ -0,0 +1,44 @@ +import sqlite3 +from logging import Logger + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration + +class Migration6Callback: + + def __call__(self, cursor: sqlite3.Cursor) -> None: + self._recreate_model_triggers(cursor) + + def _recreate_model_triggers(self, cursor: sqlite3.Cursor) -> None: + """ + Adds the timestamp trigger to the model_config table. + + This trigger was inadvertently dropped in earlier migration scripts. + """ + + cursor.execute( + """--sql + CREATE TRIGGER IF NOT EXISTS model_config_updated_at + AFTER UPDATE + ON model_config FOR EACH ROW + BEGIN + UPDATE model_config SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') + WHERE id = old.id; + END; + """ + ) + +def build_migration_6() -> Migration: + """ + Build the migration from database version 5 to 6. + + This migration does the following: + - Adds the model_config_updated_at trigger if it does not exist + """ + migration_6 = Migration( + from_version=5, + to_version=6, + callback=Migration6Callback(), + ) + + return migration_6 diff --git a/invokeai/backend/install/install_helper.py b/invokeai/backend/install/install_helper.py index e54be527d9..8c03d2ccf8 100644 --- a/invokeai/backend/install/install_helper.py +++ b/invokeai/backend/install/install_helper.py @@ -98,11 +98,13 @@ class TqdmEventService(EventServiceBase): super().__init__() self._bars: Dict[str, tqdm] = {} self._last: Dict[str, int] = {} + self._logger = InvokeAILogger.get_logger(__name__) def dispatch(self, event_name: str, payload: Any) -> None: """Dispatch an event by appending it to self.events.""" + data = payload["data"] + source = data["source"] if payload["event"] == "model_install_downloading": - data = payload["data"] dest = data["local_path"] total_bytes = data["total_bytes"] bytes = data["bytes"] @@ -111,7 +113,12 @@ class TqdmEventService(EventServiceBase): self._last[dest] = 0 self._bars[dest].update(bytes - self._last[dest]) self._last[dest] = bytes - + elif payload["event"] == "model_install_completed": + self._logger.info(f"{source}: installed successfully.") + elif payload["event"] == "model_install_error": + self._logger.warning(f"{source}: installation failed with error {data['error']}") + elif payload["event"] == "model_install_cancelled": + self._logger.warning(f"{source}: installation cancelled") class InstallHelper(object): """Capture information stored jointly in INITIAL_MODELS.yaml and the installed models db.""" diff --git a/invokeai/backend/model_manager/__init__.py b/invokeai/backend/model_manager/__init__.py index 0f16852c93..f3c84cd01f 100644 --- a/invokeai/backend/model_manager/__init__.py +++ b/invokeai/backend/model_manager/__init__.py @@ -1,6 +1,7 @@ """Re-export frequently-used symbols from the Model Manager backend.""" from .config import ( + AnyModel, AnyModelConfig, BaseModelType, InvalidModelConfigException, @@ -14,12 +15,15 @@ from .config import ( ) from .probe import ModelProbe from .search import ModelSearch +from .load import LoadedModel __all__ = [ + "AnyModel", "AnyModelConfig", "BaseModelType", "ModelRepoVariant", "InvalidModelConfigException", + "LoadedModel", "ModelConfigFactory", "ModelFormat", "ModelProbe", diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 338669c873..796ccbacde 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -19,12 +19,15 @@ Typical usage: Validation errors will raise an InvalidModelConfigException error. """ +import time +import torch from enum import Enum from typing import Literal, Optional, Type, Union from pydantic import BaseModel, ConfigDict, Field, TypeAdapter +from diffusers import ModelMixin from typing_extensions import Annotated, Any, Dict - +from .onnx_runtime import IAIOnnxRuntimeModel class InvalidModelConfigException(Exception): """Exception for when config parser doesn't recognized this combination of model type and format.""" @@ -127,6 +130,7 @@ class ModelConfigBase(BaseModel): ) # if model is converted or otherwise modified, this will hold updated hash description: Optional[str] = Field(default=None) source: Optional[str] = Field(description="Model download source (URL or repo_id)", default=None) + last_modified: Optional[float] = Field(description="Timestamp for modification time", default_factory=time.time) model_config = ConfigDict( use_enum_values=False, @@ -280,6 +284,7 @@ AnyModelConfig = Union[ ] AnyModelConfigValidator = TypeAdapter(AnyModelConfig) +AnyModel = Union[ModelMixin, torch.nn.Module, IAIOnnxRuntimeModel] # IMPLEMENTATION NOTE: # The preferred alternative to the above is a discriminated Union as shown @@ -312,6 +317,7 @@ class ModelConfigFactory(object): model_data: Union[dict, AnyModelConfig], key: Optional[str] = None, dest_class: Optional[Type] = None, + timestamp: Optional[float] = None ) -> AnyModelConfig: """ Return the appropriate config object from raw dict values. @@ -330,4 +336,6 @@ class ModelConfigFactory(object): model = AnyModelConfigValidator.validate_python(model_data) if key: model.key = key + if timestamp: + model.last_modified = timestamp return model diff --git a/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py b/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py new file mode 100644 index 0000000000..9d6fc4841f --- /dev/null +++ b/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py @@ -0,0 +1,1744 @@ +# coding=utf-8 +# Copyright 2023 The HuggingFace Inc. team. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Adapted for use in InvokeAI by Lincoln Stein, July 2023 +# +""" Conversion script for the Stable Diffusion checkpoints.""" + +import re +from contextlib import nullcontext +from io import BytesIO +from pathlib import Path +from typing import Optional, Union + +import requests +import torch +from diffusers.models import AutoencoderKL, ControlNetModel, PriorTransformer, UNet2DConditionModel +from diffusers.pipelines.latent_diffusion.pipeline_latent_diffusion import LDMBertConfig, LDMBertModel +from diffusers.pipelines.paint_by_example import PaintByExampleImageEncoder +from diffusers.pipelines.pipeline_utils import DiffusionPipeline +from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker +from diffusers.pipelines.stable_diffusion.stable_unclip_image_normalizer import StableUnCLIPImageNormalizer +from diffusers.schedulers import ( + DDIMScheduler, + DDPMScheduler, + DPMSolverMultistepScheduler, + EulerAncestralDiscreteScheduler, + EulerDiscreteScheduler, + HeunDiscreteScheduler, + LMSDiscreteScheduler, + PNDMScheduler, + UnCLIPScheduler, +) +from diffusers.utils import is_accelerate_available +from diffusers.utils.import_utils import BACKENDS_MAPPING +from picklescan.scanner import scan_file_path +from transformers import ( + AutoFeatureExtractor, + BertTokenizerFast, + CLIPImageProcessor, + CLIPTextConfig, + CLIPTextModel, + CLIPTextModelWithProjection, + CLIPTokenizer, + CLIPVisionConfig, + CLIPVisionModelWithProjection, +) + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.util.logging import InvokeAILogger +from invokeai.backend.model_manager import BaseModelType, ModelVariantType + +try: + from omegaconf import OmegaConf + from omegaconf.dictconfig import DictConfig +except ImportError: + raise ImportError( + "OmegaConf is required to convert the LDM checkpoints. Please install it with `pip install OmegaConf`." + ) + +if is_accelerate_available(): + from accelerate import init_empty_weights + from accelerate.utils import set_module_tensor_to_device + +logger = InvokeAILogger.get_logger(__name__) +CONVERT_MODEL_ROOT = InvokeAIAppConfig.get_config().models_path / "core/convert" + + +def shave_segments(path, n_shave_prefix_segments=1): + """ + Removes segments. Positive values shave the first segments, negative shave the last segments. + """ + if n_shave_prefix_segments >= 0: + return ".".join(path.split(".")[n_shave_prefix_segments:]) + else: + return ".".join(path.split(".")[:n_shave_prefix_segments]) + + +def renew_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item.replace("in_layers.0", "norm1") + new_item = new_item.replace("in_layers.2", "conv1") + + new_item = new_item.replace("out_layers.0", "norm2") + new_item = new_item.replace("out_layers.3", "conv2") + + new_item = new_item.replace("emb_layers.1", "time_emb_proj") + new_item = new_item.replace("skip_connection", "conv_shortcut") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_resnet_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside resnets to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("nin_shortcut", "conv_shortcut") + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + # new_item = new_item.replace('norm.weight', 'group_norm.weight') + # new_item = new_item.replace('norm.bias', 'group_norm.bias') + + # new_item = new_item.replace('proj_out.weight', 'proj_attn.weight') + # new_item = new_item.replace('proj_out.bias', 'proj_attn.bias') + + # new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def renew_vae_attention_paths(old_list, n_shave_prefix_segments=0): + """ + Updates paths inside attentions to the new naming scheme (local renaming) + """ + mapping = [] + for old_item in old_list: + new_item = old_item + + new_item = new_item.replace("norm.weight", "group_norm.weight") + new_item = new_item.replace("norm.bias", "group_norm.bias") + + new_item = new_item.replace("q.weight", "to_q.weight") + new_item = new_item.replace("q.bias", "to_q.bias") + + new_item = new_item.replace("k.weight", "to_k.weight") + new_item = new_item.replace("k.bias", "to_k.bias") + + new_item = new_item.replace("v.weight", "to_v.weight") + new_item = new_item.replace("v.bias", "to_v.bias") + + new_item = new_item.replace("proj_out.weight", "to_out.0.weight") + new_item = new_item.replace("proj_out.bias", "to_out.0.bias") + + new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) + + mapping.append({"old": old_item, "new": new_item}) + + return mapping + + +def assign_to_checkpoint( + paths, checkpoint, old_checkpoint, attention_paths_to_split=None, additional_replacements=None, config=None +): + """ + This does the final conversion step: take locally converted weights and apply a global renaming to them. It splits + attention layers, and takes into account additional replacements that may arise. + + Assigns the weights to the new checkpoint. + """ + assert isinstance(paths, list), "Paths should be a list of dicts containing 'old' and 'new' keys." + + # Splits the attention layers into three variables. + if attention_paths_to_split is not None: + for path, path_map in attention_paths_to_split.items(): + old_tensor = old_checkpoint[path] + channels = old_tensor.shape[0] // 3 + + target_shape = (-1, channels) if len(old_tensor.shape) == 3 else (-1) + + num_heads = old_tensor.shape[0] // config["num_head_channels"] // 3 + + old_tensor = old_tensor.reshape((num_heads, 3 * channels // num_heads) + old_tensor.shape[1:]) + query, key, value = old_tensor.split(channels // num_heads, dim=1) + + checkpoint[path_map["query"]] = query.reshape(target_shape) + checkpoint[path_map["key"]] = key.reshape(target_shape) + checkpoint[path_map["value"]] = value.reshape(target_shape) + + for path in paths: + new_path = path["new"] + + # These have already been assigned + if attention_paths_to_split is not None and new_path in attention_paths_to_split: + continue + + # Global renaming happens here + new_path = new_path.replace("middle_block.0", "mid_block.resnets.0") + new_path = new_path.replace("middle_block.1", "mid_block.attentions.0") + new_path = new_path.replace("middle_block.2", "mid_block.resnets.1") + + if additional_replacements is not None: + for replacement in additional_replacements: + new_path = new_path.replace(replacement["old"], replacement["new"]) + + # proj_attn.weight has to be converted from conv 1D to linear + is_attn_weight = "proj_attn.weight" in new_path or ("attentions" in new_path and "to_" in new_path) + shape = old_checkpoint[path["old"]].shape + if is_attn_weight and len(shape) == 3: + checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0] + elif is_attn_weight and len(shape) == 4: + checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0, 0] + else: + checkpoint[new_path] = old_checkpoint[path["old"]] + + +def conv_attn_to_linear(checkpoint): + keys = list(checkpoint.keys()) + attn_keys = ["query.weight", "key.weight", "value.weight"] + for key in keys: + if ".".join(key.split(".")[-2:]) in attn_keys: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0, 0] + elif "proj_attn.weight" in key: + if checkpoint[key].ndim > 2: + checkpoint[key] = checkpoint[key][:, :, 0] + + +def create_unet_diffusers_config(original_config, image_size: int, controlnet=False): + """ + Creates a config for the diffusers based on the config of the LDM model. + """ + if controlnet: + unet_params = original_config.model.params.control_stage_config.params + else: + if "unet_config" in original_config.model.params and original_config.model.params.unet_config is not None: + unet_params = original_config.model.params.unet_config.params + else: + unet_params = original_config.model.params.network_config.params + + vae_params = original_config.model.params.first_stage_config.params.ddconfig + + block_out_channels = [unet_params.model_channels * mult for mult in unet_params.channel_mult] + + down_block_types = [] + resolution = 1 + for i in range(len(block_out_channels)): + block_type = "CrossAttnDownBlock2D" if resolution in unet_params.attention_resolutions else "DownBlock2D" + down_block_types.append(block_type) + if i != len(block_out_channels) - 1: + resolution *= 2 + + up_block_types = [] + for _i in range(len(block_out_channels)): + block_type = "CrossAttnUpBlock2D" if resolution in unet_params.attention_resolutions else "UpBlock2D" + up_block_types.append(block_type) + resolution //= 2 + + if unet_params.transformer_depth is not None: + transformer_layers_per_block = ( + unet_params.transformer_depth + if isinstance(unet_params.transformer_depth, int) + else list(unet_params.transformer_depth) + ) + else: + transformer_layers_per_block = 1 + + vae_scale_factor = 2 ** (len(vae_params.ch_mult) - 1) + + head_dim = unet_params.num_heads if "num_heads" in unet_params else None + use_linear_projection = ( + unet_params.use_linear_in_transformer if "use_linear_in_transformer" in unet_params else False + ) + if use_linear_projection: + # stable diffusion 2-base-512 and 2-768 + if head_dim is None: + head_dim_mult = unet_params.model_channels // unet_params.num_head_channels + head_dim = [head_dim_mult * c for c in list(unet_params.channel_mult)] + + class_embed_type = None + addition_embed_type = None + addition_time_embed_dim = None + projection_class_embeddings_input_dim = None + context_dim = None + + if unet_params.context_dim is not None: + context_dim = ( + unet_params.context_dim if isinstance(unet_params.context_dim, int) else unet_params.context_dim[0] + ) + + if "num_classes" in unet_params: + if unet_params.num_classes == "sequential": + if context_dim in [2048, 1280]: + # SDXL + addition_embed_type = "text_time" + addition_time_embed_dim = 256 + else: + class_embed_type = "projection" + assert "adm_in_channels" in unet_params + projection_class_embeddings_input_dim = unet_params.adm_in_channels + else: + raise NotImplementedError(f"Unknown conditional unet num_classes config: {unet_params.num_classes}") + + config = { + "sample_size": image_size // vae_scale_factor, + "in_channels": unet_params.in_channels, + "down_block_types": tuple(down_block_types), + "block_out_channels": tuple(block_out_channels), + "layers_per_block": unet_params.num_res_blocks, + "cross_attention_dim": context_dim, + "attention_head_dim": head_dim, + "use_linear_projection": use_linear_projection, + "class_embed_type": class_embed_type, + "addition_embed_type": addition_embed_type, + "addition_time_embed_dim": addition_time_embed_dim, + "projection_class_embeddings_input_dim": projection_class_embeddings_input_dim, + "transformer_layers_per_block": transformer_layers_per_block, + } + + if controlnet: + config["conditioning_channels"] = unet_params.hint_channels + else: + config["out_channels"] = unet_params.out_channels + config["up_block_types"] = tuple(up_block_types) + + return config + + +def create_vae_diffusers_config(original_config, image_size: int): + """ + Creates a config for the diffusers based on the config of the LDM model. + """ + vae_params = original_config.model.params.first_stage_config.params.ddconfig + _ = original_config.model.params.first_stage_config.params.embed_dim + + block_out_channels = [vae_params.ch * mult for mult in vae_params.ch_mult] + down_block_types = ["DownEncoderBlock2D"] * len(block_out_channels) + up_block_types = ["UpDecoderBlock2D"] * len(block_out_channels) + + config = { + "sample_size": image_size, + "in_channels": vae_params.in_channels, + "out_channels": vae_params.out_ch, + "down_block_types": tuple(down_block_types), + "up_block_types": tuple(up_block_types), + "block_out_channels": tuple(block_out_channels), + "latent_channels": vae_params.z_channels, + "layers_per_block": vae_params.num_res_blocks, + } + return config + + +def create_diffusers_schedular(original_config): + schedular = DDIMScheduler( + num_train_timesteps=original_config.model.params.timesteps, + beta_start=original_config.model.params.linear_start, + beta_end=original_config.model.params.linear_end, + beta_schedule="scaled_linear", + ) + return schedular + + +def create_ldm_bert_config(original_config): + bert_params = original_config.model.parms.cond_stage_config.params + config = LDMBertConfig( + d_model=bert_params.n_embed, + encoder_layers=bert_params.n_layer, + encoder_ffn_dim=bert_params.n_embed * 4, + ) + return config + + +def convert_ldm_unet_checkpoint( + checkpoint, config, path=None, extract_ema=False, controlnet=False, skip_extract_state_dict=False +): + """ + Takes a state dict and a config, and returns a converted checkpoint. + """ + + if skip_extract_state_dict: + unet_state_dict = checkpoint + else: + # extract state_dict for UNet + unet_state_dict = {} + keys = list(checkpoint.keys()) + + if controlnet: + unet_key = "control_model." + else: + unet_key = "model.diffusion_model." + + # at least a 100 parameters have to start with `model_ema` in order for the checkpoint to be EMA + if sum(k.startswith("model_ema") for k in keys) > 100 and extract_ema: + logger.warning(f"Checkpoint {path} has both EMA and non-EMA weights.") + logger.warning( + "In this conversion only the EMA weights are extracted. If you want to instead extract the non-EMA" + " weights (useful to continue fine-tuning), please make sure to remove the `--extract_ema` flag." + ) + for key in keys: + if key.startswith("model.diffusion_model"): + flat_ema_key = "model_ema." + "".join(key.split(".")[1:]) + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(flat_ema_key) + else: + if sum(k.startswith("model_ema") for k in keys) > 100: + logger.warning( + "In this conversion only the non-EMA weights are extracted. If you want to instead extract the EMA" + " weights (usually better for inference), please make sure to add the `--extract_ema` flag." + ) + + for key in keys: + if key.startswith(unet_key): + unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(key) + + new_checkpoint = {} + + new_checkpoint["time_embedding.linear_1.weight"] = unet_state_dict["time_embed.0.weight"] + new_checkpoint["time_embedding.linear_1.bias"] = unet_state_dict["time_embed.0.bias"] + new_checkpoint["time_embedding.linear_2.weight"] = unet_state_dict["time_embed.2.weight"] + new_checkpoint["time_embedding.linear_2.bias"] = unet_state_dict["time_embed.2.bias"] + + if config["class_embed_type"] is None: + # No parameters to port + ... + elif config["class_embed_type"] == "timestep" or config["class_embed_type"] == "projection": + new_checkpoint["class_embedding.linear_1.weight"] = unet_state_dict["label_emb.0.0.weight"] + new_checkpoint["class_embedding.linear_1.bias"] = unet_state_dict["label_emb.0.0.bias"] + new_checkpoint["class_embedding.linear_2.weight"] = unet_state_dict["label_emb.0.2.weight"] + new_checkpoint["class_embedding.linear_2.bias"] = unet_state_dict["label_emb.0.2.bias"] + else: + raise NotImplementedError(f"Not implemented `class_embed_type`: {config['class_embed_type']}") + + if config["addition_embed_type"] == "text_time": + new_checkpoint["add_embedding.linear_1.weight"] = unet_state_dict["label_emb.0.0.weight"] + new_checkpoint["add_embedding.linear_1.bias"] = unet_state_dict["label_emb.0.0.bias"] + new_checkpoint["add_embedding.linear_2.weight"] = unet_state_dict["label_emb.0.2.weight"] + new_checkpoint["add_embedding.linear_2.bias"] = unet_state_dict["label_emb.0.2.bias"] + + new_checkpoint["conv_in.weight"] = unet_state_dict["input_blocks.0.0.weight"] + new_checkpoint["conv_in.bias"] = unet_state_dict["input_blocks.0.0.bias"] + + if not controlnet: + new_checkpoint["conv_norm_out.weight"] = unet_state_dict["out.0.weight"] + new_checkpoint["conv_norm_out.bias"] = unet_state_dict["out.0.bias"] + new_checkpoint["conv_out.weight"] = unet_state_dict["out.2.weight"] + new_checkpoint["conv_out.bias"] = unet_state_dict["out.2.bias"] + + # Retrieves the keys for the input blocks only + num_input_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "input_blocks" in layer}) + input_blocks = { + layer_id: [key for key in unet_state_dict if f"input_blocks.{layer_id}" in key] + for layer_id in range(num_input_blocks) + } + + # Retrieves the keys for the middle blocks only + num_middle_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "middle_block" in layer}) + middle_blocks = { + layer_id: [key for key in unet_state_dict if f"middle_block.{layer_id}" in key] + for layer_id in range(num_middle_blocks) + } + + # Retrieves the keys for the output blocks only + num_output_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "output_blocks" in layer}) + output_blocks = { + layer_id: [key for key in unet_state_dict if f"output_blocks.{layer_id}" in key] + for layer_id in range(num_output_blocks) + } + + for i in range(1, num_input_blocks): + block_id = (i - 1) // (config["layers_per_block"] + 1) + layer_in_block_id = (i - 1) % (config["layers_per_block"] + 1) + + resnets = [ + key for key in input_blocks[i] if f"input_blocks.{i}.0" in key and f"input_blocks.{i}.0.op" not in key + ] + attentions = [key for key in input_blocks[i] if f"input_blocks.{i}.1" in key] + + if f"input_blocks.{i}.0.op.weight" in unet_state_dict: + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.weight"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.weight" + ) + new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.bias"] = unet_state_dict.pop( + f"input_blocks.{i}.0.op.bias" + ) + + paths = renew_resnet_paths(resnets) + meta_path = {"old": f"input_blocks.{i}.0", "new": f"down_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint(paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config) + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = {"old": f"input_blocks.{i}.1", "new": f"down_blocks.{block_id}.attentions.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + resnet_0 = middle_blocks[0] + attentions = middle_blocks[1] + resnet_1 = middle_blocks[2] + + resnet_0_paths = renew_resnet_paths(resnet_0) + assign_to_checkpoint(resnet_0_paths, new_checkpoint, unet_state_dict, config=config) + + resnet_1_paths = renew_resnet_paths(resnet_1) + assign_to_checkpoint(resnet_1_paths, new_checkpoint, unet_state_dict, config=config) + + attentions_paths = renew_attention_paths(attentions) + meta_path = {"old": "middle_block.1", "new": "mid_block.attentions.0"} + assign_to_checkpoint( + attentions_paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + for i in range(num_output_blocks): + block_id = i // (config["layers_per_block"] + 1) + layer_in_block_id = i % (config["layers_per_block"] + 1) + output_block_layers = [shave_segments(name, 2) for name in output_blocks[i]] + output_block_list = {} + + for layer in output_block_layers: + layer_id, layer_name = layer.split(".")[0], shave_segments(layer, 1) + if layer_id in output_block_list: + output_block_list[layer_id].append(layer_name) + else: + output_block_list[layer_id] = [layer_name] + + if len(output_block_list) > 1: + resnets = [key for key in output_blocks[i] if f"output_blocks.{i}.0" in key] + attentions = [key for key in output_blocks[i] if f"output_blocks.{i}.1" in key] + + resnet_0_paths = renew_resnet_paths(resnets) + paths = renew_resnet_paths(resnets) + + meta_path = {"old": f"output_blocks.{i}.0", "new": f"up_blocks.{block_id}.resnets.{layer_in_block_id}"} + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + + output_block_list = {k: sorted(v) for k, v in output_block_list.items()} + if ["conv.bias", "conv.weight"] in output_block_list.values(): + index = list(output_block_list.values()).index(["conv.bias", "conv.weight"]) + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.weight"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.weight" + ] + new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.bias"] = unet_state_dict[ + f"output_blocks.{i}.{index}.conv.bias" + ] + + # Clear attentions as they have been attributed above. + if len(attentions) == 2: + attentions = [] + + if len(attentions): + paths = renew_attention_paths(attentions) + meta_path = { + "old": f"output_blocks.{i}.1", + "new": f"up_blocks.{block_id}.attentions.{layer_in_block_id}", + } + assign_to_checkpoint( + paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config + ) + else: + resnet_0_paths = renew_resnet_paths(output_block_layers, n_shave_prefix_segments=1) + for path in resnet_0_paths: + old_path = ".".join(["output_blocks", str(i), path["old"]]) + new_path = ".".join(["up_blocks", str(block_id), "resnets", str(layer_in_block_id), path["new"]]) + + new_checkpoint[new_path] = unet_state_dict[old_path] + + if controlnet: + # conditioning embedding + + orig_index = 0 + + new_checkpoint["controlnet_cond_embedding.conv_in.weight"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.weight" + ) + new_checkpoint["controlnet_cond_embedding.conv_in.bias"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.bias" + ) + + orig_index += 2 + + diffusers_index = 0 + + while diffusers_index < 6: + new_checkpoint[f"controlnet_cond_embedding.blocks.{diffusers_index}.weight"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.weight" + ) + new_checkpoint[f"controlnet_cond_embedding.blocks.{diffusers_index}.bias"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.bias" + ) + diffusers_index += 1 + orig_index += 2 + + new_checkpoint["controlnet_cond_embedding.conv_out.weight"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.weight" + ) + new_checkpoint["controlnet_cond_embedding.conv_out.bias"] = unet_state_dict.pop( + f"input_hint_block.{orig_index}.bias" + ) + + # down blocks + for i in range(num_input_blocks): + new_checkpoint[f"controlnet_down_blocks.{i}.weight"] = unet_state_dict.pop(f"zero_convs.{i}.0.weight") + new_checkpoint[f"controlnet_down_blocks.{i}.bias"] = unet_state_dict.pop(f"zero_convs.{i}.0.bias") + + # mid block + new_checkpoint["controlnet_mid_block.weight"] = unet_state_dict.pop("middle_block_out.0.weight") + new_checkpoint["controlnet_mid_block.bias"] = unet_state_dict.pop("middle_block_out.0.bias") + + return new_checkpoint + + +def convert_ldm_vae_checkpoint(checkpoint, config): + # extract state dict for VAE + vae_state_dict = {} + keys = list(checkpoint.keys()) + vae_key = "first_stage_model." if any(k.startswith("first_stage_model.") for k in keys) else "" + for key in keys: + if key.startswith(vae_key): + vae_state_dict[key.replace(vae_key, "")] = checkpoint.get(key) + + new_checkpoint = {} + + new_checkpoint["encoder.conv_in.weight"] = vae_state_dict["encoder.conv_in.weight"] + new_checkpoint["encoder.conv_in.bias"] = vae_state_dict["encoder.conv_in.bias"] + new_checkpoint["encoder.conv_out.weight"] = vae_state_dict["encoder.conv_out.weight"] + new_checkpoint["encoder.conv_out.bias"] = vae_state_dict["encoder.conv_out.bias"] + new_checkpoint["encoder.conv_norm_out.weight"] = vae_state_dict["encoder.norm_out.weight"] + new_checkpoint["encoder.conv_norm_out.bias"] = vae_state_dict["encoder.norm_out.bias"] + + new_checkpoint["decoder.conv_in.weight"] = vae_state_dict["decoder.conv_in.weight"] + new_checkpoint["decoder.conv_in.bias"] = vae_state_dict["decoder.conv_in.bias"] + new_checkpoint["decoder.conv_out.weight"] = vae_state_dict["decoder.conv_out.weight"] + new_checkpoint["decoder.conv_out.bias"] = vae_state_dict["decoder.conv_out.bias"] + new_checkpoint["decoder.conv_norm_out.weight"] = vae_state_dict["decoder.norm_out.weight"] + new_checkpoint["decoder.conv_norm_out.bias"] = vae_state_dict["decoder.norm_out.bias"] + + new_checkpoint["quant_conv.weight"] = vae_state_dict["quant_conv.weight"] + new_checkpoint["quant_conv.bias"] = vae_state_dict["quant_conv.bias"] + new_checkpoint["post_quant_conv.weight"] = vae_state_dict["post_quant_conv.weight"] + new_checkpoint["post_quant_conv.bias"] = vae_state_dict["post_quant_conv.bias"] + + # Retrieves the keys for the encoder down blocks only + num_down_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "encoder.down" in layer}) + down_blocks = { + layer_id: [key for key in vae_state_dict if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) + } + + # Retrieves the keys for the decoder up blocks only + num_up_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "decoder.up" in layer}) + up_blocks = { + layer_id: [key for key in vae_state_dict if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks) + } + + for i in range(num_down_blocks): + resnets = [key for key in down_blocks[i] if f"down.{i}" in key and f"down.{i}.downsample" not in key] + + if f"encoder.down.{i}.downsample.conv.weight" in vae_state_dict: + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.weight"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.weight" + ) + new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.bias"] = vae_state_dict.pop( + f"encoder.down.{i}.downsample.conv.bias" + ) + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"down.{i}.block", "new": f"down_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "encoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"encoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "encoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + + for i in range(num_up_blocks): + block_id = num_up_blocks - 1 - i + resnets = [ + key for key in up_blocks[block_id] if f"up.{block_id}" in key and f"up.{block_id}.upsample" not in key + ] + + if f"decoder.up.{block_id}.upsample.conv.weight" in vae_state_dict: + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.weight"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.weight" + ] + new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.bias"] = vae_state_dict[ + f"decoder.up.{block_id}.upsample.conv.bias" + ] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"up.{block_id}.block", "new": f"up_blocks.{i}.resnets"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_resnets = [key for key in vae_state_dict if "decoder.mid.block" in key] + num_mid_res_blocks = 2 + for i in range(1, num_mid_res_blocks + 1): + resnets = [key for key in mid_resnets if f"decoder.mid.block_{i}" in key] + + paths = renew_vae_resnet_paths(resnets) + meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + + mid_attentions = [key for key in vae_state_dict if "decoder.mid.attn" in key] + paths = renew_vae_attention_paths(mid_attentions) + meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} + assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) + conv_attn_to_linear(new_checkpoint) + return new_checkpoint + + +def convert_ldm_bert_checkpoint(checkpoint, config): + def _copy_attn_layer(hf_attn_layer, pt_attn_layer): + hf_attn_layer.q_proj.weight.data = pt_attn_layer.to_q.weight + hf_attn_layer.k_proj.weight.data = pt_attn_layer.to_k.weight + hf_attn_layer.v_proj.weight.data = pt_attn_layer.to_v.weight + + hf_attn_layer.out_proj.weight = pt_attn_layer.to_out.weight + hf_attn_layer.out_proj.bias = pt_attn_layer.to_out.bias + + def _copy_linear(hf_linear, pt_linear): + hf_linear.weight = pt_linear.weight + hf_linear.bias = pt_linear.bias + + def _copy_layer(hf_layer, pt_layer): + # copy layer norms + _copy_linear(hf_layer.self_attn_layer_norm, pt_layer[0][0]) + _copy_linear(hf_layer.final_layer_norm, pt_layer[1][0]) + + # copy attn + _copy_attn_layer(hf_layer.self_attn, pt_layer[0][1]) + + # copy MLP + pt_mlp = pt_layer[1][1] + _copy_linear(hf_layer.fc1, pt_mlp.net[0][0]) + _copy_linear(hf_layer.fc2, pt_mlp.net[2]) + + def _copy_layers(hf_layers, pt_layers): + for i, hf_layer in enumerate(hf_layers): + if i != 0: + i += i + pt_layer = pt_layers[i : i + 2] + _copy_layer(hf_layer, pt_layer) + + hf_model = LDMBertModel(config).eval() + + # copy embeds + hf_model.model.embed_tokens.weight = checkpoint.transformer.token_emb.weight + hf_model.model.embed_positions.weight.data = checkpoint.transformer.pos_emb.emb.weight + + # copy layer norm + _copy_linear(hf_model.model.layer_norm, checkpoint.transformer.norm) + + # copy hidden layers + _copy_layers(hf_model.model.layers, checkpoint.transformer.attn_layers.layers) + + _copy_linear(hf_model.to_logits, checkpoint.transformer.to_logits) + + return hf_model + + +def convert_ldm_clip_checkpoint(checkpoint, local_files_only=False, text_encoder=None): + if text_encoder is None: + config = CLIPTextConfig.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") + + ctx = init_empty_weights if is_accelerate_available() else nullcontext + with ctx(): + text_model = CLIPTextModel(config) + + keys = list(checkpoint.keys()) + + text_model_dict = {} + + remove_prefixes = ["cond_stage_model.transformer", "conditioner.embedders.0.transformer"] + + for key in keys: + for prefix in remove_prefixes: + if key.startswith(prefix): + text_model_dict[key[len(prefix + ".") :]] = checkpoint[key] + + if is_accelerate_available(): + for param_name, param in text_model_dict.items(): + set_module_tensor_to_device(text_model, param_name, "cpu", value=param) + else: + text_model.load_state_dict(text_model_dict) + + return text_model + + +textenc_conversion_lst = [ + ("positional_embedding", "text_model.embeddings.position_embedding.weight"), + ("token_embedding.weight", "text_model.embeddings.token_embedding.weight"), + ("ln_final.weight", "text_model.final_layer_norm.weight"), + ("ln_final.bias", "text_model.final_layer_norm.bias"), + ("text_projection", "text_projection.weight"), +] +textenc_conversion_map = {x[0]: x[1] for x in textenc_conversion_lst} + +textenc_transformer_conversion_lst = [ + # (stable-diffusion, HF Diffusers) + ("resblocks.", "text_model.encoder.layers."), + ("ln_1", "layer_norm1"), + ("ln_2", "layer_norm2"), + (".c_fc.", ".fc1."), + (".c_proj.", ".fc2."), + (".attn", ".self_attn"), + ("ln_final.", "transformer.text_model.final_layer_norm."), + ("token_embedding.weight", "transformer.text_model.embeddings.token_embedding.weight"), + ("positional_embedding", "transformer.text_model.embeddings.position_embedding.weight"), +] +protected = {re.escape(x[0]): x[1] for x in textenc_transformer_conversion_lst} +textenc_pattern = re.compile("|".join(protected.keys())) + + +def convert_paint_by_example_checkpoint(checkpoint): + config = CLIPVisionConfig.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") + model = PaintByExampleImageEncoder(config) + + keys = list(checkpoint.keys()) + + text_model_dict = {} + + for key in keys: + if key.startswith("cond_stage_model.transformer"): + text_model_dict[key[len("cond_stage_model.transformer.") :]] = checkpoint[key] + + # load clip vision + model.model.load_state_dict(text_model_dict) + + # load mapper + keys_mapper = { + k[len("cond_stage_model.mapper.res") :]: v + for k, v in checkpoint.items() + if k.startswith("cond_stage_model.mapper") + } + + MAPPING = { + "attn.c_qkv": ["attn1.to_q", "attn1.to_k", "attn1.to_v"], + "attn.c_proj": ["attn1.to_out.0"], + "ln_1": ["norm1"], + "ln_2": ["norm3"], + "mlp.c_fc": ["ff.net.0.proj"], + "mlp.c_proj": ["ff.net.2"], + } + + mapped_weights = {} + for key, value in keys_mapper.items(): + prefix = key[: len("blocks.i")] + suffix = key.split(prefix)[-1].split(".")[-1] + name = key.split(prefix)[-1].split(suffix)[0][1:-1] + mapped_names = MAPPING[name] + + num_splits = len(mapped_names) + for i, mapped_name in enumerate(mapped_names): + new_name = ".".join([prefix, mapped_name, suffix]) + shape = value.shape[0] // num_splits + mapped_weights[new_name] = value[i * shape : (i + 1) * shape] + + model.mapper.load_state_dict(mapped_weights) + + # load final layer norm + model.final_layer_norm.load_state_dict( + { + "bias": checkpoint["cond_stage_model.final_ln.bias"], + "weight": checkpoint["cond_stage_model.final_ln.weight"], + } + ) + + # load final proj + model.proj_out.load_state_dict( + { + "bias": checkpoint["proj_out.bias"], + "weight": checkpoint["proj_out.weight"], + } + ) + + # load uncond vector + model.uncond_vector.data = torch.nn.Parameter(checkpoint["learnable_vector"]) + return model + + +def convert_open_clip_checkpoint( + checkpoint, config_name, prefix="cond_stage_model.model.", has_projection=False, **config_kwargs +): + # text_model = CLIPTextModel.from_pretrained("stabilityai/stable-diffusion-2", subfolder="text_encoder") + # text_model = CLIPTextModelWithProjection.from_pretrained( + # "laion/CLIP-ViT-bigG-14-laion2B-39B-b160k", projection_dim=1280 + # ) + config = CLIPTextConfig.from_pretrained(config_name, **config_kwargs) + + ctx = init_empty_weights if is_accelerate_available() else nullcontext + with ctx(): + text_model = CLIPTextModelWithProjection(config) if has_projection else CLIPTextModel(config) + + keys = list(checkpoint.keys()) + + keys_to_ignore = [] + if config_name == "stabilityai/stable-diffusion-2" and config.num_hidden_layers == 23: + # make sure to remove all keys > 22 + keys_to_ignore += [k for k in keys if k.startswith("cond_stage_model.model.transformer.resblocks.23")] + keys_to_ignore += ["cond_stage_model.model.text_projection"] + + text_model_dict = {} + + if prefix + "text_projection" in checkpoint: + d_model = int(checkpoint[prefix + "text_projection"].shape[0]) + else: + d_model = 1024 + + text_model_dict["text_model.embeddings.position_ids"] = text_model.text_model.embeddings.get_buffer("position_ids") + + for key in keys: + if key in keys_to_ignore: + continue + if key[len(prefix) :] in textenc_conversion_map: + if key.endswith("text_projection"): + value = checkpoint[key].T.contiguous() + else: + value = checkpoint[key] + + text_model_dict[textenc_conversion_map[key[len(prefix) :]]] = value + + if key.startswith(prefix + "transformer."): + new_key = key[len(prefix + "transformer.") :] + if new_key.endswith(".in_proj_weight"): + new_key = new_key[: -len(".in_proj_weight")] + new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) + text_model_dict[new_key + ".q_proj.weight"] = checkpoint[key][:d_model, :] + text_model_dict[new_key + ".k_proj.weight"] = checkpoint[key][d_model : d_model * 2, :] + text_model_dict[new_key + ".v_proj.weight"] = checkpoint[key][d_model * 2 :, :] + elif new_key.endswith(".in_proj_bias"): + new_key = new_key[: -len(".in_proj_bias")] + new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) + text_model_dict[new_key + ".q_proj.bias"] = checkpoint[key][:d_model] + text_model_dict[new_key + ".k_proj.bias"] = checkpoint[key][d_model : d_model * 2] + text_model_dict[new_key + ".v_proj.bias"] = checkpoint[key][d_model * 2 :] + else: + new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) + + text_model_dict[new_key] = checkpoint[key] + + if is_accelerate_available(): + for param_name, param in text_model_dict.items(): + set_module_tensor_to_device(text_model, param_name, "cpu", value=param) + else: + text_model.load_state_dict(text_model_dict) + + return text_model + + +def stable_unclip_image_encoder(original_config): + """ + Returns the image processor and clip image encoder for the img2img unclip pipeline. + + We currently know of two types of stable unclip models which separately use the clip and the openclip image + encoders. + """ + + image_embedder_config = original_config.model.params.embedder_config + + sd_clip_image_embedder_class = image_embedder_config.target + sd_clip_image_embedder_class = sd_clip_image_embedder_class.split(".")[-1] + + if sd_clip_image_embedder_class == "ClipImageEmbedder": + clip_model_name = image_embedder_config.params.model + + if clip_model_name == "ViT-L/14": + feature_extractor = CLIPImageProcessor() + image_encoder = CLIPVisionModelWithProjection.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") + else: + raise NotImplementedError(f"Unknown CLIP checkpoint name in stable diffusion checkpoint {clip_model_name}") + + elif sd_clip_image_embedder_class == "FrozenOpenCLIPImageEmbedder": + feature_extractor = CLIPImageProcessor() + # InvokeAI doesn't use CLIPVisionModelWithProjection so it isn't in the core - if this code is hit a download will occur + image_encoder = CLIPVisionModelWithProjection.from_pretrained( + CONVERT_MODEL_ROOT / "CLIP-ViT-H-14-laion2B-s32B-b79K" + ) + else: + raise NotImplementedError( + f"Unknown CLIP image embedder class in stable diffusion checkpoint {sd_clip_image_embedder_class}" + ) + + return feature_extractor, image_encoder + + +def stable_unclip_image_noising_components( + original_config, clip_stats_path: Optional[str] = None, device: Optional[str] = None +): + """ + Returns the noising components for the img2img and txt2img unclip pipelines. + + Converts the stability noise augmentor into + 1. a `StableUnCLIPImageNormalizer` for holding the CLIP stats + 2. a `DDPMScheduler` for holding the noise schedule + + If the noise augmentor config specifies a clip stats path, the `clip_stats_path` must be provided. + """ + noise_aug_config = original_config.model.params.noise_aug_config + noise_aug_class = noise_aug_config.target + noise_aug_class = noise_aug_class.split(".")[-1] + + if noise_aug_class == "CLIPEmbeddingNoiseAugmentation": + noise_aug_config = noise_aug_config.params + embedding_dim = noise_aug_config.timestep_dim + max_noise_level = noise_aug_config.noise_schedule_config.timesteps + beta_schedule = noise_aug_config.noise_schedule_config.beta_schedule + + image_normalizer = StableUnCLIPImageNormalizer(embedding_dim=embedding_dim) + image_noising_scheduler = DDPMScheduler(num_train_timesteps=max_noise_level, beta_schedule=beta_schedule) + + if "clip_stats_path" in noise_aug_config: + if clip_stats_path is None: + raise ValueError("This stable unclip config requires a `clip_stats_path`") + + clip_mean, clip_std = torch.load(clip_stats_path, map_location=device) + clip_mean = clip_mean[None, :] + clip_std = clip_std[None, :] + + clip_stats_state_dict = { + "mean": clip_mean, + "std": clip_std, + } + + image_normalizer.load_state_dict(clip_stats_state_dict) + else: + raise NotImplementedError(f"Unknown noise augmentor class: {noise_aug_class}") + + return image_normalizer, image_noising_scheduler + + +def convert_controlnet_checkpoint( + checkpoint, + original_config, + checkpoint_path, + image_size, + upcast_attention, + extract_ema, + use_linear_projection=None, + cross_attention_dim=None, + precision: Optional[torch.dtype] = None, +): + ctrlnet_config = create_unet_diffusers_config(original_config, image_size=image_size, controlnet=True) + ctrlnet_config["upcast_attention"] = upcast_attention + + ctrlnet_config.pop("sample_size") + original_config = ctrlnet_config.copy() + + ctrlnet_config.pop("addition_embed_type") + ctrlnet_config.pop("addition_time_embed_dim") + ctrlnet_config.pop("transformer_layers_per_block") + + if use_linear_projection is not None: + ctrlnet_config["use_linear_projection"] = use_linear_projection + + if cross_attention_dim is not None: + ctrlnet_config["cross_attention_dim"] = cross_attention_dim + + controlnet = ControlNetModel(**ctrlnet_config) + + # Some controlnet ckpt files are distributed independently from the rest of the + # model components i.e. https://huggingface.co/thibaud/controlnet-sd21/ + if "time_embed.0.weight" in checkpoint: + skip_extract_state_dict = True + else: + skip_extract_state_dict = False + + converted_ctrl_checkpoint = convert_ldm_unet_checkpoint( + checkpoint, + original_config, + path=checkpoint_path, + extract_ema=extract_ema, + controlnet=True, + skip_extract_state_dict=skip_extract_state_dict, + ) + + controlnet.load_state_dict(converted_ctrl_checkpoint) + + return controlnet.to(precision) + + +def download_from_original_stable_diffusion_ckpt( + checkpoint_path: str, + model_version: BaseModelType, + model_variant: ModelVariantType, + original_config_file: str = None, + image_size: Optional[int] = None, + prediction_type: str = None, + model_type: str = None, + extract_ema: bool = False, + precision: Optional[torch.dtype] = None, + scheduler_type: str = "pndm", + num_in_channels: Optional[int] = None, + upcast_attention: Optional[bool] = None, + device: str = None, + from_safetensors: bool = False, + stable_unclip: Optional[str] = None, + stable_unclip_prior: Optional[str] = None, + clip_stats_path: Optional[str] = None, + controlnet: Optional[bool] = None, + load_safety_checker: bool = True, + pipeline_class: DiffusionPipeline = None, + local_files_only=False, + vae_path=None, + text_encoder=None, + tokenizer=None, + scan_needed: bool = True, +) -> DiffusionPipeline: + """ + Load a Stable Diffusion pipeline object from a CompVis-style `.ckpt`/`.safetensors` file and (ideally) a `.yaml` + config file. + + Although many of the arguments can be automatically inferred, some of these rely on brittle checks against the + global step count, which will likely fail for models that have undergone further fine-tuning. Therefore, it is + recommended that you override the default values and/or supply an `original_config_file` wherever possible. + + Args: + checkpoint_path (`str`): Path to `.ckpt` file. + original_config_file (`str`): + Path to `.yaml` config file corresponding to the original architecture. If `None`, will be automatically + inferred by looking for a key that only exists in SD2.0 models. + image_size (`int`, *optional*, defaults to 512): + The image size that the model was trained on. Use 512 for Stable Diffusion v1.X and Stable Diffusion v2 + Base. Use 768 for Stable Diffusion v2. + prediction_type (`str`, *optional*): + The prediction type that the model was trained on. Use `'epsilon'` for Stable Diffusion v1.X and Stable + Diffusion v2 Base. Use `'v_prediction'` for Stable Diffusion v2. + num_in_channels (`int`, *optional*, defaults to None): + The number of input channels. If `None`, it will be automatically inferred. + scheduler_type (`str`, *optional*, defaults to 'pndm'): + Type of scheduler to use. Should be one of `["pndm", "lms", "heun", "euler", "euler-ancestral", "dpm", + "ddim"]`. + model_type (`str`, *optional*, defaults to `None`): + The pipeline type. `None` to automatically infer, or one of `["FrozenOpenCLIPEmbedder", + "FrozenCLIPEmbedder", "PaintByExample"]`. + is_img2img (`bool`, *optional*, defaults to `False`): + Whether the model should be loaded as an img2img pipeline. + extract_ema (`bool`, *optional*, defaults to `False`): Only relevant for + checkpoints that have both EMA and non-EMA weights. Whether to extract the EMA weights or not. Defaults to + `False`. Pass `True` to extract the EMA weights. EMA weights usually yield higher quality images for + inference. Non-EMA weights are usually better to continue fine-tuning. + upcast_attention (`bool`, *optional*, defaults to `None`): + Whether the attention computation should always be upcasted. This is necessary when running stable + diffusion 2.1. + device (`str`, *optional*, defaults to `None`): + The device to use. Pass `None` to determine automatically. + from_safetensors (`str`, *optional*, defaults to `False`): + If `checkpoint_path` is in `safetensors` format, load checkpoint with safetensors instead of PyTorch. + load_safety_checker (`bool`, *optional*, defaults to `True`): + Whether to load the safety checker or not. Defaults to `True`. + pipeline_class (`str`, *optional*, defaults to `None`): + The pipeline class to use. Pass `None` to determine automatically. + local_files_only (`bool`, *optional*, defaults to `False`): + Whether or not to only look at local files (i.e., do not try to download the model). + text_encoder (`CLIPTextModel`, *optional*, defaults to `None`): + An instance of [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel) + to use, specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) + variant. If this parameter is `None`, the function will load a new instance of [CLIP] by itself, if needed. + tokenizer (`CLIPTokenizer`, *optional*, defaults to `None`): + An instance of + [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer) + to use. If this parameter is `None`, the function will load a new instance of [CLIPTokenizer] by itself, if + needed. + precision (`torch.dtype`, *optional*, defauts to `None`): + If not provided the precision will be set to the precision of the original file. + return: A StableDiffusionPipeline object representing the passed-in `.ckpt`/`.safetensors` file. + """ + + # import pipelines here to avoid circular import error when using from_single_file method + from diffusers import ( + LDMTextToImagePipeline, + PaintByExamplePipeline, + StableDiffusionControlNetPipeline, + StableDiffusionInpaintPipeline, + StableDiffusionPipeline, + StableDiffusionXLImg2ImgPipeline, + StableDiffusionXLPipeline, + StableUnCLIPImg2ImgPipeline, + StableUnCLIPPipeline, + ) + + if pipeline_class is None: + pipeline_class = StableDiffusionPipeline if not controlnet else StableDiffusionControlNetPipeline + + if prediction_type == "v-prediction": + prediction_type = "v_prediction" + + if from_safetensors: + from safetensors.torch import load_file as safe_load + + checkpoint = safe_load(checkpoint_path, device="cpu") + else: + if scan_needed: + # scan model + scan_result = scan_file_path(checkpoint_path) + if scan_result.infected_files != 0: + raise Exception("The model {checkpoint_path} is potentially infected by malware. Aborting import.") + if device is None: + device = "cuda" if torch.cuda.is_available() else "cpu" + checkpoint = torch.load(checkpoint_path, map_location=device) + else: + checkpoint = torch.load(checkpoint_path, map_location=device) + + # Sometimes models don't have the global_step item + if "global_step" in checkpoint: + global_step = checkpoint["global_step"] + else: + logger.debug("global_step key not found in model") + global_step = None + + # NOTE: this while loop isn't great but this controlnet checkpoint has one additional + # "state_dict" key https://huggingface.co/thibaud/controlnet-canny-sd21 + while "state_dict" in checkpoint: + checkpoint = checkpoint["state_dict"] + + logger.debug(f"model_type = {model_type}; original_config_file = {original_config_file}") + + precision_probing_key = "model.diffusion_model.input_blocks.0.0.bias" + logger.debug(f"original checkpoint precision == {checkpoint[precision_probing_key].dtype}") + precision = precision or checkpoint[precision_probing_key].dtype + + if original_config_file is None: + key_name_v2_1 = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" + key_name_sd_xl_base = "conditioner.embedders.1.model.transformer.resblocks.9.mlp.c_proj.bias" + key_name_sd_xl_refiner = "conditioner.embedders.0.model.transformer.resblocks.9.mlp.c_proj.bias" + + # model_type = "v1" + config_url = ( + "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/configs/stable-diffusion/v1-inference.yaml" + ) + + if key_name_v2_1 in checkpoint and checkpoint[key_name_v2_1].shape[-1] == 1024: + # model_type = "v2" + config_url = "https://raw.githubusercontent.com/Stability-AI/stablediffusion/main/configs/stable-diffusion/v2-inference-v.yaml" + + if global_step == 110000: + # v2.1 needs to upcast attention + upcast_attention = True + elif key_name_sd_xl_base in checkpoint: + # only base xl has two text embedders + config_url = "https://raw.githubusercontent.com/Stability-AI/generative-models/main/configs/inference/sd_xl_base.yaml" + elif key_name_sd_xl_refiner in checkpoint: + # only refiner xl has embedder and one text embedders + config_url = "https://raw.githubusercontent.com/Stability-AI/generative-models/main/configs/inference/sd_xl_refiner.yaml" + + original_config_file = BytesIO(requests.get(config_url).content) + + original_config = OmegaConf.load(original_config_file) + if original_config["model"]["params"].get("use_ema") is not None: + extract_ema = original_config["model"]["params"]["use_ema"] + + if ( + model_version in [BaseModelType.StableDiffusion2, BaseModelType.StableDiffusion1] + and original_config["model"]["params"].get("parameterization") == "v" + ): + prediction_type = "v_prediction" + upcast_attention = True + image_size = 768 if model_version == BaseModelType.StableDiffusion2 else 512 + else: + prediction_type = "epsilon" + upcast_attention = False + image_size = 512 + + # Convert the text model. + if ( + model_type is None + and "cond_stage_config" in original_config.model.params + and original_config.model.params.cond_stage_config is not None + ): + model_type = original_config.model.params.cond_stage_config.target.split(".")[-1] + logger.debug(f"no `model_type` given, `model_type` inferred as: {model_type}") + elif model_type is None and original_config.model.params.network_config is not None: + if original_config.model.params.network_config.params.context_dim == 2048: + model_type = "SDXL" + else: + model_type = "SDXL-Refiner" + if image_size is None: + image_size = 1024 + + if num_in_channels is None and pipeline_class == StableDiffusionInpaintPipeline: + num_in_channels = 9 + elif num_in_channels is None: + num_in_channels = 4 + + if "unet_config" in original_config.model.params: + original_config["model"]["params"]["unet_config"]["params"]["in_channels"] = num_in_channels + + if ( + "parameterization" in original_config["model"]["params"] + and original_config["model"]["params"]["parameterization"] == "v" + ): + if prediction_type is None: + # NOTE: For stable diffusion 2 base it is recommended to pass `prediction_type=="epsilon"` + # as it relies on a brittle global step parameter here + prediction_type = "epsilon" if global_step == 875000 else "v_prediction" + if image_size is None: + # NOTE: For stable diffusion 2 base one has to pass `image_size==512` + # as it relies on a brittle global step parameter here + image_size = 512 if global_step == 875000 else 768 + else: + if prediction_type is None: + prediction_type = "epsilon" + if image_size is None: + image_size = 512 + + if controlnet is None and "control_stage_config" in original_config.model.params: + controlnet = convert_controlnet_checkpoint( + checkpoint, original_config, checkpoint_path, image_size, upcast_attention, extract_ema + ) + + num_train_timesteps = getattr(original_config.model.params, "timesteps", None) or 1000 + + if model_type in ["SDXL", "SDXL-Refiner"]: + scheduler_dict = { + "beta_schedule": "scaled_linear", + "beta_start": 0.00085, + "beta_end": 0.012, + "interpolation_type": "linear", + "num_train_timesteps": num_train_timesteps, + "prediction_type": "epsilon", + "sample_max_value": 1.0, + "set_alpha_to_one": False, + "skip_prk_steps": True, + "steps_offset": 1, + "timestep_spacing": "leading", + } + scheduler = EulerDiscreteScheduler.from_config(scheduler_dict) + scheduler_type = "euler" + else: + beta_start = getattr(original_config.model.params, "linear_start", None) or 0.02 + beta_end = getattr(original_config.model.params, "linear_end", None) or 0.085 + scheduler = DDIMScheduler( + beta_end=beta_end, + beta_schedule="scaled_linear", + beta_start=beta_start, + num_train_timesteps=num_train_timesteps, + steps_offset=1, + clip_sample=False, + set_alpha_to_one=False, + prediction_type=prediction_type, + ) + # make sure scheduler works correctly with DDIM + scheduler.register_to_config(clip_sample=False) + + if scheduler_type == "pndm": + config = dict(scheduler.config) + config["skip_prk_steps"] = True + scheduler = PNDMScheduler.from_config(config) + elif scheduler_type == "lms": + scheduler = LMSDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "heun": + scheduler = HeunDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "euler": + scheduler = EulerDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "euler-ancestral": + scheduler = EulerAncestralDiscreteScheduler.from_config(scheduler.config) + elif scheduler_type == "dpm": + scheduler = DPMSolverMultistepScheduler.from_config(scheduler.config) + elif scheduler_type == "ddim": + scheduler = scheduler + else: + raise ValueError(f"Scheduler of type {scheduler_type} doesn't exist!") + + # Convert the UNet2DConditionModel model. + unet_config = create_unet_diffusers_config(original_config, image_size=image_size) + unet_config["upcast_attention"] = upcast_attention + converted_unet_checkpoint = convert_ldm_unet_checkpoint( + checkpoint, unet_config, path=checkpoint_path, extract_ema=extract_ema + ) + + ctx = init_empty_weights if is_accelerate_available() else nullcontext + with ctx(): + unet = UNet2DConditionModel(**unet_config) + + if is_accelerate_available(): + for param_name, param in converted_unet_checkpoint.items(): + set_module_tensor_to_device(unet, param_name, "cpu", value=param) + else: + unet.load_state_dict(converted_unet_checkpoint) + + # Convert the VAE model. + if vae_path is None: + vae_config = create_vae_diffusers_config(original_config, image_size=image_size) + converted_vae_checkpoint = convert_ldm_vae_checkpoint(checkpoint, vae_config) + + if ( + "model" in original_config + and "params" in original_config.model + and "scale_factor" in original_config.model.params + ): + vae_scaling_factor = original_config.model.params.scale_factor + else: + vae_scaling_factor = 0.18215 # default SD scaling factor + + vae_config["scaling_factor"] = vae_scaling_factor + + ctx = init_empty_weights if is_accelerate_available() else nullcontext + with ctx(): + vae = AutoencoderKL(**vae_config) + + if is_accelerate_available(): + for param_name, param in converted_vae_checkpoint.items(): + set_module_tensor_to_device(vae, param_name, "cpu", value=param) + else: + vae.load_state_dict(converted_vae_checkpoint) + else: + vae = AutoencoderKL.from_pretrained(vae_path) + + if model_type == "FrozenOpenCLIPEmbedder": + config_name = "stabilityai/stable-diffusion-2" + config_kwargs = {"subfolder": "text_encoder"} + + text_model = convert_open_clip_checkpoint(checkpoint, config_name, **config_kwargs) + tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "stable-diffusion-2-clip", subfolder="tokenizer") + + if stable_unclip is None: + if controlnet: + pipe = pipeline_class( + vae=vae.to(precision), + text_encoder=text_model.to(precision), + tokenizer=tokenizer, + unet=unet.to(precision), + scheduler=scheduler, + controlnet=controlnet, + safety_checker=None, + feature_extractor=None, + requires_safety_checker=False, + ) + else: + pipe = pipeline_class( + vae=vae.to(precision), + text_encoder=text_model.to(precision), + tokenizer=tokenizer, + unet=unet.to(precision), + scheduler=scheduler, + safety_checker=None, + feature_extractor=None, + requires_safety_checker=False, + ) + else: + image_normalizer, image_noising_scheduler = stable_unclip_image_noising_components( + original_config, clip_stats_path=clip_stats_path, device=device + ) + + if stable_unclip == "img2img": + feature_extractor, image_encoder = stable_unclip_image_encoder(original_config) + + pipe = StableUnCLIPImg2ImgPipeline( + # image encoding components + feature_extractor=feature_extractor, + image_encoder=image_encoder, + # image noising components + image_normalizer=image_normalizer, + image_noising_scheduler=image_noising_scheduler, + # regular denoising components + tokenizer=tokenizer, + text_encoder=text_model.to(precision), + unet=unet.to(precision), + scheduler=scheduler, + # vae + vae=vae, + ) + elif stable_unclip == "txt2img": + if stable_unclip_prior is None or stable_unclip_prior == "karlo": + karlo_model = "kakaobrain/karlo-v1-alpha" + prior = PriorTransformer.from_pretrained(karlo_model, subfolder="prior") + + prior_tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") + prior_text_model = CLIPTextModelWithProjection.from_pretrained( + CONVERT_MODEL_ROOT / "clip-vit-large-patch14" + ) + + prior_scheduler = UnCLIPScheduler.from_pretrained(karlo_model, subfolder="prior_scheduler") + prior_scheduler = DDPMScheduler.from_config(prior_scheduler.config) + else: + raise NotImplementedError(f"unknown prior for stable unclip model: {stable_unclip_prior}") + + pipe = StableUnCLIPPipeline( + # prior components + prior_tokenizer=prior_tokenizer, + prior_text_encoder=prior_text_model, + prior=prior, + prior_scheduler=prior_scheduler, + # image noising components + image_normalizer=image_normalizer, + image_noising_scheduler=image_noising_scheduler, + # regular denoising components + tokenizer=tokenizer, + text_encoder=text_model, + unet=unet, + scheduler=scheduler, + # vae + vae=vae, + ) + else: + raise NotImplementedError(f"unknown `stable_unclip` type: {stable_unclip}") + elif model_type == "PaintByExample": + vision_model = convert_paint_by_example_checkpoint(checkpoint) + tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") + feature_extractor = AutoFeatureExtractor.from_pretrained(CONVERT_MODEL_ROOT / "stable-diffusion-safety-checker") + pipe = PaintByExamplePipeline( + vae=vae, + image_encoder=vision_model, + unet=unet, + scheduler=scheduler, + safety_checker=None, + feature_extractor=feature_extractor, + ) + elif model_type == "FrozenCLIPEmbedder": + text_model = convert_ldm_clip_checkpoint( + checkpoint, local_files_only=local_files_only, text_encoder=text_encoder + ) + tokenizer = ( + CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") + if tokenizer is None + else tokenizer + ) + + if load_safety_checker: + safety_checker = StableDiffusionSafetyChecker.from_pretrained( + CONVERT_MODEL_ROOT / "stable-diffusion-safety-checker" + ) + feature_extractor = AutoFeatureExtractor.from_pretrained( + CONVERT_MODEL_ROOT / "stable-diffusion-safety-checker" + ) + else: + safety_checker = None + feature_extractor = None + + if controlnet: + pipe = pipeline_class( + vae=vae.to(precision), + text_encoder=text_model.to(precision), + tokenizer=tokenizer, + unet=unet.to(precision), + controlnet=controlnet, + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + else: + pipe = pipeline_class( + vae=vae.to(precision), + text_encoder=text_model.to(precision), + tokenizer=tokenizer, + unet=unet.to(precision), + scheduler=scheduler, + safety_checker=safety_checker, + feature_extractor=feature_extractor, + ) + elif model_type in ["SDXL", "SDXL-Refiner"]: + if model_type == "SDXL": + tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") + text_encoder = convert_ldm_clip_checkpoint(checkpoint, local_files_only=local_files_only) + + tokenizer_name = CONVERT_MODEL_ROOT / "CLIP-ViT-bigG-14-laion2B-39B-b160k" + tokenizer_2 = CLIPTokenizer.from_pretrained(tokenizer_name, pad_token="!") + + config_name = tokenizer_name + config_kwargs = {"projection_dim": 1280} + text_encoder_2 = convert_open_clip_checkpoint( + checkpoint, config_name, prefix="conditioner.embedders.1.model.", has_projection=True, **config_kwargs + ) + + pipe = StableDiffusionXLPipeline( + vae=vae.to(precision), + text_encoder=text_encoder.to(precision), + tokenizer=tokenizer, + text_encoder_2=text_encoder_2.to(precision), + tokenizer_2=tokenizer_2, + unet=unet.to(precision), + scheduler=scheduler, + force_zeros_for_empty_prompt=True, + ) + else: + tokenizer = None + text_encoder = None + tokenizer_name = CONVERT_MODEL_ROOT / "CLIP-ViT-bigG-14-laion2B-39B-b160k" + tokenizer_2 = CLIPTokenizer.from_pretrained(tokenizer_name, pad_token="!") + + config_name = tokenizer_name + config_kwargs = {"projection_dim": 1280} + text_encoder_2 = convert_open_clip_checkpoint( + checkpoint, config_name, prefix="conditioner.embedders.0.model.", has_projection=True, **config_kwargs + ) + + pipe = StableDiffusionXLImg2ImgPipeline( + vae=vae.to(precision), + text_encoder=text_encoder, + tokenizer=tokenizer, + text_encoder_2=text_encoder_2, + tokenizer_2=tokenizer_2, + unet=unet.to(precision), + scheduler=scheduler, + requires_aesthetics_score=True, + force_zeros_for_empty_prompt=False, + ) + else: + text_config = create_ldm_bert_config(original_config) + text_model = convert_ldm_bert_checkpoint(checkpoint, text_config) + tokenizer = BertTokenizerFast.from_pretrained(CONVERT_MODEL_ROOT / "bert-base-uncased") + pipe = LDMTextToImagePipeline(vqvae=vae, bert=text_model, tokenizer=tokenizer, unet=unet, scheduler=scheduler) + + return pipe + + +def download_controlnet_from_original_ckpt( + checkpoint_path: str, + original_config_file: str, + image_size: int = 512, + extract_ema: bool = False, + precision: Optional[torch.dtype] = None, + num_in_channels: Optional[int] = None, + upcast_attention: Optional[bool] = None, + device: str = None, + from_safetensors: bool = False, + use_linear_projection: Optional[bool] = None, + cross_attention_dim: Optional[bool] = None, + scan_needed: bool = False, +) -> DiffusionPipeline: + + from omegaconf import OmegaConf + + if from_safetensors: + from safetensors import safe_open + + checkpoint = {} + with safe_open(checkpoint_path, framework="pt", device="cpu") as f: + for key in f.keys(): + checkpoint[key] = f.get_tensor(key) + else: + if scan_needed: + # scan model + scan_result = scan_file_path(checkpoint_path) + if scan_result.infected_files != 0: + raise Exception("The model {checkpoint_path} is potentially infected by malware. Aborting import.") + if device is None: + device = "cuda" if torch.cuda.is_available() else "cpu" + checkpoint = torch.load(checkpoint_path, map_location=device) + else: + checkpoint = torch.load(checkpoint_path, map_location=device) + + # NOTE: this while loop isn't great but this controlnet checkpoint has one additional + # "state_dict" key https://huggingface.co/thibaud/controlnet-canny-sd21 + while "state_dict" in checkpoint: + checkpoint = checkpoint["state_dict"] + + # use original precision + precision_probing_key = "input_blocks.0.0.bias" + ckpt_precision = checkpoint[precision_probing_key].dtype + logger.debug(f"original controlnet precision = {ckpt_precision}") + precision = precision or ckpt_precision + + original_config = OmegaConf.load(original_config_file) + + if num_in_channels is not None: + original_config["model"]["params"]["unet_config"]["params"]["in_channels"] = num_in_channels + + if "control_stage_config" not in original_config.model.params: + raise ValueError("`control_stage_config` not present in original config") + + controlnet = convert_controlnet_checkpoint( + checkpoint, + original_config, + checkpoint_path, + image_size, + upcast_attention, + extract_ema, + use_linear_projection=use_linear_projection, + cross_attention_dim=cross_attention_dim, + ) + + return controlnet.to(precision) + + +def convert_ldm_vae_to_diffusers(checkpoint, vae_config: DictConfig, image_size: int) -> AutoencoderKL: + vae_config = create_vae_diffusers_config(vae_config, image_size=image_size) + + converted_vae_checkpoint = convert_ldm_vae_checkpoint(checkpoint, vae_config) + + vae = AutoencoderKL(**vae_config) + vae.load_state_dict(converted_vae_checkpoint) + return vae + + +def convert_ckpt_to_diffusers( + checkpoint_path: Union[str, Path], + dump_path: Union[str, Path], + use_safetensors: bool = True, + **kwargs, +): + """ + Takes all the arguments of download_from_original_stable_diffusion_ckpt(), + and in addition a path-like object indicating the location of the desired diffusers + model to be written. + """ + pipe = download_from_original_stable_diffusion_ckpt(checkpoint_path, **kwargs) + + # TO DO: save correct repo variant + pipe.save_pretrained( + dump_path, + safe_serialization=use_safetensors, + ) + + +def convert_controlnet_to_diffusers( + checkpoint_path: Union[str, Path], + dump_path: Union[str, Path], + **kwargs, +): + """ + Takes all the arguments of download_controlnet_from_original_ckpt(), + and in addition a path-like object indicating the location of the desired diffusers + model to be written. + """ + pipe = download_controlnet_from_original_ckpt(checkpoint_path, **kwargs) + + # TO DO: save correct repo variant + pipe.save_pretrained(dump_path, safe_serialization=True) diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py index e69de29bb2..357677bb7f 100644 --- a/invokeai/backend/model_manager/load/__init__.py +++ b/invokeai/backend/model_manager/load/__init__.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development Team +""" +Init file for the model loader. +""" +from importlib import import_module +from pathlib import Path +from typing import Optional + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.util.logging import InvokeAILogger +from .load_base import AnyModelLoader, LoadedModel +from .model_cache.model_cache_default import ModelCache +from .convert_cache.convert_cache_default import ModelConvertCache + +# This registers the subclasses that implement loaders of specific model types +loaders = [x.stem for x in Path(Path(__file__).parent,'model_loaders').glob('*.py') if x.stem != '__init__'] +for module in loaders: + print(f'module={module}') + import_module(f"{__package__}.model_loaders.{module}") + +__all__ = ["AnyModelLoader", "LoadedModel"] + + +def get_standalone_loader(app_config: Optional[InvokeAIAppConfig]) -> AnyModelLoader: + app_config = app_config or InvokeAIAppConfig.get_config() + logger = InvokeAILogger.get_logger(config=app_config) + return AnyModelLoader(app_config=app_config, + logger=logger, + ram_cache=ModelCache(logger=logger, + max_cache_size=app_config.ram_cache_size, + max_vram_cache_size=app_config.vram_cache_size + ), + convert_cache=ModelConvertCache(app_config.models_convert_cache_path) + ) + diff --git a/invokeai/backend/model_manager/load/convert_cache/__init__.py b/invokeai/backend/model_manager/load/convert_cache/__init__.py new file mode 100644 index 0000000000..eb3149be32 --- /dev/null +++ b/invokeai/backend/model_manager/load/convert_cache/__init__.py @@ -0,0 +1,4 @@ +from .convert_cache_base import ModelConvertCacheBase +from .convert_cache_default import ModelConvertCache + +__all__ = ['ModelConvertCacheBase', 'ModelConvertCache'] diff --git a/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py b/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py new file mode 100644 index 0000000000..25263f96aa --- /dev/null +++ b/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py @@ -0,0 +1,28 @@ +""" +Disk-based converted model cache. +""" +from abc import ABC, abstractmethod +from pathlib import Path + +class ModelConvertCacheBase(ABC): + + @property + @abstractmethod + def max_size(self) -> float: + """Return the maximum size of this cache directory.""" + pass + + @abstractmethod + def make_room(self, size: float) -> None: + """ + Make sufficient room in the cache directory for a model of max_size. + + :param size: Size required (GB) + """ + pass + + @abstractmethod + def cache_path(self, key: str) -> Path: + """Return the path for a model with the indicated key.""" + pass + diff --git a/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py b/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py new file mode 100644 index 0000000000..f799510ec5 --- /dev/null +++ b/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py @@ -0,0 +1,64 @@ +""" +Placeholder for convert cache implementation. +""" + +from pathlib import Path +import shutil +from invokeai.backend.util.logging import InvokeAILogger +from invokeai.backend.util import GIG, directory_size +from .convert_cache_base import ModelConvertCacheBase + +class ModelConvertCache(ModelConvertCacheBase): + + def __init__(self, cache_path: Path, max_size: float=10.0): + """Initialize the convert cache with the base directory and a limit on its maximum size (in GBs).""" + if not cache_path.exists(): + cache_path.mkdir(parents=True) + self._cache_path = cache_path + self._max_size = max_size + + @property + def max_size(self) -> float: + """Return the maximum size of this cache directory (GB).""" + return self._max_size + + def cache_path(self, key: str) -> Path: + """Return the path for a model with the indicated key.""" + return self._cache_path / key + + def make_room(self, size: float) -> None: + """ + Make sufficient room in the cache directory for a model of max_size. + + :param size: Size required (GB) + """ + size_needed = directory_size(self._cache_path) + size + max_size = int(self.max_size) * GIG + logger = InvokeAILogger.get_logger() + + if size_needed <= max_size: + return + + logger.debug( + f"Convert cache has gotten too large {(size_needed / GIG):4.2f} > {(max_size / GIG):4.2f}G.. Trimming." + ) + + # For this to work, we make the assumption that the directory contains + # a 'model_index.json', 'unet/config.json' file, or a 'config.json' file at top level. + # This should be true for any diffusers model. + def by_atime(path: Path) -> float: + for config in ["model_index.json", "unet/config.json", "config.json"]: + sentinel = path / config + if sentinel.exists(): + return sentinel.stat().st_atime + return 0.0 + + # sort by last access time - least accessed files will be at the end + lru_models = sorted(self._cache_path.iterdir(), key=by_atime, reverse=True) + logger.debug(f"cached models in descending atime order: {lru_models}") + while size_needed > max_size and len(lru_models) > 0: + next_victim = lru_models.pop() + victim_size = directory_size(next_victim) + logger.debug(f"Removing cached converted model {next_victim} to free {victim_size / GIG} GB") + shutil.rmtree(next_victim) + size_needed -= victim_size diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 7cb7222b71..3ade83160a 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -16,39 +16,11 @@ from logging import Logger from pathlib import Path from typing import Any, Callable, Dict, Optional, Type, Union -import torch -from diffusers import DiffusionPipeline -from injector import inject - from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.model_records import ModelRecordServiceBase -from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType -from invokeai.backend.model_manager.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.onnx_runtime import IAIOnnxRuntimeModel -from invokeai.backend.model_manager.ram_cache import ModelCacheBase - -AnyModel = Union[DiffusionPipeline, torch.nn.Module, IAIOnnxRuntimeModel] - - -class ModelLockerBase(ABC): - """Base class for the model locker used by the loader.""" - - @abstractmethod - def lock(self) -> None: - """Lock the contained model and move it into VRAM.""" - pass - - @abstractmethod - def unlock(self) -> None: - """Unlock the contained model, and remove it from VRAM.""" - pass - - @property - @abstractmethod - def model(self) -> AnyModel: - """Return the model.""" - pass - +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase +from invokeai.backend.model_manager.load.model_cache.model_locker import ModelLockerBase +from invokeai.backend.model_manager.load.convert_cache.convert_cache_base import ModelConvertCacheBase @dataclass class LoadedModel: @@ -69,7 +41,7 @@ class LoadedModel: @property def model(self) -> AnyModel: """Return the model without locking it.""" - return self.locker.model() + return self.locker.model class ModelLoaderBase(ABC): @@ -89,9 +61,9 @@ class ModelLoaderBase(ABC): @abstractmethod def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ - Return a model given its key. + Return a model given its confguration. - Given a model key identified in the model configuration backend, + Given a model identified in the model configuration backend, return a ModelInfo object that can be used to retrieve the model. :param model_config: Model configuration, as returned by ModelConfigRecordStore @@ -115,34 +87,32 @@ class AnyModelLoader: # this tracks the loader subclasses _registry: Dict[str, Type[ModelLoaderBase]] = {} - @inject def __init__( self, - store: ModelRecordServiceBase, app_config: InvokeAIAppConfig, logger: Logger, ram_cache: ModelCacheBase, convert_cache: ModelConvertCacheBase, ): - """Store the provided ModelRecordServiceBase and empty the registry.""" - self._store = store + """Initialize AnyModelLoader with its dependencies.""" self._app_config = app_config self._logger = logger self._ram_cache = ram_cache self._convert_cache = convert_cache - def get_model(self, key: str, submodel_type: Optional[SubModelType] = None) -> LoadedModel: - """ - Return a model given its key. + @property + def ram_cache(self) -> ModelCacheBase: + """Return the RAM cache associated used by the loaders.""" + return self._ram_cache - Given a model key identified in the model configuration backend, - return a ModelInfo object that can be used to retrieve the model. + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType]=None) -> LoadedModel: + """ + Return a model given its configuration. :param key: model key, as known to the config backend :param submodel_type: an ModelType enum indicating the portion of the model to retrieve (e.g. ModelType.Vae) """ - model_config = self._store.get_model(key) implementation = self.__class__.get_implementation( base=model_config.base, type=model_config.type, format=model_config.format ) @@ -165,7 +135,7 @@ class AnyModelLoader: implementation = cls._registry.get(key1) or cls._registry.get(key2) if not implementation: raise NotImplementedError( - "No subclass of LoadedModel is registered for base={base}, type={type}, format={format}" + f"No subclass of LoadedModel is registered for base={base}, type={type}, format={format}" ) return implementation @@ -176,18 +146,10 @@ class AnyModelLoader: """Define a decorator which registers the subclass of loader.""" def decorator(subclass: Type[ModelLoaderBase]) -> Type[ModelLoaderBase]: - print("Registering class", subclass.__name__) + print("DEBUG: Registering class", subclass.__name__) key = cls._to_registry_key(base, type, format) cls._registry[key] = subclass return subclass return decorator - -# in _init__.py will call something like -# def configure_loader_dependencies(binder): -# binder.bind(ModelRecordServiceBase, ApiDependencies.invoker.services.model_records, scope=singleton) -# binder.bind(InvokeAIAppConfig, ApiDependencies.invoker.services.configuration, scope=singleton) -# etc -# injector = Injector(configure_loader_dependencies) -# loader = injector.get(ModelFactory) diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index eb2d432aaa..0b028235fd 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -8,15 +8,14 @@ from typing import Any, Dict, Optional, Tuple from diffusers import ModelMixin from diffusers.configuration_utils import ConfigMixin -from injector import inject from invokeai.app.services.config import InvokeAIAppConfig from invokeai.backend.model_manager import AnyModelConfig, InvalidModelConfigException, ModelRepoVariant, SubModelType -from invokeai.backend.model_manager.convert_cache import ModelConvertCacheBase +from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.backend.model_manager.load.load_base import AnyModel, LoadedModel, ModelLoaderBase from invokeai.backend.model_manager.load.model_util import calc_model_size_by_fs from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init -from invokeai.backend.model_manager.ram_cache import ModelCacheBase, ModelLockerBase +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase from invokeai.backend.util.devices import choose_torch_device, torch_dtype @@ -35,7 +34,6 @@ class ConfigLoader(ConfigMixin): class ModelLoader(ModelLoaderBase): """Default implementation of ModelLoaderBase.""" - @inject # can inject instances of each of the classes in the call signature def __init__( self, app_config: InvokeAIAppConfig, @@ -87,18 +85,15 @@ class ModelLoader(ModelLoaderBase): def _convert_if_needed( self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None ) -> Path: - if not self._needs_conversion(config): - return model_path + cache_path: Path = self._convert_cache.cache_path(config.key) + + if not self._needs_conversion(config, model_path, cache_path): + return cache_path if cache_path.exists() else model_path self._convert_cache.make_room(self._size or self.get_size_fs(config, model_path, submodel_type)) - cache_path: Path = self._convert_cache.cache_path(config.key) - if cache_path.exists(): - return cache_path + return self._convert_model(config, model_path, cache_path) - self._convert_model(model_path, cache_path) - return cache_path - - def _needs_conversion(self, config: AnyModelConfig) -> bool: + def _needs_conversion(self, config: AnyModelConfig, model_path: Path, cache_path: Path) -> bool: return False def _load_if_needed( @@ -133,7 +128,7 @@ class ModelLoader(ModelLoaderBase): variant=config.repo_variant if hasattr(config, "repo_variant") else None, ) - def _convert_model(self, model_path: Path, cache_path: Path) -> None: + def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: raise NotImplementedError def _load_model( diff --git a/invokeai/backend/model_manager/load/model_cache/__init__.py b/invokeai/backend/model_manager/load/model_cache/__init__.py new file mode 100644 index 0000000000..776b9d8936 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/__init__.py @@ -0,0 +1,5 @@ +"""Init file for RamCache.""" + +from .model_cache_base import ModelCacheBase +from .model_cache_default import ModelCache +_all__ = ['ModelCacheBase', 'ModelCache'] diff --git a/invokeai/backend/model_manager/load/ram_cache/ram_cache_base.py b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py similarity index 77% rename from invokeai/backend/model_manager/load/ram_cache/ram_cache_base.py rename to invokeai/backend/model_manager/load/model_cache/model_cache_base.py index cd80d1e78b..50b69d961c 100644 --- a/invokeai/backend/model_manager/load/ram_cache/ram_cache_base.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py @@ -10,34 +10,41 @@ model will be cleared and (re)loaded from disk when next needed. from abc import ABC, abstractmethod from dataclasses import dataclass, field from logging import Logger -from typing import Dict, Optional +from typing import Dict, Optional, TypeVar, Generic import torch -from invokeai.backend.model_manager import SubModelType -from invokeai.backend.model_manager.load.load_base import AnyModel, ModelLockerBase +from invokeai.backend.model_manager import AnyModel, SubModelType +class ModelLockerBase(ABC): + """Base class for the model locker used by the loader.""" + + @abstractmethod + def lock(self) -> AnyModel: + """Lock the contained model and move it into VRAM.""" + pass + + @abstractmethod + def unlock(self) -> None: + """Unlock the contained model, and remove it from VRAM.""" + pass + + @property + @abstractmethod + def model(self) -> AnyModel: + """Return the model.""" + pass + +T = TypeVar("T") @dataclass -class CacheStats(object): - """Data object to record statistics on cache hits/misses.""" - - hits: int = 0 # cache hits - misses: int = 0 # cache misses - high_watermark: int = 0 # amount of cache used - in_cache: int = 0 # number of models in cache - cleared: int = 0 # number of models cleared to make space - cache_size: int = 0 # total size of cache - loaded_model_sizes: Dict[str, int] = field(default_factory=dict) - - -@dataclass -class CacheRecord: +class CacheRecord(Generic[T]): """Elements of the cache.""" key: str - model: AnyModel + model: T size: int + loaded: bool = False _locks: int = 0 def lock(self) -> None: @@ -55,7 +62,7 @@ class CacheRecord: return self._locks > 0 -class ModelCacheBase(ABC): +class ModelCacheBase(ABC, Generic[T]): """Virtual base class for RAM model cache.""" @property @@ -76,8 +83,14 @@ class ModelCacheBase(ABC): """Return true if the cache is configured to lazily offload models in VRAM.""" pass + @property @abstractmethod - def offload_unlocked_models(self) -> None: + def max_cache_size(self) -> float: + """Return true if the cache is configured to lazily offload models in VRAM.""" + pass + + @abstractmethod + def offload_unlocked_models(self, size_required: int) -> None: """Offload from VRAM any models not actively in use.""" pass @@ -101,7 +114,7 @@ class ModelCacheBase(ABC): def put( self, key: str, - model: AnyModel, + model: T, submodel_type: Optional[SubModelType] = None, ) -> None: """Store model under key and optional submodel_type.""" @@ -134,11 +147,6 @@ class ModelCacheBase(ABC): """Get the total size of the models currently cached.""" pass - @abstractmethod - def get_stats(self) -> CacheStats: - """Return cache hit/miss/size statistics.""" - pass - @abstractmethod def print_cuda_stats(self) -> None: """Log debugging information on CUDA usage.""" diff --git a/invokeai/backend/model_manager/load/ram_cache/ram_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py similarity index 63% rename from invokeai/backend/model_manager/load/ram_cache/ram_cache_default.py rename to invokeai/backend/model_manager/load/model_cache/model_cache_default.py index bd43e978c8..961f68a4be 100644 --- a/invokeai/backend/model_manager/load/ram_cache/ram_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -18,6 +18,7 @@ context. Use like this: """ +import gc import math import time from contextlib import suppress @@ -26,14 +27,14 @@ from typing import Any, Dict, List, Optional import torch -from invokeai.app.services.model_records import UnknownModelException from invokeai.backend.model_manager import SubModelType -from invokeai.backend.model_manager.load.load_base import AnyModel, ModelLockerBase +from invokeai.backend.model_manager.load.load_base import AnyModel from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data -from invokeai.backend.model_manager.load.ram_cache.ram_cache_base import CacheRecord, CacheStats, ModelCacheBase from invokeai.backend.util.devices import choose_torch_device from invokeai.backend.util.logging import InvokeAILogger +from .model_cache_base import CacheRecord, ModelCacheBase +from .model_locker import ModelLockerBase, ModelLocker if choose_torch_device() == torch.device("mps"): from torch import mps @@ -52,7 +53,7 @@ GIG = 1073741824 MB = 2**20 -class ModelCache(ModelCacheBase): +class ModelCache(ModelCacheBase[AnyModel]): """Implementation of ModelCacheBase.""" def __init__( @@ -92,62 +93,9 @@ class ModelCache(ModelCacheBase): self._logger = logger or InvokeAILogger.get_logger(self.__class__.__name__) self._log_memory_usage = log_memory_usage - # used for stats collection - self.stats = None - - self._cached_models: Dict[str, CacheRecord] = {} + self._cached_models: Dict[str, CacheRecord[AnyModel]] = {} self._cache_stack: List[str] = [] - class ModelLocker(ModelLockerBase): - """Internal class that mediates movement in and out of GPU.""" - - def __init__(self, cache: ModelCacheBase, cache_entry: CacheRecord): - """ - Initialize the model locker. - - :param cache: The ModelCache object - :param cache_entry: The entry in the model cache - """ - self._cache = cache - self._cache_entry = cache_entry - - @property - def model(self) -> AnyModel: - """Return the model without moving it around.""" - return self._cache_entry.model - - def lock(self) -> Any: - """Move the model into the execution device (GPU) and lock it.""" - if not hasattr(self.model, "to"): - return self.model - - # NOTE that the model has to have the to() method in order for this code to move it into GPU! - self._cache_entry.lock() - - try: - if self._cache.lazy_offloading: - self._cache.offload_unlocked_models() - - self._cache.move_model_to_device(self._cache_entry, self._cache.execution_device) - - self._cache.logger.debug(f"Locking {self._cache_entry.key} in {self._cache.execution_device}") - self._cache.print_cuda_stats() - - except Exception: - self._cache_entry.unlock() - raise - return self.model - - def unlock(self) -> None: - """Call upon exit from context.""" - if not hasattr(self.model, "to"): - return - - self._cache_entry.unlock() - if not self._cache.lazy_offloading: - self._cache.offload_unlocked_models() - self._cache.print_cuda_stats() - @property def logger(self) -> Logger: """Return the logger used by the cache.""" @@ -168,6 +116,11 @@ class ModelCache(ModelCacheBase): """Return the exection device (e.g. "cuda" for VRAM).""" return self._execution_device + @property + def max_cache_size(self) -> float: + """Return the cap on cache size.""" + return self._max_cache_size + def cache_size(self) -> int: """Get the total size of the models currently cached.""" total = 0 @@ -207,18 +160,18 @@ class ModelCache(ModelCacheBase): """ Retrieve model using key and optional submodel_type. - This may return an UnknownModelException if the model is not in the cache. + This may return an IndexError if the model is not in the cache. """ key = self._make_cache_key(key, submodel_type) if key not in self._cached_models: - raise UnknownModelException + raise IndexError(f"The model with key {key} is not in the cache.") # this moves the entry to the top (right end) of the stack with suppress(Exception): self._cache_stack.remove(key) self._cache_stack.append(key) cache_entry = self._cached_models[key] - return self.ModelLocker( + return ModelLocker( cache=self, cache_entry=cache_entry, ) @@ -234,19 +187,19 @@ class ModelCache(ModelCacheBase): else: return model_key - def offload_unlocked_models(self) -> None: + def offload_unlocked_models(self, size_required: int) -> None: """Move any unused models from VRAM.""" reserved = self._max_vram_cache_size * GIG - vram_in_use = torch.cuda.memory_allocated() - self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM used for models; max allowed={(reserved/GIG):.2f}GB") + vram_in_use = torch.cuda.memory_allocated() + size_required + self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM needed for models; max allowed={(reserved/GIG):.2f}GB") for _, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size): if vram_in_use <= reserved: break if not cache_entry.locked: self.move_model_to_device(cache_entry, self.storage_device) - - vram_in_use = torch.cuda.memory_allocated() - self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM used for models; max allowed={(reserved/GIG):.2f}GB") + cache_entry.loaded = False + vram_in_use = torch.cuda.memory_allocated() + size_required + self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM now available for models; max allowed={(reserved/GIG):.2f}GB") torch.cuda.empty_cache() if choose_torch_device() == torch.device("mps"): @@ -305,28 +258,111 @@ class ModelCache(ModelCacheBase): def print_cuda_stats(self) -> None: """Log CUDA diagnostics.""" vram = "%4.2fG" % (torch.cuda.memory_allocated() / GIG) - ram = "%4.2fG" % self.cache_size() + ram = "%4.2fG" % (self.cache_size() / GIG) - cached_models = 0 - loaded_models = 0 - locked_models = 0 + in_ram_models = 0 + in_vram_models = 0 + locked_in_vram_models = 0 for cache_record in self._cached_models.values(): - cached_models += 1 assert hasattr(cache_record.model, "device") - if cache_record.model.device is self.storage_device: - loaded_models += 1 + if cache_record.model.device == self.storage_device: + in_ram_models += 1 + else: + in_vram_models += 1 if cache_record.locked: - locked_models += 1 + locked_in_vram_models += 1 self.logger.debug( - f"Current VRAM/RAM usage: {vram}/{ram}; cached_models/loaded_models/locked_models/ =" - f" {cached_models}/{loaded_models}/{locked_models}" + f"Current VRAM/RAM usage: {vram}/{ram}; models_in_ram/models_in_vram(locked) =" + f" {in_ram_models}/{in_vram_models}({locked_in_vram_models})" ) - def get_stats(self) -> CacheStats: - """Return cache hit/miss/size statistics.""" - raise NotImplementedError - - def make_room(self, size: int) -> None: + def make_room(self, model_size: int) -> None: """Make enough room in the cache to accommodate a new model of indicated size.""" - raise NotImplementedError + # calculate how much memory this model will require + # multiplier = 2 if self.precision==torch.float32 else 1 + bytes_needed = model_size + maximum_size = self.max_cache_size * GIG # stored in GB, convert to bytes + current_size = self.cache_size() + + if current_size + bytes_needed > maximum_size: + self.logger.debug( + f"Max cache size exceeded: {(current_size/GIG):.2f}/{self.max_cache_size:.2f} GB, need an additional" + f" {(bytes_needed/GIG):.2f} GB" + ) + + self.logger.debug(f"Before unloading: cached_models={len(self._cached_models)}") + + pos = 0 + models_cleared = 0 + while current_size + bytes_needed > maximum_size and pos < len(self._cache_stack): + model_key = self._cache_stack[pos] + cache_entry = self._cached_models[model_key] + + refs = sys.getrefcount(cache_entry.model) + + # HACK: This is a workaround for a memory-management issue that we haven't tracked down yet. We are directly + # going against the advice in the Python docs by using `gc.get_referrers(...)` in this way: + # https://docs.python.org/3/library/gc.html#gc.get_referrers + + # manualy clear local variable references of just finished function calls + # for some reason python don't want to collect it even by gc.collect() immidiately + if refs > 2: + while True: + cleared = False + for referrer in gc.get_referrers(cache_entry.model): + if type(referrer).__name__ == "frame": + # RuntimeError: cannot clear an executing frame + with suppress(RuntimeError): + referrer.clear() + cleared = True + # break + + # repeat if referrers changes(due to frame clear), else exit loop + if cleared: + gc.collect() + else: + break + + device = cache_entry.model.device if hasattr(cache_entry.model, "device") else None + self.logger.debug( + f"Model: {model_key}, locks: {cache_entry._locks}, device: {device}, loaded: {cache_entry.loaded}," + f" refs: {refs}" + ) + + # Expected refs: + # 1 from cache_entry + # 1 from getrefcount function + # 1 from onnx runtime object + if not cache_entry.locked and refs <= (3 if "onnx" in model_key else 2): + self.logger.debug( + f"Unloading model {model_key} to free {(model_size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)" + ) + current_size -= cache_entry.size + models_cleared += 1 + del self._cache_stack[pos] + del self._cached_models[model_key] + del cache_entry + + else: + pos += 1 + + if models_cleared > 0: + # There would likely be some 'garbage' to be collected regardless of whether a model was cleared or not, but + # there is a significant time cost to calling `gc.collect()`, so we want to use it sparingly. (The time cost + # is high even if no garbage gets collected.) + # + # Calling gc.collect(...) when a model is cleared seems like a good middle-ground: + # - If models had to be cleared, it's a signal that we are close to our memory limit. + # - If models were cleared, there's a good chance that there's a significant amount of garbage to be + # collected. + # + # Keep in mind that gc is only responsible for handling reference cycles. Most objects should be cleaned up + # immediately when their reference count hits 0. + gc.collect() + + torch.cuda.empty_cache() + if choose_torch_device() == torch.device("mps"): + mps.empty_cache() + + self.logger.debug(f"After unloading: cached_models={len(self._cached_models)}") diff --git a/invokeai/backend/model_manager/load/model_cache/model_locker.py b/invokeai/backend/model_manager/load/model_cache/model_locker.py new file mode 100644 index 0000000000..506d012949 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_cache/model_locker.py @@ -0,0 +1,59 @@ +""" +Base class and implementation of a class that moves models in and out of VRAM. +""" + +from abc import ABC, abstractmethod +from invokeai.backend.model_manager import AnyModel +from .model_cache_base import ModelLockerBase, ModelCacheBase, CacheRecord + +class ModelLocker(ModelLockerBase): + """Internal class that mediates movement in and out of GPU.""" + + def __init__(self, cache: ModelCacheBase[AnyModel], cache_entry: CacheRecord[AnyModel]): + """ + Initialize the model locker. + + :param cache: The ModelCache object + :param cache_entry: The entry in the model cache + """ + self._cache = cache + self._cache_entry = cache_entry + + @property + def model(self) -> AnyModel: + """Return the model without moving it around.""" + return self._cache_entry.model + + def lock(self) -> AnyModel: + """Move the model into the execution device (GPU) and lock it.""" + if not hasattr(self.model, "to"): + return self.model + + # NOTE that the model has to have the to() method in order for this code to move it into GPU! + self._cache_entry.lock() + + try: + if self._cache.lazy_offloading: + self._cache.offload_unlocked_models(self._cache_entry.size) + + self._cache.move_model_to_device(self._cache_entry, self._cache.execution_device) + self._cache_entry.loaded = True + + self._cache.logger.debug(f"Locking {self._cache_entry.key} in {self._cache.execution_device}") + self._cache.print_cuda_stats() + + except Exception: + self._cache_entry.unlock() + raise + return self.model + + def unlock(self) -> None: + """Call upon exit from context.""" + if not hasattr(self.model, "to"): + return + + self._cache_entry.unlock() + if not self._cache.lazy_offloading: + self._cache.offload_unlocked_models(self._cache_entry.size) + self._cache.print_cuda_stats() + diff --git a/invokeai/backend/model_manager/load/model_loaders/__init__.py b/invokeai/backend/model_manager/load/model_loaders/__init__.py new file mode 100644 index 0000000000..962cba5481 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/__init__.py @@ -0,0 +1,3 @@ +""" +Init file for model_loaders. +""" diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py new file mode 100644 index 0000000000..6f21c3d090 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/vae.py @@ -0,0 +1,83 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for VAE model loading in InvokeAI.""" + +from pathlib import Path +from typing import Optional + +import torch +import safetensors +from omegaconf import OmegaConf, DictConfig +from invokeai.backend.util.devices import torch_dtype +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelFormat, ModelRepoVariant, ModelType, SubModelType +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ldm_vae_to_diffusers + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Vae, format=ModelFormat.Diffusers) +@AnyModelLoader.register(base=BaseModelType.StableDiffusion1, type=ModelType.Vae, format=ModelFormat.Checkpoint) +@AnyModelLoader.register(base=BaseModelType.StableDiffusion2, type=ModelType.Vae, format=ModelFormat.Checkpoint) +class VaeDiffusersModel(ModelLoader): + """Class to load VAE models.""" + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise Exception("There are no submodels in VAEs") + vae_class = self._get_hf_load_class(model_path) + variant = model_variant.value if model_variant else None + result: AnyModel = vae_class.from_pretrained( + model_path, torch_dtype=self._torch_dtype, variant=variant + ) # type: ignore + return result + + def _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: + print(f'DEBUG: last_modified={config.last_modified}') + print(f'DEBUG: cache_path={(dest_path / "config.json").stat().st_mtime}') + print(f'DEBUG: model_path={model_path.stat().st_mtime}') + if config.format != ModelFormat.Checkpoint: + return False + elif dest_path.exists() \ + and (dest_path / "config.json").stat().st_mtime >= config.last_modified \ + and (dest_path / "config.json").stat().st_mtime >= model_path.stat().st_mtime: + return False + else: + return True + + def _convert_model(self, + config: AnyModelConfig, + weights_path: Path, + output_path: Path + ) -> Path: + if config.base not in {BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2}: + raise Exception(f"Vae conversion not supported for model type: {config.base}") + else: + config_file = 'v1-inference.yaml' if config.base == BaseModelType.StableDiffusion1 else "v2-inference-v.yaml" + + if weights_path.suffix == ".safetensors": + checkpoint = safetensors.torch.load_file(weights_path, device="cpu") + else: + checkpoint = torch.load(weights_path, map_location="cpu") + + dtype = torch_dtype() + + # sometimes weights are hidden under "state_dict", and sometimes not + if "state_dict" in checkpoint: + checkpoint = checkpoint["state_dict"] + + ckpt_config = OmegaConf.load(self._app_config.legacy_conf_path / config_file) + assert isinstance(ckpt_config, DictConfig) + + print(f'DEBUG: CONVERTIGN') + vae_model = convert_ldm_vae_to_diffusers( + checkpoint=checkpoint, + vae_config=ckpt_config, + image_size=512, + ) + vae_model.to(dtype) # set precision appropriately + vae_model.save_pretrained(output_path, safe_serialization=True, torch_dtype=dtype) + return output_path + diff --git a/invokeai/backend/model_manager/load/model_util.py b/invokeai/backend/model_manager/load/model_util.py index 18407cbca2..7c27e66472 100644 --- a/invokeai/backend/model_manager/load/model_util.py +++ b/invokeai/backend/model_manager/load/model_util.py @@ -48,6 +48,9 @@ def _calc_onnx_model_by_data(model: IAIOnnxRuntimeModel) -> int: def calc_model_size_by_fs(model_path: Path, subfolder: Optional[str] = None, variant: Optional[str] = None) -> int: """Estimate the size of a model on disk in bytes.""" + if model_path.is_file(): + return model_path.stat().st_size + if subfolder is not None: model_path = model_path / subfolder diff --git a/invokeai/backend/model_manager/load/ram_cache/__init__.py b/invokeai/backend/model_manager/load/ram_cache/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/invokeai/backend/model_manager/load/vae.py b/invokeai/backend/model_manager/load/vae.py deleted file mode 100644 index a6cbe241e1..0000000000 --- a/invokeai/backend/model_manager/load/vae.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team -"""Class for VAE model loading in InvokeAI.""" - -from pathlib import Path -from typing import Dict, Optional - -import torch - -from invokeai.backend.model_manager import BaseModelType, ModelFormat, ModelRepoVariant, ModelType, SubModelType -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader - - -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Vae, format=ModelFormat.Diffusers) -class VaeDiffusersModel(ModelLoader): - """Class to load VAE models.""" - - def _load_model( - self, - model_path: Path, - model_variant: Optional[ModelRepoVariant] = None, - submodel_type: Optional[SubModelType] = None, - ) -> Dict[str, torch.Tensor]: - if submodel_type is not None: - raise Exception("There are no submodels in VAEs") - vae_class = self._get_hf_load_class(model_path) - variant = model_variant.value if model_variant else "" - result: Dict[str, torch.Tensor] = vae_class.from_pretrained( - model_path, torch_dtype=self._torch_dtype, variant=variant - ) # type: ignore - return result diff --git a/invokeai/backend/util/__init__.py b/invokeai/backend/util/__init__.py index 87ae1480f5..0164dffe30 100644 --- a/invokeai/backend/util/__init__.py +++ b/invokeai/backend/util/__init__.py @@ -12,6 +12,14 @@ from .devices import ( # noqa: F401 torch_dtype, ) from .logging import InvokeAILogger -from .util import Chdir, ask_user, download_with_resume, instantiate_from_config, url_attachment_name # noqa: F401 +from .util import ( # TO DO: Clean this up; remove the unused symbols + GIG, + Chdir, + ask_user, # noqa + directory_size, + download_with_resume, + instantiate_from_config, # noqa + url_attachment_name, # noqa + ) -__all__ = ["Chdir", "InvokeAILogger", "choose_precision", "choose_torch_device"] +__all__ = ["GIG", "directory_size","Chdir", "download_with_resume", "InvokeAILogger", "choose_precision", "choose_torch_device"] diff --git a/invokeai/backend/util/devices.py b/invokeai/backend/util/devices.py index d6d3ad727f..ad3f4e139a 100644 --- a/invokeai/backend/util/devices.py +++ b/invokeai/backend/util/devices.py @@ -1,7 +1,7 @@ from __future__ import annotations from contextlib import nullcontext -from typing import Union +from typing import Union, Optional import torch from torch import autocast @@ -43,7 +43,8 @@ def choose_precision(device: torch.device) -> str: return "float32" -def torch_dtype(device: torch.device) -> torch.dtype: +def torch_dtype(device: Optional[torch.device] = None) -> torch.dtype: + device = device or choose_torch_device() precision = choose_precision(device) if precision == "float16": return torch.float16 diff --git a/invokeai/backend/util/util.py b/invokeai/backend/util/util.py index 13751e2770..6589aa7278 100644 --- a/invokeai/backend/util/util.py +++ b/invokeai/backend/util/util.py @@ -24,6 +24,20 @@ import invokeai.backend.util.logging as logger from .devices import torch_dtype +# actual size of a gig +GIG = 1073741824 + +def directory_size(directory: Path) -> int: + """ + Return the aggregate size of all files in a directory (bytes). + """ + sum = 0 + for root, dirs, files in os.walk(directory): + for f in files: + sum += Path(root, f).stat().st_size + for d in dirs: + sum += Path(root, d).stat().st_size + return sum def log_txt_as_img(wh, xc, size=10): # wh a tuple of (width, height) From 67eb71509370214a04d7f5512eb3663dc3df5a9d Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 4 Feb 2024 17:23:10 -0500 Subject: [PATCH 113/411] loaders for main, controlnet, ip-adapter, clipvision and t2i --- .../app/services/config/config_default.py | 2 +- .../model_records/model_records_base.py | 11 +- .../model_records/model_records_sql.py | 17 +- .../sqlite_migrator/migrations/migration_6.py | 5 +- invokeai/backend/install/install_helper.py | 1 + .../model_management/models/controlnet.py | 1 - invokeai/backend/model_manager/__init__.py | 2 +- invokeai/backend/model_manager/config.py | 15 +- .../convert_ckpt_to_diffusers.py | 4 +- .../backend/model_manager/load/__init__.py | 24 +- .../load/convert_cache/__init__.py | 2 +- .../load/convert_cache/convert_cache_base.py | 3 +- .../convert_cache/convert_cache_default.py | 10 +- .../backend/model_manager/load/load_base.py | 52 +- .../model_manager/load/load_default.py | 50 +- .../model_manager/load/memory_snapshot.py | 2 +- .../load/model_cache/__init__.py | 2 +- .../load/model_cache/model_cache_base.py | 8 +- .../load/model_cache/model_cache_default.py | 46 +- .../load/model_cache/model_locker.py | 6 +- .../load/model_loaders/controlnet.py | 60 ++ .../load/model_loaders/generic_diffusers.py | 34 + .../load/model_loaders/ip_adapter.py | 39 ++ .../model_manager/load/model_loaders/lora.py | 76 +++ .../load/model_loaders/stable_diffusion.py | 93 +++ .../model_manager/load/model_loaders/vae.py | 66 +- .../backend/model_manager/load/model_util.py | 5 +- invokeai/backend/model_manager/lora.py | 620 ++++++++++++++++++ invokeai/backend/model_manager/probe.py | 6 +- invokeai/backend/util/__init__.py | 16 +- invokeai/backend/util/devices.py | 2 +- invokeai/backend/util/util.py | 2 + 32 files changed, 1123 insertions(+), 159 deletions(-) create mode 100644 invokeai/backend/model_manager/load/model_loaders/controlnet.py create mode 100644 invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py create mode 100644 invokeai/backend/model_manager/load/model_loaders/ip_adapter.py create mode 100644 invokeai/backend/model_manager/load/model_loaders/lora.py create mode 100644 invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py create mode 100644 invokeai/backend/model_manager/lora.py diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index b161ea18d6..b39e916da3 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -173,7 +173,7 @@ from __future__ import annotations import os from pathlib import Path -from typing import Any, ClassVar, Dict, List, Literal, Optional, Union +from typing import Any, ClassVar, Dict, List, Literal, Optional from omegaconf import DictConfig, OmegaConf from pydantic import Field diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py index 31cfecb4ec..42e3c8f83a 100644 --- a/invokeai/app/services/model_records/model_records_base.py +++ b/invokeai/app/services/model_records/model_records_base.py @@ -11,7 +11,14 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union from pydantic import BaseModel, Field from invokeai.app.services.shared.pagination import PaginatedResults -from invokeai.backend.model_manager import LoadedModel, AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType +from invokeai.backend.model_manager import ( + AnyModelConfig, + BaseModelType, + LoadedModel, + ModelFormat, + ModelType, + SubModelType, +) from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore @@ -108,7 +115,7 @@ class ModelRecordServiceBase(ABC): Load the indicated model into memory and return a LoadedModel object. :param key: Key of model config to be fetched. - :param submodel_type: For main (pipeline models), the submodel to fetch + :param submodel_type: For main (pipeline models), the submodel to fetch Exceptions: UnknownModelException -- model with this key not known NotImplementedException -- a model loader was not provided at initialization time diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index eee867ccb4..b50cd17a75 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -42,7 +42,6 @@ Typical usage: import json import sqlite3 -import time from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple, Union @@ -56,8 +55,8 @@ from invokeai.backend.model_manager.config import ( ModelType, SubModelType, ) -from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore, UnknownMetadataException from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel +from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore, UnknownMetadataException from ..shared.sqlite.sqlite_database import SqliteDatabase from .model_records_base import ( @@ -72,7 +71,7 @@ from .model_records_base import ( class ModelRecordServiceSQL(ModelRecordServiceBase): """Implementation of the ModelConfigStore ABC using a SQL database.""" - def __init__(self, db: SqliteDatabase, loader: Optional[AnyModelLoader]=None): + def __init__(self, db: SqliteDatabase, loader: Optional[AnyModelLoader] = None): """ Initialize a new object from preexisting sqlite3 connection and threading lock objects. @@ -289,7 +288,9 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): """, tuple(bindings), ) - results = [ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall()] + results = [ + ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall() + ] return results def search_by_path(self, path: Union[str, Path]) -> List[AnyModelConfig]: @@ -303,7 +304,9 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): """, (str(path),), ) - results = [ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall()] + results = [ + ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall() + ] return results def search_by_hash(self, hash: str) -> List[AnyModelConfig]: @@ -317,7 +320,9 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): """, (hash,), ) - results = [ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall()] + results = [ + ModelConfigFactory.make_config(json.loads(x[0]), timestamp=x[1]) for x in self._cursor.fetchall() + ] return results @property diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py index e72878f726..b473444511 100644 --- a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py @@ -1,11 +1,9 @@ import sqlite3 -from logging import Logger -from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration -class Migration6Callback: +class Migration6Callback: def __call__(self, cursor: sqlite3.Cursor) -> None: self._recreate_model_triggers(cursor) @@ -28,6 +26,7 @@ class Migration6Callback: """ ) + def build_migration_6() -> Migration: """ Build the migration from database version 5 to 6. diff --git a/invokeai/backend/install/install_helper.py b/invokeai/backend/install/install_helper.py index 8c03d2ccf8..9f219132d4 100644 --- a/invokeai/backend/install/install_helper.py +++ b/invokeai/backend/install/install_helper.py @@ -120,6 +120,7 @@ class TqdmEventService(EventServiceBase): elif payload["event"] == "model_install_cancelled": self._logger.warning(f"{source}: installation cancelled") + class InstallHelper(object): """Capture information stored jointly in INITIAL_MODELS.yaml and the installed models db.""" diff --git a/invokeai/backend/model_management/models/controlnet.py b/invokeai/backend/model_management/models/controlnet.py index da269eba4b..3b534cb9d1 100644 --- a/invokeai/backend/model_management/models/controlnet.py +++ b/invokeai/backend/model_management/models/controlnet.py @@ -139,7 +139,6 @@ def _convert_controlnet_ckpt_and_cache( cache it to disk, and return Path to converted file. If already on disk then just returns Path. """ - print(f"DEBUG: controlnet config = {model_config}") app_config = InvokeAIAppConfig.get_config() weights = app_config.root_path / model_path output_path = Path(output_path) diff --git a/invokeai/backend/model_manager/__init__.py b/invokeai/backend/model_manager/__init__.py index f3c84cd01f..98cc5054c7 100644 --- a/invokeai/backend/model_manager/__init__.py +++ b/invokeai/backend/model_manager/__init__.py @@ -13,9 +13,9 @@ from .config import ( SchedulerPredictionType, SubModelType, ) +from .load import LoadedModel from .probe import ModelProbe from .search import ModelSearch -from .load import LoadedModel __all__ = [ "AnyModel", diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 796ccbacde..e59a84d729 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -20,14 +20,16 @@ Validation errors will raise an InvalidModelConfigException error. """ import time -import torch from enum import Enum from typing import Literal, Optional, Type, Union -from pydantic import BaseModel, ConfigDict, Field, TypeAdapter +import torch from diffusers import ModelMixin +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from typing_extensions import Annotated, Any, Dict + from .onnx_runtime import IAIOnnxRuntimeModel +from ..ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus class InvalidModelConfigException(Exception): """Exception for when config parser doesn't recognized this combination of model type and format.""" @@ -204,6 +206,8 @@ class _MainConfig(ModelConfigBase): vae: Optional[str] = Field(default=None) variant: ModelVariantType = ModelVariantType.Normal + prediction_type: SchedulerPredictionType = SchedulerPredictionType.Epsilon + upcast_attention: bool = False ztsnr_training: bool = False @@ -217,8 +221,6 @@ class MainDiffusersConfig(_DiffusersConfig, _MainConfig): """Model config for main diffusers models.""" type: Literal[ModelType.Main] = ModelType.Main - prediction_type: SchedulerPredictionType = SchedulerPredictionType.Epsilon - upcast_attention: bool = False class ONNXSD1Config(_MainConfig): @@ -276,6 +278,7 @@ AnyModelConfig = Union[ _ONNXConfig, _VaeConfig, _ControlNetConfig, + # ModelConfigBase, LoRAConfig, TextualInversionConfig, IPAdapterConfig, @@ -284,7 +287,7 @@ AnyModelConfig = Union[ ] AnyModelConfigValidator = TypeAdapter(AnyModelConfig) -AnyModel = Union[ModelMixin, torch.nn.Module, IAIOnnxRuntimeModel] +AnyModel = Union[ModelMixin, torch.nn.Module, IAIOnnxRuntimeModel, IPAdapter, IPAdapterPlus] # IMPLEMENTATION NOTE: # The preferred alternative to the above is a discriminated Union as shown @@ -317,7 +320,7 @@ class ModelConfigFactory(object): model_data: Union[dict, AnyModelConfig], key: Optional[str] = None, dest_class: Optional[Type] = None, - timestamp: Optional[float] = None + timestamp: Optional[float] = None, ) -> AnyModelConfig: """ Return the appropriate config object from raw dict values. diff --git a/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py b/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py index 9d6fc4841f..6f5acd5832 100644 --- a/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py +++ b/invokeai/backend/model_manager/convert_ckpt_to_diffusers.py @@ -43,7 +43,6 @@ from diffusers.schedulers import ( UnCLIPScheduler, ) from diffusers.utils import is_accelerate_available -from diffusers.utils.import_utils import BACKENDS_MAPPING from picklescan.scanner import scan_file_path from transformers import ( AutoFeatureExtractor, @@ -58,8 +57,8 @@ from transformers import ( ) from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.util.logging import InvokeAILogger from invokeai.backend.model_manager import BaseModelType, ModelVariantType +from invokeai.backend.util.logging import InvokeAILogger try: from omegaconf import OmegaConf @@ -1643,7 +1642,6 @@ def download_controlnet_from_original_ckpt( cross_attention_dim: Optional[bool] = None, scan_needed: bool = False, ) -> DiffusionPipeline: - from omegaconf import OmegaConf if from_safetensors: diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py index 357677bb7f..19b0116ba3 100644 --- a/invokeai/backend/model_manager/load/__init__.py +++ b/invokeai/backend/model_manager/load/__init__.py @@ -8,14 +8,15 @@ from typing import Optional from invokeai.app.services.config import InvokeAIAppConfig from invokeai.backend.util.logging import InvokeAILogger + +from .convert_cache.convert_cache_default import ModelConvertCache from .load_base import AnyModelLoader, LoadedModel from .model_cache.model_cache_default import ModelCache -from .convert_cache.convert_cache_default import ModelConvertCache # This registers the subclasses that implement loaders of specific model types -loaders = [x.stem for x in Path(Path(__file__).parent,'model_loaders').glob('*.py') if x.stem != '__init__'] +loaders = [x.stem for x in Path(Path(__file__).parent, "model_loaders").glob("*.py") if x.stem != "__init__"] for module in loaders: - print(f'module={module}') + print(f"module={module}") import_module(f"{__package__}.model_loaders.{module}") __all__ = ["AnyModelLoader", "LoadedModel"] @@ -24,12 +25,11 @@ __all__ = ["AnyModelLoader", "LoadedModel"] def get_standalone_loader(app_config: Optional[InvokeAIAppConfig]) -> AnyModelLoader: app_config = app_config or InvokeAIAppConfig.get_config() logger = InvokeAILogger.get_logger(config=app_config) - return AnyModelLoader(app_config=app_config, - logger=logger, - ram_cache=ModelCache(logger=logger, - max_cache_size=app_config.ram_cache_size, - max_vram_cache_size=app_config.vram_cache_size - ), - convert_cache=ModelConvertCache(app_config.models_convert_cache_path) - ) - + return AnyModelLoader( + app_config=app_config, + logger=logger, + ram_cache=ModelCache( + logger=logger, max_cache_size=app_config.ram_cache_size, max_vram_cache_size=app_config.vram_cache_size + ), + convert_cache=ModelConvertCache(app_config.models_convert_cache_path), + ) diff --git a/invokeai/backend/model_manager/load/convert_cache/__init__.py b/invokeai/backend/model_manager/load/convert_cache/__init__.py index eb3149be32..5be56d2d58 100644 --- a/invokeai/backend/model_manager/load/convert_cache/__init__.py +++ b/invokeai/backend/model_manager/load/convert_cache/__init__.py @@ -1,4 +1,4 @@ from .convert_cache_base import ModelConvertCacheBase from .convert_cache_default import ModelConvertCache -__all__ = ['ModelConvertCacheBase', 'ModelConvertCache'] +__all__ = ["ModelConvertCacheBase", "ModelConvertCache"] diff --git a/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py b/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py index 25263f96aa..6268c099a5 100644 --- a/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py +++ b/invokeai/backend/model_manager/load/convert_cache/convert_cache_base.py @@ -4,8 +4,8 @@ Disk-based converted model cache. from abc import ABC, abstractmethod from pathlib import Path -class ModelConvertCacheBase(ABC): +class ModelConvertCacheBase(ABC): @property @abstractmethod def max_size(self) -> float: @@ -25,4 +25,3 @@ class ModelConvertCacheBase(ABC): def cache_path(self, key: str) -> Path: """Return the path for a model with the indicated key.""" pass - diff --git a/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py b/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py index f799510ec5..4c361258d9 100644 --- a/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py +++ b/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py @@ -2,15 +2,17 @@ Placeholder for convert cache implementation. """ -from pathlib import Path import shutil -from invokeai.backend.util.logging import InvokeAILogger +from pathlib import Path + from invokeai.backend.util import GIG, directory_size +from invokeai.backend.util.logging import InvokeAILogger + from .convert_cache_base import ModelConvertCacheBase -class ModelConvertCache(ModelConvertCacheBase): - def __init__(self, cache_path: Path, max_size: float=10.0): +class ModelConvertCache(ModelConvertCacheBase): + def __init__(self, cache_path: Path, max_size: float = 10.0): """Initialize the convert cache with the base directory and a limit on its maximum size (in GBs).""" if not cache_path.exists(): cache_path.mkdir(parents=True) diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 3ade83160a..7d4e8337c3 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -10,17 +10,19 @@ Use like this: # do something with loaded_model """ +import hashlib from abc import ABC, abstractmethod from dataclasses import dataclass from logging import Logger from pathlib import Path -from typing import Any, Callable, Dict, Optional, Type, Union +from typing import Any, Callable, Dict, Optional, Tuple, Type from invokeai.app.services.config import InvokeAIAppConfig from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType -from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase -from invokeai.backend.model_manager.load.model_cache.model_locker import ModelLockerBase +from invokeai.backend.model_manager.config import VaeCheckpointConfig, VaeDiffusersConfig from invokeai.backend.model_manager.load.convert_cache.convert_cache_base import ModelConvertCacheBase +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase + @dataclass class LoadedModel: @@ -52,7 +54,7 @@ class ModelLoaderBase(ABC): self, app_config: InvokeAIAppConfig, logger: Logger, - ram_cache: ModelCacheBase, + ram_cache: ModelCacheBase[AnyModel], convert_cache: ModelConvertCacheBase, ): """Initialize the loader.""" @@ -91,7 +93,7 @@ class AnyModelLoader: self, app_config: InvokeAIAppConfig, logger: Logger, - ram_cache: ModelCacheBase, + ram_cache: ModelCacheBase[AnyModel], convert_cache: ModelConvertCacheBase, ): """Initialize AnyModelLoader with its dependencies.""" @@ -101,11 +103,11 @@ class AnyModelLoader: self._convert_cache = convert_cache @property - def ram_cache(self) -> ModelCacheBase: + def ram_cache(self) -> ModelCacheBase[AnyModel]: """Return the RAM cache associated used by the loaders.""" return self._ram_cache - def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType]=None) -> LoadedModel: + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Return a model given its configuration. @@ -113,9 +115,7 @@ class AnyModelLoader: :param submodel_type: an ModelType enum indicating the portion of the model to retrieve (e.g. ModelType.Vae) """ - implementation = self.__class__.get_implementation( - base=model_config.base, type=model_config.type, format=model_config.format - ) + implementation, model_config, submodel_type = self.__class__.get_implementation(model_config, submodel_type) return implementation( app_config=self._app_config, logger=self._logger, @@ -128,16 +128,37 @@ class AnyModelLoader: return "-".join([base.value, type.value, format.value]) @classmethod - def get_implementation(cls, base: BaseModelType, type: ModelType, format: ModelFormat) -> Type[ModelLoaderBase]: + def get_implementation( + cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] + ) -> Tuple[Type[ModelLoaderBase], AnyModelConfig, Optional[SubModelType]]: """Get subclass of ModelLoaderBase registered to handle base and type.""" - key1 = cls._to_registry_key(base, type, format) # for a specific base type - key2 = cls._to_registry_key(BaseModelType.Any, type, format) # with wildcard Any + # We have to handle VAE overrides here because this will change the model type and the corresponding implementation returned + conf2, submodel_type = cls._handle_subtype_overrides(config, submodel_type) + + key1 = cls._to_registry_key(conf2.base, conf2.type, conf2.format) # for a specific base type + key2 = cls._to_registry_key(BaseModelType.Any, conf2.type, conf2.format) # with wildcard Any implementation = cls._registry.get(key1) or cls._registry.get(key2) if not implementation: raise NotImplementedError( - f"No subclass of LoadedModel is registered for base={base}, type={type}, format={format}" + f"No subclass of LoadedModel is registered for base={config.base}, type={config.type}, format={config.format}" ) - return implementation + return implementation, conf2, submodel_type + + @classmethod + def _handle_subtype_overrides( + cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] + ) -> Tuple[AnyModelConfig, Optional[SubModelType]]: + if submodel_type == SubModelType.Vae and hasattr(config, "vae") and config.vae is not None: + model_path = Path(config.vae) + config_class = ( + VaeCheckpointConfig if model_path.suffix in [".pt", ".safetensors", ".ckpt"] else VaeDiffusersConfig + ) + hash = hashlib.md5(model_path.as_posix().encode("utf-8")).hexdigest() + new_conf = config_class(path=model_path.as_posix(), name=model_path.stem, base=config.base, key=hash) + submodel_type = None + else: + new_conf = config + return new_conf, submodel_type @classmethod def register( @@ -152,4 +173,3 @@ class AnyModelLoader: return subclass return decorator - diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index 0b028235fd..453283e9b4 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -10,12 +10,12 @@ from diffusers import ModelMixin from diffusers.configuration_utils import ConfigMixin from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_manager import AnyModelConfig, InvalidModelConfigException, ModelRepoVariant, SubModelType +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, InvalidModelConfigException, ModelRepoVariant, SubModelType from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.load.load_base import AnyModel, LoadedModel, ModelLoaderBase -from invokeai.backend.model_manager.load.model_util import calc_model_size_by_fs -from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init +from invokeai.backend.model_manager.load.load_base import LoadedModel, ModelLoaderBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase +from invokeai.backend.model_manager.load.model_util import calc_model_size_by_fs, calc_model_size_by_data +from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init from invokeai.backend.util.devices import choose_torch_device, torch_dtype @@ -38,7 +38,7 @@ class ModelLoader(ModelLoaderBase): self, app_config: InvokeAIAppConfig, logger: Logger, - ram_cache: ModelCacheBase, + ram_cache: ModelCacheBase[AnyModel], convert_cache: ModelConvertCacheBase, ): """Initialize the loader.""" @@ -47,7 +47,6 @@ class ModelLoader(ModelLoaderBase): self._ram_cache = ram_cache self._convert_cache = convert_cache self._torch_dtype = torch_dtype(choose_torch_device()) - self._size: Optional[int] = None # model size def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ @@ -63,9 +62,7 @@ class ModelLoader(ModelLoaderBase): if model_config.type == "main" and not submodel_type: raise InvalidModelConfigException("submodel_type is required when loading a main model") - model_path, is_submodel_override = self._get_model_path(model_config, submodel_type) - if is_submodel_override: - submodel_type = None + model_path, model_config, submodel_type = self._get_model_path(model_config, submodel_type) if not model_path.exists(): raise InvalidModelConfigException(f"Files for model 'model_config.name' not found at {model_path}") @@ -74,13 +71,12 @@ class ModelLoader(ModelLoaderBase): locker = self._load_if_needed(model_config, model_path, submodel_type) return LoadedModel(config=model_config, locker=locker) - # IMPORTANT: This needs to be overridden in the StableDiffusion subclass so as to handle vae overrides - # and submodels!!!! def _get_model_path( self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None - ) -> Tuple[Path, bool]: + ) -> Tuple[Path, AnyModelConfig, Optional[SubModelType]]: model_base = self._app_config.models_path - return ((model_base / config.path).resolve(), False) + result = (model_base / config.path).resolve(), config, submodel_type + return result def _convert_if_needed( self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None @@ -90,7 +86,7 @@ class ModelLoader(ModelLoaderBase): if not self._needs_conversion(config, model_path, cache_path): return cache_path if cache_path.exists() else model_path - self._convert_cache.make_room(self._size or self.get_size_fs(config, model_path, submodel_type)) + self._convert_cache.make_room(self.get_size_fs(config, model_path, submodel_type)) return self._convert_model(config, model_path, cache_path) def _needs_conversion(self, config: AnyModelConfig, model_path: Path, cache_path: Path) -> bool: @@ -114,6 +110,7 @@ class ModelLoader(ModelLoaderBase): config.key, submodel_type=submodel_type, model=loaded_model, + size=calc_model_size_by_data(loaded_model), ) return self._ram_cache.get(config.key, submodel_type) @@ -128,17 +125,6 @@ class ModelLoader(ModelLoaderBase): variant=config.repo_variant if hasattr(config, "repo_variant") else None, ) - def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: - raise NotImplementedError - - def _load_model( - self, - model_path: Path, - model_variant: Optional[ModelRepoVariant] = None, - submodel_type: Optional[SubModelType] = None, - ) -> AnyModel: - raise NotImplementedError - def _load_diffusers_config(self, model_path: Path, config_name: str = "config.json") -> Dict[str, Any]: return ConfigLoader.load_config(model_path, config_name=config_name) @@ -161,3 +147,17 @@ class ModelLoader(ModelLoaderBase): config = self._load_diffusers_config(model_path, config_name="config.json") class_name = config["_class_name"] return self._hf_definition_to_type(module="diffusers", class_name=class_name) + + # This needs to be implemented in subclasses that handle checkpoints + def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + raise NotImplementedError + + # This needs to be implemented in the subclass + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + raise NotImplementedError + diff --git a/invokeai/backend/model_manager/load/memory_snapshot.py b/invokeai/backend/model_manager/load/memory_snapshot.py index 504829a427..295be0c551 100644 --- a/invokeai/backend/model_manager/load/memory_snapshot.py +++ b/invokeai/backend/model_manager/load/memory_snapshot.py @@ -97,4 +97,4 @@ def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: O if snapshot_1.vram is not None and snapshot_2.vram is not None: msg += get_msg_line("VRAM", snapshot_1.vram, snapshot_2.vram) - return msg + return "\n"+msg if len(msg)>0 else msg diff --git a/invokeai/backend/model_manager/load/model_cache/__init__.py b/invokeai/backend/model_manager/load/model_cache/__init__.py index 776b9d8936..50cafa3769 100644 --- a/invokeai/backend/model_manager/load/model_cache/__init__.py +++ b/invokeai/backend/model_manager/load/model_cache/__init__.py @@ -2,4 +2,4 @@ from .model_cache_base import ModelCacheBase from .model_cache_default import ModelCache -_all__ = ['ModelCacheBase', 'ModelCache'] +_all__ = ["ModelCacheBase", "ModelCache"] diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py index 50b69d961c..14a7dfb4a1 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py @@ -8,14 +8,15 @@ model will be cleared and (re)loaded from disk when next needed. """ from abc import ABC, abstractmethod -from dataclasses import dataclass, field +from dataclasses import dataclass from logging import Logger -from typing import Dict, Optional, TypeVar, Generic +from typing import Generic, Optional, TypeVar import torch from invokeai.backend.model_manager import AnyModel, SubModelType + class ModelLockerBase(ABC): """Base class for the model locker used by the loader.""" @@ -35,8 +36,10 @@ class ModelLockerBase(ABC): """Return the model.""" pass + T = TypeVar("T") + @dataclass class CacheRecord(Generic[T]): """Elements of the cache.""" @@ -115,6 +118,7 @@ class ModelCacheBase(ABC, Generic[T]): self, key: str, model: T, + size: int, submodel_type: Optional[SubModelType] = None, ) -> None: """Store model under key and optional submodel_type.""" diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py index 961f68a4be..688be8ceb4 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -19,22 +19,24 @@ context. Use like this: """ import gc +import logging import math +import sys import time from contextlib import suppress from logging import Logger -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional import torch from invokeai.backend.model_manager import SubModelType from invokeai.backend.model_manager.load.load_base import AnyModel from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff -from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data from invokeai.backend.util.devices import choose_torch_device from invokeai.backend.util.logging import InvokeAILogger + from .model_cache_base import CacheRecord, ModelCacheBase -from .model_locker import ModelLockerBase, ModelLocker +from .model_locker import ModelLocker, ModelLockerBase if choose_torch_device() == torch.device("mps"): from torch import mps @@ -91,7 +93,7 @@ class ModelCache(ModelCacheBase[AnyModel]): self._execution_device: torch.device = execution_device self._storage_device: torch.device = storage_device self._logger = logger or InvokeAILogger.get_logger(self.__class__.__name__) - self._log_memory_usage = log_memory_usage + self._log_memory_usage = log_memory_usage or self._logger.level == logging.DEBUG self._cached_models: Dict[str, CacheRecord[AnyModel]] = {} self._cache_stack: List[str] = [] @@ -141,14 +143,14 @@ class ModelCache(ModelCacheBase[AnyModel]): self, key: str, model: AnyModel, + size: int, submodel_type: Optional[SubModelType] = None, ) -> None: """Store model under key and optional submodel_type.""" key = self._make_cache_key(key, submodel_type) assert key not in self._cached_models - loaded_model_size = calc_model_size_by_data(model) - cache_record = CacheRecord(key, model, loaded_model_size) + cache_record = CacheRecord(key, model, size) self._cached_models[key] = cache_record self._cache_stack.append(key) @@ -195,28 +197,32 @@ class ModelCache(ModelCacheBase[AnyModel]): for _, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size): if vram_in_use <= reserved: break + if not cache_entry.loaded: + continue if not cache_entry.locked: self.move_model_to_device(cache_entry, self.storage_device) cache_entry.loaded = False vram_in_use = torch.cuda.memory_allocated() + size_required - self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM now available for models; max allowed={(reserved/GIG):.2f}GB") + self.logger.debug( + f"Removing {cache_entry.key} from VRAM to free {(cache_entry.size/GIG):.2f}GB; vram free = {(torch.cuda.memory_allocated()/GIG):.2f}GB" + ) torch.cuda.empty_cache() if choose_torch_device() == torch.device("mps"): mps.empty_cache() - # TO DO: Only reason to pass the CacheRecord rather than the model is to get the key and size - # for printing debugging messages. Revisit whether this is necessary - def move_model_to_device(self, cache_entry: CacheRecord, target_device: torch.device) -> None: + def move_model_to_device(self, cache_entry: CacheRecord[AnyModel], target_device: torch.device) -> None: """Move model into the indicated device.""" - # These attributes are not in the base class but in derived classes - assert hasattr(cache_entry.model, "device") - assert hasattr(cache_entry.model, "to") + # These attributes are not in the base ModelMixin class but in derived classes. + # Some models don't have these attributes, in which case they run in RAM/CPU. + self.logger.debug(f"Called to move {cache_entry.key} to {target_device}") + if not (hasattr(cache_entry.model, "device") and hasattr(cache_entry.model, "to")): + return source_device = cache_entry.model.device - # Note: We compare device types only so that 'cuda' == 'cuda:0'. This would need to be revised to support - # multi-GPU. + # Note: We compare device types only so that 'cuda' == 'cuda:0'. + # This would need to be revised to support multi-GPU. if torch.device(source_device).type == torch.device(target_device).type: return @@ -227,8 +233,8 @@ class ModelCache(ModelCacheBase[AnyModel]): end_model_to_time = time.time() self.logger.debug( f"Moved model '{cache_entry.key}' from {source_device} to" - f" {target_device} in {(end_model_to_time-start_model_to_time):.2f}s.\n" - f"Estimated model size: {(cache_entry.size/GIG):.3f} GB.\n" + f" {target_device} in {(end_model_to_time-start_model_to_time):.2f}s." + f"Estimated model size: {(cache_entry.size/GIG):.3f} GB." f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" ) @@ -291,7 +297,7 @@ class ModelCache(ModelCacheBase[AnyModel]): f" {(bytes_needed/GIG):.2f} GB" ) - self.logger.debug(f"Before unloading: cached_models={len(self._cached_models)}") + self.logger.debug(f"Before making_room: cached_models={len(self._cached_models)}") pos = 0 models_cleared = 0 @@ -336,7 +342,7 @@ class ModelCache(ModelCacheBase[AnyModel]): # 1 from onnx runtime object if not cache_entry.locked and refs <= (3 if "onnx" in model_key else 2): self.logger.debug( - f"Unloading model {model_key} to free {(model_size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)" + f"Removing {model_key} from RAM cache to free at least {(model_size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)" ) current_size -= cache_entry.size models_cleared += 1 @@ -365,4 +371,4 @@ class ModelCache(ModelCacheBase[AnyModel]): if choose_torch_device() == torch.device("mps"): mps.empty_cache() - self.logger.debug(f"After unloading: cached_models={len(self._cached_models)}") + self.logger.debug(f"After making room: cached_models={len(self._cached_models)}") diff --git a/invokeai/backend/model_manager/load/model_cache/model_locker.py b/invokeai/backend/model_manager/load/model_cache/model_locker.py index 506d012949..7a5fdd4284 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_locker.py +++ b/invokeai/backend/model_manager/load/model_cache/model_locker.py @@ -2,9 +2,10 @@ Base class and implementation of a class that moves models in and out of VRAM. """ -from abc import ABC, abstractmethod from invokeai.backend.model_manager import AnyModel -from .model_cache_base import ModelLockerBase, ModelCacheBase, CacheRecord + +from .model_cache_base import CacheRecord, ModelCacheBase, ModelLockerBase + class ModelLocker(ModelLockerBase): """Internal class that mediates movement in and out of GPU.""" @@ -56,4 +57,3 @@ class ModelLocker(ModelLockerBase): if not self._cache.lazy_offloading: self._cache.offload_unlocked_models(self._cache_entry.size) self._cache.print_cuda_stats() - diff --git a/invokeai/backend/model_manager/load/model_loaders/controlnet.py b/invokeai/backend/model_manager/load/model_loaders/controlnet.py new file mode 100644 index 0000000000..8e6a80ceb2 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/controlnet.py @@ -0,0 +1,60 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for ControlNet model loading in InvokeAI.""" + +from pathlib import Path + +import safetensors +import torch + +from invokeai.backend.model_manager import ( + AnyModelConfig, + BaseModelType, + ModelFormat, + ModelType, +) +from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_controlnet_to_diffusers +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from .generic_diffusers import GenericDiffusersLoader + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Diffusers) +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Checkpoint) +class ControlnetLoader(GenericDiffusersLoader): + """Class to load ControlNet models.""" + + def _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: + if config.format != ModelFormat.Checkpoint: + return False + elif ( + dest_path.exists() + and (dest_path / "config.json").stat().st_mtime >= (config.last_modified or 0.0) + and (dest_path / "config.json").stat().st_mtime >= model_path.stat().st_mtime + ): + return False + else: + return True + + def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + if config.base not in {BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2}: + raise Exception(f"Vae conversion not supported for model type: {config.base}") + else: + assert hasattr(config, 'config') + config_file = config.config + + if weights_path.suffix == ".safetensors": + checkpoint = safetensors.torch.load_file(weights_path, device="cpu") + else: + checkpoint = torch.load(weights_path, map_location="cpu") + + # sometimes weights are hidden under "state_dict", and sometimes not + if "state_dict" in checkpoint: + checkpoint = checkpoint["state_dict"] + + convert_controlnet_to_diffusers( + weights_path, + output_path, + original_config_file=self._app_config.root_path / config_file, + image_size=512, + scan_needed=True, + from_safetensors=weights_path.suffix == ".safetensors", + ) + return output_path diff --git a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py new file mode 100644 index 0000000000..f92a9048c5 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for simple diffusers model loading in InvokeAI.""" + +from pathlib import Path +from typing import Optional + +from invokeai.backend.model_manager import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelType, + SubModelType, +) +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) +class GenericDiffusersLoader(ModelLoader): + """Class to load simple diffusers models.""" + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + model_class = self._get_hf_load_class(model_path) + if submodel_type is not None: + raise Exception(f"There are no submodels in models of type {model_class}") + variant = model_variant.value if model_variant else None + result: AnyModel = model_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, variant=variant) # type: ignore + return result diff --git a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py new file mode 100644 index 0000000000..63dc3790f1 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for IP Adapter model loading in InvokeAI.""" + +import torch + +from pathlib import Path +from typing import Optional + +from invokeai.backend.ip_adapter.ip_adapter import build_ip_adapter +from invokeai.backend.model_manager import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelType, + SubModelType, +) +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.InvokeAI) +class IPAdapterInvokeAILoader(ModelLoader): + """Class to load IP Adapter diffusers models.""" + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("There are no submodels in an IP-Adapter model.") + model = build_ip_adapter( + ip_adapter_ckpt_path=model_path / "ip_adapter.bin", + device=torch.device("cpu"), + dtype=self._torch_dtype, + ) + return model + diff --git a/invokeai/backend/model_manager/load/model_loaders/lora.py b/invokeai/backend/model_manager/load/model_loaders/lora.py new file mode 100644 index 0000000000..4d19aadb7d --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/lora.py @@ -0,0 +1,76 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for LoRA model loading in InvokeAI.""" + + +from pathlib import Path +from typing import Optional, Tuple +from logging import Logger + +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase +from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.model_manager import ( + AnyModel, + AnyModelConfig, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelType, + SubModelType, +) +from invokeai.backend.model_manager.lora import LoRAModelRaw +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Diffusers) +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Lycoris) +class LoraLoader(ModelLoader): + """Class to load LoRA models.""" + + # We cheat a little bit to get access to the model base + def __init__( + self, + app_config: InvokeAIAppConfig, + logger: Logger, + ram_cache: ModelCacheBase[AnyModel], + convert_cache: ModelConvertCacheBase, + ): + """Initialize the loader.""" + super().__init__(app_config, logger, ram_cache, convert_cache) + self._model_base: Optional[BaseModelType] = None + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("There are no submodels in a LoRA model.") + model = LoRAModelRaw.from_checkpoint( + file_path=model_path, + dtype=self._torch_dtype, + base_model=self._model_base, + ) + return model + + # override + def _get_model_path( + self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None + ) -> Tuple[Path, AnyModelConfig, Optional[SubModelType]]: + self._model_base = config.base # cheating a little - setting this variable for later call to _load_model() + + model_base_path = self._app_config.models_path + model_path = model_base_path / config.path + + if config.format == ModelFormat.Diffusers: + for ext in ["safetensors", "bin"]: # return path to the safetensors file inside the folder + path = model_base_path / config.path / f"pytorch_lora_weights.{ext}" + if path.exists(): + model_path = path + break + + result = model_path.resolve(), config, submodel_type + return result + + diff --git a/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py new file mode 100644 index 0000000000..a963e8403b --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py @@ -0,0 +1,93 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for StableDiffusion model loading in InvokeAI.""" + + +from pathlib import Path +from typing import Optional + +from diffusers import StableDiffusionInpaintPipeline, StableDiffusionPipeline + +from invokeai.backend.model_manager import ( + AnyModel, + AnyModelConfig, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelType, + ModelVariantType, + SubModelType, +) +from invokeai.backend.model_manager.config import MainCheckpointConfig +from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ckpt_to_diffusers +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader + + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Diffusers) +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Checkpoint) +class StableDiffusionDiffusersModel(ModelLoader): + """Class to load main models.""" + + model_base_to_model_type = { + BaseModelType.StableDiffusion1: "FrozenCLIPEmbedder", + BaseModelType.StableDiffusion2: "FrozenOpenCLIPEmbedder", + BaseModelType.StableDiffusionXL: "SDXL", + BaseModelType.StableDiffusionXLRefiner: "SDXL-Refiner", + } + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not submodel_type is not None: + raise Exception("A submodel type must be provided when loading main pipelines.") + load_class = self._get_hf_load_class(model_path, submodel_type) + variant = model_variant.value if model_variant else None + model_path = model_path / submodel_type.value + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=self._torch_dtype, + variant=variant, + ) # type: ignore + return result + + def _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: + if config.format != ModelFormat.Checkpoint: + return False + elif ( + dest_path.exists() + and (dest_path / "model_index.json").stat().st_mtime >= (config.last_modified or 0.0) + and (dest_path / "model_index.json").stat().st_mtime >= model_path.stat().st_mtime + ): + return False + else: + return True + + def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + assert isinstance(config, MainCheckpointConfig) + variant = config.variant + base = config.base + pipeline_class = ( + StableDiffusionInpaintPipeline if variant == ModelVariantType.Inpaint else StableDiffusionPipeline + ) + + config_file = config.config + + self._logger.info(f"Converting {weights_path} to diffusers format") + convert_ckpt_to_diffusers( + weights_path, + output_path, + model_type=self.model_base_to_model_type[base], + model_version=base, + model_variant=variant, + original_config_file=self._app_config.root_path / config_file, + extract_ema=True, + scan_needed=True, + pipeline_class=pipeline_class, + from_safetensors=weights_path.suffix == ".safetensors", + precision=self._torch_dtype, + load_safety_checker=False, + ) + return output_path diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py index 6f21c3d090..7a35e53459 100644 --- a/invokeai/backend/model_manager/load/model_loaders/vae.py +++ b/invokeai/backend/model_manager/load/model_loaders/vae.py @@ -2,68 +2,54 @@ """Class for VAE model loading in InvokeAI.""" from pathlib import Path -from typing import Optional -import torch import safetensors -from omegaconf import OmegaConf, DictConfig -from invokeai.backend.util.devices import torch_dtype -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelFormat, ModelRepoVariant, ModelType, SubModelType -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader +import torch +from omegaconf import DictConfig, OmegaConf + +from invokeai.backend.model_manager import ( + AnyModelConfig, + BaseModelType, + ModelFormat, + ModelType, +) from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ldm_vae_to_diffusers +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from .generic_diffusers import GenericDiffusersLoader + @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Vae, format=ModelFormat.Diffusers) @AnyModelLoader.register(base=BaseModelType.StableDiffusion1, type=ModelType.Vae, format=ModelFormat.Checkpoint) @AnyModelLoader.register(base=BaseModelType.StableDiffusion2, type=ModelType.Vae, format=ModelFormat.Checkpoint) -class VaeDiffusersModel(ModelLoader): +class VaeLoader(GenericDiffusersLoader): """Class to load VAE models.""" - def _load_model( - self, - model_path: Path, - model_variant: Optional[ModelRepoVariant] = None, - submodel_type: Optional[SubModelType] = None, - ) -> AnyModel: - if submodel_type is not None: - raise Exception("There are no submodels in VAEs") - vae_class = self._get_hf_load_class(model_path) - variant = model_variant.value if model_variant else None - result: AnyModel = vae_class.from_pretrained( - model_path, torch_dtype=self._torch_dtype, variant=variant - ) # type: ignore - return result - def _needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool: - print(f'DEBUG: last_modified={config.last_modified}') - print(f'DEBUG: cache_path={(dest_path / "config.json").stat().st_mtime}') - print(f'DEBUG: model_path={model_path.stat().st_mtime}') if config.format != ModelFormat.Checkpoint: return False - elif dest_path.exists() \ - and (dest_path / "config.json").stat().st_mtime >= config.last_modified \ - and (dest_path / "config.json").stat().st_mtime >= model_path.stat().st_mtime: + elif ( + dest_path.exists() + and (dest_path / "config.json").stat().st_mtime >= (config.last_modified or 0.0) + and (dest_path / "config.json").stat().st_mtime >= model_path.stat().st_mtime + ): return False else: return True - def _convert_model(self, - config: AnyModelConfig, - weights_path: Path, - output_path: Path - ) -> Path: + def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + # TO DO: check whether sdxl VAE models convert. if config.base not in {BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2}: raise Exception(f"Vae conversion not supported for model type: {config.base}") else: - config_file = 'v1-inference.yaml' if config.base == BaseModelType.StableDiffusion1 else "v2-inference-v.yaml" + config_file = ( + "v1-inference.yaml" if config.base == BaseModelType.StableDiffusion1 else "v2-inference-v.yaml" + ) if weights_path.suffix == ".safetensors": checkpoint = safetensors.torch.load_file(weights_path, device="cpu") else: checkpoint = torch.load(weights_path, map_location="cpu") - dtype = torch_dtype() - # sometimes weights are hidden under "state_dict", and sometimes not if "state_dict" in checkpoint: checkpoint = checkpoint["state_dict"] @@ -71,13 +57,11 @@ class VaeDiffusersModel(ModelLoader): ckpt_config = OmegaConf.load(self._app_config.legacy_conf_path / config_file) assert isinstance(ckpt_config, DictConfig) - print(f'DEBUG: CONVERTIGN') vae_model = convert_ldm_vae_to_diffusers( checkpoint=checkpoint, vae_config=ckpt_config, image_size=512, ) - vae_model.to(dtype) # set precision appropriately - vae_model.save_pretrained(output_path, safe_serialization=True, torch_dtype=dtype) + vae_model.to(self._torch_dtype) # set precision appropriately + vae_model.save_pretrained(output_path, safe_serialization=True) return output_path - diff --git a/invokeai/backend/model_manager/load/model_util.py b/invokeai/backend/model_manager/load/model_util.py index 7c27e66472..404c88bbbc 100644 --- a/invokeai/backend/model_manager/load/model_util.py +++ b/invokeai/backend/model_manager/load/model_util.py @@ -8,10 +8,11 @@ from typing import Optional, Union import torch from diffusers import DiffusionPipeline +from invokeai.backend.model_manager.config import AnyModel from invokeai.backend.model_manager.onnx_runtime import IAIOnnxRuntimeModel -def calc_model_size_by_data(model: Union[DiffusionPipeline, torch.nn.Module, IAIOnnxRuntimeModel]) -> int: +def calc_model_size_by_data(model: AnyModel) -> int: """Get size of a model in memory in bytes.""" if isinstance(model, DiffusionPipeline): return _calc_pipeline_by_data(model) @@ -50,7 +51,7 @@ def calc_model_size_by_fs(model_path: Path, subfolder: Optional[str] = None, var """Estimate the size of a model on disk in bytes.""" if model_path.is_file(): return model_path.stat().st_size - + if subfolder is not None: model_path = model_path / subfolder diff --git a/invokeai/backend/model_manager/lora.py b/invokeai/backend/model_manager/lora.py new file mode 100644 index 0000000000..4c48de48ec --- /dev/null +++ b/invokeai/backend/model_manager/lora.py @@ -0,0 +1,620 @@ +# Copyright (c) 2024 The InvokeAI Development team +"""LoRA model support.""" + +import torch +from safetensors.torch import load_file +from pathlib import Path +from typing import Dict, Optional, Union, List, Tuple +from typing_extensions import Self +from invokeai.backend.model_manager import BaseModelType + +class LoRALayerBase: + # rank: Optional[int] + # alpha: Optional[float] + # bias: Optional[torch.Tensor] + # layer_key: str + + # @property + # def scale(self): + # return self.alpha / self.rank if (self.alpha and self.rank) else 1.0 + + def __init__( + self, + layer_key: str, + values: Dict[str, torch.Tensor], + ): + if "alpha" in values: + self.alpha = values["alpha"].item() + else: + self.alpha = None + + if "bias_indices" in values and "bias_values" in values and "bias_size" in values: + self.bias: Optional[torch.Tensor] = torch.sparse_coo_tensor( + values["bias_indices"], + values["bias_values"], + tuple(values["bias_size"]), + ) + + else: + self.bias = None + + self.rank = None # set in layer implementation + self.layer_key = layer_key + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + raise NotImplementedError() + + def calc_size(self) -> int: + model_size = 0 + for val in [self.bias]: + if val is not None: + model_size += val.nelement() * val.element_size() + return model_size + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> None: + if self.bias is not None: + self.bias = self.bias.to(device=device, dtype=dtype) + + +# TODO: find and debug lora/locon with bias +class LoRALayer(LoRALayerBase): + # up: torch.Tensor + # mid: Optional[torch.Tensor] + # down: torch.Tensor + + def __init__( + self, + layer_key: str, + values: Dict[str, torch.Tensor], + ): + super().__init__(layer_key, values) + + self.up = values["lora_up.weight"] + self.down = values["lora_down.weight"] + if "lora_mid.weight" in values: + self.mid: Optional[torch.Tensor] = values["lora_mid.weight"] + else: + self.mid = None + + self.rank = self.down.shape[0] + + def get_weight(self, orig_weight: torch.Tensor): + if self.mid is not None: + up = self.up.reshape(self.up.shape[0], self.up.shape[1]) + down = self.down.reshape(self.down.shape[0], self.down.shape[1]) + weight = torch.einsum("m n w h, i m, n j -> i j w h", self.mid, up, down) + else: + weight = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1) + + return weight + + def calc_size(self) -> int: + model_size = super().calc_size() + for val in [self.up, self.mid, self.down]: + if val is not None: + model_size += val.nelement() * val.element_size() + return model_size + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> None: + super().to(device=device, dtype=dtype) + + self.up = self.up.to(device=device, dtype=dtype) + self.down = self.down.to(device=device, dtype=dtype) + + if self.mid is not None: + self.mid = self.mid.to(device=device, dtype=dtype) + + +class LoHALayer(LoRALayerBase): + # w1_a: torch.Tensor + # w1_b: torch.Tensor + # w2_a: torch.Tensor + # w2_b: torch.Tensor + # t1: Optional[torch.Tensor] = None + # t2: Optional[torch.Tensor] = None + + def __init__( + self, + layer_key: str, + values: Dict[str, torch.Tensor] + ): + super().__init__(layer_key, values) + + self.w1_a = values["hada_w1_a"] + self.w1_b = values["hada_w1_b"] + self.w2_a = values["hada_w2_a"] + self.w2_b = values["hada_w2_b"] + + if "hada_t1" in values: + self.t1: Optional[torch.Tensor] = values["hada_t1"] + else: + self.t1 = None + + if "hada_t2" in values: + self.t2: Optional[torch.Tensor] = values["hada_t2"] + else: + self.t2 = None + + self.rank = self.w1_b.shape[0] + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + if self.t1 is None: + weight: torch.Tensor = (self.w1_a @ self.w1_b) * (self.w2_a @ self.w2_b) + + else: + rebuild1 = torch.einsum("i j k l, j r, i p -> p r k l", self.t1, self.w1_b, self.w1_a) + rebuild2 = torch.einsum("i j k l, j r, i p -> p r k l", self.t2, self.w2_b, self.w2_a) + weight = rebuild1 * rebuild2 + + return weight + + def calc_size(self) -> int: + model_size = super().calc_size() + for val in [self.w1_a, self.w1_b, self.w2_a, self.w2_b, self.t1, self.t2]: + if val is not None: + model_size += val.nelement() * val.element_size() + return model_size + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> None: + super().to(device=device, dtype=dtype) + + self.w1_a = self.w1_a.to(device=device, dtype=dtype) + self.w1_b = self.w1_b.to(device=device, dtype=dtype) + if self.t1 is not None: + self.t1 = self.t1.to(device=device, dtype=dtype) + + self.w2_a = self.w2_a.to(device=device, dtype=dtype) + self.w2_b = self.w2_b.to(device=device, dtype=dtype) + if self.t2 is not None: + self.t2 = self.t2.to(device=device, dtype=dtype) + + +class LoKRLayer(LoRALayerBase): + # w1: Optional[torch.Tensor] = None + # w1_a: Optional[torch.Tensor] = None + # w1_b: Optional[torch.Tensor] = None + # w2: Optional[torch.Tensor] = None + # w2_a: Optional[torch.Tensor] = None + # w2_b: Optional[torch.Tensor] = None + # t2: Optional[torch.Tensor] = None + + def __init__( + self, + layer_key: str, + values: Dict[str, torch.Tensor], + ): + super().__init__(layer_key, values) + + if "lokr_w1" in values: + self.w1: Optional[torch.Tensor] = values["lokr_w1"] + self.w1_a = None + self.w1_b = None + else: + self.w1 = None + self.w1_a = values["lokr_w1_a"] + self.w1_b = values["lokr_w1_b"] + + if "lokr_w2" in values: + self.w2: Optional[torch.Tensor] = values["lokr_w2"] + self.w2_a = None + self.w2_b = None + else: + self.w2 = None + self.w2_a = values["lokr_w2_a"] + self.w2_b = values["lokr_w2_b"] + + if "lokr_t2" in values: + self.t2: Optional[torch.Tensor] = values["lokr_t2"] + else: + self.t2 = None + + if "lokr_w1_b" in values: + self.rank = values["lokr_w1_b"].shape[0] + elif "lokr_w2_b" in values: + self.rank = values["lokr_w2_b"].shape[0] + else: + self.rank = None # unscaled + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + w1: Optional[torch.Tensor] = self.w1 + if w1 is None: + assert self.w1_a is not None + assert self.w1_b is not None + w1 = self.w1_a @ self.w1_b + + w2 = self.w2 + if w2 is None: + if self.t2 is None: + assert self.w2_a is not None + assert self.w2_b is not None + w2 = self.w2_a @ self.w2_b + else: + w2 = torch.einsum("i j k l, i p, j r -> p r k l", self.t2, self.w2_a, self.w2_b) + + if len(w2.shape) == 4: + w1 = w1.unsqueeze(2).unsqueeze(2) + w2 = w2.contiguous() + assert w1 is not None + assert w2 is not None + weight = torch.kron(w1, w2) + + return weight + + def calc_size(self) -> int: + model_size = super().calc_size() + for val in [self.w1, self.w1_a, self.w1_b, self.w2, self.w2_a, self.w2_b, self.t2]: + if val is not None: + model_size += val.nelement() * val.element_size() + return model_size + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> None: + super().to(device=device, dtype=dtype) + + if self.w1 is not None: + self.w1 = self.w1.to(device=device, dtype=dtype) + else: + assert self.w1_a is not None + assert self.w1_b is not None + self.w1_a = self.w1_a.to(device=device, dtype=dtype) + self.w1_b = self.w1_b.to(device=device, dtype=dtype) + + if self.w2 is not None: + self.w2 = self.w2.to(device=device, dtype=dtype) + else: + assert self.w2_a is not None + assert self.w2_b is not None + self.w2_a = self.w2_a.to(device=device, dtype=dtype) + self.w2_b = self.w2_b.to(device=device, dtype=dtype) + + if self.t2 is not None: + self.t2 = self.t2.to(device=device, dtype=dtype) + + +class FullLayer(LoRALayerBase): + # weight: torch.Tensor + + def __init__( + self, + layer_key: str, + values: Dict[str, torch.Tensor], + ): + super().__init__(layer_key, values) + + self.weight = values["diff"] + + if len(values.keys()) > 1: + _keys = list(values.keys()) + _keys.remove("diff") + raise NotImplementedError(f"Unexpected keys in lora diff layer: {_keys}") + + self.rank = None # unscaled + + def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + return self.weight + + def calc_size(self) -> int: + model_size = super().calc_size() + model_size += self.weight.nelement() * self.weight.element_size() + return model_size + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ): + super().to(device=device, dtype=dtype) + + self.weight = self.weight.to(device=device, dtype=dtype) + + +class IA3Layer(LoRALayerBase): + # weight: torch.Tensor + # on_input: torch.Tensor + + def __init__( + self, + layer_key: str, + values: Dict[str, torch.Tensor], + ): + super().__init__(layer_key, values) + + self.weight = values["weight"] + self.on_input = values["on_input"] + + self.rank = None # unscaled + + def get_weight(self, orig_weight: torch.Tensor): + weight = self.weight + if not self.on_input: + weight = weight.reshape(-1, 1) + return orig_weight * weight + + def calc_size(self) -> int: + model_size = super().calc_size() + model_size += self.weight.nelement() * self.weight.element_size() + model_size += self.on_input.nelement() * self.on_input.element_size() + return model_size + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ): + super().to(device=device, dtype=dtype) + + self.weight = self.weight.to(device=device, dtype=dtype) + self.on_input = self.on_input.to(device=device, dtype=dtype) + +AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer] + +# TODO: rename all methods used in model logic with Info postfix and remove here Raw postfix +class LoRAModelRaw: # (torch.nn.Module): + _name: str + layers: Dict[str, AnyLoRALayer] + + def __init__( + self, + name: str, + layers: Dict[str, AnyLoRALayer], + ): + self._name = name + self.layers = layers + + @property + def name(self) -> str: + return self._name + + def to( + self, + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> None: + # TODO: try revert if exception? + for _key, layer in self.layers.items(): + layer.to(device=device, dtype=dtype) + + def calc_size(self) -> int: + model_size = 0 + for _, layer in self.layers.items(): + model_size += layer.calc_size() + return model_size + + @classmethod + def _convert_sdxl_keys_to_diffusers_format(cls, state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """Convert the keys of an SDXL LoRA state_dict to diffusers format. + + The input state_dict can be in either Stability AI format or diffusers format. If the state_dict is already in + diffusers format, then this function will have no effect. + + This function is adapted from: + https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L385-L409 + + Args: + state_dict (Dict[str, Tensor]): The SDXL LoRA state_dict. + + Raises: + ValueError: If state_dict contains an unrecognized key, or not all keys could be converted. + + Returns: + Dict[str, Tensor]: The diffusers-format state_dict. + """ + converted_count = 0 # The number of Stability AI keys converted to diffusers format. + not_converted_count = 0 # The number of keys that were not converted. + + # Get a sorted list of Stability AI UNet keys so that we can efficiently search for keys with matching prefixes. + # For example, we want to efficiently find `input_blocks_4_1` in the list when searching for + # `input_blocks_4_1_proj_in`. + stability_unet_keys = list(SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP) + stability_unet_keys.sort() + + new_state_dict = {} + for full_key, value in state_dict.items(): + if full_key.startswith("lora_unet_"): + search_key = full_key.replace("lora_unet_", "") + # Use bisect to find the key in stability_unet_keys that *may* match the search_key's prefix. + position = bisect.bisect_right(stability_unet_keys, search_key) + map_key = stability_unet_keys[position - 1] + # Now, check if the map_key *actually* matches the search_key. + if search_key.startswith(map_key): + new_key = full_key.replace(map_key, SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP[map_key]) + new_state_dict[new_key] = value + converted_count += 1 + else: + new_state_dict[full_key] = value + not_converted_count += 1 + elif full_key.startswith("lora_te1_") or full_key.startswith("lora_te2_"): + # The CLIP text encoders have the same keys in both Stability AI and diffusers formats. + new_state_dict[full_key] = value + continue + else: + raise ValueError(f"Unrecognized SDXL LoRA key prefix: '{full_key}'.") + + if converted_count > 0 and not_converted_count > 0: + raise ValueError( + f"The SDXL LoRA could only be partially converted to diffusers format. converted={converted_count}," + f" not_converted={not_converted_count}" + ) + + return new_state_dict + + @classmethod + def from_checkpoint( + cls, + file_path: Union[str, Path], + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + base_model: Optional[BaseModelType] = None, + ) -> Self: + device = device or torch.device("cpu") + dtype = dtype or torch.float32 + + if isinstance(file_path, str): + file_path = Path(file_path) + + model = cls( + name=file_path.stem, # TODO: + layers={}, + ) + + if file_path.suffix == ".safetensors": + state_dict = load_file(file_path.absolute().as_posix(), device="cpu") + else: + state_dict = torch.load(file_path, map_location="cpu") + + state_dict = cls._group_state(state_dict) + + if base_model == BaseModelType.StableDiffusionXL: + state_dict = cls._convert_sdxl_keys_to_diffusers_format(state_dict) + + for layer_key, values in state_dict.items(): + # lora and locon + if "lora_down.weight" in values: + layer: AnyLoRALayer = LoRALayer(layer_key, values) + + # loha + elif "hada_w1_b" in values: + layer = LoHALayer(layer_key, values) + + # lokr + elif "lokr_w1_b" in values or "lokr_w1" in values: + layer = LoKRLayer(layer_key, values) + + # diff + elif "diff" in values: + layer = FullLayer(layer_key, values) + + # ia3 + elif "weight" in values and "on_input" in values: + layer = IA3Layer(layer_key, values) + + else: + print(f">> Encountered unknown lora layer module in {model.name}: {layer_key} - {list(values.keys())}") + raise Exception("Unknown lora format!") + + # lower memory consumption by removing already parsed layer values + state_dict[layer_key].clear() + + layer.to(device=device, dtype=dtype) + model.layers[layer_key] = layer + + return model + + @staticmethod + def _group_state(state_dict: Dict[str, torch.Tensor]) -> Dict[str, Dict[str, torch.Tensor]]: + state_dict_groupped: Dict[str, Dict[str, torch.Tensor]] = {} + + for key, value in state_dict.items(): + stem, leaf = key.split(".", 1) + if stem not in state_dict_groupped: + state_dict_groupped[stem] = {} + state_dict_groupped[stem][leaf] = value + + return state_dict_groupped + + +# code from +# https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L15C1-L97C32 +def make_sdxl_unet_conversion_map() -> List[Tuple[str,str]]: + """Create a dict mapping state_dict keys from Stability AI SDXL format to diffusers SDXL format.""" + unet_conversion_map_layer = [] + + for i in range(3): # num_blocks is 3 in sdxl + # loop over downblocks/upblocks + for j in range(2): + # loop over resnets/attentions for downblocks + hf_down_res_prefix = f"down_blocks.{i}.resnets.{j}." + sd_down_res_prefix = f"input_blocks.{3*i + j + 1}.0." + unet_conversion_map_layer.append((sd_down_res_prefix, hf_down_res_prefix)) + + if i < 3: + # no attention layers in down_blocks.3 + hf_down_atn_prefix = f"down_blocks.{i}.attentions.{j}." + sd_down_atn_prefix = f"input_blocks.{3*i + j + 1}.1." + unet_conversion_map_layer.append((sd_down_atn_prefix, hf_down_atn_prefix)) + + for j in range(3): + # loop over resnets/attentions for upblocks + hf_up_res_prefix = f"up_blocks.{i}.resnets.{j}." + sd_up_res_prefix = f"output_blocks.{3*i + j}.0." + unet_conversion_map_layer.append((sd_up_res_prefix, hf_up_res_prefix)) + + # if i > 0: commentout for sdxl + # no attention layers in up_blocks.0 + hf_up_atn_prefix = f"up_blocks.{i}.attentions.{j}." + sd_up_atn_prefix = f"output_blocks.{3*i + j}.1." + unet_conversion_map_layer.append((sd_up_atn_prefix, hf_up_atn_prefix)) + + if i < 3: + # no downsample in down_blocks.3 + hf_downsample_prefix = f"down_blocks.{i}.downsamplers.0.conv." + sd_downsample_prefix = f"input_blocks.{3*(i+1)}.0.op." + unet_conversion_map_layer.append((sd_downsample_prefix, hf_downsample_prefix)) + + # no upsample in up_blocks.3 + hf_upsample_prefix = f"up_blocks.{i}.upsamplers.0." + sd_upsample_prefix = f"output_blocks.{3*i + 2}.{2}." # change for sdxl + unet_conversion_map_layer.append((sd_upsample_prefix, hf_upsample_prefix)) + + hf_mid_atn_prefix = "mid_block.attentions.0." + sd_mid_atn_prefix = "middle_block.1." + unet_conversion_map_layer.append((sd_mid_atn_prefix, hf_mid_atn_prefix)) + + for j in range(2): + hf_mid_res_prefix = f"mid_block.resnets.{j}." + sd_mid_res_prefix = f"middle_block.{2*j}." + unet_conversion_map_layer.append((sd_mid_res_prefix, hf_mid_res_prefix)) + + unet_conversion_map_resnet = [ + # (stable-diffusion, HF Diffusers) + ("in_layers.0.", "norm1."), + ("in_layers.2.", "conv1."), + ("out_layers.0.", "norm2."), + ("out_layers.3.", "conv2."), + ("emb_layers.1.", "time_emb_proj."), + ("skip_connection.", "conv_shortcut."), + ] + + unet_conversion_map = [] + for sd, hf in unet_conversion_map_layer: + if "resnets" in hf: + for sd_res, hf_res in unet_conversion_map_resnet: + unet_conversion_map.append((sd + sd_res, hf + hf_res)) + else: + unet_conversion_map.append((sd, hf)) + + for j in range(2): + hf_time_embed_prefix = f"time_embedding.linear_{j+1}." + sd_time_embed_prefix = f"time_embed.{j*2}." + unet_conversion_map.append((sd_time_embed_prefix, hf_time_embed_prefix)) + + for j in range(2): + hf_label_embed_prefix = f"add_embedding.linear_{j+1}." + sd_label_embed_prefix = f"label_emb.0.{j*2}." + unet_conversion_map.append((sd_label_embed_prefix, hf_label_embed_prefix)) + + unet_conversion_map.append(("input_blocks.0.0.", "conv_in.")) + unet_conversion_map.append(("out.0.", "conv_norm_out.")) + unet_conversion_map.append(("out.2.", "conv_out.")) + + return unet_conversion_map + + +SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP = { + sd.rstrip(".").replace(".", "_"): hf.rstrip(".").replace(".", "_") for sd, hf in make_sdxl_unet_conversion_map() +} diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index 9fd118b782..64a20a2092 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -29,8 +29,12 @@ CkptType = Dict[str, Any] LEGACY_CONFIGS: Dict[BaseModelType, Dict[ModelVariantType, Union[str, Dict[SchedulerPredictionType, str]]]] = { BaseModelType.StableDiffusion1: { - ModelVariantType.Normal: "v1-inference.yaml", + ModelVariantType.Normal: { + SchedulerPredictionType.Epsilon: "v1-inference.yaml", + SchedulerPredictionType.VPrediction: "v1-inference-v.yaml", + }, ModelVariantType.Inpaint: "v1-inpainting-inference.yaml", + ModelVariantType.Depth: "v2-midas-inference.yaml", }, BaseModelType.StableDiffusion2: { ModelVariantType.Normal: { diff --git a/invokeai/backend/util/__init__.py b/invokeai/backend/util/__init__.py index 0164dffe30..7b48f0364e 100644 --- a/invokeai/backend/util/__init__.py +++ b/invokeai/backend/util/__init__.py @@ -12,14 +12,22 @@ from .devices import ( # noqa: F401 torch_dtype, ) from .logging import InvokeAILogger -from .util import ( # TO DO: Clean this up; remove the unused symbols +from .util import ( # TO DO: Clean this up; remove the unused symbols GIG, Chdir, ask_user, # noqa directory_size, download_with_resume, - instantiate_from_config, # noqa + instantiate_from_config, # noqa url_attachment_name, # noqa - ) +) -__all__ = ["GIG", "directory_size","Chdir", "download_with_resume", "InvokeAILogger", "choose_precision", "choose_torch_device"] +__all__ = [ + "GIG", + "directory_size", + "Chdir", + "download_with_resume", + "InvokeAILogger", + "choose_precision", + "choose_torch_device", +] diff --git a/invokeai/backend/util/devices.py b/invokeai/backend/util/devices.py index ad3f4e139a..a787f9b6f4 100644 --- a/invokeai/backend/util/devices.py +++ b/invokeai/backend/util/devices.py @@ -1,7 +1,7 @@ from __future__ import annotations from contextlib import nullcontext -from typing import Union, Optional +from typing import Optional, Union import torch from torch import autocast diff --git a/invokeai/backend/util/util.py b/invokeai/backend/util/util.py index 6589aa7278..ae376b41b2 100644 --- a/invokeai/backend/util/util.py +++ b/invokeai/backend/util/util.py @@ -27,6 +27,7 @@ from .devices import torch_dtype # actual size of a gig GIG = 1073741824 + def directory_size(directory: Path) -> int: """ Return the aggregate size of all files in a directory (bytes). @@ -39,6 +40,7 @@ def directory_size(directory: Path) -> int: sum += Path(root, d).stat().st_size return sum + def log_txt_as_img(wh, xc, size=10): # wh a tuple of (width, height) # xc a list of captions to plot From 0d3addc69bd333058b77feb1d9b9be23c1ce10e6 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 4 Feb 2024 23:18:00 -0500 Subject: [PATCH 114/411] added textual inversion and lora loaders --- .../model_install/model_install_default.py | 5 + .../{model_manager => embeddings}/lora.py | 35 +- invokeai/backend/embeddings/model_patcher.py | 586 ++++++++++++++++++ invokeai/backend/model_management/lora.py | 5 +- invokeai/backend/model_manager/config.py | 4 +- .../model_manager/load/load_default.py | 11 +- .../model_manager/load/memory_snapshot.py | 2 +- .../load/model_cache/__init__.py | 2 - .../load/model_loaders/controlnet.py | 4 +- .../load/model_loaders/generic_diffusers.py | 1 + .../load/model_loaders/ip_adapter.py | 6 +- .../model_manager/load/model_loaders/lora.py | 18 +- .../load/model_loaders/textual_inversion.py | 55 ++ .../model_manager/load/model_loaders/vae.py | 1 + .../backend/model_manager/load/model_util.py | 4 +- .../{model_manager => onnx}/onnx_runtime.py | 0 16 files changed, 701 insertions(+), 38 deletions(-) rename invokeai/backend/{model_manager => embeddings}/lora.py (96%) create mode 100644 invokeai/backend/embeddings/model_patcher.py create mode 100644 invokeai/backend/model_manager/load/model_loaders/textual_inversion.py rename invokeai/backend/{model_manager => onnx}/onnx_runtime.py (100%) diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 2b2294bfce..1c188b300d 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -178,6 +178,11 @@ class ModelInstallService(ModelInstallServiceBase): ) def import_model(self, source: ModelSource, config: Optional[Dict[str, Any]] = None) -> ModelInstallJob: # noqa D102 + similar_jobs = [x for x in self.list_jobs() if x.source == source and not x.in_terminal_state] + if similar_jobs: + self._logger.warning(f"There is already an active install job for {source}. Not enqueuing.") + return similar_jobs[0] + if isinstance(source, LocalModelSource): install_job = self._import_local_model(source, config) self._install_queue.put(install_job) # synchronously install diff --git a/invokeai/backend/model_manager/lora.py b/invokeai/backend/embeddings/lora.py similarity index 96% rename from invokeai/backend/model_manager/lora.py rename to invokeai/backend/embeddings/lora.py index 4c48de48ec..9a59a97708 100644 --- a/invokeai/backend/model_manager/lora.py +++ b/invokeai/backend/embeddings/lora.py @@ -1,13 +1,17 @@ # Copyright (c) 2024 The InvokeAI Development team """LoRA model support.""" +import bisect +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Union + import torch from safetensors.torch import load_file -from pathlib import Path -from typing import Dict, Optional, Union, List, Tuple from typing_extensions import Self + from invokeai.backend.model_manager import BaseModelType + class LoRALayerBase: # rank: Optional[int] # alpha: Optional[float] @@ -41,7 +45,7 @@ class LoRALayerBase: self.rank = None # set in layer implementation self.layer_key = layer_key - def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: raise NotImplementedError() def calc_size(self) -> int: @@ -82,7 +86,7 @@ class LoRALayer(LoRALayerBase): self.rank = self.down.shape[0] - def get_weight(self, orig_weight: torch.Tensor): + def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: if self.mid is not None: up = self.up.reshape(self.up.shape[0], self.up.shape[1]) down = self.down.reshape(self.down.shape[0], self.down.shape[1]) @@ -121,11 +125,7 @@ class LoHALayer(LoRALayerBase): # t1: Optional[torch.Tensor] = None # t2: Optional[torch.Tensor] = None - def __init__( - self, - layer_key: str, - values: Dict[str, torch.Tensor] - ): + def __init__(self, layer_key: str, values: Dict[str, torch.Tensor]): super().__init__(layer_key, values) self.w1_a = values["hada_w1_a"] @@ -145,7 +145,7 @@ class LoHALayer(LoRALayerBase): self.rank = self.w1_b.shape[0] - def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: if self.t1 is None: weight: torch.Tensor = (self.w1_a @ self.w1_b) * (self.w2_a @ self.w2_b) @@ -227,7 +227,7 @@ class LoKRLayer(LoRALayerBase): else: self.rank = None # unscaled - def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: w1: Optional[torch.Tensor] = self.w1 if w1 is None: assert self.w1_a is not None @@ -305,7 +305,7 @@ class FullLayer(LoRALayerBase): self.rank = None # unscaled - def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor: + def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: return self.weight def calc_size(self) -> int: @@ -330,7 +330,7 @@ class IA3Layer(LoRALayerBase): def __init__( self, layer_key: str, - values: Dict[str, torch.Tensor], + values: Dict[str, torch.Tensor], ): super().__init__(layer_key, values) @@ -339,10 +339,11 @@ class IA3Layer(LoRALayerBase): self.rank = None # unscaled - def get_weight(self, orig_weight: torch.Tensor): + def get_weight(self, orig_weight: Optional[torch.Tensor]) -> torch.Tensor: weight = self.weight if not self.on_input: weight = weight.reshape(-1, 1) + assert orig_weight is not None return orig_weight * weight def calc_size(self) -> int: @@ -361,8 +362,10 @@ class IA3Layer(LoRALayerBase): self.weight = self.weight.to(device=device, dtype=dtype) self.on_input = self.on_input.to(device=device, dtype=dtype) + AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer] - + + # TODO: rename all methods used in model logic with Info postfix and remove here Raw postfix class LoRAModelRaw: # (torch.nn.Module): _name: str @@ -530,7 +533,7 @@ class LoRAModelRaw: # (torch.nn.Module): # code from # https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L15C1-L97C32 -def make_sdxl_unet_conversion_map() -> List[Tuple[str,str]]: +def make_sdxl_unet_conversion_map() -> List[Tuple[str, str]]: """Create a dict mapping state_dict keys from Stability AI SDXL format to diffusers SDXL format.""" unet_conversion_map_layer = [] diff --git a/invokeai/backend/embeddings/model_patcher.py b/invokeai/backend/embeddings/model_patcher.py new file mode 100644 index 0000000000..6d73235197 --- /dev/null +++ b/invokeai/backend/embeddings/model_patcher.py @@ -0,0 +1,586 @@ +# Copyright (c) 2024 Ryan Dick, Lincoln D. Stein, and the InvokeAI Development Team +"""These classes implement model patching with LoRAs and Textual Inversions.""" +from __future__ import annotations + +import pickle +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Dict, Generator, List, Optional, Tuple, Union + +import numpy as np +import torch +from compel.embeddings_provider import BaseTextualInversionManager +from diffusers import ModelMixin, OnnxRuntimeModel, UNet2DConditionModel +from safetensors.torch import load_file +from transformers import CLIPTextModel, CLIPTokenizer +from typing_extensions import Self + +from invokeai.app.shared.models import FreeUConfig +from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init +from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel + +from .lora import LoRAModelRaw + +""" +loras = [ + (lora_model1, 0.7), + (lora_model2, 0.4), +] +with LoRAHelper.apply_lora_unet(unet, loras): + # unet with applied loras +# unmodified unet + +""" + + +# TODO: rename smth like ModelPatcher and add TI method? +class ModelPatcher: + @staticmethod + def _resolve_lora_key(model: torch.nn.Module, lora_key: str, prefix: str) -> Tuple[str, torch.nn.Module]: + assert "." not in lora_key + + if not lora_key.startswith(prefix): + raise Exception(f"lora_key with invalid prefix: {lora_key}, {prefix}") + + module = model + module_key = "" + key_parts = lora_key[len(prefix) :].split("_") + + submodule_name = key_parts.pop(0) + + while len(key_parts) > 0: + try: + module = module.get_submodule(submodule_name) + module_key += "." + submodule_name + submodule_name = key_parts.pop(0) + except Exception: + submodule_name += "_" + key_parts.pop(0) + + module = module.get_submodule(submodule_name) + module_key = (module_key + "." + submodule_name).lstrip(".") + + return (module_key, module) + + @classmethod + @contextmanager + def apply_lora_unet( + cls, + unet: UNet2DConditionModel, + loras: List[Tuple[LoRAModelRaw, float]], + ) -> Generator[None, None, None]: + with cls.apply_lora(unet, loras, "lora_unet_"): + yield + + @classmethod + @contextmanager + def apply_lora_text_encoder( + cls, + text_encoder: CLIPTextModel, + loras: List[Tuple[LoRAModelRaw, float]], + ): + with cls.apply_lora(text_encoder, loras, "lora_te_"): + yield + + @classmethod + @contextmanager + def apply_sdxl_lora_text_encoder( + cls, + text_encoder: CLIPTextModel, + loras: List[Tuple[LoRAModelRaw, float]], + ): + with cls.apply_lora(text_encoder, loras, "lora_te1_"): + yield + + @classmethod + @contextmanager + def apply_sdxl_lora_text_encoder2( + cls, + text_encoder: CLIPTextModel, + loras: List[Tuple[LoRAModelRaw, float]], + ): + with cls.apply_lora(text_encoder, loras, "lora_te2_"): + yield + + @classmethod + @contextmanager + def apply_lora( + cls, + model: Union[torch.nn.Module, ModelMixin, UNet2DConditionModel], + loras: List[Tuple[LoRAModelRaw, float]], + prefix: str, + ) -> Generator[None, None, None]: + original_weights = {} + try: + with torch.no_grad(): + for lora, lora_weight in loras: + # assert lora.device.type == "cpu" + for layer_key, layer in lora.layers.items(): + if not layer_key.startswith(prefix): + continue + + # TODO(ryand): A non-negligible amount of time is currently spent resolving LoRA keys. This + # should be improved in the following ways: + # 1. The key mapping could be more-efficiently pre-computed. This would save time every time a + # LoRA model is applied. + # 2. From an API perspective, there's no reason that the `ModelPatcher` should be aware of the + # intricacies of Stable Diffusion key resolution. It should just expect the input LoRA + # weights to have valid keys. + module_key, module = cls._resolve_lora_key(model, layer_key, prefix) + + # All of the LoRA weight calculations will be done on the same device as the module weight. + # (Performance will be best if this is a CUDA device.) + device = module.weight.device + dtype = module.weight.dtype + + if module_key not in original_weights: + original_weights[module_key] = module.weight.detach().to(device="cpu", copy=True) + + layer_scale = layer.alpha / layer.rank if (layer.alpha and layer.rank) else 1.0 + + # We intentionally move to the target device first, then cast. Experimentally, this was found to + # be significantly faster for 16-bit CPU tensors being moved to a CUDA device than doing the + # same thing in a single call to '.to(...)'. + layer.to(device=device) + layer.to(dtype=torch.float32) + # TODO(ryand): Using torch.autocast(...) over explicit casting may offer a speed benefit on CUDA + # devices here. Experimentally, it was found to be very slow on CPU. More investigation needed. + layer_weight = layer.get_weight(module.weight) * (lora_weight * layer_scale) + layer.to(device=torch.device("cpu")) + + assert isinstance(layer_weight, torch.Tensor) # mypy thinks layer_weight is a float|Any ??! + if module.weight.shape != layer_weight.shape: + # TODO: debug on lycoris + assert hasattr(layer_weight, "reshape") + layer_weight = layer_weight.reshape(module.weight.shape) + + assert isinstance(layer_weight, torch.Tensor) # mypy thinks layer_weight is a float|Any ??! + module.weight += layer_weight.to(dtype=dtype) + + yield # wait for context manager exit + + finally: + assert hasattr(model, "get_submodule") # mypy not picking up fact that torch.nn.Module has get_submodule() + with torch.no_grad(): + for module_key, weight in original_weights.items(): + model.get_submodule(module_key).weight.copy_(weight) + + @classmethod + @contextmanager + def apply_ti( + cls, + tokenizer: CLIPTokenizer, + text_encoder: CLIPTextModel, + ti_list: List[Tuple[str, TextualInversionModel]], + ) -> Generator[Tuple[CLIPTokenizer, TextualInversionManager], None, None]: + init_tokens_count = None + new_tokens_added = None + + # TODO: This is required since Transformers 4.32 see + # https://github.com/huggingface/transformers/pull/25088 + # More information by NVIDIA: + # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html#requirements-tc + # This value might need to be changed in the future and take the GPUs model into account as there seem + # to be ideal values for different GPUS. This value is temporary! + # For references to the current discussion please see https://github.com/invoke-ai/InvokeAI/pull/4817 + pad_to_multiple_of = 8 + + try: + # HACK: The CLIPTokenizer API does not include a way to remove tokens after calling add_tokens(...). As a + # workaround, we create a full copy of `tokenizer` so that its original behavior can be restored after + # exiting this `apply_ti(...)` context manager. + # + # In a previous implementation, the deep copy was obtained with `ti_tokenizer = copy.deepcopy(tokenizer)`, + # but a pickle roundtrip was found to be much faster (1 sec vs. 0.05 secs). + ti_tokenizer = pickle.loads(pickle.dumps(tokenizer)) + ti_manager = TextualInversionManager(ti_tokenizer) + init_tokens_count = text_encoder.resize_token_embeddings(None, pad_to_multiple_of).num_embeddings + + def _get_trigger(ti_name: str, index: int) -> str: + trigger = ti_name + if index > 0: + trigger += f"-!pad-{i}" + return f"<{trigger}>" + + def _get_ti_embedding(model_embeddings: torch.nn.Module, ti: TextualInversionModel) -> torch.Tensor: + # for SDXL models, select the embedding that matches the text encoder's dimensions + if ti.embedding_2 is not None: + return ( + ti.embedding_2 + if ti.embedding_2.shape[1] == model_embeddings.weight.data[0].shape[0] + else ti.embedding + ) + else: + return ti.embedding + + # modify tokenizer + new_tokens_added = 0 + for ti_name, ti in ti_list: + ti_embedding = _get_ti_embedding(text_encoder.get_input_embeddings(), ti) + + for i in range(ti_embedding.shape[0]): + new_tokens_added += ti_tokenizer.add_tokens(_get_trigger(ti_name, i)) + + # Modify text_encoder. + # resize_token_embeddings(...) constructs a new torch.nn.Embedding internally. Initializing the weights of + # this embedding is slow and unnecessary, so we wrap this step in skip_torch_weight_init() to save some + # time. + with skip_torch_weight_init(): + text_encoder.resize_token_embeddings(init_tokens_count + new_tokens_added, pad_to_multiple_of) + model_embeddings = text_encoder.get_input_embeddings() + + for ti_name, ti in ti_list: + ti_embedding = _get_ti_embedding(text_encoder.get_input_embeddings(), ti) + + ti_tokens = [] + for i in range(ti_embedding.shape[0]): + embedding = ti_embedding[i] + trigger = _get_trigger(ti_name, i) + + token_id = ti_tokenizer.convert_tokens_to_ids(trigger) + if token_id == ti_tokenizer.unk_token_id: + raise RuntimeError(f"Unable to find token id for token '{trigger}'") + + if model_embeddings.weight.data[token_id].shape != embedding.shape: + raise ValueError( + f"Cannot load embedding for {trigger}. It was trained on a model with token dimension" + f" {embedding.shape[0]}, but the current model has token dimension" + f" {model_embeddings.weight.data[token_id].shape[0]}." + ) + + model_embeddings.weight.data[token_id] = embedding.to( + device=text_encoder.device, dtype=text_encoder.dtype + ) + ti_tokens.append(token_id) + + if len(ti_tokens) > 1: + ti_manager.pad_tokens[ti_tokens[0]] = ti_tokens[1:] + + yield ti_tokenizer, ti_manager + + finally: + if init_tokens_count and new_tokens_added: + text_encoder.resize_token_embeddings(init_tokens_count, pad_to_multiple_of) + + @classmethod + @contextmanager + def apply_clip_skip( + cls, + text_encoder: CLIPTextModel, + clip_skip: int, + ) -> Generator[None, None, None]: + skipped_layers = [] + try: + for _i in range(clip_skip): + skipped_layers.append(text_encoder.text_model.encoder.layers.pop(-1)) + + yield + + finally: + while len(skipped_layers) > 0: + text_encoder.text_model.encoder.layers.append(skipped_layers.pop()) + + @classmethod + @contextmanager + def apply_freeu( + cls, + unet: UNet2DConditionModel, + freeu_config: Optional[FreeUConfig] = None, + ) -> Generator[None, None, None]: + did_apply_freeu = False + try: + assert hasattr(unet, "enable_freeu") # mypy doesn't pick up this attribute? + if freeu_config is not None: + unet.enable_freeu(b1=freeu_config.b1, b2=freeu_config.b2, s1=freeu_config.s1, s2=freeu_config.s2) + did_apply_freeu = True + + yield + + finally: + assert hasattr(unet, "disable_freeu") # mypy doesn't pick up this attribute? + if did_apply_freeu: + unet.disable_freeu() + + +class TextualInversionModel: + embedding: torch.Tensor # [n, 768]|[n, 1280] + embedding_2: Optional[torch.Tensor] = None # [n, 768]|[n, 1280] - for SDXL models + + @classmethod + def from_checkpoint( + cls, + file_path: Union[str, Path], + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> Self: + if not isinstance(file_path, Path): + file_path = Path(file_path) + + result = cls() # TODO: + + if file_path.suffix == ".safetensors": + state_dict = load_file(file_path.absolute().as_posix(), device="cpu") + else: + state_dict = torch.load(file_path, map_location="cpu") + + # both v1 and v2 format embeddings + # difference mostly in metadata + if "string_to_param" in state_dict: + if len(state_dict["string_to_param"]) > 1: + print( + f'Warn: Embedding "{file_path.name}" contains multiple tokens, which is not supported. The first', + " token will be used.", + ) + + result.embedding = next(iter(state_dict["string_to_param"].values())) + + # v3 (easynegative) + elif "emb_params" in state_dict: + result.embedding = state_dict["emb_params"] + + # v5(sdxl safetensors file) + elif "clip_g" in state_dict and "clip_l" in state_dict: + result.embedding = state_dict["clip_g"] + result.embedding_2 = state_dict["clip_l"] + + # v4(diffusers bin files) + else: + result.embedding = next(iter(state_dict.values())) + + if len(result.embedding.shape) == 1: + result.embedding = result.embedding.unsqueeze(0) + + if not isinstance(result.embedding, torch.Tensor): + raise ValueError(f"Invalid embeddings file: {file_path.name}") + + return result + + +# no type hints for BaseTextualInversionManager? +class TextualInversionManager(BaseTextualInversionManager): # type: ignore + pad_tokens: Dict[int, List[int]] + tokenizer: CLIPTokenizer + + def __init__(self, tokenizer: CLIPTokenizer): + self.pad_tokens = {} + self.tokenizer = tokenizer + + def expand_textual_inversion_token_ids_if_necessary(self, token_ids: list[int]) -> list[int]: + if len(self.pad_tokens) == 0: + return token_ids + + if token_ids[0] == self.tokenizer.bos_token_id: + raise ValueError("token_ids must not start with bos_token_id") + if token_ids[-1] == self.tokenizer.eos_token_id: + raise ValueError("token_ids must not end with eos_token_id") + + new_token_ids = [] + for token_id in token_ids: + new_token_ids.append(token_id) + if token_id in self.pad_tokens: + new_token_ids.extend(self.pad_tokens[token_id]) + + # Do not exceed the max model input size + # The -2 here is compensating for compensate compel.embeddings_provider.get_token_ids(), + # which first removes and then adds back the start and end tokens. + max_length = list(self.tokenizer.max_model_input_sizes.values())[0] - 2 + if len(new_token_ids) > max_length: + new_token_ids = new_token_ids[0:max_length] + + return new_token_ids + + +class ONNXModelPatcher: + @classmethod + @contextmanager + def apply_lora_unet( + cls, + unet: OnnxRuntimeModel, + loras: List[Tuple[LoRAModelRaw, float]], + ) -> Generator[None, None, None]: + with cls.apply_lora(unet, loras, "lora_unet_"): + yield + + @classmethod + @contextmanager + def apply_lora_text_encoder( + cls, + text_encoder: OnnxRuntimeModel, + loras: List[Tuple[LoRAModelRaw, float]], + ) -> Generator[None, None, None]: + with cls.apply_lora(text_encoder, loras, "lora_te_"): + yield + + # based on + # https://github.com/ssube/onnx-web/blob/ca2e436f0623e18b4cfe8a0363fcfcf10508acf7/api/onnx_web/convert/diffusion/lora.py#L323 + @classmethod + @contextmanager + def apply_lora( + cls, + model: IAIOnnxRuntimeModel, + loras: List[Tuple[LoRAModelRaw, float]], + prefix: str, + ) -> Generator[None, None, None]: + from .models.base import IAIOnnxRuntimeModel + + if not isinstance(model, IAIOnnxRuntimeModel): + raise Exception("Only IAIOnnxRuntimeModel models supported") + + orig_weights = {} + + try: + blended_loras: Dict[str, torch.Tensor] = {} + + for lora, lora_weight in loras: + for layer_key, layer in lora.layers.items(): + if not layer_key.startswith(prefix): + continue + + layer.to(dtype=torch.float32) + layer_key = layer_key.replace(prefix, "") + # TODO: rewrite to pass original tensor weight(required by ia3) + layer_weight = layer.get_weight(None).detach().cpu().numpy() * lora_weight + if layer_key in blended_loras: + blended_loras[layer_key] += layer_weight + else: + blended_loras[layer_key] = layer_weight + + node_names = {} + for node in model.nodes.values(): + node_names[node.name.replace("/", "_").replace(".", "_").lstrip("_")] = node.name + + for layer_key, lora_weight in blended_loras.items(): + conv_key = layer_key + "_Conv" + gemm_key = layer_key + "_Gemm" + matmul_key = layer_key + "_MatMul" + + if conv_key in node_names or gemm_key in node_names: + if conv_key in node_names: + conv_node = model.nodes[node_names[conv_key]] + else: + conv_node = model.nodes[node_names[gemm_key]] + + weight_name = [n for n in conv_node.input if ".weight" in n][0] + orig_weight = model.tensors[weight_name] + + if orig_weight.shape[-2:] == (1, 1): + if lora_weight.shape[-2:] == (1, 1): + new_weight = orig_weight.squeeze((3, 2)) + lora_weight.squeeze((3, 2)) + else: + new_weight = orig_weight.squeeze((3, 2)) + lora_weight + + new_weight = np.expand_dims(new_weight, (2, 3)) + else: + if orig_weight.shape != lora_weight.shape: + new_weight = orig_weight + lora_weight.reshape(orig_weight.shape) + else: + new_weight = orig_weight + lora_weight + + orig_weights[weight_name] = orig_weight + model.tensors[weight_name] = new_weight.astype(orig_weight.dtype) + + elif matmul_key in node_names: + weight_node = model.nodes[node_names[matmul_key]] + matmul_name = [n for n in weight_node.input if "MatMul" in n][0] + + orig_weight = model.tensors[matmul_name] + new_weight = orig_weight + lora_weight.transpose() + + orig_weights[matmul_name] = orig_weight + model.tensors[matmul_name] = new_weight.astype(orig_weight.dtype) + + else: + # warn? err? + pass + + yield + + finally: + # restore original weights + for name, orig_weight in orig_weights.items(): + model.tensors[name] = orig_weight + + @classmethod + @contextmanager + def apply_ti( + cls, + tokenizer: CLIPTokenizer, + text_encoder: IAIOnnxRuntimeModel, + ti_list: List[Tuple[str, Any]], + ) -> Generator[Tuple[CLIPTokenizer, TextualInversionManager], None, None]: + from .models.base import IAIOnnxRuntimeModel + + if not isinstance(text_encoder, IAIOnnxRuntimeModel): + raise Exception("Only IAIOnnxRuntimeModel models supported") + + orig_embeddings = None + + try: + # HACK: The CLIPTokenizer API does not include a way to remove tokens after calling add_tokens(...). As a + # workaround, we create a full copy of `tokenizer` so that its original behavior can be restored after + # exiting this `apply_ti(...)` context manager. + # + # In a previous implementation, the deep copy was obtained with `ti_tokenizer = copy.deepcopy(tokenizer)`, + # but a pickle roundtrip was found to be much faster (1 sec vs. 0.05 secs). + ti_tokenizer = pickle.loads(pickle.dumps(tokenizer)) + ti_manager = TextualInversionManager(ti_tokenizer) + + def _get_trigger(ti_name: str, index: int) -> str: + trigger = ti_name + if index > 0: + trigger += f"-!pad-{i}" + return f"<{trigger}>" + + # modify text_encoder + orig_embeddings = text_encoder.tensors["text_model.embeddings.token_embedding.weight"] + + # modify tokenizer + new_tokens_added = 0 + for ti_name, ti in ti_list: + if ti.embedding_2 is not None: + ti_embedding = ( + ti.embedding_2 if ti.embedding_2.shape[1] == orig_embeddings.shape[0] else ti.embedding + ) + else: + ti_embedding = ti.embedding + + for i in range(ti_embedding.shape[0]): + new_tokens_added += ti_tokenizer.add_tokens(_get_trigger(ti_name, i)) + + embeddings = np.concatenate( + (np.copy(orig_embeddings), np.zeros((new_tokens_added, orig_embeddings.shape[1]))), + axis=0, + ) + + for ti_name, _ in ti_list: + ti_tokens = [] + for i in range(ti_embedding.shape[0]): + embedding = ti_embedding[i].detach().numpy() + trigger = _get_trigger(ti_name, i) + + token_id = ti_tokenizer.convert_tokens_to_ids(trigger) + if token_id == ti_tokenizer.unk_token_id: + raise RuntimeError(f"Unable to find token id for token '{trigger}'") + + if embeddings[token_id].shape != embedding.shape: + raise ValueError( + f"Cannot load embedding for {trigger}. It was trained on a model with token dimension" + f" {embedding.shape[0]}, but the current model has token dimension" + f" {embeddings[token_id].shape[0]}." + ) + + embeddings[token_id] = embedding + ti_tokens.append(token_id) + + if len(ti_tokens) > 1: + ti_manager.pad_tokens[ti_tokens[0]] = ti_tokens[1:] + + text_encoder.tensors["text_model.embeddings.token_embedding.weight"] = embeddings.astype( + orig_embeddings.dtype + ) + + yield ti_tokenizer, ti_manager + + finally: + # restore + if orig_embeddings is not None: + text_encoder.tensors["text_model.embeddings.token_embedding.weight"] = orig_embeddings diff --git a/invokeai/backend/model_management/lora.py b/invokeai/backend/model_management/lora.py index d72f55794d..aed5eb60d5 100644 --- a/invokeai/backend/model_management/lora.py +++ b/invokeai/backend/model_management/lora.py @@ -102,7 +102,7 @@ class ModelPatcher: def apply_lora( cls, model: torch.nn.Module, - loras: List[Tuple[LoRAModel, float]], + loras: List[Tuple[LoRAModel, float]], # THIS IS INCORRECT. IT IS ACTUALLY A LoRAModelRaw prefix: str, ): original_weights = {} @@ -194,6 +194,8 @@ class ModelPatcher: return f"<{trigger}>" def _get_ti_embedding(model_embeddings, ti): + print(f"DEBUG: model_embeddings={type(model_embeddings)}, ti={type(ti)}") + print(f"DEBUG: is it an nn.Module? {isinstance(model_embeddings, torch.nn.Module)}") # for SDXL models, select the embedding that matches the text encoder's dimensions if ti.embedding_2 is not None: return ( @@ -202,6 +204,7 @@ class ModelPatcher: else ti.embedding ) else: + print(f"DEBUG: ti.embedding={type(ti.embedding)}") return ti.embedding # modify tokenizer diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index e59a84d729..4488f8eafc 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -28,9 +28,11 @@ from diffusers import ModelMixin from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from typing_extensions import Annotated, Any, Dict -from .onnx_runtime import IAIOnnxRuntimeModel +from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel + from ..ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus + class InvalidModelConfigException(Exception): """Exception for when config parser doesn't recognized this combination of model type and format.""" diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index 453283e9b4..adc84d2051 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -10,11 +10,17 @@ from diffusers import ModelMixin from diffusers.configuration_utils import ConfigMixin from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, InvalidModelConfigException, ModelRepoVariant, SubModelType +from invokeai.backend.model_manager import ( + AnyModel, + AnyModelConfig, + InvalidModelConfigException, + ModelRepoVariant, + SubModelType, +) from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.backend.model_manager.load.load_base import LoadedModel, ModelLoaderBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase -from invokeai.backend.model_manager.load.model_util import calc_model_size_by_fs, calc_model_size_by_data +from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data, calc_model_size_by_fs from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init from invokeai.backend.util.devices import choose_torch_device, torch_dtype @@ -160,4 +166,3 @@ class ModelLoader(ModelLoaderBase): submodel_type: Optional[SubModelType] = None, ) -> AnyModel: raise NotImplementedError - diff --git a/invokeai/backend/model_manager/load/memory_snapshot.py b/invokeai/backend/model_manager/load/memory_snapshot.py index 295be0c551..346f5dc424 100644 --- a/invokeai/backend/model_manager/load/memory_snapshot.py +++ b/invokeai/backend/model_manager/load/memory_snapshot.py @@ -97,4 +97,4 @@ def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: O if snapshot_1.vram is not None and snapshot_2.vram is not None: msg += get_msg_line("VRAM", snapshot_1.vram, snapshot_2.vram) - return "\n"+msg if len(msg)>0 else msg + return "\n" + msg if len(msg) > 0 else msg diff --git a/invokeai/backend/model_manager/load/model_cache/__init__.py b/invokeai/backend/model_manager/load/model_cache/__init__.py index 50cafa3769..6c87e2519e 100644 --- a/invokeai/backend/model_manager/load/model_cache/__init__.py +++ b/invokeai/backend/model_manager/load/model_cache/__init__.py @@ -1,5 +1,3 @@ """Init file for RamCache.""" -from .model_cache_base import ModelCacheBase -from .model_cache_default import ModelCache _all__ = ["ModelCacheBase", "ModelCache"] diff --git a/invokeai/backend/model_manager/load/model_loaders/controlnet.py b/invokeai/backend/model_manager/load/model_loaders/controlnet.py index 8e6a80ceb2..e61e2b46a6 100644 --- a/invokeai/backend/model_manager/load/model_loaders/controlnet.py +++ b/invokeai/backend/model_manager/load/model_loaders/controlnet.py @@ -14,8 +14,10 @@ from invokeai.backend.model_manager import ( ) from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_controlnet_to_diffusers from invokeai.backend.model_manager.load.load_base import AnyModelLoader + from .generic_diffusers import GenericDiffusersLoader + @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Diffusers) @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Checkpoint) class ControlnetLoader(GenericDiffusersLoader): @@ -37,7 +39,7 @@ class ControlnetLoader(GenericDiffusersLoader): if config.base not in {BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2}: raise Exception(f"Vae conversion not supported for model type: {config.base}") else: - assert hasattr(config, 'config') + assert hasattr(config, "config") config_file = config.config if weights_path.suffix == ".safetensors": diff --git a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py index f92a9048c5..03c26f3a0c 100644 --- a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py +++ b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py @@ -15,6 +15,7 @@ from invokeai.backend.model_manager import ( from invokeai.backend.model_manager.load.load_base import AnyModelLoader from invokeai.backend.model_manager.load.load_default import ModelLoader + @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) class GenericDiffusersLoader(ModelLoader): diff --git a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py index 63dc3790f1..27ced41c1e 100644 --- a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py +++ b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py @@ -1,11 +1,11 @@ # Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team """Class for IP Adapter model loading in InvokeAI.""" -import torch - from pathlib import Path from typing import Optional +import torch + from invokeai.backend.ip_adapter.ip_adapter import build_ip_adapter from invokeai.backend.model_manager import ( AnyModel, @@ -18,6 +18,7 @@ from invokeai.backend.model_manager import ( from invokeai.backend.model_manager.load.load_base import AnyModelLoader from invokeai.backend.model_manager.load.load_default import ModelLoader + @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.InvokeAI) class IPAdapterInvokeAILoader(ModelLoader): """Class to load IP Adapter diffusers models.""" @@ -36,4 +37,3 @@ class IPAdapterInvokeAILoader(ModelLoader): dtype=self._torch_dtype, ) return model - diff --git a/invokeai/backend/model_manager/load/model_loaders/lora.py b/invokeai/backend/model_manager/load/model_loaders/lora.py index 4d19aadb7d..d8e5f920e2 100644 --- a/invokeai/backend/model_manager/load/model_loaders/lora.py +++ b/invokeai/backend/model_manager/load/model_loaders/lora.py @@ -2,13 +2,12 @@ """Class for LoRA model loading in InvokeAI.""" +from logging import Logger from pathlib import Path from typing import Optional, Tuple -from logging import Logger -from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase -from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.embeddings.lora import LoRAModelRaw from invokeai.backend.model_manager import ( AnyModel, AnyModelConfig, @@ -18,9 +17,11 @@ from invokeai.backend.model_manager import ( ModelType, SubModelType, ) -from invokeai.backend.model_manager.lora import LoRAModelRaw +from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.backend.model_manager.load.load_base import AnyModelLoader from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase + @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Diffusers) @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Lycoris) @@ -47,6 +48,7 @@ class LoraLoader(ModelLoader): ) -> AnyModel: if submodel_type is not None: raise ValueError("There are no submodels in a LoRA model.") + assert self._model_base is not None model = LoRAModelRaw.from_checkpoint( file_path=model_path, dtype=self._torch_dtype, @@ -56,9 +58,11 @@ class LoraLoader(ModelLoader): # override def _get_model_path( - self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None + self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None ) -> Tuple[Path, AnyModelConfig, Optional[SubModelType]]: - self._model_base = config.base # cheating a little - setting this variable for later call to _load_model() + self._model_base = ( + config.base + ) # cheating a little - we remember this variable for using in the subsequent call to _load_model() model_base_path = self._app_config.models_path model_path = model_base_path / config.path @@ -72,5 +76,3 @@ class LoraLoader(ModelLoader): result = model_path.resolve(), config, submodel_type return result - - diff --git a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py new file mode 100644 index 0000000000..394fddc75d --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py @@ -0,0 +1,55 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for TI model loading in InvokeAI.""" + + +from pathlib import Path +from typing import Optional, Tuple + +from invokeai.backend.embeddings.model_patcher import TextualInversionModel as TextualInversionModelRaw +from invokeai.backend.model_manager import ( + AnyModel, + AnyModelConfig, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelType, + SubModelType, +) +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader + + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFile) +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFolder) +class TextualInversionLoader(ModelLoader): + """Class to load TI models.""" + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if submodel_type is not None: + raise ValueError("There are no submodels in a TI model.") + model = TextualInversionModelRaw.from_checkpoint( + file_path=model_path, + dtype=self._torch_dtype, + ) + return model + + # override + def _get_model_path( + self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None + ) -> Tuple[Path, AnyModelConfig, Optional[SubModelType]]: + model_path = self._app_config.models_path / config.path + + if config.format == ModelFormat.EmbeddingFolder: + path = model_path / "learned_embeds.bin" + else: + path = model_path + + if not path.exists(): + raise OSError(f"The embedding file at {path} was not found") + + return path, config, submodel_type diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py index 7a35e53459..882ae05577 100644 --- a/invokeai/backend/model_manager/load/model_loaders/vae.py +++ b/invokeai/backend/model_manager/load/model_loaders/vae.py @@ -15,6 +15,7 @@ from invokeai.backend.model_manager import ( ) from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ldm_vae_to_diffusers from invokeai.backend.model_manager.load.load_base import AnyModelLoader + from .generic_diffusers import GenericDiffusersLoader diff --git a/invokeai/backend/model_manager/load/model_util.py b/invokeai/backend/model_manager/load/model_util.py index 404c88bbbc..3f2d22595e 100644 --- a/invokeai/backend/model_manager/load/model_util.py +++ b/invokeai/backend/model_manager/load/model_util.py @@ -3,13 +3,13 @@ import json from pathlib import Path -from typing import Optional, Union +from typing import Optional import torch from diffusers import DiffusionPipeline from invokeai.backend.model_manager.config import AnyModel -from invokeai.backend.model_manager.onnx_runtime import IAIOnnxRuntimeModel +from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel def calc_model_size_by_data(model: AnyModel) -> int: diff --git a/invokeai/backend/model_manager/onnx_runtime.py b/invokeai/backend/onnx/onnx_runtime.py similarity index 100% rename from invokeai/backend/model_manager/onnx_runtime.py rename to invokeai/backend/onnx/onnx_runtime.py From 5745ce9c7db13aa50c09b9a0c3901456616aaf13 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 5 Feb 2024 21:55:11 -0500 Subject: [PATCH 115/411] Multiple refinements on loaders: - Cache stat collection enabled. - Implemented ONNX loading. - Add ability to specify the repo version variant in installer CLI. - If caller asks for a repo version that doesn't exist, will fall back to empty version rather than raising an error. --- .../model_install/model_install_default.py | 6 +-- invokeai/backend/install/install_helper.py | 18 ++++++-- invokeai/backend/model_manager/config.py | 14 ++++++- .../backend/model_manager/load/__init__.py | 1 - .../backend/model_manager/load/load_base.py | 4 +- .../model_manager/load/load_default.py | 32 ++++++++++---- .../load/model_cache/__init__.py | 3 +- .../load/model_cache/model_cache_base.py | 10 ++++- .../load/model_cache/model_cache_default.py | 42 +++++++++++++++++-- .../model_manager/load/model_loaders/onnx.py | 41 ++++++++++++++++++ .../model_manager/metadata/fetch/civitai.py | 7 +++- .../metadata/fetch/fetch_base.py | 7 +++- .../metadata/fetch/huggingface.py | 26 ++++++++---- .../model_manager/metadata/metadata_base.py | 1 - invokeai/backend/model_manager/probe.py | 16 +++++-- .../model_manager/util/select_hf_files.py | 14 +++++-- invokeai/backend/util/devices.py | 20 ++++++--- invokeai/frontend/install/model_install2.py | 2 +- 18 files changed, 215 insertions(+), 49 deletions(-) create mode 100644 invokeai/backend/model_manager/load/model_loaders/onnx.py diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 1c188b300d..d32af4a513 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -495,10 +495,10 @@ class ModelInstallService(ModelInstallServiceBase): return id @staticmethod - def _guess_variant() -> ModelRepoVariant: + def _guess_variant() -> Optional[ModelRepoVariant]: """Guess the best HuggingFace variant type to download.""" precision = choose_precision(choose_torch_device()) - return ModelRepoVariant.FP16 if precision == "float16" else ModelRepoVariant.DEFAULT + return ModelRepoVariant.FP16 if precision == "float16" else None def _import_local_model(self, source: LocalModelSource, config: Optional[Dict[str, Any]]) -> ModelInstallJob: return ModelInstallJob( @@ -523,7 +523,7 @@ class ModelInstallService(ModelInstallServiceBase): if not source.access_token: self._logger.info("No HuggingFace access token present; some models may not be downloadable.") - metadata = HuggingFaceMetadataFetch(self._session).from_id(source.repo_id) + metadata = HuggingFaceMetadataFetch(self._session).from_id(source.repo_id, source.variant) assert isinstance(metadata, ModelMetadataWithFiles) remote_files = metadata.download_urls( variant=source.variant or self._guess_variant(), diff --git a/invokeai/backend/install/install_helper.py b/invokeai/backend/install/install_helper.py index 9f219132d4..57dfadcaea 100644 --- a/invokeai/backend/install/install_helper.py +++ b/invokeai/backend/install/install_helper.py @@ -30,6 +30,7 @@ from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.backend.model_manager import ( BaseModelType, InvalidModelConfigException, + ModelRepoVariant, ModelType, ) from invokeai.backend.model_manager.metadata import UnknownMetadataException @@ -233,11 +234,18 @@ class InstallHelper(object): if model_path.exists(): # local file on disk return LocalModelSource(path=model_path.absolute(), inplace=True) - if re.match(r"^[^/]+/[^/]+$", model_path_id_or_url): # hugging face repo_id + + # parsing huggingface repo ids + # we're going to do a little trick that allows for extended repo_ids of form "foo/bar:fp16" + variants = "|".join([x.lower() for x in ModelRepoVariant.__members__]) + if match := re.match(f"^([^/]+/[^/]+?)(?::({variants}))?$", model_path_id_or_url): + repo_id = match.group(1) + repo_variant = ModelRepoVariant(match.group(2)) if match.group(2) else None return HFModelSource( - repo_id=model_path_id_or_url, + repo_id=repo_id, access_token=HfFolder.get_token(), subfolder=model_info.subfolder, + variant=repo_variant, ) if re.match(r"^(http|https):", model_path_id_or_url): return URLModelSource(url=AnyHttpUrl(model_path_id_or_url)) @@ -278,9 +286,11 @@ class InstallHelper(object): model_name=model_name, ) if len(matches) > 1: - print(f"{model} is ambiguous. Please use model_type:model_name (e.g. main:my_model) to disambiguate.") + print( + f"{model_to_remove} is ambiguous. Please use model_base/model_type/model_name (e.g. sd-1/main/my_model) to disambiguate." + ) elif not matches: - print(f"{model}: unknown model") + print(f"{model_to_remove}: unknown model") else: for m in matches: print(f"Deleting {m.type}:{m.name}") diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 4488f8eafc..49ce6af2b8 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -109,7 +109,7 @@ class SchedulerPredictionType(str, Enum): class ModelRepoVariant(str, Enum): """Various hugging face variants on the diffusers format.""" - DEFAULT = "default" # model files without "fp16" or other qualifier + DEFAULT = "" # model files without "fp16" or other qualifier - empty str FP16 = "fp16" FP32 = "fp32" ONNX = "onnx" @@ -246,6 +246,16 @@ class ONNXSD2Config(_MainConfig): upcast_attention: bool = True +class ONNXSDXLConfig(_MainConfig): + """Model config for ONNX format models based on sdxl.""" + + type: Literal[ModelType.ONNX] = ModelType.ONNX + format: Literal[ModelFormat.Onnx, ModelFormat.Olive] + # No yaml config file for ONNX, so these are part of config + base: Literal[BaseModelType.StableDiffusionXL] = BaseModelType.StableDiffusionXL + prediction_type: SchedulerPredictionType = SchedulerPredictionType.VPrediction + + class IPAdapterConfig(ModelConfigBase): """Model config for IP Adaptor format models.""" @@ -267,7 +277,7 @@ class T2IConfig(ModelConfigBase): format: Literal[ModelFormat.Diffusers] -_ONNXConfig = Annotated[Union[ONNXSD1Config, ONNXSD2Config], Field(discriminator="base")] +_ONNXConfig = Annotated[Union[ONNXSD1Config, ONNXSD2Config, ONNXSDXLConfig], Field(discriminator="base")] _ControlNetConfig = Annotated[ Union[ControlNetDiffusersConfig, ControlNetCheckpointConfig], Field(discriminator="format"), diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py index 19b0116ba3..e4c7077f78 100644 --- a/invokeai/backend/model_manager/load/__init__.py +++ b/invokeai/backend/model_manager/load/__init__.py @@ -16,7 +16,6 @@ from .model_cache.model_cache_default import ModelCache # This registers the subclasses that implement loaders of specific model types loaders = [x.stem for x in Path(Path(__file__).parent, "model_loaders").glob("*.py") if x.stem != "__init__"] for module in loaders: - print(f"module={module}") import_module(f"{__package__}.model_loaders.{module}") __all__ = ["AnyModelLoader", "LoadedModel"] diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 7d4e8337c3..ee9d6d53e3 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -22,6 +22,7 @@ from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelTy from invokeai.backend.model_manager.config import VaeCheckpointConfig, VaeDiffusersConfig from invokeai.backend.model_manager.load.convert_cache.convert_cache_base import ModelConvertCacheBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase +from invokeai.backend.util.logging import InvokeAILogger @dataclass @@ -88,6 +89,7 @@ class AnyModelLoader: # this tracks the loader subclasses _registry: Dict[str, Type[ModelLoaderBase]] = {} + _logger: Logger = InvokeAILogger.get_logger() def __init__( self, @@ -167,7 +169,7 @@ class AnyModelLoader: """Define a decorator which registers the subclass of loader.""" def decorator(subclass: Type[ModelLoaderBase]) -> Type[ModelLoaderBase]: - print("DEBUG: Registering class", subclass.__name__) + cls._logger.debug(f"Registering class {subclass.__name__} to load models of type {base}/{type}/{format}") key = cls._to_registry_key(base, type, format) cls._registry[key] = subclass return subclass diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index adc84d2051..757745072d 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -52,7 +52,7 @@ class ModelLoader(ModelLoaderBase): self._logger = logger self._ram_cache = ram_cache self._convert_cache = convert_cache - self._torch_dtype = torch_dtype(choose_torch_device()) + self._torch_dtype = torch_dtype(choose_torch_device(), app_config) def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ @@ -102,8 +102,10 @@ class ModelLoader(ModelLoaderBase): self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None ) -> ModelLockerBase: # TO DO: This is not thread safe! - if self._ram_cache.exists(config.key, submodel_type): + try: return self._ram_cache.get(config.key, submodel_type) + except IndexError: + pass model_variant = getattr(config, "repo_variant", None) self._ram_cache.make_room(self.get_size_fs(config, model_path, submodel_type)) @@ -119,7 +121,11 @@ class ModelLoader(ModelLoaderBase): size=calc_model_size_by_data(loaded_model), ) - return self._ram_cache.get(config.key, submodel_type) + return self._ram_cache.get( + key=config.key, + submodel_type=submodel_type, + stats_name=":".join([config.base, config.type, config.name, (submodel_type or "")]), + ) def get_size_fs( self, config: AnyModelConfig, model_path: Path, submodel_type: Optional[SubModelType] = None @@ -146,13 +152,21 @@ class ModelLoader(ModelLoaderBase): # TO DO: Add exception handling def _get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelType] = None) -> ModelMixin: if submodel_type: - config = self._load_diffusers_config(model_path, config_name="model_index.json") - module, class_name = config[submodel_type.value] - return self._hf_definition_to_type(module=module, class_name=class_name) + try: + config = self._load_diffusers_config(model_path, config_name="model_index.json") + module, class_name = config[submodel_type.value] + return self._hf_definition_to_type(module=module, class_name=class_name) + except KeyError as e: + raise InvalidModelConfigException( + f'The "{submodel_type}" submodel is not available for this model.' + ) from e else: - config = self._load_diffusers_config(model_path, config_name="config.json") - class_name = config["_class_name"] - return self._hf_definition_to_type(module="diffusers", class_name=class_name) + try: + config = self._load_diffusers_config(model_path, config_name="config.json") + class_name = config["_class_name"] + return self._hf_definition_to_type(module="diffusers", class_name=class_name) + except KeyError as e: + raise InvalidModelConfigException("An expected config.json file is missing from this model.") from e # This needs to be implemented in subclasses that handle checkpoints def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: diff --git a/invokeai/backend/model_manager/load/model_cache/__init__.py b/invokeai/backend/model_manager/load/model_cache/__init__.py index 6c87e2519e..0cb5184f3a 100644 --- a/invokeai/backend/model_manager/load/model_cache/__init__.py +++ b/invokeai/backend/model_manager/load/model_cache/__init__.py @@ -1,3 +1,4 @@ -"""Init file for RamCache.""" +"""Init file for ModelCache.""" + _all__ = ["ModelCacheBase", "ModelCache"] diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py index 14a7dfb4a1..b1a6768ee8 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py @@ -129,11 +129,17 @@ class ModelCacheBase(ABC, Generic[T]): self, key: str, submodel_type: Optional[SubModelType] = None, + stats_name: Optional[str] = None, ) -> ModelLockerBase: """ - Retrieve model locker object using key and optional submodel_type. + Retrieve model using key and optional submodel_type. - This may return an UnknownModelException if the model is not in the cache. + :param key: Opaque model key + :param submodel_type: Type of the submodel to fetch + :param stats_name: A human-readable id for the model for the purposes of + stats reporting. + + This may raise an IndexError if the model is not in the cache. """ pass diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py index 688be8ceb4..7e30512a58 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -24,6 +24,7 @@ import math import sys import time from contextlib import suppress +from dataclasses import dataclass, field from logging import Logger from typing import Dict, List, Optional @@ -55,6 +56,20 @@ GIG = 1073741824 MB = 2**20 +@dataclass +class CacheStats(object): + """Collect statistics on cache performance.""" + + hits: int = 0 # cache hits + misses: int = 0 # cache misses + high_watermark: int = 0 # amount of cache used + in_cache: int = 0 # number of models in cache + cleared: int = 0 # number of models cleared to make space + cache_size: int = 0 # total size of cache + # {submodel_key => size} + loaded_model_sizes: Dict[str, int] = field(default_factory=dict) + + class ModelCache(ModelCacheBase[AnyModel]): """Implementation of ModelCacheBase.""" @@ -94,6 +109,8 @@ class ModelCache(ModelCacheBase[AnyModel]): self._storage_device: torch.device = storage_device self._logger = logger or InvokeAILogger.get_logger(self.__class__.__name__) self._log_memory_usage = log_memory_usage or self._logger.level == logging.DEBUG + # used for stats collection + self.stats = CacheStats() self._cached_models: Dict[str, CacheRecord[AnyModel]] = {} self._cache_stack: List[str] = [] @@ -158,21 +175,40 @@ class ModelCache(ModelCacheBase[AnyModel]): self, key: str, submodel_type: Optional[SubModelType] = None, + stats_name: Optional[str] = None, ) -> ModelLockerBase: """ Retrieve model using key and optional submodel_type. - This may return an IndexError if the model is not in the cache. + :param key: Opaque model key + :param submodel_type: Type of the submodel to fetch + :param stats_name: A human-readable id for the model for the purposes of + stats reporting. + + This may raise an IndexError if the model is not in the cache. """ key = self._make_cache_key(key, submodel_type) - if key not in self._cached_models: + if key in self._cached_models: + self.stats.hits += 1 + else: + self.stats.misses += 1 raise IndexError(f"The model with key {key} is not in the cache.") + cache_entry = self._cached_models[key] + + # more stats + stats_name = stats_name or key + self.stats.cache_size = int(self._max_cache_size * GIG) + self.stats.high_watermark = max(self.stats.high_watermark, self.cache_size()) + self.stats.in_cache = len(self._cached_models) + self.stats.loaded_model_sizes[stats_name] = max( + self.stats.loaded_model_sizes.get(stats_name, 0), cache_entry.size + ) + # this moves the entry to the top (right end) of the stack with suppress(Exception): self._cache_stack.remove(key) self._cache_stack.append(key) - cache_entry = self._cached_models[key] return ModelLocker( cache=self, cache_entry=cache_entry, diff --git a/invokeai/backend/model_manager/load/model_loaders/onnx.py b/invokeai/backend/model_manager/load/model_loaders/onnx.py new file mode 100644 index 0000000000..935a6b7c95 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loaders/onnx.py @@ -0,0 +1,41 @@ +# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team +"""Class for Onnx model loading in InvokeAI.""" + +# This should work the same as Stable Diffusion pipelines +from pathlib import Path +from typing import Optional + +from invokeai.backend.model_manager import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelType, + SubModelType, +) +from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from invokeai.backend.model_manager.load.load_default import ModelLoader + + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Onnx) +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Olive) +class OnnyxDiffusersModel(ModelLoader): + """Class to load onnx models.""" + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + if not submodel_type is not None: + raise Exception("A submodel type must be provided when loading onnx pipelines.") + load_class = self._get_hf_load_class(model_path, submodel_type) + variant = model_variant.value if model_variant else None + model_path = model_path / submodel_type.value + result: AnyModel = load_class.from_pretrained( + model_path, + torch_dtype=self._torch_dtype, + variant=variant, + ) # type: ignore + return result diff --git a/invokeai/backend/model_manager/metadata/fetch/civitai.py b/invokeai/backend/model_manager/metadata/fetch/civitai.py index 6e41d6f11b..7991f6a748 100644 --- a/invokeai/backend/model_manager/metadata/fetch/civitai.py +++ b/invokeai/backend/model_manager/metadata/fetch/civitai.py @@ -32,6 +32,8 @@ import requests from pydantic.networks import AnyHttpUrl from requests.sessions import Session +from invokeai.backend.model_manager import ModelRepoVariant + from ..metadata_base import ( AnyModelRepoMetadata, CivitaiMetadata, @@ -82,10 +84,13 @@ class CivitaiMetadataFetch(ModelMetadataFetchBase): return self.from_civitai_versionid(int(version_id)) raise UnknownMetadataException("The url '{url}' does not match any known Civitai URL patterns") - def from_id(self, id: str) -> AnyModelRepoMetadata: + def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyModelRepoMetadata: """ Given a Civitai model version ID, return a ModelRepoMetadata object. + :param id: An ID. + :param variant: A model variant from the ModelRepoVariant enum (currently ignored) + May raise an `UnknownMetadataException`. """ return self.from_civitai_versionid(int(id)) diff --git a/invokeai/backend/model_manager/metadata/fetch/fetch_base.py b/invokeai/backend/model_manager/metadata/fetch/fetch_base.py index 58b65b6947..d628ab5c17 100644 --- a/invokeai/backend/model_manager/metadata/fetch/fetch_base.py +++ b/invokeai/backend/model_manager/metadata/fetch/fetch_base.py @@ -18,6 +18,8 @@ from typing import Optional from pydantic.networks import AnyHttpUrl from requests.sessions import Session +from invokeai.backend.model_manager import ModelRepoVariant + from ..metadata_base import AnyModelRepoMetadata, AnyModelRepoMetadataValidator @@ -45,10 +47,13 @@ class ModelMetadataFetchBase(ABC): pass @abstractmethod - def from_id(self, id: str) -> AnyModelRepoMetadata: + def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyModelRepoMetadata: """ Given an ID for a model, return a ModelMetadata object. + :param id: An ID. + :param variant: A model variant from the ModelRepoVariant enum. + This method will raise a `UnknownMetadataException` in the event that the requested model's metadata is not found at the provided id. """ diff --git a/invokeai/backend/model_manager/metadata/fetch/huggingface.py b/invokeai/backend/model_manager/metadata/fetch/huggingface.py index 5d1eb0cc9e..6f04e8713b 100644 --- a/invokeai/backend/model_manager/metadata/fetch/huggingface.py +++ b/invokeai/backend/model_manager/metadata/fetch/huggingface.py @@ -19,10 +19,12 @@ from typing import Optional import requests from huggingface_hub import HfApi, configure_http_backend, hf_hub_url -from huggingface_hub.utils._errors import RepositoryNotFoundError +from huggingface_hub.utils._errors import RepositoryNotFoundError, RevisionNotFoundError from pydantic.networks import AnyHttpUrl from requests.sessions import Session +from invokeai.backend.model_manager import ModelRepoVariant + from ..metadata_base import ( AnyModelRepoMetadata, HuggingFaceMetadata, @@ -53,12 +55,22 @@ class HuggingFaceMetadataFetch(ModelMetadataFetchBase): metadata = HuggingFaceMetadata.model_validate_json(json) return metadata - def from_id(self, id: str) -> AnyModelRepoMetadata: + def from_id(self, id: str, variant: Optional[ModelRepoVariant] = None) -> AnyModelRepoMetadata: """Return a HuggingFaceMetadata object given the model's repo_id.""" - try: - model_info = HfApi().model_info(repo_id=id, files_metadata=True) - except RepositoryNotFoundError as excp: - raise UnknownMetadataException(f"'{id}' not found. See trace for details.") from excp + # Little loop which tries fetching a revision corresponding to the selected variant. + # If not available, then set variant to None and get the default. + # If this too fails, raise exception. + model_info = None + while not model_info: + try: + model_info = HfApi().model_info(repo_id=id, files_metadata=True, revision=variant) + except RepositoryNotFoundError as excp: + raise UnknownMetadataException(f"'{id}' not found. See trace for details.") from excp + except RevisionNotFoundError: + if variant is None: + raise + else: + variant = None _, name = id.split("/") return HuggingFaceMetadata( @@ -70,7 +82,7 @@ class HuggingFaceMetadataFetch(ModelMetadataFetchBase): tags=model_info.tags, files=[ RemoteModelFile( - url=hf_hub_url(id, x.rfilename), + url=hf_hub_url(id, x.rfilename, revision=variant), path=Path(name, x.rfilename), size=x.size, sha256=x.lfs.get("sha256") if x.lfs else None, diff --git a/invokeai/backend/model_manager/metadata/metadata_base.py b/invokeai/backend/model_manager/metadata/metadata_base.py index 5aa883d26d..5c3afcdc96 100644 --- a/invokeai/backend/model_manager/metadata/metadata_base.py +++ b/invokeai/backend/model_manager/metadata/metadata_base.py @@ -184,7 +184,6 @@ class HuggingFaceMetadata(ModelMetadataWithFiles): [x.path for x in self.files], variant, subfolder ) # all files in the model prefix = f"{subfolder}/" if subfolder else "" - # the next step reads model_index.json to determine which subdirectories belong # to the model if Path(f"{prefix}model_index.json") in paths: diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index 64a20a2092..55a9c0464a 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -7,6 +7,7 @@ import safetensors.torch import torch from picklescan.scanner import scan_file_path +import invokeai.backend.util.logging as logger from invokeai.backend.model_management.models.base import read_checkpoint_meta from invokeai.backend.model_management.models.ip_adapter import IPAdapterModelFormat from invokeai.backend.model_management.util import lora_token_vector_length @@ -590,13 +591,20 @@ class TextualInversionFolderProbe(FolderProbeBase): return TextualInversionCheckpointProbe(path).get_base_type() -class ONNXFolderProbe(FolderProbeBase): +class ONNXFolderProbe(PipelineFolderProbe): + def get_base_type(self) -> BaseModelType: + # Due to the way the installer is set up, the configuration file for safetensors + # will come along for the ride if both the onnx and safetensors forms + # share the same directory. We take advantage of this here. + if (self.model_path / "unet" / "config.json").exists(): + return super().get_base_type() + else: + logger.warning('Base type probing is not implemented for ONNX models. Assuming "sd-1"') + return BaseModelType.StableDiffusion1 + def get_format(self) -> ModelFormat: return ModelFormat("onnx") - def get_base_type(self) -> BaseModelType: - return BaseModelType.StableDiffusion1 - def get_variant_type(self) -> ModelVariantType: return ModelVariantType.Normal diff --git a/invokeai/backend/model_manager/util/select_hf_files.py b/invokeai/backend/model_manager/util/select_hf_files.py index 6976059044..a894d915de 100644 --- a/invokeai/backend/model_manager/util/select_hf_files.py +++ b/invokeai/backend/model_manager/util/select_hf_files.py @@ -41,13 +41,21 @@ def filter_files( for file in files: if file.name.endswith((".json", ".txt")): paths.append(file) - elif file.name.endswith(("learned_embeds.bin", "ip_adapter.bin", "lora_weights.safetensors")): + elif file.name.endswith( + ( + "learned_embeds.bin", + "ip_adapter.bin", + "lora_weights.safetensors", + "weights.pb", + "onnx_data", + ) + ): paths.append(file) # BRITTLENESS WARNING!! # Diffusers models always seem to have "model" in their name, and the regex filter below is applied to avoid # downloading random checkpoints that might also be in the repo. However there is no guarantee # that a checkpoint doesn't contain "model" in its name, and no guarantee that future diffusers models - # will adhere to this naming convention, so this is an area of brittleness. + # will adhere to this naming convention, so this is an area to be careful of. elif re.search(r"model(\.[^.]+)?\.(safetensors|bin|onnx|xml|pth|pt|ckpt|msgpack)$", file.name): paths.append(file) @@ -64,7 +72,7 @@ def _filter_by_variant(files: List[Path], variant: ModelRepoVariant) -> Set[Path result = set() basenames: Dict[Path, Path] = {} for path in files: - if path.suffix == ".onnx": + if path.suffix in [".onnx", ".pb", ".onnx_data"]: if variant == ModelRepoVariant.ONNX: result.add(path) diff --git a/invokeai/backend/util/devices.py b/invokeai/backend/util/devices.py index a787f9b6f4..b4f24d8483 100644 --- a/invokeai/backend/util/devices.py +++ b/invokeai/backend/util/devices.py @@ -29,12 +29,17 @@ def choose_torch_device() -> torch.device: return torch.device(config.device) -def choose_precision(device: torch.device) -> str: - """Returns an appropriate precision for the given torch device""" +# We are in transition here from using a single global AppConfig to allowing multiple +# configurations. It is strongly recommended to pass the app_config to this function. +def choose_precision(device: torch.device, app_config: Optional[InvokeAIAppConfig] = None) -> str: + """Return an appropriate precision for the given torch device.""" + app_config = app_config or config if device.type == "cuda": device_name = torch.cuda.get_device_name(device) if not ("GeForce GTX 1660" in device_name or "GeForce GTX 1650" in device_name): - if config.precision == "bfloat16": + if app_config.precision == "float32": + return "float32" + elif app_config.precision == "bfloat16": return "bfloat16" else: return "float16" @@ -43,9 +48,14 @@ def choose_precision(device: torch.device) -> str: return "float32" -def torch_dtype(device: Optional[torch.device] = None) -> torch.dtype: +# We are in transition here from using a single global AppConfig to allowing multiple +# configurations. It is strongly recommended to pass the app_config to this function. +def torch_dtype( + device: Optional[torch.device] = None, + app_config: Optional[InvokeAIAppConfig] = None, +) -> torch.dtype: device = device or choose_torch_device() - precision = choose_precision(device) + precision = choose_precision(device, app_config) if precision == "float16": return torch.float16 if precision == "bfloat16": diff --git a/invokeai/frontend/install/model_install2.py b/invokeai/frontend/install/model_install2.py index 6eb480c8d9..51a633a565 100644 --- a/invokeai/frontend/install/model_install2.py +++ b/invokeai/frontend/install/model_install2.py @@ -505,7 +505,7 @@ def list_models(installer: ModelInstallService, model_type: ModelType): print(f"Installed models of type `{model_type}`:") for model in models: path = (config.models_path / model.path).resolve() - print(f"{model.name:40}{model.base.value:14}{path}") + print(f"{model.name:40}{model.base.value:5}{model.type.value:8}{model.format.value:12}{path}") # -------------------------------------------------------- From 78ef946e010ca9e149d91e85fe49cdc8e615bc47 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 5 Feb 2024 22:56:32 -0500 Subject: [PATCH 116/411] BREAKING CHANGES: invocations now require model key, not base/type/name - Implement new model loader and modify invocations and embeddings - Finish implementation loaders for all models currently supported by InvokeAI. - Move lora, textual_inversion, and model patching support into backend/embeddings. - Restore support for model cache statistics collection (a little ugly, needs work). - Fixed up invocations that load and patch models. - Move seamless and silencewarnings utils into better location --- invokeai/app/api/routers/download_queue.py | 2 +- invokeai/app/invocations/compel.py | 114 +++++++----- .../controlnet_image_processors.py | 7 +- invokeai/app/invocations/ip_adapter.py | 49 +++-- invokeai/app/invocations/latent.py | 86 +++++---- invokeai/app/invocations/model.py | 174 +++++------------- invokeai/app/invocations/sdxl.py | 74 ++------ invokeai/app/invocations/t2i_adapter.py | 8 +- invokeai/app/services/events/events_base.py | 27 +-- .../invocation_stats_default.py | 16 +- .../model_records/model_records_base.py | 47 ++++- .../model_records/model_records_sql.py | 92 ++++++++- invokeai/backend/embeddings/__init__.py | 4 + invokeai/backend/embeddings/embedding_base.py | 12 ++ invokeai/backend/embeddings/lora.py | 14 +- invokeai/backend/embeddings/model_patcher.py | 134 +++----------- .../backend/embeddings/textual_inversion.py | 100 ++++++++++ invokeai/backend/install/install_helper.py | 3 +- invokeai/backend/model_manager/config.py | 5 +- .../backend/model_manager/load/load_base.py | 4 +- .../model_manager/load/load_default.py | 4 +- .../load/model_cache/__init__.py | 4 +- .../load/model_cache/model_cache_base.py | 33 +++- .../load/model_cache/model_cache_default.py | 53 +++--- .../load/model_loaders/textual_inversion.py | 2 +- invokeai/backend/stable_diffusion/__init__.py | 9 + invokeai/backend/stable_diffusion/seamless.py | 102 ++++++++++ invokeai/backend/util/silence_warnings.py | 28 +++ invokeai/frontend/install/model_install2.py | 8 +- .../util/test_hf_model_select.py | 2 + tests/test_model_probe.py | 6 +- 31 files changed, 727 insertions(+), 496 deletions(-) create mode 100644 invokeai/backend/embeddings/__init__.py create mode 100644 invokeai/backend/embeddings/embedding_base.py create mode 100644 invokeai/backend/embeddings/textual_inversion.py create mode 100644 invokeai/backend/stable_diffusion/seamless.py create mode 100644 invokeai/backend/util/silence_warnings.py diff --git a/invokeai/app/api/routers/download_queue.py b/invokeai/app/api/routers/download_queue.py index 92b658c370..2dba376c18 100644 --- a/invokeai/app/api/routers/download_queue.py +++ b/invokeai/app/api/routers/download_queue.py @@ -55,7 +55,7 @@ async def download( ) -> DownloadJob: """Download the source URL to the file or directory indicted in dest.""" queue = ApiDependencies.invoker.services.download_queue - return queue.download(source, dest, priority, access_token) + return queue.download(source, Path(dest), priority, access_token) @download_queue_router.get( diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 978c6dcb17..0e1a6bdc6f 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -1,9 +1,10 @@ -from typing import List, Optional, Union +from typing import Iterator, List, Optional, Tuple, Union import torch from compel import Compel, ReturnedEmbeddingsType from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment +import invokeai.backend.util.logging as logger from invokeai.app.invocations.fields import ( FieldDescriptions, Input, @@ -12,18 +13,21 @@ from invokeai.app.invocations.fields import ( UIComponent, ) from invokeai.app.invocations.primitives import ConditioningOutput +from invokeai.app.services.model_records import UnknownModelException from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.app.util.ti_utils import extract_ti_triggers_from_prompt +from invokeai.backend.embeddings.lora import LoRAModelRaw +from invokeai.backend.embeddings.model_patcher import ModelPatcher +from invokeai.backend.embeddings.textual_inversion import TextualInversionModelRaw +from invokeai.backend.model_manager import ModelType from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, ConditioningFieldData, ExtraConditioningInfo, SDXLConditioningInfo, ) +from invokeai.backend.util.devices import torch_dtype -from ...backend.model_management.lora import ModelPatcher -from ...backend.model_management.models import ModelNotFoundException, ModelType -from ...backend.util.devices import torch_dtype -from ..util.ti_utils import extract_ti_triggers_from_prompt from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, @@ -64,13 +68,22 @@ class CompelInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> ConditioningOutput: - tokenizer_info = context.models.load(**self.clip.tokenizer.model_dump()) - text_encoder_info = context.models.load(**self.clip.text_encoder.model_dump()) + tokenizer_info = context.services.model_records.load_model( + **self.clip.tokenizer.model_dump(), + context=context, + ) + text_encoder_info = context.services.model_records.load_model( + **self.clip.text_encoder.model_dump(), + context=context, + ) - def _lora_loader(): + def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in self.clip.loras: - lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) - yield (lora_info.context.model, lora.weight) + lora_info = context.services.model_records.load_model( + **lora.model_dump(exclude={"weight"}), context=context + ) + assert isinstance(lora_info.model, LoRAModelRaw) + yield (lora_info.model, lora.weight) del lora_info return @@ -80,24 +93,20 @@ class CompelInvocation(BaseInvocation): for trigger in extract_ti_triggers_from_prompt(self.prompt): name = trigger[1:-1] try: - ti_list.append( - ( - name, - context.models.load( - model_name=name, - base_model=self.clip.text_encoder.base_model, - model_type=ModelType.TextualInversion, - ).context.model, - ) - ) - except ModelNotFoundException: + loaded_model = context.services.model_records.load_model( + **self.clip.text_encoder.model_dump(), + context=context, + ).model + assert isinstance(loaded_model, TextualInversionModelRaw) + ti_list.append((name, loaded_model)) + except UnknownModelException: # print(e) # import traceback # print(traceback.format_exc()) print(f'Warn: trigger: "{trigger}" not found') with ( - ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as ( + ModelPatcher.apply_ti(tokenizer_info.model, text_encoder_info.model, ti_list) as ( tokenizer, ti_manager, ), @@ -105,7 +114,7 @@ class CompelInvocation(BaseInvocation): # Apply the LoRA after text_encoder has been moved to its target device for faster patching. ModelPatcher.apply_lora_text_encoder(text_encoder, _lora_loader()), # Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers. - ModelPatcher.apply_clip_skip(text_encoder_info.context.model, self.clip.skipped_layers), + ModelPatcher.apply_clip_skip(text_encoder_info.model, self.clip.skipped_layers), ): compel = Compel( tokenizer=tokenizer, @@ -144,6 +153,8 @@ class CompelInvocation(BaseInvocation): class SDXLPromptInvocationBase: + """Prompt processor for SDXL models.""" + def run_clip_compel( self, context: InvocationContext, @@ -152,20 +163,27 @@ class SDXLPromptInvocationBase: get_pooled: bool, lora_prefix: str, zero_on_empty: bool, - ): - tokenizer_info = context.models.load(**clip_field.tokenizer.model_dump()) - text_encoder_info = context.models.load(**clip_field.text_encoder.model_dump()) + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[ExtraConditioningInfo]]: + tokenizer_info = context.services.model_records.load_model( + **clip_field.tokenizer.model_dump(), + context=context, + ) + text_encoder_info = context.services.model_records.load_model( + **clip_field.text_encoder.model_dump(), + context=context, + ) # return zero on empty if prompt == "" and zero_on_empty: - cpu_text_encoder = text_encoder_info.context.model + cpu_text_encoder = text_encoder_info.model + assert isinstance(cpu_text_encoder, torch.nn.Module) c = torch.zeros( ( 1, cpu_text_encoder.config.max_position_embeddings, cpu_text_encoder.config.hidden_size, ), - dtype=text_encoder_info.context.cache.precision, + dtype=cpu_text_encoder.dtype, ) if get_pooled: c_pooled = torch.zeros( @@ -176,10 +194,14 @@ class SDXLPromptInvocationBase: c_pooled = None return c, c_pooled, None - def _lora_loader(): + def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in clip_field.loras: - lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) - yield (lora_info.context.model, lora.weight) + lora_info = context.services.model_records.load_model( + **lora.model_dump(exclude={"weight"}), context=context + ) + lora_model = lora_info.model + assert isinstance(lora_model, LoRAModelRaw) + yield (lora_model, lora.weight) del lora_info return @@ -189,24 +211,24 @@ class SDXLPromptInvocationBase: for trigger in extract_ti_triggers_from_prompt(prompt): name = trigger[1:-1] try: - ti_list.append( - ( - name, - context.models.load( - model_name=name, - base_model=clip_field.text_encoder.base_model, - model_type=ModelType.TextualInversion, - ).context.model, - ) - ) - except ModelNotFoundException: + ti_model = context.services.model_records.load_model_by_attr( + model_name=name, + base_model=text_encoder_info.config.base, + model_type=ModelType.TextualInversion, + context=context, + ).model + assert isinstance(ti_model, TextualInversionModelRaw) + ti_list.append((name, ti_model)) + except UnknownModelException: # print(e) # import traceback # print(traceback.format_exc()) - print(f'Warn: trigger: "{trigger}" not found') + logger.warning(f'trigger: "{trigger}" not found') + except ValueError: + logger.warning(f'trigger: "{trigger}" more than one similarly-named textual inversion models') with ( - ModelPatcher.apply_ti(tokenizer_info.context.model, text_encoder_info.context.model, ti_list) as ( + ModelPatcher.apply_ti(tokenizer_info.model, text_encoder_info.model, ti_list) as ( tokenizer, ti_manager, ), @@ -214,7 +236,7 @@ class SDXLPromptInvocationBase: # Apply the LoRA after text_encoder has been moved to its target device for faster patching. ModelPatcher.apply_lora(text_encoder, _lora_loader(), lora_prefix), # Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers. - ModelPatcher.apply_clip_skip(text_encoder_info.context.model, clip_field.skipped_layers), + ModelPatcher.apply_clip_skip(text_encoder_info.model, clip_field.skipped_layers), ): compel = Compel( tokenizer=tokenizer, @@ -332,6 +354,7 @@ class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase): dim=1, ) + assert c2_pooled is not None conditioning_data = ConditioningFieldData( conditionings=[ SDXLConditioningInfo( @@ -380,6 +403,7 @@ class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase add_time_ids = torch.tensor([original_size + crop_coords + (self.aesthetic_score,)]) + assert c2_pooled is not None conditioning_data = ConditioningFieldData( conditionings=[ SDXLConditioningInfo( diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 37954c1097..580ee08562 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -23,7 +23,7 @@ from controlnet_aux import ( ) from controlnet_aux.util import HWC3, ade_palette from PIL import Image -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import BaseModel, Field, field_validator, model_validator from invokeai.app.invocations.fields import ( FieldDescriptions, @@ -60,10 +60,7 @@ CONTROLNET_RESIZE_VALUES = Literal[ class ControlNetModelField(BaseModel): """ControlNet model field""" - model_name: str = Field(description="Name of the ControlNet model") - base_model: BaseModelType = Field(description="Base model") - - model_config = ConfigDict(protected_namespaces=()) + key: str = Field(description="Model config record key for the ControlNet model") class ControlField(BaseModel): diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index 845fcfa284..700b285a45 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -2,7 +2,8 @@ import os from builtins import float from typing import List, Union -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import BaseModel, Field, field_validator, model_validator +from typing_extensions import Self from invokeai.app.invocations.baseinvocation import ( BaseInvocation, @@ -18,18 +19,13 @@ from invokeai.backend.model_management.models.base import BaseModelType, ModelTy from invokeai.backend.model_management.models.ip_adapter import get_ip_adapter_image_encoder_model_id +# LS: Consider moving these two classes into model.py class IPAdapterModelField(BaseModel): - model_name: str = Field(description="Name of the IP-Adapter model") - base_model: BaseModelType = Field(description="Base model") - - model_config = ConfigDict(protected_namespaces=()) + key: str = Field(description="Key to the IP-Adapter model") class CLIPVisionModelField(BaseModel): - model_name: str = Field(description="Name of the CLIP Vision image encoder model") - base_model: BaseModelType = Field(description="Base model (usually 'Any')") - - model_config = ConfigDict(protected_namespaces=()) + key: str = Field(description="Key to the CLIP Vision image encoder model") class IPAdapterField(BaseModel): @@ -46,16 +42,26 @@ class IPAdapterField(BaseModel): @field_validator("weight") @classmethod - def validate_ip_adapter_weight(cls, v): + def validate_ip_adapter_weight(cls, v: float) -> float: validate_weights(v) return v @model_validator(mode="after") - def validate_begin_end_step_percent(self): + def validate_begin_end_step_percent(self) -> Self: validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self +def get_ip_adapter_image_encoder_model_id(model_path: str): + """Read the ID of the image encoder associated with the IP-Adapter at `model_path`.""" + image_encoder_config_file = os.path.join(model_path, "image_encoder.txt") + + with open(image_encoder_config_file, "r") as f: + image_encoder_model = f.readline().strip() + + return image_encoder_model + + @invocation_output("ip_adapter_output") class IPAdapterOutput(BaseInvocationOutput): # Outputs @@ -84,33 +90,36 @@ class IPAdapterInvocation(BaseInvocation): @field_validator("weight") @classmethod - def validate_ip_adapter_weight(cls, v): + def validate_ip_adapter_weight(cls, v: float) -> float: validate_weights(v) return v @model_validator(mode="after") - def validate_begin_end_step_percent(self): + def validate_begin_end_step_percent(self) -> Self: validate_begin_end_step(self.begin_step_percent, self.end_step_percent) return self def invoke(self, context: InvocationContext) -> IPAdapterOutput: # Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model. - ip_adapter_info = context.models.get_info( - self.ip_adapter_model.model_name, self.ip_adapter_model.base_model, ModelType.IPAdapter - ) + ip_adapter_info = context.services.model_records.get_model(self.ip_adapter_model.key) # HACK(ryand): This is bad for a couple of reasons: 1) we are bypassing the model manager to read the model # directly, and 2) we are reading from disk every time this invocation is called without caching the result. # A better solution would be to store the image encoder model reference in the IP-Adapter model info, but this # is currently messy due to differences between how the model info is generated when installing a model from # disk vs. downloading the model. + # TODO (LS): Fix the issue above by: + # 1. Change IPAdapterConfig definition to include a field for the repo_id of the image encoder model. + # 2. Update probe.py to read `image_encoder.txt` and store it in the config. + # 3. Change below to get the image encoder from the configuration record. image_encoder_model_id = get_ip_adapter_image_encoder_model_id( - os.path.join(context.config.get().models_path, ip_adapter_info["path"]) + os.path.join(context.services.configuration.get_config().models_path, ip_adapter_info.path) ) image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip() - image_encoder_model = CLIPVisionModelField( - model_name=image_encoder_model_name, - base_model=BaseModelType.Any, + image_encoder_models = context.services.model_records.search_by_attr( + model_name=image_encoder_model_name, base_model=BaseModelType.Any, model_type=ModelType.CLIPVision ) + assert len(image_encoder_models) == 1 + image_encoder_model = CLIPVisionModelField(key=image_encoder_models[0].key) return IPAdapterOutput( ip_adapter=IPAdapterField( image=self.image, diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 69e3f055ca..063b23fa58 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -3,13 +3,13 @@ import math from contextlib import ExitStack from functools import singledispatchmethod -from typing import List, Literal, Optional, Union +from typing import Iterator, List, Literal, Optional, Tuple, Union import einops import numpy as np import torch import torchvision.transforms as T -from diffusers import AutoencoderKL, AutoencoderTiny +from diffusers import AutoencoderKL, AutoencoderTiny, UNet2DConditionModel from diffusers.image_processor import VaeImageProcessor from diffusers.models.adapter import T2IAdapter from diffusers.models.attention_processor import ( @@ -46,14 +46,13 @@ from invokeai.app.invocations.primitives import ( from invokeai.app.invocations.t2i_adapter import T2IAdapterField from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.controlnet_utils import prepare_control_image +from invokeai.backend.embeddings.model_patcher import ModelPatcher from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus -from invokeai.backend.model_management.models import ModelType, SilenceWarnings +from invokeai.backend.model_manager import AnyModel, BaseModelType +from invokeai.backend.stable_diffusion import PipelineIntermediateState, set_seamless from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningData, IPAdapterConditioningInfo +from invokeai.backend.util.silence_warnings import SilenceWarnings -from ...backend.model_management.lora import ModelPatcher -from ...backend.model_management.models import BaseModelType -from ...backend.model_management.seamless import set_seamless -from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.stable_diffusion.diffusers_pipeline import ( ControlNetData, IPAdapterData, @@ -149,7 +148,10 @@ class CreateDenoiseMaskInvocation(BaseInvocation): ) if image is not None: - vae_info = context.models.load(**self.vae.vae.model_dump()) + vae_info = context.services.model_records.load_model( + **self.vae.vae.model_dump(), + context=context, + ) img_mask = tv_resize(mask, image.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) masked_image = image * torch.where(img_mask < 0.5, 0.0, 1.0) @@ -175,7 +177,10 @@ def get_scheduler( seed: int, ) -> Scheduler: scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP["ddim"]) - orig_scheduler_info = context.models.load(**scheduler_info.model_dump()) + orig_scheduler_info = context.services.model_records.load_model( + **scheduler_info.model_dump(), + context=context, + ) with orig_scheduler_info as orig_scheduler: scheduler_config = orig_scheduler.config @@ -389,10 +394,9 @@ class DenoiseLatentsInvocation(BaseInvocation): controlnet_data = [] for control_info in control_list: control_model = exit_stack.enter_context( - context.models.load( - model_name=control_info.control_model.model_name, - model_type=ModelType.ControlNet, - base_model=control_info.control_model.base_model, + context.services.model_records.load_model( + key=control_info.control_model.key, + context=context, ) ) @@ -456,17 +460,15 @@ class DenoiseLatentsInvocation(BaseInvocation): conditioning_data.ip_adapter_conditioning = [] for single_ip_adapter in ip_adapter: ip_adapter_model: Union[IPAdapter, IPAdapterPlus] = exit_stack.enter_context( - context.models.load( - model_name=single_ip_adapter.ip_adapter_model.model_name, - model_type=ModelType.IPAdapter, - base_model=single_ip_adapter.ip_adapter_model.base_model, + context.services.model_records.load_model( + key=single_ip_adapter.ip_adapter_model.key, + context=context, ) ) - image_encoder_model_info = context.models.load( - model_name=single_ip_adapter.image_encoder_model.model_name, - model_type=ModelType.CLIPVision, - base_model=single_ip_adapter.image_encoder_model.base_model, + image_encoder_model_info = context.services.model_records.load_model( + key=single_ip_adapter.image_encoder_model.key, + context=context, ) # `single_ip_adapter.image` could be a list or a single ImageField. Normalize to a list here. @@ -518,10 +520,9 @@ class DenoiseLatentsInvocation(BaseInvocation): t2i_adapter_data = [] for t2i_adapter_field in t2i_adapter: - t2i_adapter_model_info = context.models.load( - model_name=t2i_adapter_field.t2i_adapter_model.model_name, - model_type=ModelType.T2IAdapter, - base_model=t2i_adapter_field.t2i_adapter_model.base_model, + t2i_adapter_model_info = context.services.model_records.load_model( + key=t2i_adapter_field.t2i_adapter_model.key, + context=context, ) image = context.images.get_pil(t2i_adapter_field.image.image_name) @@ -556,7 +557,7 @@ class DenoiseLatentsInvocation(BaseInvocation): do_classifier_free_guidance=False, width=t2i_input_width, height=t2i_input_height, - num_channels=t2i_adapter_model.config.in_channels, + num_channels=t2i_adapter_model.config["in_channels"], # mypy treats this as a FrozenDict device=t2i_adapter_model.device, dtype=t2i_adapter_model.dtype, resize_mode=t2i_adapter_field.resize_mode, @@ -662,22 +663,30 @@ class DenoiseLatentsInvocation(BaseInvocation): def step_callback(state: PipelineIntermediateState): context.util.sd_step_callback(state, self.unet.unet.base_model) - def _lora_loader(): + def _lora_loader() -> Iterator[Tuple[AnyModel, float]]: for lora in self.unet.loras: - lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) - yield (lora_info.context.model, lora.weight) + lora_info = context.services.model_records.load_model( + **lora.model_dump(exclude={"weight"}), + context=context, + ) + yield (lora_info.model, lora.weight) del lora_info return - unet_info = context.models.load(**self.unet.unet.model_dump()) + unet_info = context.services.model_records.load_model( + **self.unet.unet.model_dump(), + context=context, + ) + assert isinstance(unet_info.model, UNet2DConditionModel) with ( ExitStack() as exit_stack, - ModelPatcher.apply_freeu(unet_info.context.model, self.unet.freeu_config), - set_seamless(unet_info.context.model, self.unet.seamless_axes), + ModelPatcher.apply_freeu(unet_info.model, self.unet.freeu_config), + set_seamless(unet_info.model, self.unet.seamless_axes), # FIXME unet_info as unet, # Apply the LoRA after unet has been moved to its target device for faster patching. ModelPatcher.apply_lora_unet(unet, _lora_loader()), ): + assert isinstance(unet, torch.Tensor) latents = latents.to(device=unet.device, dtype=unet.dtype) if noise is not None: noise = noise.to(device=unet.device, dtype=unet.dtype) @@ -774,9 +783,13 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): def invoke(self, context: InvocationContext) -> ImageOutput: latents = context.tensors.load(self.latents.latents_name) - vae_info = context.models.load(**self.vae.vae.model_dump()) + vae_info = context.services.model_records.load_model( + **self.vae.vae.model_dump(), + context=context, + ) - with set_seamless(vae_info.context.model, self.vae.seamless_axes), vae_info as vae: + with set_seamless(vae_info.model, self.vae.seamless_axes), vae_info as vae: + assert isinstance(vae, torch.Tensor) latents = latents.to(vae.device) if self.fp32: vae.to(dtype=torch.float32) @@ -995,7 +1008,10 @@ class ImageToLatentsInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> LatentsOutput: image = context.images.get_pil(self.image.image_name) - vae_info = context.models.load(**self.vae.vae.model_dump()) + vae_info = context.services.model_records.load_model( + **self.vae.vae.model_dump(), + context=context, + ) image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB")) if image_tensor.dim() == 3: diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index 6a1fd6d36b..e2ea744283 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -1,13 +1,13 @@ import copy from typing import List, Optional -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, Field from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.shared.models import FreeUConfig -from ...backend.model_management import BaseModelType, ModelType, SubModelType +from ...backend.model_manager import SubModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, @@ -17,13 +17,9 @@ from .baseinvocation import ( class ModelInfo(BaseModel): - model_name: str = Field(description="Info to load submodel") - base_model: BaseModelType = Field(description="Base model") - model_type: ModelType = Field(description="Info to load submodel") + key: str = Field(description="Info to load submodel") submodel: Optional[SubModelType] = Field(default=None, description="Info to load submodel") - model_config = ConfigDict(protected_namespaces=()) - class LoraInfo(ModelInfo): weight: float = Field(description="Lora's weight which to use when apply to model") @@ -52,7 +48,7 @@ class VaeField(BaseModel): @invocation_output("unet_output") class UNetOutput(BaseInvocationOutput): - """Base class for invocations that output a UNet field""" + """Base class for invocations that output a UNet field.""" unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet") @@ -81,20 +77,13 @@ class ModelLoaderOutput(UNetOutput, CLIPOutput, VAEOutput): class MainModelField(BaseModel): """Main model field""" - model_name: str = Field(description="Name of the model") - base_model: BaseModelType = Field(description="Base model") - model_type: ModelType = Field(description="Model Type") - - model_config = ConfigDict(protected_namespaces=()) + key: str = Field(description="Model key") class LoRAModelField(BaseModel): """LoRA model field""" - model_name: str = Field(description="Name of the LoRA model") - base_model: BaseModelType = Field(description="Base model") - - model_config = ConfigDict(protected_namespaces=()) + key: str = Field(description="LoRA model key") @invocation( @@ -111,74 +100,31 @@ class MainModelLoaderInvocation(BaseInvocation): # TODO: precision? def invoke(self, context: InvocationContext) -> ModelLoaderOutput: - base_model = self.model.base_model - model_name = self.model.model_name - model_type = ModelType.Main + key = self.model.key # TODO: not found exceptions - if not context.models.exists( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ): - raise Exception(f"Unknown {base_model} {model_type} model: {model_name}") - - """ - if not context.services.model_manager.model_exists( - model_name=self.model_name, - model_type=SDModelType.Diffusers, - submodel=SDModelType.Tokenizer, - ): - raise Exception( - f"Failed to find tokenizer submodel in {self.model_name}! Check if model corrupted" - ) - - if not context.services.model_manager.model_exists( - model_name=self.model_name, - model_type=SDModelType.Diffusers, - submodel=SDModelType.TextEncoder, - ): - raise Exception( - f"Failed to find text_encoder submodel in {self.model_name}! Check if model corrupted" - ) - - if not context.services.model_manager.model_exists( - model_name=self.model_name, - model_type=SDModelType.Diffusers, - submodel=SDModelType.UNet, - ): - raise Exception( - f"Failed to find unet submodel from {self.model_name}! Check if model corrupted" - ) - """ + if not context.services.model_records.exists(key): + raise Exception(f"Unknown model {key}") return ModelLoaderOutput( unet=UNetField( unet=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=key, submodel=SubModelType.UNet, ), scheduler=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=key, submodel=SubModelType.Scheduler, ), loras=[], ), clip=ClipField( tokenizer=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=key, submodel=SubModelType.Tokenizer, ), text_encoder=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=key, submodel=SubModelType.TextEncoder, ), loras=[], @@ -186,9 +132,7 @@ class MainModelLoaderInvocation(BaseInvocation): ), vae=VaeField( vae=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=key, submodel=SubModelType.Vae, ), ), @@ -226,21 +170,16 @@ class LoraLoaderInvocation(BaseInvocation): if self.lora is None: raise Exception("No LoRA provided") - base_model = self.lora.base_model - lora_name = self.lora.model_name + lora_key = self.lora.key - if not context.models.exists( - base_model=base_model, - model_name=lora_name, - model_type=ModelType.Lora, - ): - raise Exception(f"Unkown lora name: {lora_name}!") + if not context.services.model_records.exists(lora_key): + raise Exception(f"Unkown lora: {lora_key}!") - if self.unet is not None and any(lora.model_name == lora_name for lora in self.unet.loras): - raise Exception(f'Lora "{lora_name}" already applied to unet') + if self.unet is not None and any(lora.key == lora_key for lora in self.unet.loras): + raise Exception(f'Lora "{lora_key}" already applied to unet') - if self.clip is not None and any(lora.model_name == lora_name for lora in self.clip.loras): - raise Exception(f'Lora "{lora_name}" already applied to clip') + if self.clip is not None and any(lora.key == lora_key for lora in self.clip.loras): + raise Exception(f'Lora "{lora_key}" already applied to clip') output = LoraLoaderOutput() @@ -248,9 +187,7 @@ class LoraLoaderInvocation(BaseInvocation): output.unet = copy.deepcopy(self.unet) output.unet.loras.append( LoraInfo( - base_model=base_model, - model_name=lora_name, - model_type=ModelType.Lora, + key=lora_key, submodel=None, weight=self.weight, ) @@ -260,9 +197,7 @@ class LoraLoaderInvocation(BaseInvocation): output.clip = copy.deepcopy(self.clip) output.clip.loras.append( LoraInfo( - base_model=base_model, - model_name=lora_name, - model_type=ModelType.Lora, + key=lora_key, submodel=None, weight=self.weight, ) @@ -315,24 +250,19 @@ class SDXLLoraLoaderInvocation(BaseInvocation): if self.lora is None: raise Exception("No LoRA provided") - base_model = self.lora.base_model - lora_name = self.lora.model_name + lora_key = self.lora.key - if not context.models.exists( - base_model=base_model, - model_name=lora_name, - model_type=ModelType.Lora, - ): - raise Exception(f"Unknown lora name: {lora_name}!") + if not context.services.model_records.exists(lora_key): + raise Exception(f"Unknown lora: {lora_key}!") - if self.unet is not None and any(lora.model_name == lora_name for lora in self.unet.loras): - raise Exception(f'Lora "{lora_name}" already applied to unet') + if self.unet is not None and any(lora.key == lora_key for lora in self.unet.loras): + raise Exception(f'Lora "{lora_key}" already applied to unet') - if self.clip is not None and any(lora.model_name == lora_name for lora in self.clip.loras): - raise Exception(f'Lora "{lora_name}" already applied to clip') + if self.clip is not None and any(lora.key == lora_key for lora in self.clip.loras): + raise Exception(f'Lora "{lora_key}" already applied to clip') - if self.clip2 is not None and any(lora.model_name == lora_name for lora in self.clip2.loras): - raise Exception(f'Lora "{lora_name}" already applied to clip2') + if self.clip2 is not None and any(lora.key == lora_key for lora in self.clip2.loras): + raise Exception(f'Lora "{lora_key}" already applied to clip2') output = SDXLLoraLoaderOutput() @@ -340,9 +270,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): output.unet = copy.deepcopy(self.unet) output.unet.loras.append( LoraInfo( - base_model=base_model, - model_name=lora_name, - model_type=ModelType.Lora, + key=lora_key, submodel=None, weight=self.weight, ) @@ -352,9 +280,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): output.clip = copy.deepcopy(self.clip) output.clip.loras.append( LoraInfo( - base_model=base_model, - model_name=lora_name, - model_type=ModelType.Lora, + key=lora_key, submodel=None, weight=self.weight, ) @@ -364,9 +290,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): output.clip2 = copy.deepcopy(self.clip2) output.clip2.loras.append( LoraInfo( - base_model=base_model, - model_name=lora_name, - model_type=ModelType.Lora, + key=lora_key, submodel=None, weight=self.weight, ) @@ -378,10 +302,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): class VAEModelField(BaseModel): """Vae model field""" - model_name: str = Field(description="Name of the model") - base_model: BaseModelType = Field(description="Base model") - - model_config = ConfigDict(protected_namespaces=()) + key: str = Field(description="Model's key") @invocation("vae_loader", title="VAE", tags=["vae", "model"], category="model", version="1.0.1") @@ -395,25 +316,12 @@ class VaeLoaderInvocation(BaseInvocation): ) def invoke(self, context: InvocationContext) -> VAEOutput: - base_model = self.vae_model.base_model - model_name = self.vae_model.model_name - model_type = ModelType.Vae + key = self.vae_model.key - if not context.models.exists( - base_model=base_model, - model_name=model_name, - model_type=model_type, - ): - raise Exception(f"Unkown vae name: {model_name}!") - return VAEOutput( - vae=VaeField( - vae=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ) - ) - ) + if not context.services.model_records.exists(key): + raise Exception(f"Unkown vae: {key}!") + + return VAEOutput(vae=VaeField(vae=ModelInfo(key=key))) @invocation_output("seamless_output") diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 8d51674a04..633a6477fd 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -1,7 +1,7 @@ from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType from invokeai.app.services.shared.invocation_context import InvocationContext +from invokeai.backend.model_manager import SubModelType -from ...backend.model_management import ModelType, SubModelType from .baseinvocation import ( BaseInvocation, BaseInvocationOutput, @@ -40,45 +40,31 @@ class SDXLModelLoaderInvocation(BaseInvocation): # TODO: precision? def invoke(self, context: InvocationContext) -> SDXLModelLoaderOutput: - base_model = self.model.base_model - model_name = self.model.model_name - model_type = ModelType.Main + model_key = self.model.key # TODO: not found exceptions - if not context.models.exists( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ): - raise Exception(f"Unknown {base_model} {model_type} model: {model_name}") + if not context.services.model_records.exists(model_key): + raise Exception(f"Unknown model: {model_key}") return SDXLModelLoaderOutput( unet=UNetField( unet=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.UNet, ), scheduler=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.Scheduler, ), loras=[], ), clip=ClipField( tokenizer=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.Tokenizer, ), text_encoder=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.TextEncoder, ), loras=[], @@ -86,15 +72,11 @@ class SDXLModelLoaderInvocation(BaseInvocation): ), clip2=ClipField( tokenizer=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.Tokenizer2, ), text_encoder=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.TextEncoder2, ), loras=[], @@ -102,9 +84,7 @@ class SDXLModelLoaderInvocation(BaseInvocation): ), vae=VaeField( vae=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.Vae, ), ), @@ -129,45 +109,31 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation): # TODO: precision? def invoke(self, context: InvocationContext) -> SDXLRefinerModelLoaderOutput: - base_model = self.model.base_model - model_name = self.model.model_name - model_type = ModelType.Main + model_key = self.model.key # TODO: not found exceptions - if not context.models.exists( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ): - raise Exception(f"Unknown {base_model} {model_type} model: {model_name}") + if not context.services.model_records.exists(model_key): + raise Exception(f"Unknown model: {model_key}") return SDXLRefinerModelLoaderOutput( unet=UNetField( unet=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.UNet, ), scheduler=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.Scheduler, ), loras=[], ), clip2=ClipField( tokenizer=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.Tokenizer2, ), text_encoder=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.TextEncoder2, ), loras=[], @@ -175,9 +141,7 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation): ), vae=VaeField( vae=ModelInfo( - model_name=model_name, - base_model=base_model, - model_type=model_type, + key=model_key, submodel=SubModelType.Vae, ), ), diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py index 0f4fe66ada..0f1e251bb3 100644 --- a/invokeai/app/invocations/t2i_adapter.py +++ b/invokeai/app/invocations/t2i_adapter.py @@ -1,6 +1,6 @@ from typing import Union -from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import BaseModel, Field, field_validator, model_validator from invokeai.app.invocations.baseinvocation import ( BaseInvocation, @@ -12,14 +12,10 @@ from invokeai.app.invocations.controlnet_image_processors import CONTROLNET_RESI from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.app.services.shared.invocation_context import InvocationContext -from invokeai.backend.model_management.models.base import BaseModelType class T2IAdapterModelField(BaseModel): - model_name: str = Field(description="Name of the T2I-Adapter model") - base_model: BaseModelType = Field(description="Base model") - - model_config = ConfigDict(protected_namespaces=()) + key: str = Field(description="Model record key for the T2I-Adapter model") class T2IAdapterField(BaseModel): diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 6b441efc2b..90d9068b88 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -11,8 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import ( SessionQueueStatus, ) from invokeai.app.util.misc import get_timestamp -from invokeai.backend.model_management.model_manager import LoadedModelInfo -from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType +from invokeai.backend.model_manager import AnyModelConfig class EventServiceBase: @@ -171,10 +170,7 @@ class EventServiceBase: queue_item_id: int, queue_batch_id: str, graph_execution_state_id: str, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: SubModelType, + model_config: AnyModelConfig, ) -> None: """Emitted when a model is requested""" self.__emit_queue_event( @@ -184,10 +180,7 @@ class EventServiceBase: "queue_item_id": queue_item_id, "queue_batch_id": queue_batch_id, "graph_execution_state_id": graph_execution_state_id, - "model_name": model_name, - "base_model": base_model, - "model_type": model_type, - "submodel": submodel, + "model_config": model_config.model_dump(), }, ) @@ -197,11 +190,7 @@ class EventServiceBase: queue_item_id: int, queue_batch_id: str, graph_execution_state_id: str, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: SubModelType, - loaded_model_info: LoadedModelInfo, + model_config: AnyModelConfig, ) -> None: """Emitted when a model is correctly loaded (returns model info)""" self.__emit_queue_event( @@ -211,13 +200,7 @@ class EventServiceBase: "queue_item_id": queue_item_id, "queue_batch_id": queue_batch_id, "graph_execution_state_id": graph_execution_state_id, - "model_name": model_name, - "base_model": base_model, - "model_type": model_type, - "submodel": submodel, - "hash": loaded_model_info.hash, - "location": str(loaded_model_info.location), - "precision": str(loaded_model_info.precision), + "model_config": model_config.model_dump(), }, ) diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index be58aaad2d..0c63b545ff 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -2,6 +2,7 @@ import json import time from contextlib import contextmanager from pathlib import Path +from typing import Iterator import psutil import torch @@ -10,7 +11,7 @@ import invokeai.backend.util.logging as logger from invokeai.app.invocations.baseinvocation import BaseInvocation from invokeai.app.services.invoker import Invoker from invokeai.app.services.item_storage.item_storage_common import ItemNotFoundError -from invokeai.backend.model_management.model_cache import CacheStats +from invokeai.backend.model_manager.load.model_cache import CacheStats from .invocation_stats_base import InvocationStatsServiceBase from .invocation_stats_common import ( @@ -41,7 +42,10 @@ class InvocationStatsService(InvocationStatsServiceBase): self._invoker = invoker @contextmanager - def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str): + def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str) -> Iterator[None]: + services = self._invoker.services + if services.model_records is None or services.model_records.loader is None: + yield None if not self._stats.get(graph_execution_state_id): # First time we're seeing this graph_execution_state_id. self._stats[graph_execution_state_id] = GraphExecutionStats() @@ -55,8 +59,10 @@ class InvocationStatsService(InvocationStatsServiceBase): start_ram = psutil.Process().memory_info().rss if torch.cuda.is_available(): torch.cuda.reset_peak_memory_stats() - if self._invoker.services.model_manager: - self._invoker.services.model_manager.collect_cache_stats(self._cache_stats[graph_execution_state_id]) + + # TO DO [LS]: clean up loader service - shouldn't be an attribute of model records + assert services.model_records.loader is not None + services.model_records.loader.ram_cache.stats = self._cache_stats[graph_execution_state_id] try: # Let the invocation run. @@ -73,7 +79,7 @@ class InvocationStatsService(InvocationStatsServiceBase): ) self._stats[graph_execution_state_id].add_node_execution_stats(node_stats) - def _prune_stale_stats(self): + def _prune_stale_stats(self) -> None: """Check all graphs being tracked and prune any that have completed/errored. This shouldn't be necessary, but we don't have totally robust upstream handling of graph completions/errors, so diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py index 42e3c8f83a..e00dd4169d 100644 --- a/invokeai/app/services/model_records/model_records_base.py +++ b/invokeai/app/services/model_records/model_records_base.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union from pydantic import BaseModel, Field +from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.app.services.shared.pagination import PaginatedResults from invokeai.backend.model_manager import ( AnyModelConfig, @@ -19,6 +20,7 @@ from invokeai.backend.model_manager import ( ModelType, SubModelType, ) +from invokeai.backend.model_manager.load import AnyModelLoader from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore @@ -110,12 +112,45 @@ class ModelRecordServiceBase(ABC): pass @abstractmethod - def load_model(self, key: str, submodel_type: Optional[SubModelType]) -> LoadedModel: + def load_model( + self, + key: str, + submodel: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: """ Load the indicated model into memory and return a LoadedModel object. :param key: Key of model config to be fetched. - :param submodel_type: For main (pipeline models), the submodel to fetch + :param submodel: For main (pipeline models), the submodel to fetch + :param context: Invocation context, used for event issuing. + + Exceptions: UnknownModelException -- model with this key not known + NotImplementedException -- a model loader was not provided at initialization time + """ + pass + + @abstractmethod + def load_model_by_attr( + self, + model_name: str, + base_model: BaseModelType, + model_type: ModelType, + submodel: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Load the indicated model into memory and return a LoadedModel object. + + This is provided for API compatability with the get_model() method + in the original model manager. However, note that LoadedModel is + not the same as the original ModelInfo that ws returned. + + :param model_name: Key of model config to be fetched. + :param base_model: Base model + :param model_type: Type of the model + :param submodel: For main (pipeline models), the submodel to fetch + :param context: The invocation context. Exceptions: UnknownModelException -- model with this key not known NotImplementedException -- a model loader was not provided at initialization time @@ -166,7 +201,7 @@ class ModelRecordServiceBase(ABC): @abstractmethod def exists(self, key: str) -> bool: """ - Return True if a model with the indicated key exists in the databse. + Return True if a model with the indicated key exists in the database. :param key: Unique key for the model to be deleted """ @@ -209,6 +244,12 @@ class ModelRecordServiceBase(ABC): """ pass + @property + @abstractmethod + def loader(self) -> Optional[AnyModelLoader]: + """Return the model loader used by this instance.""" + pass + def all_models(self) -> List[AnyModelConfig]: """Return all the model configs in the database.""" return self.search_by_attr() diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index b50cd17a75..28a77b1b1a 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -46,6 +46,8 @@ from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple, Union +from invokeai.app.invocations.baseinvocation import InvocationContext +from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException from invokeai.app.services.shared.pagination import PaginatedResults from invokeai.backend.model_manager.config import ( AnyModelConfig, @@ -88,6 +90,11 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): """Return the underlying database.""" return self._db + @property + def loader(self) -> Optional[AnyModelLoader]: + """Return the model loader used by this instance.""" + return self._loader + def add_model(self, key: str, config: Union[Dict[str, Any], AnyModelConfig]) -> AnyModelConfig: """ Add a model to the database. @@ -213,20 +220,73 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): model = ModelConfigFactory.make_config(json.loads(rows[0]), timestamp=rows[1]) return model - def load_model(self, key: str, submodel_type: Optional[SubModelType]) -> LoadedModel: + def load_model( + self, + key: str, + submodel: Optional[SubModelType], + context: Optional[InvocationContext] = None, + ) -> LoadedModel: """ Load the indicated model into memory and return a LoadedModel object. :param key: Key of model config to be fetched. - :param submodel_type: For main (pipeline models), the submodel to fetch. + :param submodel: For main (pipeline models), the submodel to fetch. + :param context: Invocation context used for event reporting Exceptions: UnknownModelException -- model with this key not known NotImplementedException -- a model loader was not provided at initialization time """ if not self._loader: raise NotImplementedError(f"Class {self.__class__} was not initialized with a model loader") + # we can emit model loading events if we are executing with access to the invocation context + model_config = self.get_model(key) - return self._loader.load_model(model_config, submodel_type) + if context: + self._emit_load_event( + context=context, + model_config=model_config, + ) + loaded_model = self._loader.load_model(model_config, submodel) + if context: + self._emit_load_event( + context=context, + model_config=model_config, + loaded=True, + ) + return loaded_model + + def load_model_by_attr( + self, + model_name: str, + base_model: BaseModelType, + model_type: ModelType, + submodel: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Load the indicated model into memory and return a LoadedModel object. + + This is provided for API compatability with the get_model() method + in the original model manager. However, note that LoadedModel is + not the same as the original ModelInfo that ws returned. + + :param model_name: Key of model config to be fetched. + :param base_model: Base model + :param model_type: Type of the model + :param submodel: For main (pipeline models), the submodel to fetch + :param context: The invocation context. + + Exceptions: UnknownModelException -- model with this key not known + NotImplementedException -- a model loader was not provided at initialization time + ValueError -- more than one model matches this combination + """ + configs = self.search_by_attr(model_name, base_model, model_type) + if len(configs) == 0: + raise UnknownModelException(f"{base_model}/{model_type}/{model_name}: Unknown model") + elif len(configs) > 1: + raise ValueError(f"{base_model}/{model_type}/{model_name}: More than one model matches.") + else: + return self.load_model(configs[0].key, submodel) def exists(self, key: str) -> bool: """ @@ -416,3 +476,29 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): return PaginatedResults( page=page, pages=ceil(total / per_page), per_page=per_page, total=total, items=items ) + + def _emit_load_event( + self, + context: InvocationContext, + model_config: AnyModelConfig, + loaded: Optional[bool] = False, + ) -> None: + if context.services.queue.is_canceled(context.graph_execution_state_id): + raise CanceledException() + + if not loaded: + context.services.events.emit_model_load_started( + queue_id=context.queue_id, + queue_item_id=context.queue_item_id, + queue_batch_id=context.queue_batch_id, + graph_execution_state_id=context.graph_execution_state_id, + model_config=model_config, + ) + else: + context.services.events.emit_model_load_completed( + queue_id=context.queue_id, + queue_item_id=context.queue_item_id, + queue_batch_id=context.queue_batch_id, + graph_execution_state_id=context.graph_execution_state_id, + model_config=model_config, + ) diff --git a/invokeai/backend/embeddings/__init__.py b/invokeai/backend/embeddings/__init__.py new file mode 100644 index 0000000000..46ead533c4 --- /dev/null +++ b/invokeai/backend/embeddings/__init__.py @@ -0,0 +1,4 @@ +"""Initialization file for invokeai.backend.embeddings modules.""" + +# from .model_patcher import ModelPatcher +# __all__ = ["ModelPatcher"] diff --git a/invokeai/backend/embeddings/embedding_base.py b/invokeai/backend/embeddings/embedding_base.py new file mode 100644 index 0000000000..5e752a29e1 --- /dev/null +++ b/invokeai/backend/embeddings/embedding_base.py @@ -0,0 +1,12 @@ +"""Base class for LoRA and Textual Inversion models. + +The EmbeddingRaw class is the base class of LoRAModelRaw and TextualInversionModelRaw, +and is used for type checking of calls to the model patcher. + +The use of "Raw" here is a historical artifact, and carried forward in +order to avoid confusion. +""" + + +class EmbeddingModelRaw: + """Base class for LoRA and Textual Inversion models.""" diff --git a/invokeai/backend/embeddings/lora.py b/invokeai/backend/embeddings/lora.py index 9a59a97708..3c7ef074ef 100644 --- a/invokeai/backend/embeddings/lora.py +++ b/invokeai/backend/embeddings/lora.py @@ -11,6 +11,8 @@ from typing_extensions import Self from invokeai.backend.model_manager import BaseModelType +from .embedding_base import EmbeddingModelRaw + class LoRALayerBase: # rank: Optional[int] @@ -317,7 +319,7 @@ class FullLayer(LoRALayerBase): self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None, - ): + ) -> None: super().to(device=device, dtype=dtype) self.weight = self.weight.to(device=device, dtype=dtype) @@ -367,7 +369,7 @@ AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer] # TODO: rename all methods used in model logic with Info postfix and remove here Raw postfix -class LoRAModelRaw: # (torch.nn.Module): +class LoRAModelRaw(EmbeddingModelRaw): # (torch.nn.Module): _name: str layers: Dict[str, AnyLoRALayer] @@ -471,16 +473,16 @@ class LoRAModelRaw: # (torch.nn.Module): file_path = Path(file_path) model = cls( - name=file_path.stem, # TODO: + name=file_path.stem, layers={}, ) if file_path.suffix == ".safetensors": - state_dict = load_file(file_path.absolute().as_posix(), device="cpu") + sd = load_file(file_path.absolute().as_posix(), device="cpu") else: - state_dict = torch.load(file_path, map_location="cpu") + sd = torch.load(file_path, map_location="cpu") - state_dict = cls._group_state(state_dict) + state_dict = cls._group_state(sd) if base_model == BaseModelType.StableDiffusionXL: state_dict = cls._convert_sdxl_keys_to_diffusers_format(state_dict) diff --git a/invokeai/backend/embeddings/model_patcher.py b/invokeai/backend/embeddings/model_patcher.py index 6d73235197..4725181b8e 100644 --- a/invokeai/backend/embeddings/model_patcher.py +++ b/invokeai/backend/embeddings/model_patcher.py @@ -4,22 +4,20 @@ from __future__ import annotations import pickle from contextlib import contextmanager -from pathlib import Path -from typing import Any, Dict, Generator, List, Optional, Tuple, Union +from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np import torch -from compel.embeddings_provider import BaseTextualInversionManager -from diffusers import ModelMixin, OnnxRuntimeModel, UNet2DConditionModel -from safetensors.torch import load_file +from diffusers import OnnxRuntimeModel, UNet2DConditionModel from transformers import CLIPTextModel, CLIPTokenizer -from typing_extensions import Self from invokeai.app.shared.models import FreeUConfig +from invokeai.backend.model_manager import AnyModel from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel from .lora import LoRAModelRaw +from .textual_inversion import TextualInversionManager, TextualInversionModelRaw """ loras = [ @@ -67,7 +65,7 @@ class ModelPatcher: cls, unet: UNet2DConditionModel, loras: List[Tuple[LoRAModelRaw, float]], - ) -> Generator[None, None, None]: + ) -> None: with cls.apply_lora(unet, loras, "lora_unet_"): yield @@ -76,8 +74,8 @@ class ModelPatcher: def apply_lora_text_encoder( cls, text_encoder: CLIPTextModel, - loras: List[Tuple[LoRAModelRaw, float]], - ): + loras: Iterator[Tuple[LoRAModelRaw, float]], + ) -> None: with cls.apply_lora(text_encoder, loras, "lora_te_"): yield @@ -87,7 +85,7 @@ class ModelPatcher: cls, text_encoder: CLIPTextModel, loras: List[Tuple[LoRAModelRaw, float]], - ): + ) -> None: with cls.apply_lora(text_encoder, loras, "lora_te1_"): yield @@ -97,7 +95,7 @@ class ModelPatcher: cls, text_encoder: CLIPTextModel, loras: List[Tuple[LoRAModelRaw, float]], - ): + ) -> None: with cls.apply_lora(text_encoder, loras, "lora_te2_"): yield @@ -105,10 +103,10 @@ class ModelPatcher: @contextmanager def apply_lora( cls, - model: Union[torch.nn.Module, ModelMixin, UNet2DConditionModel], - loras: List[Tuple[LoRAModelRaw, float]], + model: AnyModel, + loras: Iterator[Tuple[LoRAModelRaw, float]], prefix: str, - ) -> Generator[None, None, None]: + ) -> None: original_weights = {} try: with torch.no_grad(): @@ -125,6 +123,7 @@ class ModelPatcher: # 2. From an API perspective, there's no reason that the `ModelPatcher` should be aware of the # intricacies of Stable Diffusion key resolution. It should just expect the input LoRA # weights to have valid keys. + assert isinstance(model, torch.nn.Module) module_key, module = cls._resolve_lora_key(model, layer_key, prefix) # All of the LoRA weight calculations will be done on the same device as the module weight. @@ -170,8 +169,8 @@ class ModelPatcher: cls, tokenizer: CLIPTokenizer, text_encoder: CLIPTextModel, - ti_list: List[Tuple[str, TextualInversionModel]], - ) -> Generator[Tuple[CLIPTokenizer, TextualInversionManager], None, None]: + ti_list: List[Tuple[str, TextualInversionModelRaw]], + ) -> Iterator[Tuple[CLIPTokenizer, TextualInversionManager]]: init_tokens_count = None new_tokens_added = None @@ -201,7 +200,7 @@ class ModelPatcher: trigger += f"-!pad-{i}" return f"<{trigger}>" - def _get_ti_embedding(model_embeddings: torch.nn.Module, ti: TextualInversionModel) -> torch.Tensor: + def _get_ti_embedding(model_embeddings: torch.nn.Module, ti: TextualInversionModelRaw) -> torch.Tensor: # for SDXL models, select the embedding that matches the text encoder's dimensions if ti.embedding_2 is not None: return ( @@ -229,6 +228,7 @@ class ModelPatcher: model_embeddings = text_encoder.get_input_embeddings() for ti_name, ti in ti_list: + assert isinstance(ti, TextualInversionModelRaw) ti_embedding = _get_ti_embedding(text_encoder.get_input_embeddings(), ti) ti_tokens = [] @@ -267,7 +267,7 @@ class ModelPatcher: cls, text_encoder: CLIPTextModel, clip_skip: int, - ) -> Generator[None, None, None]: + ) -> None: skipped_layers = [] try: for _i in range(clip_skip): @@ -285,7 +285,7 @@ class ModelPatcher: cls, unet: UNet2DConditionModel, freeu_config: Optional[FreeUConfig] = None, - ) -> Generator[None, None, None]: + ) -> None: did_apply_freeu = False try: assert hasattr(unet, "enable_freeu") # mypy doesn't pick up this attribute? @@ -301,94 +301,6 @@ class ModelPatcher: unet.disable_freeu() -class TextualInversionModel: - embedding: torch.Tensor # [n, 768]|[n, 1280] - embedding_2: Optional[torch.Tensor] = None # [n, 768]|[n, 1280] - for SDXL models - - @classmethod - def from_checkpoint( - cls, - file_path: Union[str, Path], - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ) -> Self: - if not isinstance(file_path, Path): - file_path = Path(file_path) - - result = cls() # TODO: - - if file_path.suffix == ".safetensors": - state_dict = load_file(file_path.absolute().as_posix(), device="cpu") - else: - state_dict = torch.load(file_path, map_location="cpu") - - # both v1 and v2 format embeddings - # difference mostly in metadata - if "string_to_param" in state_dict: - if len(state_dict["string_to_param"]) > 1: - print( - f'Warn: Embedding "{file_path.name}" contains multiple tokens, which is not supported. The first', - " token will be used.", - ) - - result.embedding = next(iter(state_dict["string_to_param"].values())) - - # v3 (easynegative) - elif "emb_params" in state_dict: - result.embedding = state_dict["emb_params"] - - # v5(sdxl safetensors file) - elif "clip_g" in state_dict and "clip_l" in state_dict: - result.embedding = state_dict["clip_g"] - result.embedding_2 = state_dict["clip_l"] - - # v4(diffusers bin files) - else: - result.embedding = next(iter(state_dict.values())) - - if len(result.embedding.shape) == 1: - result.embedding = result.embedding.unsqueeze(0) - - if not isinstance(result.embedding, torch.Tensor): - raise ValueError(f"Invalid embeddings file: {file_path.name}") - - return result - - -# no type hints for BaseTextualInversionManager? -class TextualInversionManager(BaseTextualInversionManager): # type: ignore - pad_tokens: Dict[int, List[int]] - tokenizer: CLIPTokenizer - - def __init__(self, tokenizer: CLIPTokenizer): - self.pad_tokens = {} - self.tokenizer = tokenizer - - def expand_textual_inversion_token_ids_if_necessary(self, token_ids: list[int]) -> list[int]: - if len(self.pad_tokens) == 0: - return token_ids - - if token_ids[0] == self.tokenizer.bos_token_id: - raise ValueError("token_ids must not start with bos_token_id") - if token_ids[-1] == self.tokenizer.eos_token_id: - raise ValueError("token_ids must not end with eos_token_id") - - new_token_ids = [] - for token_id in token_ids: - new_token_ids.append(token_id) - if token_id in self.pad_tokens: - new_token_ids.extend(self.pad_tokens[token_id]) - - # Do not exceed the max model input size - # The -2 here is compensating for compensate compel.embeddings_provider.get_token_ids(), - # which first removes and then adds back the start and end tokens. - max_length = list(self.tokenizer.max_model_input_sizes.values())[0] - 2 - if len(new_token_ids) > max_length: - new_token_ids = new_token_ids[0:max_length] - - return new_token_ids - - class ONNXModelPatcher: @classmethod @contextmanager @@ -396,7 +308,7 @@ class ONNXModelPatcher: cls, unet: OnnxRuntimeModel, loras: List[Tuple[LoRAModelRaw, float]], - ) -> Generator[None, None, None]: + ) -> None: with cls.apply_lora(unet, loras, "lora_unet_"): yield @@ -406,7 +318,7 @@ class ONNXModelPatcher: cls, text_encoder: OnnxRuntimeModel, loras: List[Tuple[LoRAModelRaw, float]], - ) -> Generator[None, None, None]: + ) -> None: with cls.apply_lora(text_encoder, loras, "lora_te_"): yield @@ -419,7 +331,7 @@ class ONNXModelPatcher: model: IAIOnnxRuntimeModel, loras: List[Tuple[LoRAModelRaw, float]], prefix: str, - ) -> Generator[None, None, None]: + ) -> None: from .models.base import IAIOnnxRuntimeModel if not isinstance(model, IAIOnnxRuntimeModel): @@ -506,7 +418,7 @@ class ONNXModelPatcher: tokenizer: CLIPTokenizer, text_encoder: IAIOnnxRuntimeModel, ti_list: List[Tuple[str, Any]], - ) -> Generator[Tuple[CLIPTokenizer, TextualInversionManager], None, None]: + ) -> Iterator[Tuple[CLIPTokenizer, TextualInversionManager]]: from .models.base import IAIOnnxRuntimeModel if not isinstance(text_encoder, IAIOnnxRuntimeModel): diff --git a/invokeai/backend/embeddings/textual_inversion.py b/invokeai/backend/embeddings/textual_inversion.py new file mode 100644 index 0000000000..389edff039 --- /dev/null +++ b/invokeai/backend/embeddings/textual_inversion.py @@ -0,0 +1,100 @@ +"""Textual Inversion wrapper class.""" + +from pathlib import Path +from typing import Dict, List, Optional, Union + +import torch +from compel.embeddings_provider import BaseTextualInversionManager +from safetensors.torch import load_file +from transformers import CLIPTokenizer +from typing_extensions import Self + +from .embedding_base import EmbeddingModelRaw + + +class TextualInversionModelRaw(EmbeddingModelRaw): + embedding: torch.Tensor # [n, 768]|[n, 1280] + embedding_2: Optional[torch.Tensor] = None # [n, 768]|[n, 1280] - for SDXL models + + @classmethod + def from_checkpoint( + cls, + file_path: Union[str, Path], + device: Optional[torch.device] = None, + dtype: Optional[torch.dtype] = None, + ) -> Self: + if not isinstance(file_path, Path): + file_path = Path(file_path) + + result = cls() # TODO: + + if file_path.suffix == ".safetensors": + state_dict = load_file(file_path.absolute().as_posix(), device="cpu") + else: + state_dict = torch.load(file_path, map_location="cpu") + + # both v1 and v2 format embeddings + # difference mostly in metadata + if "string_to_param" in state_dict: + if len(state_dict["string_to_param"]) > 1: + print( + f'Warn: Embedding "{file_path.name}" contains multiple tokens, which is not supported. The first', + " token will be used.", + ) + + result.embedding = next(iter(state_dict["string_to_param"].values())) + + # v3 (easynegative) + elif "emb_params" in state_dict: + result.embedding = state_dict["emb_params"] + + # v5(sdxl safetensors file) + elif "clip_g" in state_dict and "clip_l" in state_dict: + result.embedding = state_dict["clip_g"] + result.embedding_2 = state_dict["clip_l"] + + # v4(diffusers bin files) + else: + result.embedding = next(iter(state_dict.values())) + + if len(result.embedding.shape) == 1: + result.embedding = result.embedding.unsqueeze(0) + + if not isinstance(result.embedding, torch.Tensor): + raise ValueError(f"Invalid embeddings file: {file_path.name}") + + return result + + +# no type hints for BaseTextualInversionManager? +class TextualInversionManager(BaseTextualInversionManager): # type: ignore + pad_tokens: Dict[int, List[int]] + tokenizer: CLIPTokenizer + + def __init__(self, tokenizer: CLIPTokenizer): + self.pad_tokens = {} + self.tokenizer = tokenizer + + def expand_textual_inversion_token_ids_if_necessary(self, token_ids: list[int]) -> list[int]: + if len(self.pad_tokens) == 0: + return token_ids + + if token_ids[0] == self.tokenizer.bos_token_id: + raise ValueError("token_ids must not start with bos_token_id") + if token_ids[-1] == self.tokenizer.eos_token_id: + raise ValueError("token_ids must not end with eos_token_id") + + new_token_ids = [] + for token_id in token_ids: + new_token_ids.append(token_id) + if token_id in self.pad_tokens: + new_token_ids.extend(self.pad_tokens[token_id]) + + # Do not exceed the max model input size + # The -2 here is compensating for compensate compel.embeddings_provider.get_token_ids(), + # which first removes and then adds back the start and end tokens. + max_length = list(self.tokenizer.max_model_input_sizes.values())[0] - 2 + if len(new_token_ids) > max_length: + new_token_ids = new_token_ids[0:max_length] + + return new_token_ids diff --git a/invokeai/backend/install/install_helper.py b/invokeai/backend/install/install_helper.py index 57dfadcaea..8877e33092 100644 --- a/invokeai/backend/install/install_helper.py +++ b/invokeai/backend/install/install_helper.py @@ -241,10 +241,11 @@ class InstallHelper(object): if match := re.match(f"^([^/]+/[^/]+?)(?::({variants}))?$", model_path_id_or_url): repo_id = match.group(1) repo_variant = ModelRepoVariant(match.group(2)) if match.group(2) else None + subfolder = Path(model_info.subfolder) if model_info.subfolder else None return HFModelSource( repo_id=repo_id, access_token=HfFolder.get_token(), - subfolder=model_info.subfolder, + subfolder=subfolder, variant=repo_variant, ) if re.match(r"^(http|https):", model_path_id_or_url): diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 49ce6af2b8..0dcd925c84 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -30,8 +30,11 @@ from typing_extensions import Annotated, Any, Dict from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel +from ..embeddings.embedding_base import EmbeddingModelRaw from ..ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus +AnyModel = Union[ModelMixin, torch.nn.Module, IAIOnnxRuntimeModel, IPAdapter, IPAdapterPlus, EmbeddingModelRaw] + class InvalidModelConfigException(Exception): """Exception for when config parser doesn't recognized this combination of model type and format.""" @@ -299,7 +302,7 @@ AnyModelConfig = Union[ ] AnyModelConfigValidator = TypeAdapter(AnyModelConfig) -AnyModel = Union[ModelMixin, torch.nn.Module, IAIOnnxRuntimeModel, IPAdapter, IPAdapterPlus] + # IMPLEMENTATION NOTE: # The preferred alternative to the above is a discriminated Union as shown diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index ee9d6d53e3..9d98ee3053 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -18,8 +18,8 @@ from pathlib import Path from typing import Any, Callable, Dict, Optional, Tuple, Type from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType -from invokeai.backend.model_manager.config import VaeCheckpointConfig, VaeDiffusersConfig +from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType +from invokeai.backend.model_manager.config import AnyModel, VaeCheckpointConfig, VaeDiffusersConfig from invokeai.backend.model_manager.load.convert_cache.convert_cache_base import ModelConvertCacheBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase from invokeai.backend.util.logging import InvokeAILogger diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index 757745072d..2192c88ac2 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -19,7 +19,7 @@ from invokeai.backend.model_manager import ( ) from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.backend.model_manager.load.load_base import LoadedModel, ModelLoaderBase -from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase +from invokeai.backend.model_manager.load.model_cache.model_cache_base import CacheStats, ModelCacheBase, ModelLockerBase from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data, calc_model_size_by_fs from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init from invokeai.backend.util.devices import choose_torch_device, torch_dtype @@ -71,7 +71,7 @@ class ModelLoader(ModelLoaderBase): model_path, model_config, submodel_type = self._get_model_path(model_config, submodel_type) if not model_path.exists(): - raise InvalidModelConfigException(f"Files for model 'model_config.name' not found at {model_path}") + raise InvalidModelConfigException(f"Files for model '{model_config.name}' not found at {model_path}") model_path = self._convert_if_needed(model_config, model_path, submodel_type) locker = self._load_if_needed(model_config, model_path, submodel_type) diff --git a/invokeai/backend/model_manager/load/model_cache/__init__.py b/invokeai/backend/model_manager/load/model_cache/__init__.py index 0cb5184f3a..32c682d042 100644 --- a/invokeai/backend/model_manager/load/model_cache/__init__.py +++ b/invokeai/backend/model_manager/load/model_cache/__init__.py @@ -1,4 +1,6 @@ """Init file for ModelCache.""" +from .model_cache_base import ModelCacheBase, CacheStats # noqa F401 +from .model_cache_default import ModelCache # noqa F401 -_all__ = ["ModelCacheBase", "ModelCache"] +_all__ = ["ModelCacheBase", "ModelCache", "CacheStats"] diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py index b1a6768ee8..4a4a3c7d29 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_base.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_base.py @@ -8,13 +8,13 @@ model will be cleared and (re)loaded from disk when next needed. """ from abc import ABC, abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field from logging import Logger -from typing import Generic, Optional, TypeVar +from typing import Dict, Generic, Optional, TypeVar import torch -from invokeai.backend.model_manager import AnyModel, SubModelType +from invokeai.backend.model_manager.config import AnyModel, SubModelType class ModelLockerBase(ABC): @@ -65,6 +65,19 @@ class CacheRecord(Generic[T]): return self._locks > 0 +@dataclass +class CacheStats(object): + """Collect statistics on cache performance.""" + + hits: int = 0 # cache hits + misses: int = 0 # cache misses + high_watermark: int = 0 # amount of cache used + in_cache: int = 0 # number of models in cache + cleared: int = 0 # number of models cleared to make space + cache_size: int = 0 # total size of cache + loaded_model_sizes: Dict[str, int] = field(default_factory=dict) + + class ModelCacheBase(ABC, Generic[T]): """Virtual base class for RAM model cache.""" @@ -98,10 +111,22 @@ class ModelCacheBase(ABC, Generic[T]): pass @abstractmethod - def move_model_to_device(self, cache_entry: CacheRecord, device: torch.device) -> None: + def move_model_to_device(self, cache_entry: CacheRecord[AnyModel], device: torch.device) -> None: """Move model into the indicated device.""" pass + @property + @abstractmethod + def stats(self) -> CacheStats: + """Return collected CacheStats object.""" + pass + + @stats.setter + @abstractmethod + def stats(self, stats: CacheStats) -> None: + """Set the CacheStats object for collectin cache statistics.""" + pass + @property @abstractmethod def logger(self) -> Logger: diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py index 7e30512a58..b1deb215b2 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -24,19 +24,17 @@ import math import sys import time from contextlib import suppress -from dataclasses import dataclass, field from logging import Logger from typing import Dict, List, Optional import torch -from invokeai.backend.model_manager import SubModelType -from invokeai.backend.model_manager.load.load_base import AnyModel +from invokeai.backend.model_manager import AnyModel, SubModelType from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff from invokeai.backend.util.devices import choose_torch_device from invokeai.backend.util.logging import InvokeAILogger -from .model_cache_base import CacheRecord, ModelCacheBase +from .model_cache_base import CacheRecord, CacheStats, ModelCacheBase from .model_locker import ModelLocker, ModelLockerBase if choose_torch_device() == torch.device("mps"): @@ -56,20 +54,6 @@ GIG = 1073741824 MB = 2**20 -@dataclass -class CacheStats(object): - """Collect statistics on cache performance.""" - - hits: int = 0 # cache hits - misses: int = 0 # cache misses - high_watermark: int = 0 # amount of cache used - in_cache: int = 0 # number of models in cache - cleared: int = 0 # number of models cleared to make space - cache_size: int = 0 # total size of cache - # {submodel_key => size} - loaded_model_sizes: Dict[str, int] = field(default_factory=dict) - - class ModelCache(ModelCacheBase[AnyModel]): """Implementation of ModelCacheBase.""" @@ -110,7 +94,7 @@ class ModelCache(ModelCacheBase[AnyModel]): self._logger = logger or InvokeAILogger.get_logger(self.__class__.__name__) self._log_memory_usage = log_memory_usage or self._logger.level == logging.DEBUG # used for stats collection - self.stats = CacheStats() + self._stats: Optional[CacheStats] = None self._cached_models: Dict[str, CacheRecord[AnyModel]] = {} self._cache_stack: List[str] = [] @@ -140,6 +124,16 @@ class ModelCache(ModelCacheBase[AnyModel]): """Return the cap on cache size.""" return self._max_cache_size + @property + def stats(self) -> Optional[CacheStats]: + """Return collected CacheStats object.""" + return self._stats + + @stats.setter + def stats(self, stats: CacheStats) -> None: + """Set the CacheStats object for collectin cache statistics.""" + self._stats = stats + def cache_size(self) -> int: """Get the total size of the models currently cached.""" total = 0 @@ -189,21 +183,24 @@ class ModelCache(ModelCacheBase[AnyModel]): """ key = self._make_cache_key(key, submodel_type) if key in self._cached_models: - self.stats.hits += 1 + if self.stats: + self.stats.hits += 1 else: - self.stats.misses += 1 + if self.stats: + self.stats.misses += 1 raise IndexError(f"The model with key {key} is not in the cache.") cache_entry = self._cached_models[key] # more stats - stats_name = stats_name or key - self.stats.cache_size = int(self._max_cache_size * GIG) - self.stats.high_watermark = max(self.stats.high_watermark, self.cache_size()) - self.stats.in_cache = len(self._cached_models) - self.stats.loaded_model_sizes[stats_name] = max( - self.stats.loaded_model_sizes.get(stats_name, 0), cache_entry.size - ) + if self.stats: + stats_name = stats_name or key + self.stats.cache_size = int(self._max_cache_size * GIG) + self.stats.high_watermark = max(self.stats.high_watermark, self.cache_size()) + self.stats.in_cache = len(self._cached_models) + self.stats.loaded_model_sizes[stats_name] = max( + self.stats.loaded_model_sizes.get(stats_name, 0), cache_entry.size + ) # this moves the entry to the top (right end) of the stack with suppress(Exception): diff --git a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py index 394fddc75d..6635f6b43f 100644 --- a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py +++ b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Optional, Tuple -from invokeai.backend.embeddings.model_patcher import TextualInversionModel as TextualInversionModelRaw +from invokeai.backend.embeddings.textual_inversion import TextualInversionModelRaw from invokeai.backend.model_manager import ( AnyModel, AnyModelConfig, diff --git a/invokeai/backend/stable_diffusion/__init__.py b/invokeai/backend/stable_diffusion/__init__.py index 212045f81b..75e6aa0a5d 100644 --- a/invokeai/backend/stable_diffusion/__init__.py +++ b/invokeai/backend/stable_diffusion/__init__.py @@ -4,3 +4,12 @@ Initialization file for the invokeai.backend.stable_diffusion package from .diffusers_pipeline import PipelineIntermediateState, StableDiffusionGeneratorPipeline # noqa: F401 from .diffusion import InvokeAIDiffuserComponent # noqa: F401 from .diffusion.cross_attention_map_saving import AttentionMapSaver # noqa: F401 +from .seamless import set_seamless # noqa: F401 + +__all__ = [ + "PipelineIntermediateState", + "StableDiffusionGeneratorPipeline", + "InvokeAIDiffuserComponent", + "AttentionMapSaver", + "set_seamless", +] diff --git a/invokeai/backend/stable_diffusion/seamless.py b/invokeai/backend/stable_diffusion/seamless.py new file mode 100644 index 0000000000..bfdf9e0c53 --- /dev/null +++ b/invokeai/backend/stable_diffusion/seamless.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from contextlib import contextmanager +from typing import List, Union + +import torch.nn as nn +from diffusers.models import AutoencoderKL, UNet2DConditionModel + + +def _conv_forward_asymmetric(self, input, weight, bias): + """ + Patch for Conv2d._conv_forward that supports asymmetric padding + """ + working = nn.functional.pad(input, self.asymmetric_padding["x"], mode=self.asymmetric_padding_mode["x"]) + working = nn.functional.pad(working, self.asymmetric_padding["y"], mode=self.asymmetric_padding_mode["y"]) + return nn.functional.conv2d( + working, + weight, + bias, + self.stride, + nn.modules.utils._pair(0), + self.dilation, + self.groups, + ) + + +@contextmanager +def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]): + try: + to_restore = [] + + for m_name, m in model.named_modules(): + if isinstance(model, UNet2DConditionModel): + if ".attentions." in m_name: + continue + + if ".resnets." in m_name: + if ".conv2" in m_name: + continue + if ".conv_shortcut" in m_name: + continue + + """ + if isinstance(model, UNet2DConditionModel): + if False and ".upsamplers." in m_name: + continue + + if False and ".downsamplers." in m_name: + continue + + if True and ".resnets." in m_name: + if True and ".conv1" in m_name: + if False and "down_blocks" in m_name: + continue + if False and "mid_block" in m_name: + continue + if False and "up_blocks" in m_name: + continue + + if True and ".conv2" in m_name: + continue + + if True and ".conv_shortcut" in m_name: + continue + + if True and ".attentions." in m_name: + continue + + if False and m_name in ["conv_in", "conv_out"]: + continue + """ + + if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): + m.asymmetric_padding_mode = {} + m.asymmetric_padding = {} + m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" + m.asymmetric_padding["x"] = ( + m._reversed_padding_repeated_twice[0], + m._reversed_padding_repeated_twice[1], + 0, + 0, + ) + m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" + m.asymmetric_padding["y"] = ( + 0, + 0, + m._reversed_padding_repeated_twice[2], + m._reversed_padding_repeated_twice[3], + ) + + to_restore.append((m, m._conv_forward)) + m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) + + yield + + finally: + for module, orig_conv_forward in to_restore: + module._conv_forward = orig_conv_forward + if hasattr(module, "asymmetric_padding_mode"): + del module.asymmetric_padding_mode + if hasattr(module, "asymmetric_padding"): + del module.asymmetric_padding diff --git a/invokeai/backend/util/silence_warnings.py b/invokeai/backend/util/silence_warnings.py new file mode 100644 index 0000000000..068b605da9 --- /dev/null +++ b/invokeai/backend/util/silence_warnings.py @@ -0,0 +1,28 @@ +"""Context class to silence transformers and diffusers warnings.""" +import warnings +from typing import Any + +from diffusers import logging as diffusers_logging +from transformers import logging as transformers_logging + + +class SilenceWarnings(object): + """Use in context to temporarily turn off warnings from transformers & diffusers modules. + + with SilenceWarnings(): + # do something + """ + + def __init__(self) -> None: + self.transformers_verbosity = transformers_logging.get_verbosity() + self.diffusers_verbosity = diffusers_logging.get_verbosity() + + def __enter__(self) -> None: + transformers_logging.set_verbosity_error() + diffusers_logging.set_verbosity_error() + warnings.simplefilter("ignore") + + def __exit__(self, *args: Any) -> None: + transformers_logging.set_verbosity(self.transformers_verbosity) + diffusers_logging.set_verbosity(self.diffusers_verbosity) + warnings.simplefilter("default") diff --git a/invokeai/frontend/install/model_install2.py b/invokeai/frontend/install/model_install2.py index 51a633a565..22b132370e 100644 --- a/invokeai/frontend/install/model_install2.py +++ b/invokeai/frontend/install/model_install2.py @@ -23,7 +23,7 @@ import torch from npyscreen import widget from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.model_install import ModelInstallService +from invokeai.app.services.model_install import ModelInstallServiceBase from invokeai.backend.install.install_helper import InstallHelper, InstallSelections, UnifiedModelInfo from invokeai.backend.model_manager import ModelType from invokeai.backend.util import choose_precision, choose_torch_device @@ -499,7 +499,7 @@ class AddModelApplication(npyscreen.NPSAppManaged): # type: ignore ) -def list_models(installer: ModelInstallService, model_type: ModelType): +def list_models(installer: ModelInstallServiceBase, model_type: ModelType): """Print out all models of type model_type.""" models = installer.record_store.search_by_attr(model_type=model_type) print(f"Installed models of type `{model_type}`:") @@ -527,7 +527,9 @@ def select_and_download_models(opt: Namespace) -> None: install_helper.add_or_delete(selections) elif opt.default_only: - selections = InstallSelections(install_models=[install_helper.default_model()]) + default_model = install_helper.default_model() + assert default_model is not None + selections = InstallSelections(install_models=[default_model]) install_helper.add_or_delete(selections) elif opt.yes_to_all: diff --git a/tests/backend/model_manager_2/util/test_hf_model_select.py b/tests/backend/model_manager_2/util/test_hf_model_select.py index f14d9a6823..5bef9cb2e1 100644 --- a/tests/backend/model_manager_2/util/test_hf_model_select.py +++ b/tests/backend/model_manager_2/util/test_hf_model_select.py @@ -192,6 +192,7 @@ def sdxl_base_files() -> List[Path]: "text_encoder/model.onnx", "text_encoder_2/config.json", "text_encoder_2/model.onnx", + "text_encoder_2/model.onnx_data", "tokenizer/merges.txt", "tokenizer/special_tokens_map.json", "tokenizer/tokenizer_config.json", @@ -202,6 +203,7 @@ def sdxl_base_files() -> List[Path]: "tokenizer_2/vocab.json", "unet/config.json", "unet/model.onnx", + "unet/model.onnx_data", "vae_decoder/config.json", "vae_decoder/model.onnx", "vae_encoder/config.json", diff --git a/tests/test_model_probe.py b/tests/test_model_probe.py index aacae06a8b..be823e2be9 100644 --- a/tests/test_model_probe.py +++ b/tests/test_model_probe.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from invokeai.backend import BaseModelType +from invokeai.backend.model_manager import BaseModelType, ModelRepoVariant from invokeai.backend.model_manager.probe import VaeFolderProbe @@ -21,10 +21,10 @@ def test_get_base_type(vae_path: str, expected_type: BaseModelType, datadir: Pat base_type = probe.get_base_type() assert base_type == expected_type repo_variant = probe.get_repo_variant() - assert repo_variant == "default" + assert repo_variant == ModelRepoVariant.DEFAULT def test_repo_variant(datadir: Path): probe = VaeFolderProbe(datadir / "vae" / "taesdxl-fp16") repo_variant = probe.get_repo_variant() - assert repo_variant == "fp16" + assert repo_variant == ModelRepoVariant.FP16 From db340bc25376a0302b4ff508cee5bc35146bd471 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 9 Feb 2024 16:42:33 -0500 Subject: [PATCH 117/411] fix invokeai_configure script to work with new mm; rename CLIs --- .../app/services/config/config_default.py | 10 +- invokeai/backend/install/install_helper.py | 2 +- .../backend/install/invokeai_configure.py | 197 ++++---- .../model_manager/load/load_default.py | 2 +- invokeai/backend/util/devices.py | 6 +- invokeai/configs/INITIAL_MODELS.yaml | 106 +++-- ...L_MODELS2.yaml => INITIAL_MODELS.yaml.OLD} | 106 ++--- invokeai/frontend/install/model_install.py | 444 +++++------------- ...model_install2.py => model_install.py.OLD} | 444 +++++++++++++----- invokeai/frontend/install/widgets.py | 11 + ...e_diffusers2.py => merge_diffusers.py.OLD} | 0 pyproject.toml | 3 +- tests/test_model_manager.py | 47 -- 13 files changed, 686 insertions(+), 692 deletions(-) rename invokeai/configs/{INITIAL_MODELS2.yaml => INITIAL_MODELS.yaml.OLD} (59%) rename invokeai/frontend/install/{model_install2.py => model_install.py.OLD} (57%) rename invokeai/frontend/merge/{merge_diffusers2.py => merge_diffusers.py.OLD} (100%) delete mode 100644 tests/test_model_manager.py diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index b39e916da3..2af775372d 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -185,7 +185,9 @@ from .config_base import InvokeAISettings INIT_FILE = Path("invokeai.yaml") DB_FILE = Path("invokeai.db") LEGACY_INIT_FILE = Path("invokeai.init") -DEFAULT_MAX_VRAM = 0.5 +DEFAULT_RAM_CACHE = 10.0 +DEFAULT_VRAM_CACHE = 0.25 +DEFAULT_CONVERT_CACHE = 20.0 class Categories(object): @@ -261,9 +263,9 @@ class InvokeAIAppConfig(InvokeAISettings): version : bool = Field(default=False, description="Show InvokeAI version and exit", json_schema_extra=Categories.Other) # CACHE - ram : float = Field(default=7.5, gt=0, description="Maximum memory amount used by model cache for rapid switching (floating point number, GB)", json_schema_extra=Categories.ModelCache, ) - vram : float = Field(default=0.25, ge=0, description="Amount of VRAM reserved for model storage (floating point number, GB)", json_schema_extra=Categories.ModelCache, ) - convert_cache : float = Field(default=10.0, ge=0, description="Maximum size of on-disk converted models cache (GB)", json_schema_extra=Categories.ModelCache) + ram : float = Field(default=DEFAULT_RAM_CACHE, gt=0, description="Maximum memory amount used by model cache for rapid switching (floating point number, GB)", json_schema_extra=Categories.ModelCache, ) + vram : float = Field(default=DEFAULT_VRAM_CACHE, ge=0, description="Amount of VRAM reserved for model storage (floating point number, GB)", json_schema_extra=Categories.ModelCache, ) + convert_cache : float = Field(default=DEFAULT_CONVERT_CACHE, ge=0, description="Maximum size of on-disk converted models cache (GB)", json_schema_extra=Categories.ModelCache) lazy_offload : bool = Field(default=True, description="Keep models in VRAM until their space is needed", json_schema_extra=Categories.ModelCache, ) log_memory_usage : bool = Field(default=False, description="If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.", json_schema_extra=Categories.ModelCache) diff --git a/invokeai/backend/install/install_helper.py b/invokeai/backend/install/install_helper.py index 8877e33092..9c386c209c 100644 --- a/invokeai/backend/install/install_helper.py +++ b/invokeai/backend/install/install_helper.py @@ -37,7 +37,7 @@ from invokeai.backend.model_manager.metadata import UnknownMetadataException from invokeai.backend.util.logging import InvokeAILogger # name of the starter models file -INITIAL_MODELS = "INITIAL_MODELS2.yaml" +INITIAL_MODELS = "INITIAL_MODELS.yaml" def initialize_record_store(app_config: InvokeAIAppConfig) -> ModelRecordServiceBase: diff --git a/invokeai/backend/install/invokeai_configure.py b/invokeai/backend/install/invokeai_configure.py index 3cb7db6c82..4dfa2b070c 100755 --- a/invokeai/backend/install/invokeai_configure.py +++ b/invokeai/backend/install/invokeai_configure.py @@ -18,31 +18,30 @@ from argparse import Namespace from enum import Enum from pathlib import Path from shutil import get_terminal_size -from typing import Any, get_args, get_type_hints +from typing import Any, Optional, Set, Tuple, Type, get_args, get_type_hints from urllib import request import npyscreen -import omegaconf import psutil import torch import transformers -import yaml -from diffusers import AutoencoderKL +from diffusers import AutoencoderKL, ModelMixin from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker from huggingface_hub import HfFolder from huggingface_hub import login as hf_hub_login -from omegaconf import OmegaConf -from pydantic import ValidationError +from omegaconf import DictConfig, OmegaConf +from pydantic.error_wrappers import ValidationError from tqdm import tqdm from transformers import AutoFeatureExtractor, BertTokenizerFast, CLIPTextConfig, CLIPTextModel, CLIPTokenizer import invokeai.configs as configs from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.backend.install.install_helper import InstallHelper, InstallSelections from invokeai.backend.install.legacy_arg_parsing import legacy_parser -from invokeai.backend.install.model_install_backend import InstallSelections, ModelInstall, hf_download_from_pretrained -from invokeai.backend.model_management.model_probe import BaseModelType, ModelType +from invokeai.backend.model_manager import BaseModelType, ModelType +from invokeai.backend.util import choose_precision, choose_torch_device from invokeai.backend.util.logging import InvokeAILogger -from invokeai.frontend.install.model_install import addModelsForm, process_and_execute +from invokeai.frontend.install.model_install import addModelsForm # TO DO - Move all the frontend code into invokeai.frontend.install from invokeai.frontend.install.widgets import ( @@ -61,7 +60,7 @@ warnings.filterwarnings("ignore") transformers.logging.set_verbosity_error() -def get_literal_fields(field) -> list[Any]: +def get_literal_fields(field: str) -> Tuple[Any]: return get_args(get_type_hints(InvokeAIAppConfig).get(field)) @@ -80,8 +79,7 @@ ATTENTION_SLICE_CHOICES = get_literal_fields("attention_slice_size") GENERATION_OPT_CHOICES = ["sequential_guidance", "force_tiled_decode", "lazy_offload"] GB = 1073741824 # GB in bytes HAS_CUDA = torch.cuda.is_available() -_, MAX_VRAM = torch.cuda.mem_get_info() if HAS_CUDA else (0, 0) - +_, MAX_VRAM = torch.cuda.mem_get_info() if HAS_CUDA else (0.0, 0.0) MAX_VRAM /= GB MAX_RAM = psutil.virtual_memory().total / GB @@ -96,13 +94,15 @@ logger = InvokeAILogger.get_logger() class DummyWidgetValue(Enum): + """Dummy widget values.""" + zero = 0 true = True false = False # -------------------------------------------- -def postscript(errors: None): +def postscript(errors: Set[str]) -> None: if not any(errors): message = f""" ** INVOKEAI INSTALLATION SUCCESSFUL ** @@ -143,7 +143,7 @@ def yes_or_no(prompt: str, default_yes=True): # --------------------------------------------- -def HfLogin(access_token) -> str: +def HfLogin(access_token) -> None: """ Helper for logging in to Huggingface The stdout capture is needed to hide the irrelevant "git credential helper" warning @@ -162,7 +162,7 @@ def HfLogin(access_token) -> str: # ------------------------------------- class ProgressBar: - def __init__(self, model_name="file"): + def __init__(self, model_name: str = "file"): self.pbar = None self.name = model_name @@ -179,6 +179,22 @@ class ProgressBar: self.pbar.update(block_size) +# --------------------------------------------- +def hf_download_from_pretrained(model_class: Type[ModelMixin], model_name: str, destination: Path, **kwargs: Any): + filter = lambda x: "fp16 is not a valid" not in x.getMessage() # noqa E731 + logger.addFilter(filter) + try: + model = model_class.from_pretrained( + model_name, + resume_download=True, + **kwargs, + ) + model.save_pretrained(destination, safe_serialization=True) + finally: + logger.removeFilter(filter) + return destination + + # --------------------------------------------- def download_with_progress_bar(model_url: str, model_dest: str, label: str = "the"): try: @@ -249,6 +265,7 @@ def download_conversion_models(): # --------------------------------------------- +# TO DO: use the download queue here. def download_realesrgan(): logger.info("Installing ESRGAN Upscaling models...") URLs = [ @@ -288,18 +305,19 @@ def download_lama(): # --------------------------------------------- -def download_support_models(): +def download_support_models() -> None: download_realesrgan() download_lama() download_conversion_models() # ------------------------------------- -def get_root(root: str = None) -> str: +def get_root(root: Optional[str] = None) -> str: if root: return root - elif os.environ.get("INVOKEAI_ROOT"): - return os.environ.get("INVOKEAI_ROOT") + elif root := os.environ.get("INVOKEAI_ROOT"): + assert root is not None + return root else: return str(config.root_path) @@ -455,6 +473,25 @@ Use cursor arrows to make a checkbox selection, and space to toggle. max_width=110, scroll_exit=True, ) + self.add_widget_intelligent( + npyscreen.TitleFixedText, + name="Model disk conversion cache size (GB). This is used to cache safetensors files that need to be converted to diffusers..", + begin_entry_at=0, + editable=False, + color="CONTROL", + scroll_exit=True, + ) + self.nextrely -= 1 + self.disk = self.add_widget_intelligent( + npyscreen.Slider, + value=clip(old_opts.convert_cache, range=(0, 100), step=0.5), + out_of=100, + lowest=0.0, + step=0.5, + relx=8, + scroll_exit=True, + ) + self.nextrely += 1 self.add_widget_intelligent( npyscreen.TitleFixedText, name="Model RAM cache size (GB). Make this at least large enough to hold a single full model (2GB for SD-1, 6GB for SDXL).", @@ -495,6 +532,14 @@ Use cursor arrows to make a checkbox selection, and space to toggle. ) else: self.vram = DummyWidgetValue.zero + + self.nextrely += 1 + self.add_widget_intelligent( + npyscreen.FixedText, + value="Location of the database used to store model path and configuration information:", + editable=False, + color="CONTROL", + ) self.nextrely += 1 self.outdir = self.add_widget_intelligent( FileBox, @@ -506,19 +551,21 @@ Use cursor arrows to make a checkbox selection, and space to toggle. labelColor="GOOD", begin_entry_at=40, max_height=3, + max_width=127, scroll_exit=True, ) self.autoimport_dirs = {} self.autoimport_dirs["autoimport_dir"] = self.add_widget_intelligent( FileBox, - name="Folder to recursively scan for new checkpoints, ControlNets, LoRAs and TI models", - value=str(config.root_path / config.autoimport_dir), + name="Optional folder to scan for new checkpoints, ControlNets, LoRAs and TI models", + value=str(config.root_path / config.autoimport_dir) if config.autoimport_dir else "", select_dir=True, must_exist=False, use_two_lines=False, labelColor="GOOD", begin_entry_at=32, max_height=3, + max_width=127, scroll_exit=True, ) self.nextrely += 1 @@ -555,6 +602,10 @@ https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/LICENS self.attention_slice_label.hidden = not show self.attention_slice_size.hidden = not show + def show_hide_model_conf_override(self, value): + self.model_conf_override.hidden = value + self.model_conf_override.display() + def on_ok(self): options = self.marshall_arguments() if self.validate_field_values(options): @@ -584,18 +635,21 @@ https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/LICENS else: return True - def marshall_arguments(self): + def marshall_arguments(self) -> Namespace: new_opts = Namespace() for attr in [ "ram", "vram", + "convert_cache", "outdir", ]: if hasattr(self, attr): setattr(new_opts, attr, getattr(self, attr).value) for attr in self.autoimport_dirs: + if not self.autoimport_dirs[attr].value: + continue directory = Path(self.autoimport_dirs[attr].value) if directory.is_relative_to(config.root_path): directory = directory.relative_to(config.root_path) @@ -615,13 +669,14 @@ https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/LICENS class EditOptApplication(npyscreen.NPSAppManaged): - def __init__(self, program_opts: Namespace, invokeai_opts: Namespace): + def __init__(self, program_opts: Namespace, invokeai_opts: InvokeAIAppConfig, install_helper: InstallHelper): super().__init__() self.program_opts = program_opts self.invokeai_opts = invokeai_opts self.user_cancelled = False self.autoload_pending = True - self.install_selections = default_user_selections(program_opts) + self.install_helper = install_helper + self.install_selections = default_user_selections(program_opts, install_helper) def onStart(self): npyscreen.setTheme(npyscreen.Themes.DefaultTheme) @@ -640,16 +695,10 @@ class EditOptApplication(npyscreen.NPSAppManaged): cycle_widgets=False, ) - def new_opts(self): + def new_opts(self) -> Namespace: return self.options.marshall_arguments() -def edit_opts(program_opts: Namespace, invokeai_opts: Namespace) -> argparse.Namespace: - editApp = EditOptApplication(program_opts, invokeai_opts) - editApp.run() - return editApp.new_opts() - - def default_ramcache() -> float: """Run a heuristic for the default RAM cache based on installed RAM.""" @@ -660,27 +709,18 @@ def default_ramcache() -> float: ) # 2.1 is just large enough for sd 1.5 ;-) -def default_startup_options(init_file: Path) -> Namespace: +def default_startup_options(init_file: Path) -> InvokeAIAppConfig: opts = InvokeAIAppConfig.get_config() - opts.ram = opts.ram or default_ramcache() + opts.ram = default_ramcache() return opts -def default_user_selections(program_opts: Namespace) -> InstallSelections: - try: - installer = ModelInstall(config) - except omegaconf.errors.ConfigKeyError: - logger.warning("Your models.yaml file is corrupt or out of date. Reinitializing") - initialize_rootdir(config.root_path, True) - installer = ModelInstall(config) - - models = installer.all_models() +def default_user_selections(program_opts: Namespace, install_helper: InstallHelper) -> InstallSelections: + default_model = install_helper.default_model() + assert default_model is not None + default_models = [default_model] if program_opts.default_only else install_helper.recommended_models() return InstallSelections( - install_models=[models[installer.default_model()].path or models[installer.default_model()].repo_id] - if program_opts.default_only - else [models[x].path or models[x].repo_id for x in installer.recommended_models()] - if program_opts.yes_to_all - else [], + install_models=default_models if program_opts.yes_to_all else [], ) @@ -716,21 +756,10 @@ def initialize_rootdir(root: Path, yes_to_all: bool = False): path.mkdir(parents=True, exist_ok=True) -def maybe_create_models_yaml(root: Path): - models_yaml = root / "configs" / "models.yaml" - if models_yaml.exists(): - if OmegaConf.load(models_yaml).get("__metadata__"): # up to date - return - else: - logger.info("Creating new models.yaml, original saved as models.yaml.orig") - models_yaml.rename(models_yaml.parent / "models.yaml.orig") - - with open(models_yaml, "w") as yaml_file: - yaml_file.write(yaml.dump({"__metadata__": {"version": "3.0.0"}})) - - # ------------------------------------- -def run_console_ui(program_opts: Namespace, initfile: Path = None) -> (Namespace, Namespace): +def run_console_ui( + program_opts: Namespace, initfile: Path, install_helper: InstallHelper +) -> Tuple[Optional[Namespace], Optional[InstallSelections]]: invokeai_opts = default_startup_options(initfile) invokeai_opts.root = program_opts.root @@ -739,22 +768,16 @@ def run_console_ui(program_opts: Namespace, initfile: Path = None) -> (Namespace "Could not increase terminal size. Try running again with a larger window or smaller font size." ) - # the install-models application spawns a subprocess to install - # models, and will crash unless this is set before running. - import torch - - torch.multiprocessing.set_start_method("spawn") - - editApp = EditOptApplication(program_opts, invokeai_opts) + editApp = EditOptApplication(program_opts, invokeai_opts, install_helper) editApp.run() if editApp.user_cancelled: return (None, None) else: - return (editApp.new_opts, editApp.install_selections) + return (editApp.new_opts(), editApp.install_selections) # ------------------------------------- -def write_opts(opts: Namespace, init_file: Path): +def write_opts(opts: InvokeAIAppConfig, init_file: Path) -> None: """ Update the invokeai.yaml file with values from current settings. """ @@ -762,7 +785,7 @@ def write_opts(opts: Namespace, init_file: Path): new_config = InvokeAIAppConfig.get_config() new_config.root = config.root - for key, value in opts.__dict__.items(): + for key, value in opts.model_dump().items(): if hasattr(new_config, key): setattr(new_config, key, value) @@ -779,7 +802,7 @@ def default_output_dir() -> Path: # ------------------------------------- -def write_default_options(program_opts: Namespace, initfile: Path): +def write_default_options(program_opts: Namespace, initfile: Path) -> None: opt = default_startup_options(initfile) write_opts(opt, initfile) @@ -789,16 +812,11 @@ def write_default_options(program_opts: Namespace, initfile: Path): # the legacy Args object in order to parse # the old init file and write out the new # yaml format. -def migrate_init_file(legacy_format: Path): +def migrate_init_file(legacy_format: Path) -> None: old = legacy_parser.parse_args([f"@{str(legacy_format)}"]) new = InvokeAIAppConfig.get_config() - fields = [ - x - for x, y in InvokeAIAppConfig.model_fields.items() - if (y.json_schema_extra.get("category", None) if y.json_schema_extra else None) != "DEPRECATED" - ] - for attr in fields: + for attr in InvokeAIAppConfig.model_fields.keys(): if hasattr(old, attr): try: setattr(new, attr, getattr(old, attr)) @@ -819,7 +837,7 @@ def migrate_init_file(legacy_format: Path): # ------------------------------------- -def migrate_models(root: Path): +def migrate_models(root: Path) -> None: from invokeai.backend.install.migrate_to_3 import do_migrate do_migrate(root, root) @@ -838,7 +856,9 @@ def migrate_if_needed(opt: Namespace, root: Path) -> bool: ): logger.info("** Migrating invokeai.init to invokeai.yaml") migrate_init_file(old_init_file) - config.parse_args(argv=[], conf=OmegaConf.load(new_init_file)) + omegaconf = OmegaConf.load(new_init_file) + assert isinstance(omegaconf, DictConfig) + config.parse_args(argv=[], conf=omegaconf) if old_hub.exists(): migrate_models(config.root_path) @@ -849,7 +869,7 @@ def migrate_if_needed(opt: Namespace, root: Path) -> bool: # ------------------------------------- -def main() -> None: +def main(): parser = argparse.ArgumentParser(description="InvokeAI model downloader") parser.add_argument( "--skip-sd-weights", @@ -908,6 +928,7 @@ def main() -> None: if opt.full_precision: invoke_args.extend(["--precision", "float32"]) config.parse_args(invoke_args) + config.precision = "float32" if opt.full_precision else choose_precision(torch.device(choose_torch_device())) logger = InvokeAILogger().get_logger(config=config) errors = set() @@ -921,14 +942,18 @@ def main() -> None: # run this unconditionally in case new directories need to be added initialize_rootdir(config.root_path, opt.yes_to_all) - models_to_download = default_user_selections(opt) + # this will initialize the models.yaml file if not present + install_helper = InstallHelper(config, logger) + + models_to_download = default_user_selections(opt, install_helper) new_init_file = config.root_path / "invokeai.yaml" if opt.yes_to_all: write_default_options(opt, new_init_file) init_options = Namespace(precision="float32" if opt.full_precision else "float16") + else: - init_options, models_to_download = run_console_ui(opt, new_init_file) + init_options, models_to_download = run_console_ui(opt, new_init_file, install_helper) if init_options: write_opts(init_options, new_init_file) else: @@ -943,10 +968,12 @@ def main() -> None: if opt.skip_sd_weights: logger.warning("Skipping diffusion weights download per user request") + elif models_to_download: - process_and_execute(opt, models_to_download) + install_helper.add_or_delete(models_to_download) postscript(errors=errors) + if not opt.yes_to_all: input("Press any key to continue...") except WindowTooSmallException as e: diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index 2192c88ac2..c1dfe729af 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -19,7 +19,7 @@ from invokeai.backend.model_manager import ( ) from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.backend.model_manager.load.load_base import LoadedModel, ModelLoaderBase -from invokeai.backend.model_manager.load.model_cache.model_cache_base import CacheStats, ModelCacheBase, ModelLockerBase +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data, calc_model_size_by_fs from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init from invokeai.backend.util.devices import choose_torch_device, torch_dtype diff --git a/invokeai/backend/util/devices.py b/invokeai/backend/util/devices.py index b4f24d8483..a83d1045f7 100644 --- a/invokeai/backend/util/devices.py +++ b/invokeai/backend/util/devices.py @@ -1,7 +1,7 @@ from __future__ import annotations from contextlib import nullcontext -from typing import Optional, Union +from typing import Literal, Optional, Union import torch from torch import autocast @@ -31,7 +31,9 @@ def choose_torch_device() -> torch.device: # We are in transition here from using a single global AppConfig to allowing multiple # configurations. It is strongly recommended to pass the app_config to this function. -def choose_precision(device: torch.device, app_config: Optional[InvokeAIAppConfig] = None) -> str: +def choose_precision( + device: torch.device, app_config: Optional[InvokeAIAppConfig] = None +) -> Literal["float32", "float16", "bfloat16"]: """Return an appropriate precision for the given torch device.""" app_config = app_config or config if device.type == "cuda": diff --git a/invokeai/configs/INITIAL_MODELS.yaml b/invokeai/configs/INITIAL_MODELS.yaml index c230665e3a..ca2283ab81 100644 --- a/invokeai/configs/INITIAL_MODELS.yaml +++ b/invokeai/configs/INITIAL_MODELS.yaml @@ -1,153 +1,157 @@ # This file predefines a few models that the user may want to install. sd-1/main/stable-diffusion-v1-5: description: Stable Diffusion version 1.5 diffusers model (4.27 GB) - repo_id: runwayml/stable-diffusion-v1-5 + source: runwayml/stable-diffusion-v1-5 recommended: True default: True sd-1/main/stable-diffusion-v1-5-inpainting: description: RunwayML SD 1.5 model optimized for inpainting, diffusers version (4.27 GB) - repo_id: runwayml/stable-diffusion-inpainting + source: runwayml/stable-diffusion-inpainting recommended: True sd-2/main/stable-diffusion-2-1: description: Stable Diffusion version 2.1 diffusers model, trained on 768 pixel images (5.21 GB) - repo_id: stabilityai/stable-diffusion-2-1 + source: stabilityai/stable-diffusion-2-1 recommended: False sd-2/main/stable-diffusion-2-inpainting: description: Stable Diffusion version 2.0 inpainting model (5.21 GB) - repo_id: stabilityai/stable-diffusion-2-inpainting + source: stabilityai/stable-diffusion-2-inpainting recommended: False sdxl/main/stable-diffusion-xl-base-1-0: description: Stable Diffusion XL base model (12 GB) - repo_id: stabilityai/stable-diffusion-xl-base-1.0 + source: stabilityai/stable-diffusion-xl-base-1.0 recommended: True sdxl-refiner/main/stable-diffusion-xl-refiner-1-0: description: Stable Diffusion XL refiner model (12 GB) - repo_id: stabilityai/stable-diffusion-xl-refiner-1.0 + source: stabilityai/stable-diffusion-xl-refiner-1.0 recommended: False -sdxl/vae/sdxl-1-0-vae-fix: - description: Fine tuned version of the SDXL-1.0 VAE - repo_id: madebyollin/sdxl-vae-fp16-fix +sdxl/vae/sdxl-vae-fp16-fix: + description: Version of the SDXL-1.0 VAE that works in half precision mode + source: madebyollin/sdxl-vae-fp16-fix recommended: True sd-1/main/Analog-Diffusion: description: An SD-1.5 model trained on diverse analog photographs (2.13 GB) - repo_id: wavymulder/Analog-Diffusion + source: wavymulder/Analog-Diffusion recommended: False -sd-1/main/Deliberate_v5: +sd-1/main/Deliberate: description: Versatile model that produces detailed images up to 768px (4.27 GB) - path: https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate_v5.safetensors + source: XpucT/Deliberate recommended: False sd-1/main/Dungeons-and-Diffusion: description: Dungeons & Dragons characters (2.13 GB) - repo_id: 0xJustin/Dungeons-and-Diffusion + source: 0xJustin/Dungeons-and-Diffusion recommended: False sd-1/main/dreamlike-photoreal-2: description: A photorealistic model trained on 768 pixel images based on SD 1.5 (2.13 GB) - repo_id: dreamlike-art/dreamlike-photoreal-2.0 + source: dreamlike-art/dreamlike-photoreal-2.0 recommended: False sd-1/main/Inkpunk-Diffusion: description: Stylized illustrations inspired by Gorillaz, FLCL and Shinkawa; prompt with "nvinkpunk" (4.27 GB) - repo_id: Envvi/Inkpunk-Diffusion + source: Envvi/Inkpunk-Diffusion recommended: False sd-1/main/openjourney: description: An SD 1.5 model fine tuned on Midjourney; prompt with "mdjrny-v4 style" (2.13 GB) - repo_id: prompthero/openjourney + source: prompthero/openjourney recommended: False sd-1/main/seek.art_MEGA: - repo_id: coreco/seek.art_MEGA + source: coreco/seek.art_MEGA description: A general use SD-1.5 "anything" model that supports multiple styles (2.1 GB) recommended: False sd-1/main/trinart_stable_diffusion_v2: description: An SD-1.5 model finetuned with ~40K assorted high resolution manga/anime-style images (2.13 GB) - repo_id: naclbit/trinart_stable_diffusion_v2 + source: naclbit/trinart_stable_diffusion_v2 recommended: False sd-1/controlnet/qrcode_monster: - repo_id: monster-labs/control_v1p_sd15_qrcode_monster + source: monster-labs/control_v1p_sd15_qrcode_monster subfolder: v2 sd-1/controlnet/canny: - repo_id: lllyasviel/control_v11p_sd15_canny + source: lllyasviel/control_v11p_sd15_canny recommended: True sd-1/controlnet/inpaint: - repo_id: lllyasviel/control_v11p_sd15_inpaint + source: lllyasviel/control_v11p_sd15_inpaint sd-1/controlnet/mlsd: - repo_id: lllyasviel/control_v11p_sd15_mlsd + source: lllyasviel/control_v11p_sd15_mlsd sd-1/controlnet/depth: - repo_id: lllyasviel/control_v11f1p_sd15_depth + source: lllyasviel/control_v11f1p_sd15_depth recommended: True sd-1/controlnet/normal_bae: - repo_id: lllyasviel/control_v11p_sd15_normalbae + source: lllyasviel/control_v11p_sd15_normalbae sd-1/controlnet/seg: - repo_id: lllyasviel/control_v11p_sd15_seg + source: lllyasviel/control_v11p_sd15_seg sd-1/controlnet/lineart: - repo_id: lllyasviel/control_v11p_sd15_lineart + source: lllyasviel/control_v11p_sd15_lineart recommended: True sd-1/controlnet/lineart_anime: - repo_id: lllyasviel/control_v11p_sd15s2_lineart_anime + source: lllyasviel/control_v11p_sd15s2_lineart_anime sd-1/controlnet/openpose: - repo_id: lllyasviel/control_v11p_sd15_openpose + source: lllyasviel/control_v11p_sd15_openpose recommended: True sd-1/controlnet/scribble: - repo_id: lllyasviel/control_v11p_sd15_scribble + source: lllyasviel/control_v11p_sd15_scribble recommended: False sd-1/controlnet/softedge: - repo_id: lllyasviel/control_v11p_sd15_softedge + source: lllyasviel/control_v11p_sd15_softedge sd-1/controlnet/shuffle: - repo_id: lllyasviel/control_v11e_sd15_shuffle + source: lllyasviel/control_v11e_sd15_shuffle sd-1/controlnet/tile: - repo_id: lllyasviel/control_v11f1e_sd15_tile + source: lllyasviel/control_v11f1e_sd15_tile sd-1/controlnet/ip2p: - repo_id: lllyasviel/control_v11e_sd15_ip2p + source: lllyasviel/control_v11e_sd15_ip2p sd-1/t2i_adapter/canny-sd15: - repo_id: TencentARC/t2iadapter_canny_sd15v2 + source: TencentARC/t2iadapter_canny_sd15v2 sd-1/t2i_adapter/sketch-sd15: - repo_id: TencentARC/t2iadapter_sketch_sd15v2 + source: TencentARC/t2iadapter_sketch_sd15v2 sd-1/t2i_adapter/depth-sd15: - repo_id: TencentARC/t2iadapter_depth_sd15v2 + source: TencentARC/t2iadapter_depth_sd15v2 sd-1/t2i_adapter/zoedepth-sd15: - repo_id: TencentARC/t2iadapter_zoedepth_sd15v1 + source: TencentARC/t2iadapter_zoedepth_sd15v1 sdxl/t2i_adapter/canny-sdxl: - repo_id: TencentARC/t2i-adapter-canny-sdxl-1.0 + source: TencentARC/t2i-adapter-canny-sdxl-1.0 sdxl/t2i_adapter/zoedepth-sdxl: - repo_id: TencentARC/t2i-adapter-depth-zoe-sdxl-1.0 + source: TencentARC/t2i-adapter-depth-zoe-sdxl-1.0 sdxl/t2i_adapter/lineart-sdxl: - repo_id: TencentARC/t2i-adapter-lineart-sdxl-1.0 + source: TencentARC/t2i-adapter-lineart-sdxl-1.0 sdxl/t2i_adapter/sketch-sdxl: - repo_id: TencentARC/t2i-adapter-sketch-sdxl-1.0 + source: TencentARC/t2i-adapter-sketch-sdxl-1.0 sd-1/embedding/EasyNegative: - path: https://huggingface.co/embed/EasyNegative/resolve/main/EasyNegative.safetensors + source: https://huggingface.co/embed/EasyNegative/resolve/main/EasyNegative.safetensors recommended: True -sd-1/embedding/ahx-beta-453407d: - repo_id: sd-concepts-library/ahx-beta-453407d + description: A textual inversion to use in the negative prompt to reduce bad anatomy +sd-1/lora/FlatColor: + source: https://civitai.com/models/6433/loraflatcolor + recommended: True + description: A LoRA that generates scenery using solid blocks of color sd-1/lora/Ink scenery: - path: https://civitai.com/api/download/models/83390 + source: https://civitai.com/api/download/models/83390 + description: Generate india ink-like landscapes sd-1/ip_adapter/ip_adapter_sd15: - repo_id: InvokeAI/ip_adapter_sd15 + source: InvokeAI/ip_adapter_sd15 recommended: True requires: - InvokeAI/ip_adapter_sd_image_encoder description: IP-Adapter for SD 1.5 models sd-1/ip_adapter/ip_adapter_plus_sd15: - repo_id: InvokeAI/ip_adapter_plus_sd15 + source: InvokeAI/ip_adapter_plus_sd15 recommended: False requires: - InvokeAI/ip_adapter_sd_image_encoder description: Refined IP-Adapter for SD 1.5 models sd-1/ip_adapter/ip_adapter_plus_face_sd15: - repo_id: InvokeAI/ip_adapter_plus_face_sd15 + source: InvokeAI/ip_adapter_plus_face_sd15 recommended: False requires: - InvokeAI/ip_adapter_sd_image_encoder description: Refined IP-Adapter for SD 1.5 models, adapted for faces sdxl/ip_adapter/ip_adapter_sdxl: - repo_id: InvokeAI/ip_adapter_sdxl + source: InvokeAI/ip_adapter_sdxl recommended: False requires: - InvokeAI/ip_adapter_sdxl_image_encoder description: IP-Adapter for SDXL models any/clip_vision/ip_adapter_sd_image_encoder: - repo_id: InvokeAI/ip_adapter_sd_image_encoder + source: InvokeAI/ip_adapter_sd_image_encoder recommended: False description: Required model for using IP-Adapters with SD-1/2 models any/clip_vision/ip_adapter_sdxl_image_encoder: - repo_id: InvokeAI/ip_adapter_sdxl_image_encoder + source: InvokeAI/ip_adapter_sdxl_image_encoder recommended: False description: Required model for using IP-Adapters with SDXL models diff --git a/invokeai/configs/INITIAL_MODELS2.yaml b/invokeai/configs/INITIAL_MODELS.yaml.OLD similarity index 59% rename from invokeai/configs/INITIAL_MODELS2.yaml rename to invokeai/configs/INITIAL_MODELS.yaml.OLD index ca2283ab81..c230665e3a 100644 --- a/invokeai/configs/INITIAL_MODELS2.yaml +++ b/invokeai/configs/INITIAL_MODELS.yaml.OLD @@ -1,157 +1,153 @@ # This file predefines a few models that the user may want to install. sd-1/main/stable-diffusion-v1-5: description: Stable Diffusion version 1.5 diffusers model (4.27 GB) - source: runwayml/stable-diffusion-v1-5 + repo_id: runwayml/stable-diffusion-v1-5 recommended: True default: True sd-1/main/stable-diffusion-v1-5-inpainting: description: RunwayML SD 1.5 model optimized for inpainting, diffusers version (4.27 GB) - source: runwayml/stable-diffusion-inpainting + repo_id: runwayml/stable-diffusion-inpainting recommended: True sd-2/main/stable-diffusion-2-1: description: Stable Diffusion version 2.1 diffusers model, trained on 768 pixel images (5.21 GB) - source: stabilityai/stable-diffusion-2-1 + repo_id: stabilityai/stable-diffusion-2-1 recommended: False sd-2/main/stable-diffusion-2-inpainting: description: Stable Diffusion version 2.0 inpainting model (5.21 GB) - source: stabilityai/stable-diffusion-2-inpainting + repo_id: stabilityai/stable-diffusion-2-inpainting recommended: False sdxl/main/stable-diffusion-xl-base-1-0: description: Stable Diffusion XL base model (12 GB) - source: stabilityai/stable-diffusion-xl-base-1.0 + repo_id: stabilityai/stable-diffusion-xl-base-1.0 recommended: True sdxl-refiner/main/stable-diffusion-xl-refiner-1-0: description: Stable Diffusion XL refiner model (12 GB) - source: stabilityai/stable-diffusion-xl-refiner-1.0 + repo_id: stabilityai/stable-diffusion-xl-refiner-1.0 recommended: False -sdxl/vae/sdxl-vae-fp16-fix: - description: Version of the SDXL-1.0 VAE that works in half precision mode - source: madebyollin/sdxl-vae-fp16-fix +sdxl/vae/sdxl-1-0-vae-fix: + description: Fine tuned version of the SDXL-1.0 VAE + repo_id: madebyollin/sdxl-vae-fp16-fix recommended: True sd-1/main/Analog-Diffusion: description: An SD-1.5 model trained on diverse analog photographs (2.13 GB) - source: wavymulder/Analog-Diffusion + repo_id: wavymulder/Analog-Diffusion recommended: False -sd-1/main/Deliberate: +sd-1/main/Deliberate_v5: description: Versatile model that produces detailed images up to 768px (4.27 GB) - source: XpucT/Deliberate + path: https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate_v5.safetensors recommended: False sd-1/main/Dungeons-and-Diffusion: description: Dungeons & Dragons characters (2.13 GB) - source: 0xJustin/Dungeons-and-Diffusion + repo_id: 0xJustin/Dungeons-and-Diffusion recommended: False sd-1/main/dreamlike-photoreal-2: description: A photorealistic model trained on 768 pixel images based on SD 1.5 (2.13 GB) - source: dreamlike-art/dreamlike-photoreal-2.0 + repo_id: dreamlike-art/dreamlike-photoreal-2.0 recommended: False sd-1/main/Inkpunk-Diffusion: description: Stylized illustrations inspired by Gorillaz, FLCL and Shinkawa; prompt with "nvinkpunk" (4.27 GB) - source: Envvi/Inkpunk-Diffusion + repo_id: Envvi/Inkpunk-Diffusion recommended: False sd-1/main/openjourney: description: An SD 1.5 model fine tuned on Midjourney; prompt with "mdjrny-v4 style" (2.13 GB) - source: prompthero/openjourney + repo_id: prompthero/openjourney recommended: False sd-1/main/seek.art_MEGA: - source: coreco/seek.art_MEGA + repo_id: coreco/seek.art_MEGA description: A general use SD-1.5 "anything" model that supports multiple styles (2.1 GB) recommended: False sd-1/main/trinart_stable_diffusion_v2: description: An SD-1.5 model finetuned with ~40K assorted high resolution manga/anime-style images (2.13 GB) - source: naclbit/trinart_stable_diffusion_v2 + repo_id: naclbit/trinart_stable_diffusion_v2 recommended: False sd-1/controlnet/qrcode_monster: - source: monster-labs/control_v1p_sd15_qrcode_monster + repo_id: monster-labs/control_v1p_sd15_qrcode_monster subfolder: v2 sd-1/controlnet/canny: - source: lllyasviel/control_v11p_sd15_canny + repo_id: lllyasviel/control_v11p_sd15_canny recommended: True sd-1/controlnet/inpaint: - source: lllyasviel/control_v11p_sd15_inpaint + repo_id: lllyasviel/control_v11p_sd15_inpaint sd-1/controlnet/mlsd: - source: lllyasviel/control_v11p_sd15_mlsd + repo_id: lllyasviel/control_v11p_sd15_mlsd sd-1/controlnet/depth: - source: lllyasviel/control_v11f1p_sd15_depth + repo_id: lllyasviel/control_v11f1p_sd15_depth recommended: True sd-1/controlnet/normal_bae: - source: lllyasviel/control_v11p_sd15_normalbae + repo_id: lllyasviel/control_v11p_sd15_normalbae sd-1/controlnet/seg: - source: lllyasviel/control_v11p_sd15_seg + repo_id: lllyasviel/control_v11p_sd15_seg sd-1/controlnet/lineart: - source: lllyasviel/control_v11p_sd15_lineart + repo_id: lllyasviel/control_v11p_sd15_lineart recommended: True sd-1/controlnet/lineart_anime: - source: lllyasviel/control_v11p_sd15s2_lineart_anime + repo_id: lllyasviel/control_v11p_sd15s2_lineart_anime sd-1/controlnet/openpose: - source: lllyasviel/control_v11p_sd15_openpose + repo_id: lllyasviel/control_v11p_sd15_openpose recommended: True sd-1/controlnet/scribble: - source: lllyasviel/control_v11p_sd15_scribble + repo_id: lllyasviel/control_v11p_sd15_scribble recommended: False sd-1/controlnet/softedge: - source: lllyasviel/control_v11p_sd15_softedge + repo_id: lllyasviel/control_v11p_sd15_softedge sd-1/controlnet/shuffle: - source: lllyasviel/control_v11e_sd15_shuffle + repo_id: lllyasviel/control_v11e_sd15_shuffle sd-1/controlnet/tile: - source: lllyasviel/control_v11f1e_sd15_tile + repo_id: lllyasviel/control_v11f1e_sd15_tile sd-1/controlnet/ip2p: - source: lllyasviel/control_v11e_sd15_ip2p + repo_id: lllyasviel/control_v11e_sd15_ip2p sd-1/t2i_adapter/canny-sd15: - source: TencentARC/t2iadapter_canny_sd15v2 + repo_id: TencentARC/t2iadapter_canny_sd15v2 sd-1/t2i_adapter/sketch-sd15: - source: TencentARC/t2iadapter_sketch_sd15v2 + repo_id: TencentARC/t2iadapter_sketch_sd15v2 sd-1/t2i_adapter/depth-sd15: - source: TencentARC/t2iadapter_depth_sd15v2 + repo_id: TencentARC/t2iadapter_depth_sd15v2 sd-1/t2i_adapter/zoedepth-sd15: - source: TencentARC/t2iadapter_zoedepth_sd15v1 + repo_id: TencentARC/t2iadapter_zoedepth_sd15v1 sdxl/t2i_adapter/canny-sdxl: - source: TencentARC/t2i-adapter-canny-sdxl-1.0 + repo_id: TencentARC/t2i-adapter-canny-sdxl-1.0 sdxl/t2i_adapter/zoedepth-sdxl: - source: TencentARC/t2i-adapter-depth-zoe-sdxl-1.0 + repo_id: TencentARC/t2i-adapter-depth-zoe-sdxl-1.0 sdxl/t2i_adapter/lineart-sdxl: - source: TencentARC/t2i-adapter-lineart-sdxl-1.0 + repo_id: TencentARC/t2i-adapter-lineart-sdxl-1.0 sdxl/t2i_adapter/sketch-sdxl: - source: TencentARC/t2i-adapter-sketch-sdxl-1.0 + repo_id: TencentARC/t2i-adapter-sketch-sdxl-1.0 sd-1/embedding/EasyNegative: - source: https://huggingface.co/embed/EasyNegative/resolve/main/EasyNegative.safetensors + path: https://huggingface.co/embed/EasyNegative/resolve/main/EasyNegative.safetensors recommended: True - description: A textual inversion to use in the negative prompt to reduce bad anatomy -sd-1/lora/FlatColor: - source: https://civitai.com/models/6433/loraflatcolor - recommended: True - description: A LoRA that generates scenery using solid blocks of color +sd-1/embedding/ahx-beta-453407d: + repo_id: sd-concepts-library/ahx-beta-453407d sd-1/lora/Ink scenery: - source: https://civitai.com/api/download/models/83390 - description: Generate india ink-like landscapes + path: https://civitai.com/api/download/models/83390 sd-1/ip_adapter/ip_adapter_sd15: - source: InvokeAI/ip_adapter_sd15 + repo_id: InvokeAI/ip_adapter_sd15 recommended: True requires: - InvokeAI/ip_adapter_sd_image_encoder description: IP-Adapter for SD 1.5 models sd-1/ip_adapter/ip_adapter_plus_sd15: - source: InvokeAI/ip_adapter_plus_sd15 + repo_id: InvokeAI/ip_adapter_plus_sd15 recommended: False requires: - InvokeAI/ip_adapter_sd_image_encoder description: Refined IP-Adapter for SD 1.5 models sd-1/ip_adapter/ip_adapter_plus_face_sd15: - source: InvokeAI/ip_adapter_plus_face_sd15 + repo_id: InvokeAI/ip_adapter_plus_face_sd15 recommended: False requires: - InvokeAI/ip_adapter_sd_image_encoder description: Refined IP-Adapter for SD 1.5 models, adapted for faces sdxl/ip_adapter/ip_adapter_sdxl: - source: InvokeAI/ip_adapter_sdxl + repo_id: InvokeAI/ip_adapter_sdxl recommended: False requires: - InvokeAI/ip_adapter_sdxl_image_encoder description: IP-Adapter for SDXL models any/clip_vision/ip_adapter_sd_image_encoder: - source: InvokeAI/ip_adapter_sd_image_encoder + repo_id: InvokeAI/ip_adapter_sd_image_encoder recommended: False description: Required model for using IP-Adapters with SD-1/2 models any/clip_vision/ip_adapter_sdxl_image_encoder: - source: InvokeAI/ip_adapter_sdxl_image_encoder + repo_id: InvokeAI/ip_adapter_sdxl_image_encoder recommended: False description: Required model for using IP-Adapters with SDXL models diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index e23538ffd6..22b132370e 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -6,47 +6,45 @@ """ This is the npyscreen frontend to the model installation application. -The work is actually done in backend code in model_install_backend.py. +It is currently named model_install2.py, but will ultimately replace model_install.py. """ import argparse import curses -import logging import sys -import textwrap import traceback +import warnings from argparse import Namespace -from multiprocessing import Process -from multiprocessing.connection import Connection, Pipe -from pathlib import Path from shutil import get_terminal_size -from typing import Optional +from typing import Any, Dict, List, Optional, Set import npyscreen import torch from npyscreen import widget from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.install.model_install_backend import InstallSelections, ModelInstall, SchedulerPredictionType -from invokeai.backend.model_management import ModelManager, ModelType +from invokeai.app.services.model_install import ModelInstallServiceBase +from invokeai.backend.install.install_helper import InstallHelper, InstallSelections, UnifiedModelInfo +from invokeai.backend.model_manager import ModelType from invokeai.backend.util import choose_precision, choose_torch_device from invokeai.backend.util.logging import InvokeAILogger from invokeai.frontend.install.widgets import ( MIN_COLS, MIN_LINES, - BufferBox, CenteredTitleText, CyclingForm, MultiSelectColumns, SingleSelectColumns, TextBox, WindowTooSmallException, - select_stable_diffusion_config_file, set_min_terminal_size, ) +warnings.filterwarnings("ignore", category=UserWarning) # noqa: E402 config = InvokeAIAppConfig.get_config() -logger = InvokeAILogger.get_logger() +logger = InvokeAILogger.get_logger("ModelInstallService") +logger.setLevel("WARNING") +# logger.setLevel('DEBUG') # build a table mapping all non-printable characters to None # for stripping control characters @@ -58,44 +56,42 @@ MAX_OTHER_MODELS = 72 def make_printable(s: str) -> str: - """Replace non-printable characters in a string""" + """Replace non-printable characters in a string.""" return s.translate(NOPRINT_TRANS_TABLE) class addModelsForm(CyclingForm, npyscreen.FormMultiPage): + """Main form for interactive TUI.""" + # for responsive resizing set to False, but this seems to cause a crash! FIX_MINIMUM_SIZE_WHEN_CREATED = True # for persistence current_tab = 0 - def __init__(self, parentApp, name, multipage=False, *args, **keywords): + def __init__(self, parentApp: npyscreen.NPSAppManaged, name: str, multipage: bool = False, **keywords: Any): self.multipage = multipage self.subprocess = None - super().__init__(parentApp=parentApp, name=name, *args, **keywords) # noqa: B026 # TODO: maybe this is bad? + super().__init__(parentApp=parentApp, name=name, **keywords) - def create(self): + def create(self) -> None: + self.installer = self.parentApp.install_helper.installer + self.model_labels = self._get_model_labels() self.keypress_timeout = 10 self.counter = 0 self.subprocess_connection = None - if not config.model_conf_path.exists(): - with open(config.model_conf_path, "w") as file: - print("# InvokeAI model configuration file", file=file) - self.installer = ModelInstall(config) - self.all_models = self.installer.all_models() - self.starter_models = self.installer.starter_models() - self.model_labels = self._get_model_labels() window_width, window_height = get_terminal_size() - self.nextrely -= 1 + # npyscreen has no typing hints + self.nextrely -= 1 # type: ignore self.add_widget_intelligent( npyscreen.FixedText, value="Use ctrl-N and ctrl-P to move to the ext and

revious fields. Cursor keys navigate, and selects.", editable=False, color="CAUTION", ) - self.nextrely += 1 + self.nextrely += 1 # type: ignore self.tabs = self.add_widget_intelligent( SingleSelectColumns, values=[ @@ -115,9 +111,9 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): ) self.tabs.on_changed = self._toggle_tables - top_of_table = self.nextrely + top_of_table = self.nextrely # type: ignore self.starter_pipelines = self.add_starter_pipelines() - bottom_of_table = self.nextrely + bottom_of_table = self.nextrely # type: ignore self.nextrely = top_of_table self.pipeline_models = self.add_pipeline_widgets( @@ -162,15 +158,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.nextrely = bottom_of_table + 1 - self.monitor = self.add_widget_intelligent( - BufferBox, - name="Log Messages", - editable=False, - max_height=6, - ) - self.nextrely += 1 - done_label = "APPLY CHANGES" back_label = "BACK" cancel_label = "CANCEL" current_position = self.nextrely @@ -186,14 +174,8 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): npyscreen.ButtonPress, name=cancel_label, when_pressed_function=self.on_cancel ) self.nextrely = current_position - self.ok_button = self.add_widget_intelligent( - npyscreen.ButtonPress, - name=done_label, - relx=(window_width - len(done_label)) // 2, - when_pressed_function=self.on_execute, - ) - label = "APPLY CHANGES & EXIT" + label = "APPLY CHANGES" self.nextrely = current_position self.done = self.add_widget_intelligent( npyscreen.ButtonPress, @@ -210,17 +192,16 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): ############# diffusers tab ########## def add_starter_pipelines(self) -> dict[str, npyscreen.widget]: """Add widgets responsible for selecting diffusers models""" - widgets = {} - models = self.all_models - starters = self.starter_models - starter_model_labels = self.model_labels + widgets: Dict[str, npyscreen.widget] = {} - self.installed_models = sorted([x for x in starters if models[x].installed]) + all_models = self.all_models # master dict of all models, indexed by key + model_list = [x for x in self.starter_models if all_models[x].type in ["main", "vae"]] + model_labels = [self.model_labels[x] for x in model_list] widgets.update( label1=self.add_widget_intelligent( CenteredTitleText, - name="Select from a starter set of Stable Diffusion models from HuggingFace.", + name="Select from a starter set of Stable Diffusion models from HuggingFace and Civitae.", editable=False, labelColor="CAUTION", ) @@ -230,23 +211,24 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): # if user has already installed some initial models, then don't patronize them # by showing more recommendations show_recommended = len(self.installed_models) == 0 - keys = [x for x in models.keys() if x in starters] + + checked = [ + model_list.index(x) + for x in model_list + if (show_recommended and all_models[x].recommended) or all_models[x].installed + ] widgets.update( models_selected=self.add_widget_intelligent( MultiSelectColumns, columns=1, name="Install Starter Models", - values=[starter_model_labels[x] for x in keys], - value=[ - keys.index(x) - for x in keys - if (show_recommended and models[x].recommended) or (x in self.installed_models) - ], - max_height=len(starters) + 1, + values=model_labels, + value=checked, + max_height=len(model_list) + 1, relx=4, scroll_exit=True, ), - models=keys, + models=model_list, ) self.nextrely += 1 @@ -257,14 +239,18 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self, model_type: ModelType, window_width: int = 120, - install_prompt: str = None, - exclude: set = None, + install_prompt: Optional[str] = None, + exclude: Optional[Set[str]] = None, ) -> dict[str, npyscreen.widget]: """Generic code to create model selection widgets""" if exclude is None: exclude = set() - widgets = {} - model_list = [x for x in self.all_models if self.all_models[x].model_type == model_type and x not in exclude] + widgets: Dict[str, npyscreen.widget] = {} + all_models = self.all_models + model_list = sorted( + [x for x in all_models if all_models[x].type == model_type and x not in exclude], + key=lambda x: all_models[x].name or "", + ) model_labels = [self.model_labels[x] for x in model_list] show_recommended = len(self.installed_models) == 0 @@ -300,7 +286,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): value=[ model_list.index(x) for x in model_list - if (show_recommended and self.all_models[x].recommended) or self.all_models[x].installed + if (show_recommended and all_models[x].recommended) or all_models[x].installed ], max_height=len(model_list) // columns + 1, relx=4, @@ -324,7 +310,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): download_ids=self.add_widget_intelligent( TextBox, name="Additional URLs, or HuggingFace repo_ids to install (Space separated. Use shift-control-V to paste):", - max_height=4, + max_height=6, scroll_exit=True, editable=True, ) @@ -349,13 +335,13 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): return widgets - def resize(self): + def resize(self) -> None: super().resize() if s := self.starter_pipelines.get("models_selected"): - keys = [x for x in self.all_models.keys() if x in self.starter_models] - s.values = [self.model_labels[x] for x in keys] + if model_list := self.starter_pipelines.get("models"): + s.values = [self.model_labels[x] for x in model_list] - def _toggle_tables(self, value=None): + def _toggle_tables(self, value: List[int]) -> None: selected_tab = value[0] widgets = [ self.starter_pipelines, @@ -385,17 +371,18 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.display() def _get_model_labels(self) -> dict[str, str]: + """Return a list of trimmed labels for all models.""" window_width, window_height = get_terminal_size() checkbox_width = 4 spacing_width = 2 + result = {} models = self.all_models - label_width = max([len(models[x].name) for x in models]) + label_width = max([len(models[x].name or "") for x in self.starter_models]) description_width = window_width - label_width - checkbox_width - spacing_width - result = {} - for x in models.keys(): - description = models[x].description + for key in self.all_models: + description = models[key].description description = ( description[0 : description_width - 3] + "..." if description and len(description) > description_width @@ -403,7 +390,8 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): if description else "" ) - result[x] = f"%-{label_width}s %s" % (models[x].name, description) + result[key] = f"%-{label_width}s %s" % (models[key].name, description) + return result def _get_columns(self) -> int: @@ -413,50 +401,40 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): def confirm_deletions(self, selections: InstallSelections) -> bool: remove_models = selections.remove_models - if len(remove_models) > 0: - mods = "\n".join([ModelManager.parse_key(x)[0] for x in remove_models]) - return npyscreen.notify_ok_cancel( + if remove_models: + model_names = [self.all_models[x].name or "" for x in remove_models] + mods = "\n".join(model_names) + is_ok = npyscreen.notify_ok_cancel( f"These unchecked models will be deleted from disk. Continue?\n---------\n{mods}" ) + assert isinstance(is_ok, bool) # npyscreen doesn't have return type annotations + return is_ok else: return True - def on_execute(self): - self.marshall_arguments() - app = self.parentApp - if not self.confirm_deletions(app.install_selections): - return + @property + def all_models(self) -> Dict[str, UnifiedModelInfo]: + # npyscreen doesn't having typing hints + return self.parentApp.install_helper.all_models # type: ignore - self.monitor.entry_widget.buffer(["Processing..."], scroll_end=True) - self.ok_button.hidden = True - self.display() + @property + def starter_models(self) -> List[str]: + return self.parentApp.install_helper._starter_models # type: ignore - # TO DO: Spawn a worker thread, not a subprocess - parent_conn, child_conn = Pipe() - p = Process( - target=process_and_execute, - kwargs={ - "opt": app.program_opts, - "selections": app.install_selections, - "conn_out": child_conn, - }, - ) - p.start() - child_conn.close() - self.subprocess_connection = parent_conn - self.subprocess = p - app.install_selections = InstallSelections() + @property + def installed_models(self) -> List[str]: + return self.parentApp.install_helper._installed_models # type: ignore - def on_back(self): + def on_back(self) -> None: self.parentApp.switchFormPrevious() self.editing = False - def on_cancel(self): + def on_cancel(self) -> None: self.parentApp.setNextForm(None) self.parentApp.user_cancelled = True self.editing = False - def on_done(self): + def on_done(self) -> None: self.marshall_arguments() if not self.confirm_deletions(self.parentApp.install_selections): return @@ -464,77 +442,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.parentApp.user_cancelled = False self.editing = False - ########## This routine monitors the child process that is performing model installation and removal ##### - def while_waiting(self): - """Called during idle periods. Main task is to update the Log Messages box with messages - from the child process that does the actual installation/removal""" - c = self.subprocess_connection - if not c: - return - - monitor_widget = self.monitor.entry_widget - while c.poll(): - try: - data = c.recv_bytes().decode("utf-8") - data.strip("\n") - - # processing child is requesting user input to select the - # right configuration file - if data.startswith("*need v2 config"): - _, model_path, *_ = data.split(":", 2) - self._return_v2_config(model_path) - - # processing child is done - elif data == "*done*": - self._close_subprocess_and_regenerate_form() - break - - # update the log message box - else: - data = make_printable(data) - data = data.replace("[A", "") - monitor_widget.buffer( - textwrap.wrap( - data, - width=monitor_widget.width, - subsequent_indent=" ", - ), - scroll_end=True, - ) - self.display() - except (EOFError, OSError): - self.subprocess_connection = None - - def _return_v2_config(self, model_path: str): - c = self.subprocess_connection - model_name = Path(model_path).name - message = select_stable_diffusion_config_file(model_name=model_name) - c.send_bytes(message.encode("utf-8")) - - def _close_subprocess_and_regenerate_form(self): - app = self.parentApp - self.subprocess_connection.close() - self.subprocess_connection = None - self.monitor.entry_widget.buffer(["** Action Complete **"]) - self.display() - - # rebuild the form, saving and restoring some of the fields that need to be preserved. - saved_messages = self.monitor.entry_widget.values - - app.main_form = app.addForm( - "MAIN", - addModelsForm, - name="Install Stable Diffusion Models", - multipage=self.multipage, - ) - app.switchForm("MAIN") - - app.main_form.monitor.entry_widget.values = saved_messages - app.main_form.monitor.entry_widget.buffer([""], scroll_end=True) - # app.main_form.pipeline_models['autoload_directory'].value = autoload_dir - # app.main_form.pipeline_models['autoscan_on_startup'].value = autoscan - - def marshall_arguments(self): + def marshall_arguments(self) -> None: """ Assemble arguments and store as attributes of the application: .starter_models: dict of model names to install from INITIAL_CONFIGURE.yaml @@ -564,46 +472,24 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): models_to_install = [x for x in selected if not self.all_models[x].installed] models_to_remove = [x for x in section["models"] if x not in selected and self.all_models[x].installed] selections.remove_models.extend(models_to_remove) - selections.install_models.extend( - all_models[x].path or all_models[x].repo_id - for x in models_to_install - if all_models[x].path or all_models[x].repo_id - ) + selections.install_models.extend([all_models[x] for x in models_to_install]) # models located in the 'download_ids" section for section in ui_sections: if downloads := section.get("download_ids"): - selections.install_models.extend(downloads.value.split()) - - # NOT NEEDED - DONE IN BACKEND NOW - # # special case for the ipadapter_models. If any of the adapters are - # # chosen, then we add the corresponding encoder(s) to the install list. - # section = self.ipadapter_models - # if section.get("models_selected"): - # selected_adapters = [ - # self.all_models[section["models"][x]].name for x in section.get("models_selected").value - # ] - # encoders = [] - # if any(["sdxl" in x for x in selected_adapters]): - # encoders.append("ip_adapter_sdxl_image_encoder") - # if any(["sd15" in x for x in selected_adapters]): - # encoders.append("ip_adapter_sd_image_encoder") - # for encoder in encoders: - # key = f"any/clip_vision/{encoder}" - # repo_id = f"InvokeAI/{encoder}" - # if key not in self.all_models: - # selections.install_models.append(repo_id) + models = [UnifiedModelInfo(source=x) for x in downloads.value.split()] + selections.install_models.extend(models) -class AddModelApplication(npyscreen.NPSAppManaged): - def __init__(self, opt): +class AddModelApplication(npyscreen.NPSAppManaged): # type: ignore + def __init__(self, opt: Namespace, install_helper: InstallHelper): super().__init__() self.program_opts = opt self.user_cancelled = False - # self.autoload_pending = True self.install_selections = InstallSelections() + self.install_helper = install_helper - def onStart(self): + def onStart(self) -> None: npyscreen.setTheme(npyscreen.Themes.DefaultTheme) self.main_form = self.addForm( "MAIN", @@ -613,138 +499,62 @@ class AddModelApplication(npyscreen.NPSAppManaged): ) -class StderrToMessage: - def __init__(self, connection: Connection): - self.connection = connection - - def write(self, data: str): - self.connection.send_bytes(data.encode("utf-8")) - - def flush(self): - pass +def list_models(installer: ModelInstallServiceBase, model_type: ModelType): + """Print out all models of type model_type.""" + models = installer.record_store.search_by_attr(model_type=model_type) + print(f"Installed models of type `{model_type}`:") + for model in models: + path = (config.models_path / model.path).resolve() + print(f"{model.name:40}{model.base.value:5}{model.type.value:8}{model.format.value:12}{path}") # -------------------------------------------------------- -def ask_user_for_prediction_type(model_path: Path, tui_conn: Connection = None) -> SchedulerPredictionType: - if tui_conn: - logger.debug("Waiting for user response...") - return _ask_user_for_pt_tui(model_path, tui_conn) - else: - return _ask_user_for_pt_cmdline(model_path) - - -def _ask_user_for_pt_cmdline(model_path: Path) -> Optional[SchedulerPredictionType]: - choices = [SchedulerPredictionType.Epsilon, SchedulerPredictionType.VPrediction, None] - print( - f""" -Please select the scheduler prediction type of the checkpoint named {model_path.name}: -[1] "epsilon" - most v1.5 models and v2 models trained on 512 pixel images -[2] "vprediction" - v2 models trained on 768 pixel images and a few v1.5 models -[3] Accept the best guess; you can fix it in the Web UI later -""" - ) - choice = None - ok = False - while not ok: - try: - choice = input("select [3]> ").strip() - if not choice: - return None - choice = choices[int(choice) - 1] - ok = True - except (ValueError, IndexError): - print(f"{choice} is not a valid choice") - except EOFError: - return - return choice - - -def _ask_user_for_pt_tui(model_path: Path, tui_conn: Connection) -> SchedulerPredictionType: - tui_conn.send_bytes(f"*need v2 config for:{model_path}".encode("utf-8")) - # note that we don't do any status checking here - response = tui_conn.recv_bytes().decode("utf-8") - if response is None: - return None - elif response == "epsilon": - return SchedulerPredictionType.epsilon - elif response == "v": - return SchedulerPredictionType.VPrediction - elif response == "guess": - return None - else: - return None - - -# -------------------------------------------------------- -def process_and_execute( - opt: Namespace, - selections: InstallSelections, - conn_out: Connection = None, -): - # need to reinitialize config in subprocess - config = InvokeAIAppConfig.get_config() - args = ["--root", opt.root] if opt.root else [] - config.parse_args(args) - - # set up so that stderr is sent to conn_out - if conn_out: - translator = StderrToMessage(conn_out) - sys.stderr = translator - sys.stdout = translator - logger = InvokeAILogger.get_logger() - logger.handlers.clear() - logger.addHandler(logging.StreamHandler(translator)) - - installer = ModelInstall(config, prediction_type_helper=lambda x: ask_user_for_prediction_type(x, conn_out)) - installer.install(selections) - - if conn_out: - conn_out.send_bytes("*done*".encode("utf-8")) - conn_out.close() - - -# -------------------------------------------------------- -def select_and_download_models(opt: Namespace): +def select_and_download_models(opt: Namespace) -> None: + """Prompt user for install/delete selections and execute.""" precision = "float32" if opt.full_precision else choose_precision(torch.device(choose_torch_device())) - config.precision = precision - installer = ModelInstall(config, prediction_type_helper=ask_user_for_prediction_type) + # unsure how to avoid a typing complaint in the next line: config.precision is an enumerated Literal + config.precision = precision # type: ignore + install_helper = InstallHelper(config, logger) + installer = install_helper.installer + if opt.list_models: - installer.list_models(opt.list_models) + list_models(installer, opt.list_models) + elif opt.add or opt.delete: - selections = InstallSelections(install_models=opt.add or [], remove_models=opt.delete or []) - installer.install(selections) + selections = InstallSelections( + install_models=[UnifiedModelInfo(source=x) for x in (opt.add or [])], remove_models=opt.delete or [] + ) + install_helper.add_or_delete(selections) + elif opt.default_only: - selections = InstallSelections(install_models=installer.default_model()) - installer.install(selections) + default_model = install_helper.default_model() + assert default_model is not None + selections = InstallSelections(install_models=[default_model]) + install_helper.add_or_delete(selections) + elif opt.yes_to_all: - selections = InstallSelections(install_models=installer.recommended_models()) - installer.install(selections) + selections = InstallSelections(install_models=install_helper.recommended_models()) + install_helper.add_or_delete(selections) # this is where the TUI is called else: - # needed to support the probe() method running under a subprocess - torch.multiprocessing.set_start_method("spawn") - if not set_min_terminal_size(MIN_COLS, MIN_LINES): raise WindowTooSmallException( "Could not increase terminal size. Try running again with a larger window or smaller font size." ) - installApp = AddModelApplication(opt) + installApp = AddModelApplication(opt, install_helper) try: installApp.run() - except KeyboardInterrupt as e: - if hasattr(installApp, "main_form"): - if installApp.main_form.subprocess and installApp.main_form.subprocess.is_alive(): - logger.info("Terminating subprocesses") - installApp.main_form.subprocess.terminate() - installApp.main_form.subprocess = None - raise e - process_and_execute(opt, installApp.install_selections) + except KeyboardInterrupt: + print("Aborted...") + sys.exit(-1) + + install_helper.add_or_delete(installApp.install_selections) # ------------------------------------- -def main(): +def main() -> None: parser = argparse.ArgumentParser(description="InvokeAI model downloader") parser.add_argument( "--add", @@ -754,7 +564,7 @@ def main(): parser.add_argument( "--delete", nargs="*", - help="List of names of models to idelete", + help="List of names of models to delete. Use type:name to disambiguate, as in `controlnet:my_model`", ) parser.add_argument( "--full-precision", @@ -781,14 +591,6 @@ def main(): choices=[x.value for x in ModelType], help="list installed models", ) - parser.add_argument( - "--config_file", - "-c", - dest="config_file", - type=str, - default=None, - help="path to configuration file to create", - ) parser.add_argument( "--root_dir", dest="root", diff --git a/invokeai/frontend/install/model_install2.py b/invokeai/frontend/install/model_install.py.OLD similarity index 57% rename from invokeai/frontend/install/model_install2.py rename to invokeai/frontend/install/model_install.py.OLD index 22b132370e..e23538ffd6 100644 --- a/invokeai/frontend/install/model_install2.py +++ b/invokeai/frontend/install/model_install.py.OLD @@ -6,45 +6,47 @@ """ This is the npyscreen frontend to the model installation application. -It is currently named model_install2.py, but will ultimately replace model_install.py. +The work is actually done in backend code in model_install_backend.py. """ import argparse import curses +import logging import sys +import textwrap import traceback -import warnings from argparse import Namespace +from multiprocessing import Process +from multiprocessing.connection import Connection, Pipe +from pathlib import Path from shutil import get_terminal_size -from typing import Any, Dict, List, Optional, Set +from typing import Optional import npyscreen import torch from npyscreen import widget from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.model_install import ModelInstallServiceBase -from invokeai.backend.install.install_helper import InstallHelper, InstallSelections, UnifiedModelInfo -from invokeai.backend.model_manager import ModelType +from invokeai.backend.install.model_install_backend import InstallSelections, ModelInstall, SchedulerPredictionType +from invokeai.backend.model_management import ModelManager, ModelType from invokeai.backend.util import choose_precision, choose_torch_device from invokeai.backend.util.logging import InvokeAILogger from invokeai.frontend.install.widgets import ( MIN_COLS, MIN_LINES, + BufferBox, CenteredTitleText, CyclingForm, MultiSelectColumns, SingleSelectColumns, TextBox, WindowTooSmallException, + select_stable_diffusion_config_file, set_min_terminal_size, ) -warnings.filterwarnings("ignore", category=UserWarning) # noqa: E402 config = InvokeAIAppConfig.get_config() -logger = InvokeAILogger.get_logger("ModelInstallService") -logger.setLevel("WARNING") -# logger.setLevel('DEBUG') +logger = InvokeAILogger.get_logger() # build a table mapping all non-printable characters to None # for stripping control characters @@ -56,42 +58,44 @@ MAX_OTHER_MODELS = 72 def make_printable(s: str) -> str: - """Replace non-printable characters in a string.""" + """Replace non-printable characters in a string""" return s.translate(NOPRINT_TRANS_TABLE) class addModelsForm(CyclingForm, npyscreen.FormMultiPage): - """Main form for interactive TUI.""" - # for responsive resizing set to False, but this seems to cause a crash! FIX_MINIMUM_SIZE_WHEN_CREATED = True # for persistence current_tab = 0 - def __init__(self, parentApp: npyscreen.NPSAppManaged, name: str, multipage: bool = False, **keywords: Any): + def __init__(self, parentApp, name, multipage=False, *args, **keywords): self.multipage = multipage self.subprocess = None - super().__init__(parentApp=parentApp, name=name, **keywords) + super().__init__(parentApp=parentApp, name=name, *args, **keywords) # noqa: B026 # TODO: maybe this is bad? - def create(self) -> None: - self.installer = self.parentApp.install_helper.installer - self.model_labels = self._get_model_labels() + def create(self): self.keypress_timeout = 10 self.counter = 0 self.subprocess_connection = None + if not config.model_conf_path.exists(): + with open(config.model_conf_path, "w") as file: + print("# InvokeAI model configuration file", file=file) + self.installer = ModelInstall(config) + self.all_models = self.installer.all_models() + self.starter_models = self.installer.starter_models() + self.model_labels = self._get_model_labels() window_width, window_height = get_terminal_size() - # npyscreen has no typing hints - self.nextrely -= 1 # type: ignore + self.nextrely -= 1 self.add_widget_intelligent( npyscreen.FixedText, value="Use ctrl-N and ctrl-P to move to the ext and

revious fields. Cursor keys navigate, and selects.", editable=False, color="CAUTION", ) - self.nextrely += 1 # type: ignore + self.nextrely += 1 self.tabs = self.add_widget_intelligent( SingleSelectColumns, values=[ @@ -111,9 +115,9 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): ) self.tabs.on_changed = self._toggle_tables - top_of_table = self.nextrely # type: ignore + top_of_table = self.nextrely self.starter_pipelines = self.add_starter_pipelines() - bottom_of_table = self.nextrely # type: ignore + bottom_of_table = self.nextrely self.nextrely = top_of_table self.pipeline_models = self.add_pipeline_widgets( @@ -158,7 +162,15 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.nextrely = bottom_of_table + 1 + self.monitor = self.add_widget_intelligent( + BufferBox, + name="Log Messages", + editable=False, + max_height=6, + ) + self.nextrely += 1 + done_label = "APPLY CHANGES" back_label = "BACK" cancel_label = "CANCEL" current_position = self.nextrely @@ -174,8 +186,14 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): npyscreen.ButtonPress, name=cancel_label, when_pressed_function=self.on_cancel ) self.nextrely = current_position + self.ok_button = self.add_widget_intelligent( + npyscreen.ButtonPress, + name=done_label, + relx=(window_width - len(done_label)) // 2, + when_pressed_function=self.on_execute, + ) - label = "APPLY CHANGES" + label = "APPLY CHANGES & EXIT" self.nextrely = current_position self.done = self.add_widget_intelligent( npyscreen.ButtonPress, @@ -192,16 +210,17 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): ############# diffusers tab ########## def add_starter_pipelines(self) -> dict[str, npyscreen.widget]: """Add widgets responsible for selecting diffusers models""" - widgets: Dict[str, npyscreen.widget] = {} + widgets = {} + models = self.all_models + starters = self.starter_models + starter_model_labels = self.model_labels - all_models = self.all_models # master dict of all models, indexed by key - model_list = [x for x in self.starter_models if all_models[x].type in ["main", "vae"]] - model_labels = [self.model_labels[x] for x in model_list] + self.installed_models = sorted([x for x in starters if models[x].installed]) widgets.update( label1=self.add_widget_intelligent( CenteredTitleText, - name="Select from a starter set of Stable Diffusion models from HuggingFace and Civitae.", + name="Select from a starter set of Stable Diffusion models from HuggingFace.", editable=False, labelColor="CAUTION", ) @@ -211,24 +230,23 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): # if user has already installed some initial models, then don't patronize them # by showing more recommendations show_recommended = len(self.installed_models) == 0 - - checked = [ - model_list.index(x) - for x in model_list - if (show_recommended and all_models[x].recommended) or all_models[x].installed - ] + keys = [x for x in models.keys() if x in starters] widgets.update( models_selected=self.add_widget_intelligent( MultiSelectColumns, columns=1, name="Install Starter Models", - values=model_labels, - value=checked, - max_height=len(model_list) + 1, + values=[starter_model_labels[x] for x in keys], + value=[ + keys.index(x) + for x in keys + if (show_recommended and models[x].recommended) or (x in self.installed_models) + ], + max_height=len(starters) + 1, relx=4, scroll_exit=True, ), - models=model_list, + models=keys, ) self.nextrely += 1 @@ -239,18 +257,14 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self, model_type: ModelType, window_width: int = 120, - install_prompt: Optional[str] = None, - exclude: Optional[Set[str]] = None, + install_prompt: str = None, + exclude: set = None, ) -> dict[str, npyscreen.widget]: """Generic code to create model selection widgets""" if exclude is None: exclude = set() - widgets: Dict[str, npyscreen.widget] = {} - all_models = self.all_models - model_list = sorted( - [x for x in all_models if all_models[x].type == model_type and x not in exclude], - key=lambda x: all_models[x].name or "", - ) + widgets = {} + model_list = [x for x in self.all_models if self.all_models[x].model_type == model_type and x not in exclude] model_labels = [self.model_labels[x] for x in model_list] show_recommended = len(self.installed_models) == 0 @@ -286,7 +300,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): value=[ model_list.index(x) for x in model_list - if (show_recommended and all_models[x].recommended) or all_models[x].installed + if (show_recommended and self.all_models[x].recommended) or self.all_models[x].installed ], max_height=len(model_list) // columns + 1, relx=4, @@ -310,7 +324,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): download_ids=self.add_widget_intelligent( TextBox, name="Additional URLs, or HuggingFace repo_ids to install (Space separated. Use shift-control-V to paste):", - max_height=6, + max_height=4, scroll_exit=True, editable=True, ) @@ -335,13 +349,13 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): return widgets - def resize(self) -> None: + def resize(self): super().resize() if s := self.starter_pipelines.get("models_selected"): - if model_list := self.starter_pipelines.get("models"): - s.values = [self.model_labels[x] for x in model_list] + keys = [x for x in self.all_models.keys() if x in self.starter_models] + s.values = [self.model_labels[x] for x in keys] - def _toggle_tables(self, value: List[int]) -> None: + def _toggle_tables(self, value=None): selected_tab = value[0] widgets = [ self.starter_pipelines, @@ -371,18 +385,17 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.display() def _get_model_labels(self) -> dict[str, str]: - """Return a list of trimmed labels for all models.""" window_width, window_height = get_terminal_size() checkbox_width = 4 spacing_width = 2 - result = {} models = self.all_models - label_width = max([len(models[x].name or "") for x in self.starter_models]) + label_width = max([len(models[x].name) for x in models]) description_width = window_width - label_width - checkbox_width - spacing_width - for key in self.all_models: - description = models[key].description + result = {} + for x in models.keys(): + description = models[x].description description = ( description[0 : description_width - 3] + "..." if description and len(description) > description_width @@ -390,8 +403,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): if description else "" ) - result[key] = f"%-{label_width}s %s" % (models[key].name, description) - + result[x] = f"%-{label_width}s %s" % (models[x].name, description) return result def _get_columns(self) -> int: @@ -401,40 +413,50 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): def confirm_deletions(self, selections: InstallSelections) -> bool: remove_models = selections.remove_models - if remove_models: - model_names = [self.all_models[x].name or "" for x in remove_models] - mods = "\n".join(model_names) - is_ok = npyscreen.notify_ok_cancel( + if len(remove_models) > 0: + mods = "\n".join([ModelManager.parse_key(x)[0] for x in remove_models]) + return npyscreen.notify_ok_cancel( f"These unchecked models will be deleted from disk. Continue?\n---------\n{mods}" ) - assert isinstance(is_ok, bool) # npyscreen doesn't have return type annotations - return is_ok else: return True - @property - def all_models(self) -> Dict[str, UnifiedModelInfo]: - # npyscreen doesn't having typing hints - return self.parentApp.install_helper.all_models # type: ignore + def on_execute(self): + self.marshall_arguments() + app = self.parentApp + if not self.confirm_deletions(app.install_selections): + return - @property - def starter_models(self) -> List[str]: - return self.parentApp.install_helper._starter_models # type: ignore + self.monitor.entry_widget.buffer(["Processing..."], scroll_end=True) + self.ok_button.hidden = True + self.display() - @property - def installed_models(self) -> List[str]: - return self.parentApp.install_helper._installed_models # type: ignore + # TO DO: Spawn a worker thread, not a subprocess + parent_conn, child_conn = Pipe() + p = Process( + target=process_and_execute, + kwargs={ + "opt": app.program_opts, + "selections": app.install_selections, + "conn_out": child_conn, + }, + ) + p.start() + child_conn.close() + self.subprocess_connection = parent_conn + self.subprocess = p + app.install_selections = InstallSelections() - def on_back(self) -> None: + def on_back(self): self.parentApp.switchFormPrevious() self.editing = False - def on_cancel(self) -> None: + def on_cancel(self): self.parentApp.setNextForm(None) self.parentApp.user_cancelled = True self.editing = False - def on_done(self) -> None: + def on_done(self): self.marshall_arguments() if not self.confirm_deletions(self.parentApp.install_selections): return @@ -442,7 +464,77 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): self.parentApp.user_cancelled = False self.editing = False - def marshall_arguments(self) -> None: + ########## This routine monitors the child process that is performing model installation and removal ##### + def while_waiting(self): + """Called during idle periods. Main task is to update the Log Messages box with messages + from the child process that does the actual installation/removal""" + c = self.subprocess_connection + if not c: + return + + monitor_widget = self.monitor.entry_widget + while c.poll(): + try: + data = c.recv_bytes().decode("utf-8") + data.strip("\n") + + # processing child is requesting user input to select the + # right configuration file + if data.startswith("*need v2 config"): + _, model_path, *_ = data.split(":", 2) + self._return_v2_config(model_path) + + # processing child is done + elif data == "*done*": + self._close_subprocess_and_regenerate_form() + break + + # update the log message box + else: + data = make_printable(data) + data = data.replace("[A", "") + monitor_widget.buffer( + textwrap.wrap( + data, + width=monitor_widget.width, + subsequent_indent=" ", + ), + scroll_end=True, + ) + self.display() + except (EOFError, OSError): + self.subprocess_connection = None + + def _return_v2_config(self, model_path: str): + c = self.subprocess_connection + model_name = Path(model_path).name + message = select_stable_diffusion_config_file(model_name=model_name) + c.send_bytes(message.encode("utf-8")) + + def _close_subprocess_and_regenerate_form(self): + app = self.parentApp + self.subprocess_connection.close() + self.subprocess_connection = None + self.monitor.entry_widget.buffer(["** Action Complete **"]) + self.display() + + # rebuild the form, saving and restoring some of the fields that need to be preserved. + saved_messages = self.monitor.entry_widget.values + + app.main_form = app.addForm( + "MAIN", + addModelsForm, + name="Install Stable Diffusion Models", + multipage=self.multipage, + ) + app.switchForm("MAIN") + + app.main_form.monitor.entry_widget.values = saved_messages + app.main_form.monitor.entry_widget.buffer([""], scroll_end=True) + # app.main_form.pipeline_models['autoload_directory'].value = autoload_dir + # app.main_form.pipeline_models['autoscan_on_startup'].value = autoscan + + def marshall_arguments(self): """ Assemble arguments and store as attributes of the application: .starter_models: dict of model names to install from INITIAL_CONFIGURE.yaml @@ -472,24 +564,46 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): models_to_install = [x for x in selected if not self.all_models[x].installed] models_to_remove = [x for x in section["models"] if x not in selected and self.all_models[x].installed] selections.remove_models.extend(models_to_remove) - selections.install_models.extend([all_models[x] for x in models_to_install]) + selections.install_models.extend( + all_models[x].path or all_models[x].repo_id + for x in models_to_install + if all_models[x].path or all_models[x].repo_id + ) # models located in the 'download_ids" section for section in ui_sections: if downloads := section.get("download_ids"): - models = [UnifiedModelInfo(source=x) for x in downloads.value.split()] - selections.install_models.extend(models) + selections.install_models.extend(downloads.value.split()) + + # NOT NEEDED - DONE IN BACKEND NOW + # # special case for the ipadapter_models. If any of the adapters are + # # chosen, then we add the corresponding encoder(s) to the install list. + # section = self.ipadapter_models + # if section.get("models_selected"): + # selected_adapters = [ + # self.all_models[section["models"][x]].name for x in section.get("models_selected").value + # ] + # encoders = [] + # if any(["sdxl" in x for x in selected_adapters]): + # encoders.append("ip_adapter_sdxl_image_encoder") + # if any(["sd15" in x for x in selected_adapters]): + # encoders.append("ip_adapter_sd_image_encoder") + # for encoder in encoders: + # key = f"any/clip_vision/{encoder}" + # repo_id = f"InvokeAI/{encoder}" + # if key not in self.all_models: + # selections.install_models.append(repo_id) -class AddModelApplication(npyscreen.NPSAppManaged): # type: ignore - def __init__(self, opt: Namespace, install_helper: InstallHelper): +class AddModelApplication(npyscreen.NPSAppManaged): + def __init__(self, opt): super().__init__() self.program_opts = opt self.user_cancelled = False + # self.autoload_pending = True self.install_selections = InstallSelections() - self.install_helper = install_helper - def onStart(self) -> None: + def onStart(self): npyscreen.setTheme(npyscreen.Themes.DefaultTheme) self.main_form = self.addForm( "MAIN", @@ -499,62 +613,138 @@ class AddModelApplication(npyscreen.NPSAppManaged): # type: ignore ) -def list_models(installer: ModelInstallServiceBase, model_type: ModelType): - """Print out all models of type model_type.""" - models = installer.record_store.search_by_attr(model_type=model_type) - print(f"Installed models of type `{model_type}`:") - for model in models: - path = (config.models_path / model.path).resolve() - print(f"{model.name:40}{model.base.value:5}{model.type.value:8}{model.format.value:12}{path}") +class StderrToMessage: + def __init__(self, connection: Connection): + self.connection = connection + + def write(self, data: str): + self.connection.send_bytes(data.encode("utf-8")) + + def flush(self): + pass # -------------------------------------------------------- -def select_and_download_models(opt: Namespace) -> None: - """Prompt user for install/delete selections and execute.""" +def ask_user_for_prediction_type(model_path: Path, tui_conn: Connection = None) -> SchedulerPredictionType: + if tui_conn: + logger.debug("Waiting for user response...") + return _ask_user_for_pt_tui(model_path, tui_conn) + else: + return _ask_user_for_pt_cmdline(model_path) + + +def _ask_user_for_pt_cmdline(model_path: Path) -> Optional[SchedulerPredictionType]: + choices = [SchedulerPredictionType.Epsilon, SchedulerPredictionType.VPrediction, None] + print( + f""" +Please select the scheduler prediction type of the checkpoint named {model_path.name}: +[1] "epsilon" - most v1.5 models and v2 models trained on 512 pixel images +[2] "vprediction" - v2 models trained on 768 pixel images and a few v1.5 models +[3] Accept the best guess; you can fix it in the Web UI later +""" + ) + choice = None + ok = False + while not ok: + try: + choice = input("select [3]> ").strip() + if not choice: + return None + choice = choices[int(choice) - 1] + ok = True + except (ValueError, IndexError): + print(f"{choice} is not a valid choice") + except EOFError: + return + return choice + + +def _ask_user_for_pt_tui(model_path: Path, tui_conn: Connection) -> SchedulerPredictionType: + tui_conn.send_bytes(f"*need v2 config for:{model_path}".encode("utf-8")) + # note that we don't do any status checking here + response = tui_conn.recv_bytes().decode("utf-8") + if response is None: + return None + elif response == "epsilon": + return SchedulerPredictionType.epsilon + elif response == "v": + return SchedulerPredictionType.VPrediction + elif response == "guess": + return None + else: + return None + + +# -------------------------------------------------------- +def process_and_execute( + opt: Namespace, + selections: InstallSelections, + conn_out: Connection = None, +): + # need to reinitialize config in subprocess + config = InvokeAIAppConfig.get_config() + args = ["--root", opt.root] if opt.root else [] + config.parse_args(args) + + # set up so that stderr is sent to conn_out + if conn_out: + translator = StderrToMessage(conn_out) + sys.stderr = translator + sys.stdout = translator + logger = InvokeAILogger.get_logger() + logger.handlers.clear() + logger.addHandler(logging.StreamHandler(translator)) + + installer = ModelInstall(config, prediction_type_helper=lambda x: ask_user_for_prediction_type(x, conn_out)) + installer.install(selections) + + if conn_out: + conn_out.send_bytes("*done*".encode("utf-8")) + conn_out.close() + + +# -------------------------------------------------------- +def select_and_download_models(opt: Namespace): precision = "float32" if opt.full_precision else choose_precision(torch.device(choose_torch_device())) - # unsure how to avoid a typing complaint in the next line: config.precision is an enumerated Literal - config.precision = precision # type: ignore - install_helper = InstallHelper(config, logger) - installer = install_helper.installer - + config.precision = precision + installer = ModelInstall(config, prediction_type_helper=ask_user_for_prediction_type) if opt.list_models: - list_models(installer, opt.list_models) - + installer.list_models(opt.list_models) elif opt.add or opt.delete: - selections = InstallSelections( - install_models=[UnifiedModelInfo(source=x) for x in (opt.add or [])], remove_models=opt.delete or [] - ) - install_helper.add_or_delete(selections) - + selections = InstallSelections(install_models=opt.add or [], remove_models=opt.delete or []) + installer.install(selections) elif opt.default_only: - default_model = install_helper.default_model() - assert default_model is not None - selections = InstallSelections(install_models=[default_model]) - install_helper.add_or_delete(selections) - + selections = InstallSelections(install_models=installer.default_model()) + installer.install(selections) elif opt.yes_to_all: - selections = InstallSelections(install_models=install_helper.recommended_models()) - install_helper.add_or_delete(selections) + selections = InstallSelections(install_models=installer.recommended_models()) + installer.install(selections) # this is where the TUI is called else: + # needed to support the probe() method running under a subprocess + torch.multiprocessing.set_start_method("spawn") + if not set_min_terminal_size(MIN_COLS, MIN_LINES): raise WindowTooSmallException( "Could not increase terminal size. Try running again with a larger window or smaller font size." ) - installApp = AddModelApplication(opt, install_helper) + installApp = AddModelApplication(opt) try: installApp.run() - except KeyboardInterrupt: - print("Aborted...") - sys.exit(-1) - - install_helper.add_or_delete(installApp.install_selections) + except KeyboardInterrupt as e: + if hasattr(installApp, "main_form"): + if installApp.main_form.subprocess and installApp.main_form.subprocess.is_alive(): + logger.info("Terminating subprocesses") + installApp.main_form.subprocess.terminate() + installApp.main_form.subprocess = None + raise e + process_and_execute(opt, installApp.install_selections) # ------------------------------------- -def main() -> None: +def main(): parser = argparse.ArgumentParser(description="InvokeAI model downloader") parser.add_argument( "--add", @@ -564,7 +754,7 @@ def main() -> None: parser.add_argument( "--delete", nargs="*", - help="List of names of models to delete. Use type:name to disambiguate, as in `controlnet:my_model`", + help="List of names of models to idelete", ) parser.add_argument( "--full-precision", @@ -591,6 +781,14 @@ def main() -> None: choices=[x.value for x in ModelType], help="list installed models", ) + parser.add_argument( + "--config_file", + "-c", + dest="config_file", + type=str, + default=None, + help="path to configuration file to create", + ) parser.add_argument( "--root_dir", dest="root", diff --git a/invokeai/frontend/install/widgets.py b/invokeai/frontend/install/widgets.py index 5905ae29da..4dbc6349a0 100644 --- a/invokeai/frontend/install/widgets.py +++ b/invokeai/frontend/install/widgets.py @@ -267,6 +267,17 @@ class SingleSelectWithChanged(npyscreen.SelectOne): self.on_changed(self.value) +class CheckboxWithChanged(npyscreen.Checkbox): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.on_changed = None + + def whenToggled(self): + super().whenToggled() + if self.on_changed: + self.on_changed(self.value) + + class SingleSelectColumnsSimple(SelectColumnBase, SingleSelectWithChanged): """Row of radio buttons. Spacebar to select.""" diff --git a/invokeai/frontend/merge/merge_diffusers2.py b/invokeai/frontend/merge/merge_diffusers.py.OLD similarity index 100% rename from invokeai/frontend/merge/merge_diffusers2.py rename to invokeai/frontend/merge/merge_diffusers.py.OLD diff --git a/pyproject.toml b/pyproject.toml index 8b28375e29..2958e3629a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,8 +136,7 @@ dependencies = [ # full commands "invokeai-configure" = "invokeai.frontend.install.invokeai_configure:invokeai_configure" -"invokeai-merge" = "invokeai.frontend.merge:invokeai_merge_diffusers" -"invokeai-merge2" = "invokeai.frontend.merge.merge_diffusers2:main" +"invokeai-merge" = "invokeai.frontend.merge.merge_diffusers:main" "invokeai-ti" = "invokeai.frontend.training:invokeai_textual_inversion" "invokeai-model-install" = "invokeai.frontend.install.model_install:main" "invokeai-model-install2" = "invokeai.frontend.install.model_install2:main" # will eventually be renamed to invokeai-model-install diff --git a/tests/test_model_manager.py b/tests/test_model_manager.py deleted file mode 100644 index 3e48c7ed6f..0000000000 --- a/tests/test_model_manager.py +++ /dev/null @@ -1,47 +0,0 @@ -from pathlib import Path - -import pytest - -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.backend import BaseModelType, ModelManager, ModelType, SubModelType - -BASIC_MODEL_NAME = ("SDXL base", BaseModelType.StableDiffusionXL, ModelType.Main) -VAE_OVERRIDE_MODEL_NAME = ("SDXL with VAE", BaseModelType.StableDiffusionXL, ModelType.Main) -VAE_NULL_OVERRIDE_MODEL_NAME = ("SDXL with empty VAE", BaseModelType.StableDiffusionXL, ModelType.Main) - - -@pytest.fixture -def model_manager(datadir) -> ModelManager: - InvokeAIAppConfig.get_config(root=datadir) - return ModelManager(datadir / "configs" / "relative_sub.models.yaml") - - -def test_get_model_names(model_manager: ModelManager): - names = model_manager.model_names() - assert names[:2] == [BASIC_MODEL_NAME, VAE_OVERRIDE_MODEL_NAME] - - -def test_get_model_path_for_diffusers(model_manager: ModelManager, datadir: Path): - model_config = model_manager._get_model_config(BASIC_MODEL_NAME[1], BASIC_MODEL_NAME[0], BASIC_MODEL_NAME[2]) - top_model_path, is_override = model_manager._get_model_path(model_config) - expected_model_path = datadir / "models" / "sdxl" / "main" / "SDXL base 1_0" - assert top_model_path == expected_model_path - assert not is_override - - -def test_get_model_path_for_overridden_vae(model_manager: ModelManager, datadir: Path): - model_config = model_manager._get_model_config( - VAE_OVERRIDE_MODEL_NAME[1], VAE_OVERRIDE_MODEL_NAME[0], VAE_OVERRIDE_MODEL_NAME[2] - ) - vae_model_path, is_override = model_manager._get_model_path(model_config, SubModelType.Vae) - expected_vae_path = datadir / "models" / "sdxl" / "vae" / "sdxl-vae-fp16-fix" - assert vae_model_path == expected_vae_path - assert is_override - - -def test_get_model_path_for_null_overridden_vae(model_manager: ModelManager, datadir: Path): - model_config = model_manager._get_model_config( - VAE_NULL_OVERRIDE_MODEL_NAME[1], VAE_NULL_OVERRIDE_MODEL_NAME[0], VAE_NULL_OVERRIDE_MODEL_NAME[2] - ) - vae_model_path, is_override = model_manager._get_model_path(model_config, SubModelType.Vae) - assert not is_override From 8db01ab1b3478e7b967c1354e84813f38c5723fe Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 9 Feb 2024 20:46:47 -0500 Subject: [PATCH 118/411] probe for required encoder for IPAdapters and add to config --- invokeai/app/invocations/ip_adapter.py | 24 +----------------------- invokeai/backend/model_manager/config.py | 1 + invokeai/backend/model_manager/probe.py | 13 +++++++++++++ 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index 700b285a45..f64b3266bb 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -1,4 +1,3 @@ -import os from builtins import float from typing import List, Union @@ -52,16 +51,6 @@ class IPAdapterField(BaseModel): return self -def get_ip_adapter_image_encoder_model_id(model_path: str): - """Read the ID of the image encoder associated with the IP-Adapter at `model_path`.""" - image_encoder_config_file = os.path.join(model_path, "image_encoder.txt") - - with open(image_encoder_config_file, "r") as f: - image_encoder_model = f.readline().strip() - - return image_encoder_model - - @invocation_output("ip_adapter_output") class IPAdapterOutput(BaseInvocationOutput): # Outputs @@ -102,18 +91,7 @@ class IPAdapterInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> IPAdapterOutput: # Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model. ip_adapter_info = context.services.model_records.get_model(self.ip_adapter_model.key) - # HACK(ryand): This is bad for a couple of reasons: 1) we are bypassing the model manager to read the model - # directly, and 2) we are reading from disk every time this invocation is called without caching the result. - # A better solution would be to store the image encoder model reference in the IP-Adapter model info, but this - # is currently messy due to differences between how the model info is generated when installing a model from - # disk vs. downloading the model. - # TODO (LS): Fix the issue above by: - # 1. Change IPAdapterConfig definition to include a field for the repo_id of the image encoder model. - # 2. Update probe.py to read `image_encoder.txt` and store it in the config. - # 3. Change below to get the image encoder from the configuration record. - image_encoder_model_id = get_ip_adapter_image_encoder_model_id( - os.path.join(context.services.configuration.get_config().models_path, ip_adapter_info.path) - ) + image_encoder_model_id = ip_adapter_info.image_encoder_model_id image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip() image_encoder_models = context.services.model_records.search_by_attr( model_name=image_encoder_model_name, base_model=BaseModelType.Any, model_type=ModelType.CLIPVision diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 0dcd925c84..d2e7a0923a 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -263,6 +263,7 @@ class IPAdapterConfig(ModelConfigBase): """Model config for IP Adaptor format models.""" type: Literal[ModelType.IPAdapter] = ModelType.IPAdapter + image_encoder_model_id: str format: Literal[ModelFormat.InvokeAI] diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index 55a9c0464a..e7d21c578f 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -78,6 +78,10 @@ class ProbeBase(object): """Get model scheduler prediction type.""" return None + def get_image_encoder_model_id(self) -> Optional[str]: + """Get image encoder (IP adapters only).""" + return None + class ModelProbe(object): PROBES: Dict[str, Dict[ModelType, type[ProbeBase]]] = { @@ -153,6 +157,7 @@ class ModelProbe(object): fields["base"] = fields.get("base") or probe.get_base_type() fields["variant"] = fields.get("variant") or probe.get_variant_type() fields["prediction_type"] = fields.get("prediction_type") or probe.get_scheduler_prediction_type() + fields["image_encoder_model_id"] = fields.get("image_encoder_model_id") or probe.get_image_encoder_model_id() fields["name"] = fields.get("name") or cls.get_model_name(model_path) fields["description"] = ( fields.get("description") or f"{fields['base'].value} {fields['type'].value} model {fields['name']}" @@ -669,6 +674,14 @@ class IPAdapterFolderProbe(FolderProbeBase): f"IP-Adapter had unexpected cross-attention dimension: {cross_attention_dim}." ) + def get_image_encoder_model_id(self) -> Optional[str]: + encoder_id_path = self.model_path / "image_encoder.txt" + if not encoder_id_path.exists(): + return None + with open(encoder_id_path, "r") as f: + image_encoder_model = f.readline().strip() + return image_encoder_model + class CLIPVisionFolderProbe(FolderProbeBase): def get_base_type(self) -> BaseModelType: From 7956602b193cab0eaacfe8a85007aa45f0569ad6 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 9 Feb 2024 23:08:38 -0500 Subject: [PATCH 119/411] consolidate model manager parts into a single class --- invokeai/app/services/model_load/__init__.py | 6 + .../services/model_load/model_load_base.py | 22 + .../services/model_load/model_load_default.py | 54 +++ .../app/services/model_manager/__init__.py | 17 +- .../model_manager/model_manager_base.py | 298 ++---------- .../model_manager/model_manager_default.py | 456 ++---------------- invokeai/backend/__init__.py | 9 - invokeai/backend/model_manager/config.py | 6 +- .../backend/model_manager/load/__init__.py | 2 +- invokeai/backend/model_manager/search.py | 12 +- 10 files changed, 186 insertions(+), 696 deletions(-) create mode 100644 invokeai/app/services/model_load/__init__.py create mode 100644 invokeai/app/services/model_load/model_load_base.py create mode 100644 invokeai/app/services/model_load/model_load_default.py diff --git a/invokeai/app/services/model_load/__init__.py b/invokeai/app/services/model_load/__init__.py new file mode 100644 index 0000000000..b4a86e9348 --- /dev/null +++ b/invokeai/app/services/model_load/__init__.py @@ -0,0 +1,6 @@ +"""Initialization file for model load service module.""" + +from .model_load_base import ModelLoadServiceBase +from .model_load_default import ModelLoadService + +__all__ = ["ModelLoadServiceBase", "ModelLoadService"] diff --git a/invokeai/app/services/model_load/model_load_base.py b/invokeai/app/services/model_load/model_load_base.py new file mode 100644 index 0000000000..7228806e80 --- /dev/null +++ b/invokeai/app/services/model_load/model_load_base.py @@ -0,0 +1,22 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Team +"""Base class for model loader.""" + +from abc import ABC, abstractmethod +from typing import Optional + +from invokeai.backend.model_manager import AnyModelConfig, SubModelType +from invokeai.backend.model_manager.load import LoadedModel + + +class ModelLoadServiceBase(ABC): + """Wrapper around AnyModelLoader.""" + + @abstractmethod + def load_model_by_key(self, key: str, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """Given a model's key, load it and return the LoadedModel object.""" + pass + + @abstractmethod + def load_model_by_config(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """Given a model's configuration, load it and return the LoadedModel object.""" + pass diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py new file mode 100644 index 0000000000..80e2fe161d --- /dev/null +++ b/invokeai/app/services/model_load/model_load_default.py @@ -0,0 +1,54 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Team +"""Implementation of model loader service.""" + +from typing import Optional + +from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.app.services.model_records import ModelRecordServiceBase +from invokeai.backend.model_manager import AnyModelConfig, SubModelType +from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel, ModelCache, ModelConvertCache +from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase +from invokeai.backend.model_manager.load.ram_cache import ModelCacheBase +from invokeai.backend.util.logging import InvokeAILogger + +from .model_load_base import ModelLoadServiceBase + + +class ModelLoadService(ModelLoadServiceBase): + """Wrapper around AnyModelLoader.""" + + def __init__( + self, + app_config: InvokeAIAppConfig, + record_store: ModelRecordServiceBase, + ram_cache: Optional[ModelCacheBase] = None, + convert_cache: Optional[ModelConvertCacheBase] = None, + ): + """Initialize the model load service.""" + logger = InvokeAILogger.get_logger(self.__class__.__name__) + logger.setLevel(app_config.log_level.upper()) + self._store = record_store + self._any_loader = AnyModelLoader( + app_config=app_config, + logger=logger, + ram_cache=ram_cache + or ModelCache( + max_cache_size=app_config.ram_cache_size, + max_vram_cache_size=app_config.vram_cache_size, + logger=logger, + ), + convert_cache=convert_cache + or ModelConvertCache( + cache_path=app_config.models_convert_cache_path, + max_size=app_config.convert_cache_size, + ), + ) + + def load_model_by_key(self, key: str, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """Given a model's key, load it and return the LoadedModel object.""" + config = self._store.get_model(key) + return self.load_model_by_config(config, submodel_type) + + def load_model_by_config(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + """Given a model's configuration, load it and return the LoadedModel object.""" + return self._any_loader.load_model(config, submodel_type) diff --git a/invokeai/app/services/model_manager/__init__.py b/invokeai/app/services/model_manager/__init__.py index 3d6a9c248c..5e281922a8 100644 --- a/invokeai/app/services/model_manager/__init__.py +++ b/invokeai/app/services/model_manager/__init__.py @@ -1 +1,16 @@ -from .model_manager_default import ModelManagerService # noqa F401 +"""Initialization file for model manager service.""" + +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType +from invokeai.backend.model_manager.load import LoadedModel + +from .model_manager_default import ModelManagerService + +__all__ = [ + "ModelManagerService", + "AnyModel", + "AnyModelConfig", + "BaseModelType", + "ModelType", + "SubModelType", + "LoadedModel", +] diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index f888c0ec97..c6e77fa163 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -1,283 +1,39 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team -from __future__ import annotations - from abc import ABC, abstractmethod -from logging import Logger -from pathlib import Path -from typing import Callable, List, Literal, Optional, Tuple, Union -from pydantic import Field +from pydantic import BaseModel, Field +from typing_extensions import Self -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.shared.invocation_context import InvocationContextData -from invokeai.backend.model_management import ( - AddModelResult, - BaseModelType, - LoadedModelInfo, - MergeInterpolationMethod, - ModelType, - SchedulerPredictionType, - SubModelType, -) -from invokeai.backend.model_management.model_cache import CacheStats +from ..config import InvokeAIAppConfig +from ..download import DownloadQueueServiceBase +from ..events.events_base import EventServiceBase +from ..model_install import ModelInstallServiceBase +from ..model_load import ModelLoadServiceBase +from ..model_records import ModelRecordServiceBase +from ..shared.sqlite.sqlite_database import SqliteDatabase -class ModelManagerServiceBase(ABC): - """Responsible for managing models on disk and in memory""" +class ModelManagerServiceBase(BaseModel, ABC): + """Abstract base class for the model manager service.""" + store: ModelRecordServiceBase = Field(description="An instance of the model record configuration service.") + install: ModelInstallServiceBase = Field(description="An instance of the model install service.") + load: ModelLoadServiceBase = Field(description="An instance of the model load service.") + + @classmethod @abstractmethod - def __init__( - self, - config: InvokeAIAppConfig, - logger: Logger, - ): + def build_model_manager( + cls, + app_config: InvokeAIAppConfig, + db: SqliteDatabase, + download_queue: DownloadQueueServiceBase, + events: EventServiceBase, + ) -> Self: """ - Initialize with the path to the models.yaml config file. - Optional parameters are the torch device type, precision, max_models, - and sequential_offload boolean. Note that the default device - type and precision are set up for a CUDA system running at half precision. - """ - pass - - @abstractmethod - def get_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModelInfo: - """Retrieve the indicated model with name and type. - submodel can be used to get a part (such as the vae) - of a diffusers pipeline.""" - pass - - @property - @abstractmethod - def logger(self): - pass - - @abstractmethod - def model_exists( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ) -> bool: - pass - - @abstractmethod - def model_info(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> dict: - """ - Given a model name returns a dict-like (OmegaConf) object describing it. - Uses the exact format as the omegaconf stanza. - """ - pass - - @abstractmethod - def list_models(self, base_model: Optional[BaseModelType] = None, model_type: Optional[ModelType] = None) -> dict: - """ - Return a dict of models in the format: - { model_type1: - { model_name1: {'status': 'active'|'cached'|'not loaded', - 'model_name' : name, - 'model_type' : SDModelType, - 'description': description, - 'format': 'folder'|'safetensors'|'ckpt' - }, - model_name2: { etc } - }, - model_type2: - { model_name_n: etc - } - """ - pass - - @abstractmethod - def list_model(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> dict: - """ - Return information about the model using the same format as list_models() - """ - pass - - @abstractmethod - def model_names(self) -> List[Tuple[str, BaseModelType, ModelType]]: - """ - Returns a list of all the model names known. - """ - pass - - @abstractmethod - def add_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - model_attributes: dict, - clobber: bool = False, - ) -> AddModelResult: - """ - Update the named model with a dictionary of attributes. Will fail with an - assertion error if the name already exists. Pass clobber=True to overwrite. - On a successful update, the config will be changed in memory. Will fail - with an assertion error if provided attributes are incorrect or - the model name is missing. Call commit() to write changes to disk. - """ - pass - - @abstractmethod - def update_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - model_attributes: dict, - ) -> AddModelResult: - """ - Update the named model with a dictionary of attributes. Will fail with a - ModelNotFoundException if the name does not already exist. - - On a successful update, the config will be changed in memory. Will fail - with an assertion error if provided attributes are incorrect or - the model name is missing. Call commit() to write changes to disk. - """ - pass - - @abstractmethod - def del_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ): - """ - Delete the named model from configuration. If delete_files is true, - then the underlying weight file or diffusers directory will be deleted - as well. Call commit() to write to disk. - """ - pass - - @abstractmethod - def rename_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - new_name: str, - ): - """ - Rename the indicated model. - """ - pass - - @abstractmethod - def list_checkpoint_configs(self) -> List[Path]: - """ - List the checkpoint config paths from ROOT/configs/stable-diffusion. - """ - pass - - @abstractmethod - def convert_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: Literal[ModelType.Main, ModelType.Vae], - ) -> AddModelResult: - """ - Convert a checkpoint file into a diffusers folder, deleting the cached - version and deleting the original checkpoint file if it is in the models - directory. - :param model_name: Name of the model to convert - :param base_model: Base model type - :param model_type: Type of model ['vae' or 'main'] - - This will raise a ValueError unless the model is not a checkpoint. It will - also raise a ValueError in the event that there is a similarly-named diffusers - directory already in place. - """ - pass - - @abstractmethod - def heuristic_import( - self, - items_to_import: set[str], - prediction_type_helper: Optional[Callable[[Path], SchedulerPredictionType]] = None, - ) -> dict[str, AddModelResult]: - """Import a list of paths, repo_ids or URLs. Returns the set of - successfully imported items. - :param items_to_import: Set of strings corresponding to models to be imported. - :param prediction_type_helper: A callback that receives the Path of a Stable Diffusion 2 checkpoint model and returns a SchedulerPredictionType. - - The prediction type helper is necessary to distinguish between - models based on Stable Diffusion 2 Base (requiring - SchedulerPredictionType.Epsilson) and Stable Diffusion 768 - (requiring SchedulerPredictionType.VPrediction). It is - generally impossible to do this programmatically, so the - prediction_type_helper usually asks the user to choose. - - The result is a set of successfully installed models. Each element - of the set is a dict corresponding to the newly-created OmegaConf stanza for - that model. - """ - pass - - @abstractmethod - def merge_models( - self, - model_names: List[str] = Field( - default=None, min_length=2, max_length=3, description="List of model names to merge" - ), - base_model: Union[BaseModelType, str] = Field( - default=None, description="Base model shared by all models to be merged" - ), - merged_model_name: str = Field(default=None, description="Name of destination model after merging"), - alpha: Optional[float] = 0.5, - interp: Optional[MergeInterpolationMethod] = None, - force: Optional[bool] = False, - merge_dest_directory: Optional[Path] = None, - ) -> AddModelResult: - """ - Merge two to three diffusrs pipeline models and save as a new model. - :param model_names: List of 2-3 models to merge - :param base_model: Base model to use for all models - :param merged_model_name: Name of destination merged model - :param alpha: Alpha strength to apply to 2d and 3d model - :param interp: Interpolation method. None (default) - :param merge_dest_directory: Save the merged model to the designated directory (with 'merged_model_name' appended) - """ - pass - - @abstractmethod - def search_for_models(self, directory: Path) -> List[Path]: - """ - Return list of all models found in the designated directory. - """ - pass - - @abstractmethod - def sync_to_config(self): - """ - Re-read models.yaml, rescan the models directory, and reimport models - in the autoimport directories. Call after making changes outside the - model manager API. - """ - pass - - @abstractmethod - def collect_cache_stats(self, cache_stats: CacheStats): - """ - Reset model cache statistics for graph with graph_id. - """ - pass - - @abstractmethod - def commit(self, conf_file: Optional[Path] = None) -> None: - """ - Write current configuration out to the indicated file. - If no conf_file is provided, then replaces the - original file/database used to initialize the object. + Construct the model manager service instance. + + Use it rather than the __init__ constructor. This class + method simplifies the construction considerably. """ pass diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index c3712abf8e..ad0fd66dbb 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -1,421 +1,67 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team +"""Implementation of ModelManagerServiceBase.""" -from __future__ import annotations +from typing_extensions import Self -from logging import Logger -from pathlib import Path -from typing import TYPE_CHECKING, Callable, List, Literal, Optional, Tuple, Union - -import torch -from pydantic import Field - -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException -from invokeai.app.services.invoker import Invoker -from invokeai.app.services.shared.invocation_context import InvocationContextData -from invokeai.backend.model_management import ( - AddModelResult, - BaseModelType, - LoadedModelInfo, - MergeInterpolationMethod, - ModelManager, - ModelMerger, - ModelNotFoundException, - ModelType, - SchedulerPredictionType, - SubModelType, -) -from invokeai.backend.model_management.model_cache import CacheStats -from invokeai.backend.model_management.model_search import FindModels -from invokeai.backend.util import choose_precision, choose_torch_device +from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache +from invokeai.backend.model_manager.metadata import ModelMetadataStore +from invokeai.backend.util.logging import InvokeAILogger +from ..config import InvokeAIAppConfig +from ..download import DownloadQueueServiceBase +from ..events.events_base import EventServiceBase +from ..model_install import ModelInstallService +from ..model_load import ModelLoadService +from ..model_records import ModelRecordServiceSQL +from ..shared.sqlite.sqlite_database import SqliteDatabase from .model_manager_base import ModelManagerServiceBase -if TYPE_CHECKING: - pass - -# simple implementation class ModelManagerService(ModelManagerServiceBase): - """Responsible for managing models on disk and in memory""" + """ + The ModelManagerService handles various aspects of model installation, maintenance and loading. - def __init__( - self, - config: InvokeAIAppConfig, - logger: Logger, - ): + It bundles three distinct services: + model_manager.store -- Routines to manage the database of model configuration records. + model_manager.install -- Routines to install, move and delete models. + model_manager.load -- Routines to load models into memory. + """ + + @classmethod + def build_model_manager( + cls, + app_config: InvokeAIAppConfig, + db: SqliteDatabase, + download_queue: DownloadQueueServiceBase, + events: EventServiceBase, + ) -> Self: """ - Initialize with the path to the models.yaml config file. - Optional parameters are the torch device type, precision, max_models, - and sequential_offload boolean. Note that the default device - type and precision are set up for a CUDA system running at half precision. + Construct the model manager service instance. + + For simplicity, use this class method rather than the __init__ constructor. """ - if config.model_conf_path and config.model_conf_path.exists(): - config_file = config.model_conf_path - else: - config_file = config.root_dir / "configs/models.yaml" + logger = InvokeAILogger.get_logger(cls.__name__) + logger.setLevel(app_config.log_level.upper()) - logger.debug(f"Config file={config_file}") - - device = torch.device(choose_torch_device()) - device_name = torch.cuda.get_device_name() if device == torch.device("cuda") else "" - logger.info(f"GPU device = {device} {device_name}") - - precision = config.precision - if precision == "auto": - precision = choose_precision(device) - dtype = torch.float32 if precision == "float32" else torch.float16 - - # this is transitional backward compatibility - # support for the deprecated `max_loaded_models` - # configuration value. If present, then the - # cache size is set to 2.5 GB times - # the number of max_loaded_models. Otherwise - # use new `ram_cache_size` config setting - max_cache_size = config.ram_cache_size - - logger.debug(f"Maximum RAM cache size: {max_cache_size} GiB") - - sequential_offload = config.sequential_guidance - - self.mgr = ModelManager( - config=config_file, - device_type=device, - precision=dtype, - max_cache_size=max_cache_size, - sequential_offload=sequential_offload, - logger=logger, + ram_cache = ModelCache( + max_cache_size=app_config.ram_cache_size, max_vram_cache_size=app_config.vram_cache_size, logger=logger ) - logger.info("Model manager service initialized") - - def start(self, invoker: Invoker) -> None: - self._invoker: Optional[Invoker] = invoker - - def get_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModelInfo: - """ - Retrieve the indicated model. submodel can be used to get a - part (such as the vae) of a diffusers mode. - """ - - # we can emit model loading events if we are executing with access to the invocation context - if context_data is not None: - self._emit_load_event( - context_data=context_data, - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=submodel, - ) - - loaded_model_info = self.mgr.get_model( - model_name, - base_model, - model_type, - submodel, + convert_cache = ModelConvertCache( + cache_path=app_config.models_convert_cache_path, max_size=app_config.convert_cache_size ) - - if context_data is not None: - self._emit_load_event( - context_data=context_data, - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=submodel, - loaded_model_info=loaded_model_info, - ) - - return loaded_model_info - - def model_exists( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ) -> bool: - """ - Given a model name, returns True if it is a valid - identifier. - """ - return self.mgr.model_exists( - model_name, - base_model, - model_type, + record_store = ModelRecordServiceSQL(db=db) + loader = ModelLoadService( + app_config=app_config, + record_store=record_store, + ram_cache=ram_cache, + convert_cache=convert_cache, ) - - def model_info(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> Union[dict, None]: - """ - Given a model name returns a dict-like (OmegaConf) object describing it. - """ - return self.mgr.model_info(model_name, base_model, model_type) - - def model_names(self) -> List[Tuple[str, BaseModelType, ModelType]]: - """ - Returns a list of all the model names known. - """ - return self.mgr.model_names() - - def list_models( - self, base_model: Optional[BaseModelType] = None, model_type: Optional[ModelType] = None - ) -> list[dict]: - """ - Return a list of models. - """ - return self.mgr.list_models(base_model, model_type) - - def list_model(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> Union[dict, None]: - """ - Return information about the model using the same format as list_models() - """ - return self.mgr.list_model(model_name=model_name, base_model=base_model, model_type=model_type) - - def add_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - model_attributes: dict, - clobber: bool = False, - ) -> AddModelResult: - """ - Update the named model with a dictionary of attributes. Will fail with an - assertion error if the name already exists. Pass clobber=True to overwrite. - On a successful update, the config will be changed in memory. Will fail - with an assertion error if provided attributes are incorrect or - the model name is missing. Call commit() to write changes to disk. - """ - self.logger.debug(f"add/update model {model_name}") - return self.mgr.add_model(model_name, base_model, model_type, model_attributes, clobber) - - def update_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - model_attributes: dict, - ) -> AddModelResult: - """ - Update the named model with a dictionary of attributes. Will fail with a - ModelNotFoundException exception if the name does not already exist. - On a successful update, the config will be changed in memory. Will fail - with an assertion error if provided attributes are incorrect or - the model name is missing. Call commit() to write changes to disk. - """ - self.logger.debug(f"update model {model_name}") - if not self.model_exists(model_name, base_model, model_type): - raise ModelNotFoundException(f"Unknown model {model_name}") - return self.add_model(model_name, base_model, model_type, model_attributes, clobber=True) - - def del_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ): - """ - Delete the named model from configuration. If delete_files is true, - then the underlying weight file or diffusers directory will be deleted - as well. - """ - self.logger.debug(f"delete model {model_name}") - self.mgr.del_model(model_name, base_model, model_type) - self.mgr.commit() - - def convert_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: Literal[ModelType.Main, ModelType.Vae], - convert_dest_directory: Optional[Path] = Field( - default=None, description="Optional directory location for merged model" - ), - ) -> AddModelResult: - """ - Convert a checkpoint file into a diffusers folder, deleting the cached - version and deleting the original checkpoint file if it is in the models - directory. - :param model_name: Name of the model to convert - :param base_model: Base model type - :param model_type: Type of model ['vae' or 'main'] - :param convert_dest_directory: Save the converted model to the designated directory (`models/etc/etc` by default) - - This will raise a ValueError unless the model is not a checkpoint. It will - also raise a ValueError in the event that there is a similarly-named diffusers - directory already in place. - """ - self.logger.debug(f"convert model {model_name}") - return self.mgr.convert_model(model_name, base_model, model_type, convert_dest_directory) - - def collect_cache_stats(self, cache_stats: CacheStats): - """ - Reset model cache statistics for graph with graph_id. - """ - self.mgr.cache.stats = cache_stats - - def commit(self, conf_file: Optional[Path] = None): - """ - Write current configuration out to the indicated file. - If no conf_file is provided, then replaces the - original file/database used to initialize the object. - """ - return self.mgr.commit(conf_file) - - def _emit_load_event( - self, - context_data: InvocationContextData, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - loaded_model_info: Optional[LoadedModelInfo] = None, - ): - if self._invoker is None: - return - - if self._invoker.services.queue.is_canceled(context_data.session_id): - raise CanceledException() - - if loaded_model_info: - self._invoker.services.events.emit_model_load_completed( - queue_id=context_data.queue_id, - queue_item_id=context_data.queue_item_id, - queue_batch_id=context_data.batch_id, - graph_execution_state_id=context_data.session_id, - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=submodel, - loaded_model_info=loaded_model_info, - ) - else: - self._invoker.services.events.emit_model_load_started( - queue_id=context_data.queue_id, - queue_item_id=context_data.queue_item_id, - queue_batch_id=context_data.batch_id, - graph_execution_state_id=context_data.session_id, - model_name=model_name, - base_model=base_model, - model_type=model_type, - submodel=submodel, - ) - - @property - def logger(self): - return self.mgr.logger - - def heuristic_import( - self, - items_to_import: set[str], - prediction_type_helper: Optional[Callable[[Path], SchedulerPredictionType]] = None, - ) -> dict[str, AddModelResult]: - """Import a list of paths, repo_ids or URLs. Returns the set of - successfully imported items. - :param items_to_import: Set of strings corresponding to models to be imported. - :param prediction_type_helper: A callback that receives the Path of a Stable Diffusion 2 checkpoint model and returns a SchedulerPredictionType. - - The prediction type helper is necessary to distinguish between - models based on Stable Diffusion 2 Base (requiring - SchedulerPredictionType.Epsilson) and Stable Diffusion 768 - (requiring SchedulerPredictionType.VPrediction). It is - generally impossible to do this programmatically, so the - prediction_type_helper usually asks the user to choose. - - The result is a set of successfully installed models. Each element - of the set is a dict corresponding to the newly-created OmegaConf stanza for - that model. - """ - return self.mgr.heuristic_import(items_to_import, prediction_type_helper) - - def merge_models( - self, - model_names: List[str] = Field( - default=None, min_length=2, max_length=3, description="List of model names to merge" - ), - base_model: Union[BaseModelType, str] = Field( - default=None, description="Base model shared by all models to be merged" - ), - merged_model_name: str = Field(default=None, description="Name of destination model after merging"), - alpha: float = 0.5, - interp: Optional[MergeInterpolationMethod] = None, - force: bool = False, - merge_dest_directory: Optional[Path] = Field( - default=None, description="Optional directory location for merged model" - ), - ) -> AddModelResult: - """ - Merge two to three diffusrs pipeline models and save as a new model. - :param model_names: List of 2-3 models to merge - :param base_model: Base model to use for all models - :param merged_model_name: Name of destination merged model - :param alpha: Alpha strength to apply to 2d and 3d model - :param interp: Interpolation method. None (default) - :param merge_dest_directory: Save the merged model to the designated directory (with 'merged_model_name' appended) - """ - merger = ModelMerger(self.mgr) - try: - result = merger.merge_diffusion_models_and_save( - model_names=model_names, - base_model=base_model, - merged_model_name=merged_model_name, - alpha=alpha, - interp=interp, - force=force, - merge_dest_directory=merge_dest_directory, - ) - except AssertionError as e: - raise ValueError(e) - return result - - def search_for_models(self, directory: Path) -> List[Path]: - """ - Return list of all models found in the designated directory. - """ - search = FindModels([directory], self.logger) - return search.list_models() - - def sync_to_config(self): - """ - Re-read models.yaml, rescan the models directory, and reimport models - in the autoimport directories. Call after making changes outside the - model manager API. - """ - return self.mgr.sync_to_config() - - def list_checkpoint_configs(self) -> List[Path]: - """ - List the checkpoint config paths from ROOT/configs/stable-diffusion. - """ - config = self.mgr.app_config - conf_path = config.legacy_conf_path - root_path = config.root_path - return [(conf_path / x).relative_to(root_path) for x in conf_path.glob("**/*.yaml")] - - def rename_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - new_name: Optional[str] = None, - new_base: Optional[BaseModelType] = None, - ): - """ - Rename the indicated model. Can provide a new name and/or a new base. - :param model_name: Current name of the model - :param base_model: Current base of the model - :param model_type: Model type (can't be changed) - :param new_name: New name for the model - :param new_base: New base for the model - """ - self.mgr.rename_model( - base_model=base_model, - model_type=model_type, - model_name=model_name, - new_name=new_name, - new_base=new_base, + record_store._loader = loader # yeah, there is a circular reference here + installer = ModelInstallService( + app_config=app_config, + record_store=record_store, + download_queue=download_queue, + metadata_store=ModelMetadataStore(db=db), + event_bus=events, ) + return cls(store=record_store, install=installer, load=loader) diff --git a/invokeai/backend/__init__.py b/invokeai/backend/__init__.py index 54a1843d46..9fe97ee525 100644 --- a/invokeai/backend/__init__.py +++ b/invokeai/backend/__init__.py @@ -1,12 +1,3 @@ """ Initialization file for invokeai.backend """ -from .model_management import ( # noqa: F401 - BaseModelType, - LoadedModelInfo, - ModelCache, - ModelManager, - ModelType, - SubModelType, -) -from .model_management.models import SilenceWarnings # noqa: F401 diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index d2e7a0923a..4534a4892f 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -21,7 +21,7 @@ Validation errors will raise an InvalidModelConfigException error. """ import time from enum import Enum -from typing import Literal, Optional, Type, Union +from typing import Literal, Optional, Type, Union, Class import torch from diffusers import ModelMixin @@ -333,9 +333,9 @@ class ModelConfigFactory(object): @classmethod def make_config( cls, - model_data: Union[dict, AnyModelConfig], + model_data: Union[Dict[str, Any], AnyModelConfig], key: Optional[str] = None, - dest_class: Optional[Type] = None, + dest_class: Optional[Type[Class]] = None, timestamp: Optional[float] = None, ) -> AnyModelConfig: """ diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py index e4c7077f78..966a739237 100644 --- a/invokeai/backend/model_manager/load/__init__.py +++ b/invokeai/backend/model_manager/load/__init__.py @@ -18,7 +18,7 @@ loaders = [x.stem for x in Path(Path(__file__).parent, "model_loaders").glob("*. for module in loaders: import_module(f"{__package__}.model_loaders.{module}") -__all__ = ["AnyModelLoader", "LoadedModel"] +__all__ = ["AnyModelLoader", "LoadedModel", "ModelCache", "ModelConvertCache"] def get_standalone_loader(app_config: Optional[InvokeAIAppConfig]) -> AnyModelLoader: diff --git a/invokeai/backend/model_manager/search.py b/invokeai/backend/model_manager/search.py index 4cc3caebe4..a54938fdd5 100644 --- a/invokeai/backend/model_manager/search.py +++ b/invokeai/backend/model_manager/search.py @@ -26,10 +26,10 @@ from pathlib import Path from typing import Callable, Optional, Set, Union from pydantic import BaseModel, Field - +from logging import Logger from invokeai.backend.util.logging import InvokeAILogger -default_logger = InvokeAILogger.get_logger() +default_logger: Logger = InvokeAILogger.get_logger() class SearchStats(BaseModel): @@ -56,7 +56,7 @@ class ModelSearchBase(ABC, BaseModel): on_model_found : Optional[Callable[[Path], bool]] = Field(default=None, description="Called when a model is found.") # noqa E221 on_search_completed : Optional[Callable[[Set[Path]], None]] = Field(default=None, description="Called when search is complete.") # noqa E221 stats : SearchStats = Field(default_factory=SearchStats, description="Summary statistics after search") # noqa E221 - logger : InvokeAILogger = Field(default=default_logger, description="Logger instance.") # noqa E221 + logger : Logger = Field(default=default_logger, description="Logger instance.") # noqa E221 # fmt: on class Config: @@ -128,13 +128,13 @@ class ModelSearch(ModelSearchBase): def model_found(self, model: Path) -> None: self.stats.models_found += 1 - if not self.on_model_found or self.on_model_found(model): + if self.on_model_found is None or self.on_model_found(model): self.stats.models_filtered += 1 self.models_found.add(model) def search_completed(self) -> None: - if self.on_search_completed: - self.on_search_completed(self._models_found) + if self.on_search_completed is not None: + self.on_search_completed(self.models_found) def search(self, directory: Union[Path, str]) -> Set[Path]: self._directory = Path(directory) From a23dedd2ee00c3d6bf6da6d71c78997bd311da30 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 10 Feb 2024 18:09:45 -0500 Subject: [PATCH 120/411] make model manager v2 ready for PR review - Replace legacy model manager service with the v2 manager. - Update invocations to use new load interface. - Fixed many but not all type checking errors in the invocations. Most were unrelated to model manager - Updated routes. All the new routes live under the route tag `model_manager_v2`. To avoid confusion with the old routes, they have the URL prefix `/api/v2/models`. The old routes have been de-registered. - Added a pytest for the loader. - Updated documentation in contributing/MODEL_MANAGER.md --- docs/contributing/MODEL_MANAGER.md | 223 ++++++++++++------ invokeai/app/api/dependencies.py | 29 +-- .../{model_records.py => model_manager_v2.py} | 82 ++++--- invokeai/app/api/routers/models.py | 3 +- invokeai/app/api_app.py | 5 +- invokeai/app/invocations/compel.py | 43 ++-- invokeai/app/invocations/latent.py | 168 ++++++++----- invokeai/app/invocations/model.py | 2 +- invokeai/app/services/invocation_services.py | 6 - .../invocation_stats_default.py | 9 +- .../services/model_load/model_load_base.py | 60 ++++- .../services/model_load/model_load_default.py | 117 ++++++++- .../model_manager/model_manager_base.py | 38 ++- .../model_manager/model_manager_default.py | 39 ++- .../model_records/model_records_base.py | 49 ---- .../model_records/model_records_sql.py | 99 +------- .../sqlite_migrator/migrations/migration_6.py | 19 ++ invokeai/backend/embeddings/model_patcher.py | 4 +- invokeai/backend/image_util/safety_checker.py | 2 +- invokeai/backend/ip_adapter/ip_adapter.py | 4 +- invokeai/backend/model_manager/config.py | 13 +- .../backend/model_manager/load/load_base.py | 18 +- .../model_manager/load/load_default.py | 2 +- .../load/model_cache/model_cache_default.py | 2 +- .../load/model_loaders/controlnet.py | 12 +- .../load/model_loaders/generic_diffusers.py | 5 +- .../load/model_loaders/stable_diffusion.py | 8 +- .../model_manager/load/model_loaders/vae.py | 8 +- .../backend/model_manager/load/model_util.py | 2 +- invokeai/backend/model_manager/search.py | 3 +- .../stable_diffusion/schedulers/__init__.py | 2 + invokeai/frontend/install/model_install.py | 2 +- tests/aa_nodes/test_graph_execution_state.py | 2 - tests/aa_nodes/test_invoker.py | 2 - .../model_loading/test_model_load.py | 22 ++ .../model_manager_2_fixtures.py | 11 + 36 files changed, 680 insertions(+), 435 deletions(-) rename invokeai/app/api/routers/{model_records.py => model_manager_v2.py} (86%) create mode 100644 tests/backend/model_manager_2/model_loading/test_model_load.py diff --git a/docs/contributing/MODEL_MANAGER.md b/docs/contributing/MODEL_MANAGER.md index 880c8b2480..39220f4ba8 100644 --- a/docs/contributing/MODEL_MANAGER.md +++ b/docs/contributing/MODEL_MANAGER.md @@ -28,7 +28,7 @@ model. These are the: Hugging Face, as well as discriminating among model versions in Civitai, but can be used for arbitrary content. - * _ModelLoadServiceBase_ (**CURRENTLY UNDER DEVELOPMENT - NOT IMPLEMENTED**) + * _ModelLoadServiceBase_ Responsible for loading a model from disk into RAM and VRAM and getting it ready for inference. @@ -41,10 +41,10 @@ The four main services can be found in * `invokeai/app/services/model_records/` * `invokeai/app/services/model_install/` * `invokeai/app/services/downloads/` -* `invokeai/app/services/model_loader/` (**under development**) +* `invokeai/app/services/model_load/` Code related to the FastAPI web API can be found in -`invokeai/app/api/routers/model_records.py`. +`invokeai/app/api/routers/model_manager_v2.py`. *** @@ -84,10 +84,10 @@ diffusers model. When this happens, `original_hash` is unchanged, but `ModelType`, `ModelFormat` and `BaseModelType` are string enums that are defined in `invokeai.backend.model_manager.config`. They are also imported by, and can be reexported from, -`invokeai.app.services.model_record_service`: +`invokeai.app.services.model_manager.model_records`: ``` -from invokeai.app.services.model_record_service import ModelType, ModelFormat, BaseModelType +from invokeai.app.services.model_records import ModelType, ModelFormat, BaseModelType ``` The `path` field can be absolute or relative. If relative, it is taken @@ -123,7 +123,7 @@ taken to be the `models_dir` directory. `variant` is an enumerated string class with values `normal`, `inpaint` and `depth`. If needed, it can be imported if needed from -either `invokeai.app.services.model_record_service` or +either `invokeai.app.services.model_records` or `invokeai.backend.model_manager.config`. ### ONNXSD2Config @@ -134,7 +134,7 @@ either `invokeai.app.services.model_record_service` or | `upcast_attention` | bool | Model requires its attention module to be upcast | The `SchedulerPredictionType` enum can be imported from either -`invokeai.app.services.model_record_service` or +`invokeai.app.services.model_records` or `invokeai.backend.model_manager.config`. ### Other config classes @@ -157,15 +157,6 @@ indicates that the model is compatible with any of the base models. This works OK for some models, such as the IP Adapter image encoders, but is an all-or-nothing proposition. -Another issue is that the config class hierarchy is paralleled to some -extent by a `ModelBase` class hierarchy defined in -`invokeai.backend.model_manager.models.base` and its subclasses. These -are classes representing the models after they are loaded into RAM and -include runtime information such as load status and bytes used. Some -of the fields, including `name`, `model_type` and `base_model`, are -shared between `ModelConfigBase` and `ModelBase`, and this is a -potential source of confusion. - ## Reading and Writing Model Configuration Records The `ModelRecordService` provides the ability to retrieve model @@ -177,11 +168,11 @@ initialization and can be retrieved within an invocation from the `InvocationContext` object: ``` -store = context.services.model_record_store +store = context.services.model_manager.store ``` or from elsewhere in the code by accessing -`ApiDependencies.invoker.services.model_record_store`. +`ApiDependencies.invoker.services.model_manager.store`. ### Creating a `ModelRecordService` @@ -190,7 +181,7 @@ you can directly create either a `ModelRecordServiceSQL` or a `ModelRecordServiceFile` object: ``` -from invokeai.app.services.model_record_service import ModelRecordServiceSQL, ModelRecordServiceFile +from invokeai.app.services.model_records import ModelRecordServiceSQL, ModelRecordServiceFile store = ModelRecordServiceSQL.from_connection(connection, lock) store = ModelRecordServiceSQL.from_db_file('/path/to/sqlite_database.db') @@ -252,7 +243,7 @@ So a typical startup pattern would be: ``` import sqlite3 from invokeai.app.services.thread import lock -from invokeai.app.services.model_record_service import ModelRecordServiceBase +from invokeai.app.services.model_records import ModelRecordServiceBase from invokeai.app.services.config import InvokeAIAppConfig config = InvokeAIAppConfig.get_config() @@ -260,19 +251,6 @@ db_conn = sqlite3.connect(config.db_path.as_posix(), check_same_thread=False) store = ModelRecordServiceBase.open(config, db_conn, lock) ``` -_A note on simultaneous access to `invokeai.db`_: The current InvokeAI -service architecture for the image and graph databases is careful to -use a shared sqlite3 connection and a thread lock to ensure that two -threads don't attempt to access the database simultaneously. However, -the default `sqlite3` library used by Python reports using -**Serialized** mode, which allows multiple threads to access the -database simultaneously using multiple database connections (see -https://www.sqlite.org/threadsafe.html and -https://ricardoanderegg.com/posts/python-sqlite-thread-safety/). Therefore -it should be safe to allow the record service to open its own SQLite -database connection. Opening a model record service should then be as -simple as `ModelRecordServiceBase.open(config)`. - ### Fetching a Model's Configuration from `ModelRecordServiceBase` Configurations can be retrieved in several ways. @@ -1465,7 +1443,7 @@ create alternative instances if you wish. ### Creating a ModelLoadService object The class is defined in -`invokeai.app.services.model_loader_service`. It is initialized with +`invokeai.app.services.model_load`. It is initialized with an InvokeAIAppConfig object, from which it gets configuration information such as the user's desired GPU and precision, and with a previously-created `ModelRecordServiceBase` object, from which it @@ -1475,8 +1453,8 @@ Here is a typical initialization pattern: ``` from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.model_record_service import ModelRecordServiceBase -from invokeai.app.services.model_loader_service import ModelLoadService +from invokeai.app.services.model_records import ModelRecordServiceBase +from invokeai.app.services.model_load import ModelLoadService config = InvokeAIAppConfig.get_config() store = ModelRecordServiceBase.open(config) @@ -1487,14 +1465,11 @@ Note that we are relying on the contents of the application configuration to choose the implementation of `ModelRecordServiceBase`. -### get_model(key, [submodel_type], [context]) -> ModelInfo: +### load_model_by_key(key, [submodel_type], [context]) -> LoadedModel -*** TO DO: change to get_model(key, context=None, **kwargs) - -The `get_model()` method, like its similarly-named cousin in -`ModelRecordService`, receives the unique key that identifies the -model. It loads the model into memory, gets the model ready for use, -and returns a `ModelInfo` object. +The `load_model_by_key()` method receives the unique key that +identifies the model. It loads the model into memory, gets the model +ready for use, and returns a `LoadedModel` object. The optional second argument, `subtype` is a `SubModelType` string enum, such as "vae". It is mandatory when used with a main model, and @@ -1504,46 +1479,64 @@ The optional third argument, `context` can be provided by an invocation to trigger model load event reporting. See below for details. -The returned `ModelInfo` object shares some fields in common with -`ModelConfigBase`, but is otherwise a completely different beast: +The returned `LoadedModel` object contains a copy of the configuration +record returned by the model record `get_model()` method, as well as +the in-memory loaded model: -| **Field Name** | **Type** | **Description** | + +| **Attribute Name** | **Type** | **Description** | |----------------|-----------------|------------------| -| `key` | str | The model key derived from the ModelRecordService database | -| `name` | str | Name of this model | -| `base_model` | BaseModelType | Base model for this model | -| `type` | ModelType or SubModelType | Either the model type (non-main) or the submodel type (main models)| -| `location` | Path or str | Location of the model on the filesystem | -| `precision` | torch.dtype | The torch.precision to use for inference | -| `context` | ModelCache.ModelLocker | A context class used to lock the model in VRAM while in use | +| `config` | AnyModelConfig | A copy of the model's configuration record for retrieving base type, etc. | +| `model` | AnyModel | The instantiated model (details below) | +| `locker` | ModelLockerBase | A context manager that mediates the movement of the model into VRAM | -The types for `ModelInfo` and `SubModelType` can be imported from -`invokeai.app.services.model_loader_service`. +Because the loader can return multiple model types, it is typed to +return `AnyModel`, a Union `ModelMixin`, `torch.nn.Module`, +`IAIOnnxRuntimeModel`, `IPAdapter`, `IPAdapterPlus`, and +`EmbeddingModelRaw`. `ModelMixin` is the base class of all diffusers +models, `EmbeddingModelRaw` is used for LoRA and TextualInversion +models. The others are obvious. -To use the model, you use the `ModelInfo` as a context manager using -the following pattern: + +`LoadedModel` acts as a context manager. The context loads the model +into the execution device (e.g. VRAM on CUDA systems), locks the model +in the execution device for the duration of the context, and returns +the model. Use it like this: ``` -model_info = loader.get_model('f13dd932c0c35c22dcb8d6cda4203764', SubModelType('vae')) +model_info = loader.get_model_by_key('f13dd932c0c35c22dcb8d6cda4203764', SubModelType('vae')) with model_info as vae: image = vae.decode(latents)[0] ``` -The `vae` model will stay locked in the GPU during the period of time -it is in the context manager's scope. +`get_model_by_key()` may raise any of the following exceptions: -`get_model()` may raise any of the following exceptions: - -- `UnknownModelException` -- key not in database -- `ModelNotFoundException` -- key in database but model not found at path -- `InvalidModelException` -- the model is guilty of a variety of sins +- `UnknownModelException` -- key not in database +- `ModelNotFoundException` -- key in database but model not found at path +- `NotImplementedException` -- the loader doesn't know how to load this type of model -** TO DO: ** Resolve discrepancy between ModelInfo.location and -ModelConfig.path. +### load_model_by_attr(model_name, base_model, model_type, [submodel], [context]) -> LoadedModel + +This is similar to `load_model_by_key`, but instead it accepts the +combination of the model's name, type and base, which it passes to the +model record config store for retrieval. If successful, this method +returns a `LoadedModel`. It can raise the following exceptions: + +``` +UnknownModelException -- model with these attributes not known +NotImplementedException -- the loader doesn't know how to load this type of model +ValueError -- more than one model matches this combination of base/type/name +``` + +### load_model_by_config(config, [submodel], [context]) -> LoadedModel + +This method takes an `AnyModelConfig` returned by +ModelRecordService.get_model() and returns the corresponding loaded +model. It may raise a `NotImplementedException`. ### Emitting model loading events -When the `context` argument is passed to `get_model()`, it will +When the `context` argument is passed to `load_model_*()`, it will retrieve the invocation event bus from the passed `InvocationContext` object to emit events on the invocation bus. The two events are "model_load_started" and "model_load_completed". Both carry the @@ -1563,3 +1556,97 @@ payload=dict( ) ``` +### Adding Model Loaders + +Model loaders are small classes that inherit from the `ModelLoader` +base class. They typically implement one method `_load_model()` whose +signature is: + +``` +def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, +) -> AnyModel: +``` + +`_load_model()` will be passed the path to the model on disk, an +optional repository variant (used by the diffusers loaders to select, +e.g. the `fp16` variant, and an optional submodel_type for main and +onnx models. + +To install a new loader, place it in +`invokeai/backend/model_manager/load/model_loaders`. Inherit from +`ModelLoader` and use the `@AnyModelLoader.register()` decorator to +indicate what type of models the loader can handle. + +Here is a complete example from `generic_diffusers.py`, which is able +to load several different diffusers types: + +``` +from pathlib import Path +from typing import Optional + +from invokeai.backend.model_manager import ( + AnyModel, + BaseModelType, + ModelFormat, + ModelRepoVariant, + ModelType, + SubModelType, +) +from ..load_base import AnyModelLoader +from ..load_default import ModelLoader + + +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) +@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) +class GenericDiffusersLoader(ModelLoader): + """Class to load simple diffusers models.""" + + def _load_model( + self, + model_path: Path, + model_variant: Optional[ModelRepoVariant] = None, + submodel_type: Optional[SubModelType] = None, + ) -> AnyModel: + model_class = self._get_hf_load_class(model_path) + if submodel_type is not None: + raise Exception(f"There are no submodels in models of type {model_class}") + variant = model_variant.value if model_variant else None + result: AnyModel = model_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, variant=variant) # type: ignore + return result +``` + +Note that a loader can register itself to handle several different +model types. An exception will be raised if more than one loader tries +to register the same model type. + +#### Conversion + +Some models require conversion to diffusers format before they can be +loaded. These loaders should override two additional methods: + +``` +_needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool +_convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Path) -> Path: +``` + +The first method accepts the model configuration, the path to where +the unmodified model is currently installed, and a proposed +destination for the converted model. This method returns True if the +model needs to be converted. It typically does this by comparing the +last modification time of the original model file to the modification +time of the converted model. In some cases you will also want to check +the modification date of the configuration record, in the event that +the user has changed something like the scheduler prediction type that +will require the model to be re-converted. See `controlnet.py` for an +example of this logic. + +The second method accepts the model configuration, the path to the +original model on disk, and the desired output path for the converted +model. It does whatever it needs to do to get the model into diffusers +format, and returns the Path of the resulting model. (The path should +ordinarily be the same as `output_path`.) + diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index dcb8d21997..378961a055 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -8,9 +8,6 @@ from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMe from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache from invokeai.app.services.shared.sqlite.sqlite_util import init_db -from invokeai.backend.model_manager.load import AnyModelLoader, ModelConvertCache -from invokeai.backend.model_manager.load.model_cache import ModelCache -from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData from invokeai.backend.util.logging import InvokeAILogger from invokeai.version.invokeai_version import __version__ @@ -30,9 +27,7 @@ from ..services.invocation_queue.invocation_queue_memory import MemoryInvocation from ..services.invocation_services import InvocationServices from ..services.invocation_stats.invocation_stats_default import InvocationStatsService from ..services.invoker import Invoker -from ..services.model_install import ModelInstallService from ..services.model_manager.model_manager_default import ModelManagerService -from ..services.model_records import ModelRecordServiceSQL from ..services.names.names_default import SimpleNameService from ..services.session_processor.session_processor_default import DefaultSessionProcessor from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue @@ -98,28 +93,10 @@ class ApiDependencies: conditioning = ObjectSerializerForwardCache( ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", ephemeral=True) ) - model_manager = ModelManagerService(config, logger) - model_record_service = ModelRecordServiceSQL(db=db) - model_loader = AnyModelLoader( - app_config=config, - logger=logger, - ram_cache=ModelCache( - max_cache_size=config.ram_cache_size, max_vram_cache_size=config.vram_cache_size, logger=logger - ), - convert_cache=ModelConvertCache( - cache_path=config.models_convert_cache_path, max_size=config.convert_cache_size - ), - ) - model_record_service = ModelRecordServiceSQL(db=db, loader=model_loader) download_queue_service = DownloadQueueService(event_bus=events) - model_install_service = ModelInstallService( - app_config=config, - record_store=model_record_service, - download_queue=download_queue_service, - metadata_store=ModelMetadataStore(db=db), - event_bus=events, + model_manager = ModelManagerService.build_model_manager( + app_config=configuration, db=db, download_queue=download_queue_service, events=events ) - model_manager = ModelManagerService(config, logger) # TO DO: legacy model manager v1. Remove names = SimpleNameService() performance_statistics = InvocationStatsService() processor = DefaultInvocationProcessor() @@ -143,9 +120,7 @@ class ApiDependencies: invocation_cache=invocation_cache, logger=logger, model_manager=model_manager, - model_records=model_record_service, download_queue=download_queue_service, - model_install=model_install_service, names=names, performance_statistics=performance_statistics, processor=processor, diff --git a/invokeai/app/api/routers/model_records.py b/invokeai/app/api/routers/model_manager_v2.py similarity index 86% rename from invokeai/app/api/routers/model_records.py rename to invokeai/app/api/routers/model_manager_v2.py index f9a3e40898..4fc785e4f7 100644 --- a/invokeai/app/api/routers/model_records.py +++ b/invokeai/app/api/routers/model_manager_v2.py @@ -32,7 +32,7 @@ from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata from ..dependencies import ApiDependencies -model_records_router = APIRouter(prefix="/v1/model/record", tags=["model_manager_v2_unstable"]) +model_manager_v2_router = APIRouter(prefix="/v2/models", tags=["model_manager_v2"]) class ModelsList(BaseModel): @@ -52,7 +52,7 @@ class ModelTagSet(BaseModel): tags: Set[str] -@model_records_router.get( +@model_manager_v2_router.get( "/", operation_id="list_model_records", ) @@ -65,7 +65,7 @@ async def list_model_records( ), ) -> ModelsList: """Get a list of models.""" - record_store = ApiDependencies.invoker.services.model_records + record_store = ApiDependencies.invoker.services.model_manager.store found_models: list[AnyModelConfig] = [] if base_models: for base_model in base_models: @@ -81,7 +81,7 @@ async def list_model_records( return ModelsList(models=found_models) -@model_records_router.get( +@model_manager_v2_router.get( "/i/{key}", operation_id="get_model_record", responses={ @@ -94,24 +94,27 @@ async def get_model_record( key: str = Path(description="Key of the model record to fetch."), ) -> AnyModelConfig: """Get a model record""" - record_store = ApiDependencies.invoker.services.model_records + record_store = ApiDependencies.invoker.services.model_manager.store try: - return record_store.get_model(key) + config: AnyModelConfig = record_store.get_model(key) + return config except UnknownModelException as e: raise HTTPException(status_code=404, detail=str(e)) -@model_records_router.get("/meta", operation_id="list_model_summary") +@model_manager_v2_router.get("/meta", operation_id="list_model_summary") async def list_model_summary( page: int = Query(default=0, description="The page to get"), per_page: int = Query(default=10, description="The number of models per page"), order_by: ModelRecordOrderBy = Query(default=ModelRecordOrderBy.Default, description="The attribute to order by"), ) -> PaginatedResults[ModelSummary]: """Gets a page of model summary data.""" - return ApiDependencies.invoker.services.model_records.list_models(page=page, per_page=per_page, order_by=order_by) + record_store = ApiDependencies.invoker.services.model_manager.store + results: PaginatedResults[ModelSummary] = record_store.list_models(page=page, per_page=per_page, order_by=order_by) + return results -@model_records_router.get( +@model_manager_v2_router.get( "/meta/i/{key}", operation_id="get_model_metadata", responses={ @@ -124,24 +127,25 @@ async def get_model_metadata( key: str = Path(description="Key of the model repo metadata to fetch."), ) -> Optional[AnyModelRepoMetadata]: """Get a model metadata object.""" - record_store = ApiDependencies.invoker.services.model_records - result = record_store.get_metadata(key) + record_store = ApiDependencies.invoker.services.model_manager.store + result: Optional[AnyModelRepoMetadata] = record_store.get_metadata(key) if not result: raise HTTPException(status_code=404, detail="No metadata for a model with this key") return result -@model_records_router.get( +@model_manager_v2_router.get( "/tags", operation_id="list_tags", ) async def list_tags() -> Set[str]: """Get a unique set of all the model tags.""" - record_store = ApiDependencies.invoker.services.model_records - return record_store.list_tags() + record_store = ApiDependencies.invoker.services.model_manager.store + result: Set[str] = record_store.list_tags() + return result -@model_records_router.get( +@model_manager_v2_router.get( "/tags/search", operation_id="search_by_metadata_tags", ) @@ -149,12 +153,12 @@ async def search_by_metadata_tags( tags: Set[str] = Query(default=None, description="Tags to search for"), ) -> ModelsList: """Get a list of models.""" - record_store = ApiDependencies.invoker.services.model_records + record_store = ApiDependencies.invoker.services.model_manager.store results = record_store.search_by_metadata_tag(tags) return ModelsList(models=results) -@model_records_router.patch( +@model_manager_v2_router.patch( "/i/{key}", operation_id="update_model_record", responses={ @@ -172,9 +176,9 @@ async def update_model_record( ) -> AnyModelConfig: """Update model contents with a new config. If the model name or base fields are changed, then the model is renamed.""" logger = ApiDependencies.invoker.services.logger - record_store = ApiDependencies.invoker.services.model_records + record_store = ApiDependencies.invoker.services.model_manager.store try: - model_response = record_store.update_model(key, config=info) + model_response: AnyModelConfig = record_store.update_model(key, config=info) logger.info(f"Updated model: {key}") except UnknownModelException as e: raise HTTPException(status_code=404, detail=str(e)) @@ -184,7 +188,7 @@ async def update_model_record( return model_response -@model_records_router.delete( +@model_manager_v2_router.delete( "/i/{key}", operation_id="del_model_record", responses={ @@ -205,7 +209,7 @@ async def del_model_record( logger = ApiDependencies.invoker.services.logger try: - installer = ApiDependencies.invoker.services.model_install + installer = ApiDependencies.invoker.services.model_manager.install installer.delete(key) logger.info(f"Deleted model: {key}") return Response(status_code=204) @@ -214,7 +218,7 @@ async def del_model_record( raise HTTPException(status_code=404, detail=str(e)) -@model_records_router.post( +@model_manager_v2_router.post( "/i/", operation_id="add_model_record", responses={ @@ -229,7 +233,7 @@ async def add_model_record( ) -> AnyModelConfig: """Add a model using the configuration information appropriate for its type.""" logger = ApiDependencies.invoker.services.logger - record_store = ApiDependencies.invoker.services.model_records + record_store = ApiDependencies.invoker.services.model_manager.store if config.key == "": config.key = sha1(randbytes(100)).hexdigest() logger.info(f"Created model {config.key} for {config.name}") @@ -243,10 +247,11 @@ async def add_model_record( raise HTTPException(status_code=415) # now fetch it out - return record_store.get_model(config.key) + result: AnyModelConfig = record_store.get_model(config.key) + return result -@model_records_router.post( +@model_manager_v2_router.post( "/import", operation_id="import_model_record", responses={ @@ -322,7 +327,7 @@ async def import_model( logger = ApiDependencies.invoker.services.logger try: - installer = ApiDependencies.invoker.services.model_install + installer = ApiDependencies.invoker.services.model_manager.install result: ModelInstallJob = installer.import_model( source=source, config=config, @@ -340,17 +345,17 @@ async def import_model( return result -@model_records_router.get( +@model_manager_v2_router.get( "/import", operation_id="list_model_install_jobs", ) async def list_model_install_jobs() -> List[ModelInstallJob]: """Return list of model install jobs.""" - jobs: List[ModelInstallJob] = ApiDependencies.invoker.services.model_install.list_jobs() + jobs: List[ModelInstallJob] = ApiDependencies.invoker.services.model_manager.install.list_jobs() return jobs -@model_records_router.get( +@model_manager_v2_router.get( "/import/{id}", operation_id="get_model_install_job", responses={ @@ -361,12 +366,13 @@ async def list_model_install_jobs() -> List[ModelInstallJob]: async def get_model_install_job(id: int = Path(description="Model install id")) -> ModelInstallJob: """Return model install job corresponding to the given source.""" try: - return ApiDependencies.invoker.services.model_install.get_job_by_id(id) + result: ModelInstallJob = ApiDependencies.invoker.services.model_manager.install.get_job_by_id(id) + return result except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) -@model_records_router.delete( +@model_manager_v2_router.delete( "/import/{id}", operation_id="cancel_model_install_job", responses={ @@ -377,7 +383,7 @@ async def get_model_install_job(id: int = Path(description="Model install id")) ) async def cancel_model_install_job(id: int = Path(description="Model install job ID")) -> None: """Cancel the model install job(s) corresponding to the given job ID.""" - installer = ApiDependencies.invoker.services.model_install + installer = ApiDependencies.invoker.services.model_manager.install try: job = installer.get_job_by_id(id) except ValueError as e: @@ -385,7 +391,7 @@ async def cancel_model_install_job(id: int = Path(description="Model install job installer.cancel_job(job) -@model_records_router.patch( +@model_manager_v2_router.patch( "/import", operation_id="prune_model_install_jobs", responses={ @@ -395,11 +401,11 @@ async def cancel_model_install_job(id: int = Path(description="Model install job ) async def prune_model_install_jobs() -> Response: """Prune all completed and errored jobs from the install job list.""" - ApiDependencies.invoker.services.model_install.prune_jobs() + ApiDependencies.invoker.services.model_manager.install.prune_jobs() return Response(status_code=204) -@model_records_router.patch( +@model_manager_v2_router.patch( "/sync", operation_id="sync_models_to_config", responses={ @@ -414,11 +420,11 @@ async def sync_models_to_config() -> Response: Model files without a corresponding record in the database are added. Orphan records without a models file are deleted. """ - ApiDependencies.invoker.services.model_install.sync_to_config() + ApiDependencies.invoker.services.model_manager.install.sync_to_config() return Response(status_code=204) -@model_records_router.put( +@model_manager_v2_router.put( "/merge", operation_id="merge", ) @@ -451,7 +457,7 @@ async def merge( try: logger.info(f"Merging models: {keys} into {merge_dest_directory or ''}/{merged_model_name}") dest = pathlib.Path(merge_dest_directory) if merge_dest_directory else None - installer = ApiDependencies.invoker.services.model_install + installer = ApiDependencies.invoker.services.model_manager.install merger = ModelMerger(installer) model_names = [installer.record_store.get_model(x).name for x in keys] response = merger.merge_diffusion_models_and_save( diff --git a/invokeai/app/api/routers/models.py b/invokeai/app/api/routers/models.py index 8f83820cf8..0aa7aa0ecb 100644 --- a/invokeai/app/api/routers/models.py +++ b/invokeai/app/api/routers/models.py @@ -8,8 +8,7 @@ from fastapi.routing import APIRouter from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from starlette.exceptions import HTTPException -from invokeai.backend import BaseModelType, ModelType -from invokeai.backend.model_management import MergeInterpolationMethod +from invokeai.backend.model_management import BaseModelType, MergeInterpolationMethod, ModelType from invokeai.backend.model_management.models import ( OPENAPI_MODEL_CONFIGS, InvalidModelException, diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index f48074de7c..851cbc8160 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -48,7 +48,7 @@ if True: # hack to make flake8 happy with imports coming after setting up the c boards, download_queue, images, - model_records, + model_manager_v2, models, session_queue, sessions, @@ -114,8 +114,7 @@ async def shutdown_event() -> None: app.include_router(sessions.session_router, prefix="/api") app.include_router(utilities.utilities_router, prefix="/api") -app.include_router(models.models_router, prefix="/api") -app.include_router(model_records.model_records_router, prefix="/api") +app.include_router(model_manager_v2.model_manager_v2_router, prefix="/api") app.include_router(download_queue.download_queue_router, prefix="/api") app.include_router(images.images_router, prefix="/api") app.include_router(boards.boards_router, prefix="/api") diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 0e1a6bdc6f..3850fb6cc3 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -3,6 +3,7 @@ from typing import Iterator, List, Optional, Tuple, Union import torch from compel import Compel, ReturnedEmbeddingsType from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment +from transformers import CLIPTokenizer import invokeai.backend.util.logging as logger from invokeai.app.invocations.fields import ( @@ -68,18 +69,18 @@ class CompelInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> ConditioningOutput: - tokenizer_info = context.services.model_records.load_model( + tokenizer_info = context.services.model_manager.load.load_model_by_key( **self.clip.tokenizer.model_dump(), context=context, ) - text_encoder_info = context.services.model_records.load_model( + text_encoder_info = context.services.model_manager.load.load_model_by_key( **self.clip.text_encoder.model_dump(), context=context, ) def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in self.clip.loras: - lora_info = context.services.model_records.load_model( + lora_info = context.services.model_manager.load.load_model_by_key( **lora.model_dump(exclude={"weight"}), context=context ) assert isinstance(lora_info.model, LoRAModelRaw) @@ -93,7 +94,7 @@ class CompelInvocation(BaseInvocation): for trigger in extract_ti_triggers_from_prompt(self.prompt): name = trigger[1:-1] try: - loaded_model = context.services.model_records.load_model( + loaded_model = context.services.model_manager.load.load_model_by_key( **self.clip.text_encoder.model_dump(), context=context, ).model @@ -164,11 +165,11 @@ class SDXLPromptInvocationBase: lora_prefix: str, zero_on_empty: bool, ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[ExtraConditioningInfo]]: - tokenizer_info = context.services.model_records.load_model( + tokenizer_info = context.services.model_manager.load.load_model_by_key( **clip_field.tokenizer.model_dump(), context=context, ) - text_encoder_info = context.services.model_records.load_model( + text_encoder_info = context.services.model_manager.load.load_model_by_key( **clip_field.text_encoder.model_dump(), context=context, ) @@ -196,7 +197,7 @@ class SDXLPromptInvocationBase: def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in clip_field.loras: - lora_info = context.services.model_records.load_model( + lora_info = context.services.model_manager.load.load_model_by_key( **lora.model_dump(exclude={"weight"}), context=context ) lora_model = lora_info.model @@ -211,7 +212,7 @@ class SDXLPromptInvocationBase: for trigger in extract_ti_triggers_from_prompt(prompt): name = trigger[1:-1] try: - ti_model = context.services.model_records.load_model_by_attr( + ti_model = context.services.model_manager.load.load_model_by_attr( model_name=name, base_model=text_encoder_info.config.base, model_type=ModelType.TextualInversion, @@ -448,9 +449,9 @@ class ClipSkipInvocation(BaseInvocation): def get_max_token_count( - tokenizer, + tokenizer: CLIPTokenizer, prompt: Union[FlattenedPrompt, Blend, Conjunction], - truncate_if_too_long=False, + truncate_if_too_long: bool = False, ) -> int: if type(prompt) is Blend: blend: Blend = prompt @@ -462,7 +463,9 @@ def get_max_token_count( return len(get_tokens_for_prompt_object(tokenizer, prompt, truncate_if_too_long)) -def get_tokens_for_prompt_object(tokenizer, parsed_prompt: FlattenedPrompt, truncate_if_too_long=True) -> List[str]: +def get_tokens_for_prompt_object( + tokenizer: CLIPTokenizer, parsed_prompt: FlattenedPrompt, truncate_if_too_long: bool = True +) -> List[str]: if type(parsed_prompt) is Blend: raise ValueError("Blend is not supported here - you need to get tokens for each of its .children") @@ -475,24 +478,29 @@ def get_tokens_for_prompt_object(tokenizer, parsed_prompt: FlattenedPrompt, trun for x in parsed_prompt.children ] text = " ".join(text_fragments) - tokens = tokenizer.tokenize(text) + tokens: List[str] = tokenizer.tokenize(text) if truncate_if_too_long: max_tokens_length = tokenizer.model_max_length - 2 # typically 75 tokens = tokens[0:max_tokens_length] return tokens -def log_tokenization_for_conjunction(c: Conjunction, tokenizer, display_label_prefix=None): +def log_tokenization_for_conjunction( + c: Conjunction, tokenizer: CLIPTokenizer, display_label_prefix: Optional[str] = None +) -> None: display_label_prefix = display_label_prefix or "" for i, p in enumerate(c.prompts): if len(c.prompts) > 1: this_display_label_prefix = f"{display_label_prefix}(conjunction part {i + 1}, weight={c.weights[i]})" else: + assert display_label_prefix is not None this_display_label_prefix = display_label_prefix log_tokenization_for_prompt_object(p, tokenizer, display_label_prefix=this_display_label_prefix) -def log_tokenization_for_prompt_object(p: Union[Blend, FlattenedPrompt], tokenizer, display_label_prefix=None): +def log_tokenization_for_prompt_object( + p: Union[Blend, FlattenedPrompt], tokenizer: CLIPTokenizer, display_label_prefix: Optional[str] = None +) -> None: display_label_prefix = display_label_prefix or "" if type(p) is Blend: blend: Blend = p @@ -532,7 +540,12 @@ def log_tokenization_for_prompt_object(p: Union[Blend, FlattenedPrompt], tokeniz log_tokenization_for_text(text, tokenizer, display_label=display_label_prefix) -def log_tokenization_for_text(text, tokenizer, display_label=None, truncate_if_too_long=False): +def log_tokenization_for_text( + text: str, + tokenizer: CLIPTokenizer, + display_label: Optional[str] = None, + truncate_if_too_long: Optional[bool] = False, +) -> None: """shows how the prompt is tokenized # usually tokens have '' to indicate end-of-word, # but for readability it has been replaced with ' ' diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 063b23fa58..289da2dd73 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -3,13 +3,15 @@ import math from contextlib import ExitStack from functools import singledispatchmethod -from typing import Iterator, List, Literal, Optional, Tuple, Union +from typing import Any, Iterator, List, Literal, Optional, Tuple, Union import einops import numpy as np +import numpy.typing as npt import torch import torchvision.transforms as T -from diffusers import AutoencoderKL, AutoencoderTiny, UNet2DConditionModel +from diffusers import AutoencoderKL, AutoencoderTiny +from diffusers.configuration_utils import ConfigMixin from diffusers.image_processor import VaeImageProcessor from diffusers.models.adapter import T2IAdapter from diffusers.models.attention_processor import ( @@ -18,8 +20,10 @@ from diffusers.models.attention_processor import ( LoRAXFormersAttnProcessor, XFormersAttnProcessor, ) +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel from diffusers.schedulers import DPMSolverSDEScheduler from diffusers.schedulers import SchedulerMixin as Scheduler +from PIL import Image from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize @@ -46,9 +50,10 @@ from invokeai.app.invocations.primitives import ( from invokeai.app.invocations.t2i_adapter import T2IAdapterField from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.controlnet_utils import prepare_control_image +from invokeai.backend.embeddings.lora import LoRAModelRaw from invokeai.backend.embeddings.model_patcher import ModelPatcher from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus -from invokeai.backend.model_manager import AnyModel, BaseModelType +from invokeai.backend.model_manager import BaseModelType, LoadedModel from invokeai.backend.stable_diffusion import PipelineIntermediateState, set_seamless from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningData, IPAdapterConditioningInfo from invokeai.backend.util.silence_warnings import SilenceWarnings @@ -123,10 +128,10 @@ class CreateDenoiseMaskInvocation(BaseInvocation): ui_order=4, ) - def prep_mask_tensor(self, mask_image): + def prep_mask_tensor(self, mask_image: Image) -> torch.Tensor: if mask_image.mode != "L": mask_image = mask_image.convert("L") - mask_tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False) + mask_tensor: torch.Tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False) if mask_tensor.dim() == 3: mask_tensor = mask_tensor.unsqueeze(0) # if shape is not None: @@ -136,25 +141,25 @@ class CreateDenoiseMaskInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> DenoiseMaskOutput: if self.image is not None: - image = context.images.get_pil(self.image.image_name) - image = image_resized_to_grid_as_tensor(image.convert("RGB")) - if image.dim() == 3: - image = image.unsqueeze(0) + image = context.services.images.get_pil_image(self.image.image_name) + image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB")) + if image_tensor.dim() == 3: + image_tensor = image_tensor.unsqueeze(0) else: - image = None + image_tensor = None mask = self.prep_mask_tensor( context.images.get_pil(self.mask.image_name), ) - if image is not None: - vae_info = context.services.model_records.load_model( + if image_tensor is not None: + vae_info = context.services.model_manager.load.load_model_by_key( **self.vae.vae.model_dump(), context=context, ) - img_mask = tv_resize(mask, image.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) - masked_image = image * torch.where(img_mask < 0.5, 0.0, 1.0) + img_mask = tv_resize(mask, image_tensor.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) + masked_image = image_tensor * torch.where(img_mask < 0.5, 0.0, 1.0) # TODO: masked_latents = ImageToLatentsInvocation.vae_encode(vae_info, self.fp32, self.tiled, masked_image.clone()) @@ -177,7 +182,7 @@ def get_scheduler( seed: int, ) -> Scheduler: scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP["ddim"]) - orig_scheduler_info = context.services.model_records.load_model( + orig_scheduler_info = context.services.model_manager.load.load_model_by_key( **scheduler_info.model_dump(), context=context, ) @@ -188,7 +193,7 @@ def get_scheduler( scheduler_config = scheduler_config["_backup"] scheduler_config = { **scheduler_config, - **scheduler_extra_config, + **scheduler_extra_config, # FIXME "_backup": scheduler_config, } @@ -201,6 +206,7 @@ def get_scheduler( # hack copied over from generate.py if not hasattr(scheduler, "uses_inpainting_model"): scheduler.uses_inpainting_model = lambda: False + assert isinstance(scheduler, Scheduler) return scheduler @@ -284,7 +290,7 @@ class DenoiseLatentsInvocation(BaseInvocation): ) @field_validator("cfg_scale") - def ge_one(cls, v): + def ge_one(cls, v: Union[List[float], float]) -> Union[List[float], float]: """validate that all cfg_scale values are >= 1""" if isinstance(v, list): for i in v: @@ -298,9 +304,9 @@ class DenoiseLatentsInvocation(BaseInvocation): def get_conditioning_data( self, context: InvocationContext, - scheduler, - unet, - seed, + scheduler: Scheduler, + unet: UNet2DConditionModel, + seed: int, ) -> ConditioningData: positive_cond_data = context.conditioning.load(self.positive_conditioning.conditioning_name) c = positive_cond_data.conditionings[0].to(device=unet.device, dtype=unet.dtype) @@ -323,7 +329,7 @@ class DenoiseLatentsInvocation(BaseInvocation): ), ) - conditioning_data = conditioning_data.add_scheduler_args_if_applicable( + conditioning_data = conditioning_data.add_scheduler_args_if_applicable( # FIXME scheduler, # for ddim scheduler eta=0.0, # ddim_eta @@ -335,8 +341,8 @@ class DenoiseLatentsInvocation(BaseInvocation): def create_pipeline( self, - unet, - scheduler, + unet: UNet2DConditionModel, + scheduler: Scheduler, ) -> StableDiffusionGeneratorPipeline: # TODO: # configure_model_padding( @@ -347,10 +353,10 @@ class DenoiseLatentsInvocation(BaseInvocation): class FakeVae: class FakeVaeConfig: - def __init__(self): + def __init__(self) -> None: self.block_out_channels = [0] - def __init__(self): + def __init__(self) -> None: self.config = FakeVae.FakeVaeConfig() return StableDiffusionGeneratorPipeline( @@ -367,11 +373,11 @@ class DenoiseLatentsInvocation(BaseInvocation): def prep_control_data( self, context: InvocationContext, - control_input: Union[ControlField, List[ControlField]], + control_input: Optional[Union[ControlField, List[ControlField]]], latents_shape: List[int], exit_stack: ExitStack, do_classifier_free_guidance: bool = True, - ) -> List[ControlNetData]: + ) -> Optional[List[ControlNetData]]: # Assuming fixed dimensional scaling of LATENT_SCALE_FACTOR. control_height_resize = latents_shape[2] * LATENT_SCALE_FACTOR control_width_resize = latents_shape[3] * LATENT_SCALE_FACTOR @@ -394,7 +400,7 @@ class DenoiseLatentsInvocation(BaseInvocation): controlnet_data = [] for control_info in control_list: control_model = exit_stack.enter_context( - context.services.model_records.load_model( + context.services.model_manager.load.load_model_by_key( key=control_info.control_model.key, context=context, ) @@ -460,23 +466,25 @@ class DenoiseLatentsInvocation(BaseInvocation): conditioning_data.ip_adapter_conditioning = [] for single_ip_adapter in ip_adapter: ip_adapter_model: Union[IPAdapter, IPAdapterPlus] = exit_stack.enter_context( - context.services.model_records.load_model( + context.services.model_manager.load.load_model_by_key( key=single_ip_adapter.ip_adapter_model.key, context=context, ) ) - image_encoder_model_info = context.services.model_records.load_model( + image_encoder_model_info = context.services.model_manager.load.load_model_by_key( key=single_ip_adapter.image_encoder_model.key, context=context, ) # `single_ip_adapter.image` could be a list or a single ImageField. Normalize to a list here. - single_ipa_images = single_ip_adapter.image - if not isinstance(single_ipa_images, list): - single_ipa_images = [single_ipa_images] + single_ipa_image_fields = single_ip_adapter.image + if not isinstance(single_ipa_image_fields, list): + single_ipa_image_fields = [single_ipa_image_fields] - single_ipa_images = [context.images.get_pil(image.image_name) for image in single_ipa_images] + single_ipa_images = [ + context.services.images.get_pil_image(image.image_name) for image in single_ipa_image_fields + ] # TODO(ryand): With some effort, the step of running the CLIP Vision encoder could be done before any other # models are needed in memory. This would help to reduce peak memory utilization in low-memory environments. @@ -520,21 +528,19 @@ class DenoiseLatentsInvocation(BaseInvocation): t2i_adapter_data = [] for t2i_adapter_field in t2i_adapter: - t2i_adapter_model_info = context.services.model_records.load_model( + t2i_adapter_model_info = context.services.model_manager.load.load_model_by_key( key=t2i_adapter_field.t2i_adapter_model.key, context=context, ) image = context.images.get_pil(t2i_adapter_field.image.image_name) # The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally. - if t2i_adapter_field.t2i_adapter_model.base_model == BaseModelType.StableDiffusion1: + if t2i_adapter_model_info.base == BaseModelType.StableDiffusion1: max_unet_downscale = 8 - elif t2i_adapter_field.t2i_adapter_model.base_model == BaseModelType.StableDiffusionXL: + elif t2i_adapter_model_info.base == BaseModelType.StableDiffusionXL: max_unet_downscale = 4 else: - raise ValueError( - f"Unexpected T2I-Adapter base model type: '{t2i_adapter_field.t2i_adapter_model.base_model}'." - ) + raise ValueError(f"Unexpected T2I-Adapter base model type: '{t2i_adapter_model_info.base}'.") t2i_adapter_model: T2IAdapter with t2i_adapter_model_info as t2i_adapter_model: @@ -582,7 +588,15 @@ class DenoiseLatentsInvocation(BaseInvocation): # original idea by https://github.com/AmericanPresidentJimmyCarter # TODO: research more for second order schedulers timesteps - def init_scheduler(self, scheduler, device, steps, denoising_start, denoising_end): + def init_scheduler( + self, + scheduler: Union[Scheduler, ConfigMixin], + device: torch.device, + steps: int, + denoising_start: float, + denoising_end: float, + ) -> Tuple[int, List[int], int]: + assert isinstance(scheduler, ConfigMixin) if scheduler.config.get("cpu_only", False): scheduler.set_timesteps(steps, device="cpu") timesteps = scheduler.timesteps.to(device=device) @@ -594,11 +608,11 @@ class DenoiseLatentsInvocation(BaseInvocation): _timesteps = timesteps[:: scheduler.order] # get start timestep index - t_start_val = int(round(scheduler.config.num_train_timesteps * (1 - denoising_start))) + t_start_val = int(round(scheduler.config["num_train_timesteps"] * (1 - denoising_start))) t_start_idx = len(list(filter(lambda ts: ts >= t_start_val, _timesteps))) # get end timestep index - t_end_val = int(round(scheduler.config.num_train_timesteps * (1 - denoising_end))) + t_end_val = int(round(scheduler.config["num_train_timesteps"] * (1 - denoising_end))) t_end_idx = len(list(filter(lambda ts: ts >= t_end_val, _timesteps[t_start_idx:]))) # apply order to indexes @@ -611,7 +625,9 @@ class DenoiseLatentsInvocation(BaseInvocation): return num_inference_steps, timesteps, init_timestep - def prep_inpaint_mask(self, context: InvocationContext, latents): + def prep_inpaint_mask( + self, context: InvocationContext, latents: torch.Tensor + ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor]]: if self.denoise_mask is None: return None, None @@ -660,12 +676,19 @@ class DenoiseLatentsInvocation(BaseInvocation): do_classifier_free_guidance=True, ) - def step_callback(state: PipelineIntermediateState): - context.util.sd_step_callback(state, self.unet.unet.base_model) + # Get the source node id (we are invoking the prepared node) + graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) + source_node_id = graph_execution_state.prepared_source_mapping[self.id] - def _lora_loader() -> Iterator[Tuple[AnyModel, float]]: + # get the unet's config so that we can pass the base to dispatch_progress() + unet_config = context.services.model_manager.store.get_model(**self.unet.unet.model_dump()) + + def step_callback(state: PipelineIntermediateState) -> None: + self.dispatch_progress(context, source_node_id, state, unet_config.base) + + def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in self.unet.loras: - lora_info = context.services.model_records.load_model( + lora_info = context.services.model_manager.load.load_model_by_key( **lora.model_dump(exclude={"weight"}), context=context, ) @@ -673,7 +696,7 @@ class DenoiseLatentsInvocation(BaseInvocation): del lora_info return - unet_info = context.services.model_records.load_model( + unet_info = context.services.model_manager.load.load_model_by_key( **self.unet.unet.model_dump(), context=context, ) @@ -783,7 +806,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): def invoke(self, context: InvocationContext) -> ImageOutput: latents = context.tensors.load(self.latents.latents_name) - vae_info = context.services.model_records.load_model( + vae_info = context.services.model_manager.load.load_model_by_key( **self.vae.vae.model_dump(), context=context, ) @@ -961,8 +984,9 @@ class ImageToLatentsInvocation(BaseInvocation): fp32: bool = InputField(default=DEFAULT_PRECISION == "float32", description=FieldDescriptions.fp32) @staticmethod - def vae_encode(vae_info, upcast, tiled, image_tensor): + def vae_encode(vae_info: LoadedModel, upcast: bool, tiled: bool, image_tensor: torch.Tensor) -> torch.Tensor: with vae_info as vae: + assert isinstance(vae, torch.nn.Module) orig_dtype = vae.dtype if upcast: vae.to(dtype=torch.float32) @@ -1008,7 +1032,7 @@ class ImageToLatentsInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> LatentsOutput: image = context.images.get_pil(self.image.image_name) - vae_info = context.services.model_records.load_model( + vae_info = context.services.model_manager.load.load_model_by_key( **self.vae.vae.model_dump(), context=context, ) @@ -1026,14 +1050,19 @@ class ImageToLatentsInvocation(BaseInvocation): @singledispatchmethod @staticmethod def _encode_to_tensor(vae: AutoencoderKL, image_tensor: torch.FloatTensor) -> torch.FloatTensor: + assert isinstance(vae, torch.nn.Module) image_tensor_dist = vae.encode(image_tensor).latent_dist - latents = image_tensor_dist.sample().to(dtype=vae.dtype) # FIXME: uses torch.randn. make reproducible! + latents: torch.Tensor = image_tensor_dist.sample().to( + dtype=vae.dtype + ) # FIXME: uses torch.randn. make reproducible! return latents @_encode_to_tensor.register @staticmethod def _(vae: AutoencoderTiny, image_tensor: torch.FloatTensor) -> torch.FloatTensor: - return vae.encode(image_tensor).latents + assert isinstance(vae, torch.nn.Module) + latents: torch.FloatTensor = vae.encode(image_tensor).latents + return latents @invocation( @@ -1066,7 +1095,12 @@ class BlendLatentsInvocation(BaseInvocation): # TODO: device = choose_torch_device() - def slerp(t, v0, v1, DOT_THRESHOLD=0.9995): + def slerp( + t: Union[float, npt.NDArray[Any]], # FIXME: maybe use np.float32 here? + v0: Union[torch.Tensor, npt.NDArray[Any]], + v1: Union[torch.Tensor, npt.NDArray[Any]], + DOT_THRESHOLD: float = 0.9995, + ) -> Union[torch.Tensor, npt.NDArray[Any]]: """ Spherical linear interpolation Args: @@ -1099,12 +1133,16 @@ class BlendLatentsInvocation(BaseInvocation): v2 = s0 * v0 + s1 * v1 if inputs_are_torch: - v2 = torch.from_numpy(v2).to(device) - - return v2 + v2_torch: torch.Tensor = torch.from_numpy(v2).to(device) + return v2_torch + else: + assert isinstance(v2, np.ndarray) + return v2 # blend - blended_latents = slerp(self.alpha, latents_a, latents_b) + bl = slerp(self.alpha, latents_a, latents_b) + assert isinstance(bl, torch.Tensor) + blended_latents: torch.Tensor = bl # for type checking convenience # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699 blended_latents = blended_latents.to("cpu") @@ -1197,15 +1235,19 @@ class IdealSizeInvocation(BaseInvocation): description="Amount to multiply the model's dimensions by when calculating the ideal size (may result in initial generation artifacts if too large)", ) - def trim_to_multiple_of(self, *args, multiple_of=LATENT_SCALE_FACTOR): + def trim_to_multiple_of(self, *args: int, multiple_of: int = LATENT_SCALE_FACTOR) -> Tuple[int, ...]: return tuple((x - x % multiple_of) for x in args) def invoke(self, context: InvocationContext) -> IdealSizeOutput: + unet_config = context.services.model_manager.load.load_model_by_key( + **self.unet.unet.model_dump(), + context=context, + ) aspect = self.width / self.height - dimension = 512 - if self.unet.unet.base_model == BaseModelType.StableDiffusion2: + dimension: float = 512 + if unet_config.base == BaseModelType.StableDiffusion2: dimension = 768 - elif self.unet.unet.base_model == BaseModelType.StableDiffusionXL: + elif unet_config.base == BaseModelType.StableDiffusionXL: dimension = 1024 dimension = dimension * self.multiplier min_dimension = math.floor(dimension * 0.5) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index e2ea744283..fa6e8b98da 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -17,7 +17,7 @@ from .baseinvocation import ( class ModelInfo(BaseModel): - key: str = Field(description="Info to load submodel") + key: str = Field(description="Key of model as returned by ModelRecordServiceBase.get_model()") submodel: Optional[SubModelType] = Field(default=None, description="Info to load submodel") diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index e893be8763..0a1fa1e922 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -27,9 +27,7 @@ if TYPE_CHECKING: from .invocation_queue.invocation_queue_base import InvocationQueueABC from .invocation_stats.invocation_stats_base import InvocationStatsServiceBase from .item_storage.item_storage_base import ItemStorageABC - from .model_install import ModelInstallServiceBase from .model_manager.model_manager_base import ModelManagerServiceBase - from .model_records import ModelRecordServiceBase from .names.names_base import NameServiceBase from .session_processor.session_processor_base import SessionProcessorBase from .session_queue.session_queue_base import SessionQueueBase @@ -55,9 +53,7 @@ class InvocationServices: image_records: "ImageRecordStorageBase", logger: "Logger", model_manager: "ModelManagerServiceBase", - model_records: "ModelRecordServiceBase", download_queue: "DownloadQueueServiceBase", - model_install: "ModelInstallServiceBase", processor: "InvocationProcessorABC", performance_statistics: "InvocationStatsServiceBase", queue: "InvocationQueueABC", @@ -82,9 +78,7 @@ class InvocationServices: self.image_records = image_records self.logger = logger self.model_manager = model_manager - self.model_records = model_records self.download_queue = download_queue - self.model_install = model_install self.processor = processor self.performance_statistics = performance_statistics self.queue = queue diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index 0c63b545ff..6c893021de 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -43,8 +43,10 @@ class InvocationStatsService(InvocationStatsServiceBase): @contextmanager def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str) -> Iterator[None]: + # This is to handle case of the model manager not being initialized, which happens + # during some tests. services = self._invoker.services - if services.model_records is None or services.model_records.loader is None: + if services.model_manager is None or services.model_manager.load is None: yield None if not self._stats.get(graph_execution_state_id): # First time we're seeing this graph_execution_state_id. @@ -60,9 +62,8 @@ class InvocationStatsService(InvocationStatsServiceBase): if torch.cuda.is_available(): torch.cuda.reset_peak_memory_stats() - # TO DO [LS]: clean up loader service - shouldn't be an attribute of model records - assert services.model_records.loader is not None - services.model_records.loader.ram_cache.stats = self._cache_stats[graph_execution_state_id] + assert services.model_manager.load is not None + services.model_manager.load.ram_cache.stats = self._cache_stats[graph_execution_state_id] try: # Let the invocation run. diff --git a/invokeai/app/services/model_load/model_load_base.py b/invokeai/app/services/model_load/model_load_base.py index 7228806e80..f298d98ce6 100644 --- a/invokeai/app/services/model_load/model_load_base.py +++ b/invokeai/app/services/model_load/model_load_base.py @@ -4,7 +4,8 @@ from abc import ABC, abstractmethod from typing import Optional -from invokeai.backend.model_manager import AnyModelConfig, SubModelType +from invokeai.app.invocations.baseinvocation import InvocationContext +from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import LoadedModel @@ -12,11 +13,60 @@ class ModelLoadServiceBase(ABC): """Wrapper around AnyModelLoader.""" @abstractmethod - def load_model_by_key(self, key: str, submodel_type: Optional[SubModelType] = None) -> LoadedModel: - """Given a model's key, load it and return the LoadedModel object.""" + def load_model_by_key( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Given a model's key, load it and return the LoadedModel object. + + :param key: Key of model config to be fetched. + :param submodel: For main (pipeline models), the submodel to fetch. + :param context: Invocation context used for event reporting + """ pass @abstractmethod - def load_model_by_config(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: - """Given a model's configuration, load it and return the LoadedModel object.""" + def load_model_by_config( + self, + model_config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Given a model's configuration, load it and return the LoadedModel object. + + :param model_config: Model configuration record (as returned by ModelRecordBase.get_model()) + :param submodel: For main (pipeline models), the submodel to fetch. + :param context: Invocation context used for event reporting + """ pass + + @abstractmethod + def load_model_by_attr( + self, + model_name: str, + base_model: BaseModelType, + model_type: ModelType, + submodel: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Given a model's attributes, search the database for it, and if found, load and return the LoadedModel object. + + This is provided for API compatability with the get_model() method + in the original model manager. However, note that LoadedModel is + not the same as the original ModelInfo that ws returned. + + :param model_name: Name of to be fetched. + :param base_model: Base model + :param model_type: Type of the model + :param submodel: For main (pipeline models), the submodel to fetch + :param context: The invocation context. + + Exceptions: UnknownModelException -- model with these attributes not known + NotImplementedException -- a model loader was not provided at initialization time + ValueError -- more than one model matches this combination + """ diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index 80e2fe161d..67107cada6 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -3,12 +3,14 @@ from typing import Optional +from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.model_records import ModelRecordServiceBase -from invokeai.backend.model_manager import AnyModelConfig, SubModelType +from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException +from invokeai.app.services.model_records import ModelRecordServiceBase, UnknownModelException +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel, ModelCache, ModelConvertCache from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.load.ram_cache import ModelCacheBase +from invokeai.backend.model_manager.load.model_cache import ModelCacheBase from invokeai.backend.util.logging import InvokeAILogger from .model_load_base import ModelLoadServiceBase @@ -21,7 +23,7 @@ class ModelLoadService(ModelLoadServiceBase): self, app_config: InvokeAIAppConfig, record_store: ModelRecordServiceBase, - ram_cache: Optional[ModelCacheBase] = None, + ram_cache: Optional[ModelCacheBase[AnyModel]] = None, convert_cache: Optional[ModelConvertCacheBase] = None, ): """Initialize the model load service.""" @@ -44,11 +46,104 @@ class ModelLoadService(ModelLoadServiceBase): ), ) - def load_model_by_key(self, key: str, submodel_type: Optional[SubModelType] = None) -> LoadedModel: - """Given a model's key, load it and return the LoadedModel object.""" - config = self._store.get_model(key) - return self.load_model_by_config(config, submodel_type) + def load_model_by_key( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Given a model's key, load it and return the LoadedModel object. - def load_model_by_config(self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: - """Given a model's configuration, load it and return the LoadedModel object.""" - return self._any_loader.load_model(config, submodel_type) + :param key: Key of model config to be fetched. + :param submodel: For main (pipeline models), the submodel to fetch. + :param context: Invocation context used for event reporting + """ + config = self._store.get_model(key) + return self.load_model_by_config(config, submodel_type, context) + + def load_model_by_attr( + self, + model_name: str, + base_model: BaseModelType, + model_type: ModelType, + submodel: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Given a model's attributes, search the database for it, and if found, load and return the LoadedModel object. + + This is provided for API compatability with the get_model() method + in the original model manager. However, note that LoadedModel is + not the same as the original ModelInfo that ws returned. + + :param model_name: Name of to be fetched. + :param base_model: Base model + :param model_type: Type of the model + :param submodel: For main (pipeline models), the submodel to fetch + :param context: The invocation context. + + Exceptions: UnknownModelException -- model with this key not known + NotImplementedException -- a model loader was not provided at initialization time + ValueError -- more than one model matches this combination + """ + configs = self._store.search_by_attr(model_name, base_model, model_type) + if len(configs) == 0: + raise UnknownModelException(f"{base_model}/{model_type}/{model_name}: Unknown model") + elif len(configs) > 1: + raise ValueError(f"{base_model}/{model_type}/{model_name}: More than one model matches.") + else: + return self.load_model_by_key(configs[0].key, submodel) + + def load_model_by_config( + self, + model_config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + context: Optional[InvocationContext] = None, + ) -> LoadedModel: + """ + Given a model's configuration, load it and return the LoadedModel object. + + :param model_config: Model configuration record (as returned by ModelRecordBase.get_model()) + :param submodel: For main (pipeline models), the submodel to fetch. + :param context: Invocation context used for event reporting + """ + if context: + self._emit_load_event( + context=context, + model_config=model_config, + ) + loaded_model = self._any_loader.load_model(model_config, submodel_type) + if context: + self._emit_load_event( + context=context, + model_config=model_config, + loaded=True, + ) + return loaded_model + + def _emit_load_event( + self, + context: InvocationContext, + model_config: AnyModelConfig, + loaded: Optional[bool] = False, + ) -> None: + if context.services.queue.is_canceled(context.graph_execution_state_id): + raise CanceledException() + + if not loaded: + context.services.events.emit_model_load_started( + queue_id=context.queue_id, + queue_item_id=context.queue_item_id, + queue_batch_id=context.queue_batch_id, + graph_execution_state_id=context.graph_execution_state_id, + model_config=model_config, + ) + else: + context.services.events.emit_model_load_completed( + queue_id=context.queue_id, + queue_item_id=context.queue_item_id, + queue_batch_id=context.queue_batch_id, + graph_execution_state_id=context.graph_execution_state_id, + model_config=model_config, + ) diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index c6e77fa163..1116c82ff1 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -2,9 +2,10 @@ from abc import ABC, abstractmethod -from pydantic import BaseModel, Field from typing_extensions import Self +from invokeai.app.services.invoker import Invoker + from ..config import InvokeAIAppConfig from ..download import DownloadQueueServiceBase from ..events.events_base import EventServiceBase @@ -14,12 +15,13 @@ from ..model_records import ModelRecordServiceBase from ..shared.sqlite.sqlite_database import SqliteDatabase -class ModelManagerServiceBase(BaseModel, ABC): +class ModelManagerServiceBase(ABC): """Abstract base class for the model manager service.""" - store: ModelRecordServiceBase = Field(description="An instance of the model record configuration service.") - install: ModelInstallServiceBase = Field(description="An instance of the model install service.") - load: ModelLoadServiceBase = Field(description="An instance of the model load service.") + # attributes: + # store: ModelRecordServiceBase = Field(description="An instance of the model record configuration service.") + # install: ModelInstallServiceBase = Field(description="An instance of the model install service.") + # load: ModelLoadServiceBase = Field(description="An instance of the model load service.") @classmethod @abstractmethod @@ -37,3 +39,29 @@ class ModelManagerServiceBase(BaseModel, ABC): method simplifies the construction considerably. """ pass + + @property + @abstractmethod + def store(self) -> ModelRecordServiceBase: + """Return the ModelRecordServiceBase used to store and retrieve configuration records.""" + pass + + @property + @abstractmethod + def load(self) -> ModelLoadServiceBase: + """Return the ModelLoadServiceBase used to load models from their configuration records.""" + pass + + @property + @abstractmethod + def install(self) -> ModelInstallServiceBase: + """Return the ModelInstallServiceBase used to download and manipulate model files.""" + pass + + @abstractmethod + def start(self, invoker: Invoker) -> None: + pass + + @abstractmethod + def stop(self, invoker: Invoker) -> None: + pass diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index ad0fd66dbb..028d4af615 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -3,6 +3,7 @@ from typing_extensions import Self +from invokeai.app.services.invoker import Invoker from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.util.logging import InvokeAILogger @@ -10,9 +11,9 @@ from invokeai.backend.util.logging import InvokeAILogger from ..config import InvokeAIAppConfig from ..download import DownloadQueueServiceBase from ..events.events_base import EventServiceBase -from ..model_install import ModelInstallService -from ..model_load import ModelLoadService -from ..model_records import ModelRecordServiceSQL +from ..model_install import ModelInstallService, ModelInstallServiceBase +from ..model_load import ModelLoadService, ModelLoadServiceBase +from ..model_records import ModelRecordServiceBase, ModelRecordServiceSQL from ..shared.sqlite.sqlite_database import SqliteDatabase from .model_manager_base import ModelManagerServiceBase @@ -27,6 +28,38 @@ class ModelManagerService(ModelManagerServiceBase): model_manager.load -- Routines to load models into memory. """ + def __init__( + self, + store: ModelRecordServiceBase, + install: ModelInstallServiceBase, + load: ModelLoadServiceBase, + ): + self._store = store + self._install = install + self._load = load + + @property + def store(self) -> ModelRecordServiceBase: + return self._store + + @property + def install(self) -> ModelInstallServiceBase: + return self._install + + @property + def load(self) -> ModelLoadServiceBase: + return self._load + + def start(self, invoker: Invoker) -> None: + for service in [self._store, self._install, self._load]: + if hasattr(service, "start"): + service.start(invoker) + + def stop(self, invoker: Invoker) -> None: + for service in [self._store, self._install, self._load]: + if hasattr(service, "stop"): + service.stop(invoker) + @classmethod def build_model_manager( cls, diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py index e00dd4169d..e2e98c7e89 100644 --- a/invokeai/app/services/model_records/model_records_base.py +++ b/invokeai/app/services/model_records/model_records_base.py @@ -10,15 +10,12 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union from pydantic import BaseModel, Field -from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.app.services.shared.pagination import PaginatedResults from invokeai.backend.model_manager import ( AnyModelConfig, BaseModelType, - LoadedModel, ModelFormat, ModelType, - SubModelType, ) from invokeai.backend.model_manager.load import AnyModelLoader from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore @@ -111,52 +108,6 @@ class ModelRecordServiceBase(ABC): """ pass - @abstractmethod - def load_model( - self, - key: str, - submodel: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, - ) -> LoadedModel: - """ - Load the indicated model into memory and return a LoadedModel object. - - :param key: Key of model config to be fetched. - :param submodel: For main (pipeline models), the submodel to fetch - :param context: Invocation context, used for event issuing. - - Exceptions: UnknownModelException -- model with this key not known - NotImplementedException -- a model loader was not provided at initialization time - """ - pass - - @abstractmethod - def load_model_by_attr( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, - ) -> LoadedModel: - """ - Load the indicated model into memory and return a LoadedModel object. - - This is provided for API compatability with the get_model() method - in the original model manager. However, note that LoadedModel is - not the same as the original ModelInfo that ws returned. - - :param model_name: Key of model config to be fetched. - :param base_model: Base model - :param model_type: Type of the model - :param submodel: For main (pipeline models), the submodel to fetch - :param context: The invocation context. - - Exceptions: UnknownModelException -- model with this key not known - NotImplementedException -- a model loader was not provided at initialization time - """ - pass - @property @abstractmethod def metadata_store(self) -> ModelMetadataStore: diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index 28a77b1b1a..f48175351d 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -46,8 +46,6 @@ from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional, Set, Tuple, Union -from invokeai.app.invocations.baseinvocation import InvocationContext -from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException from invokeai.app.services.shared.pagination import PaginatedResults from invokeai.backend.model_manager.config import ( AnyModelConfig, @@ -55,9 +53,8 @@ from invokeai.backend.model_manager.config import ( ModelConfigFactory, ModelFormat, ModelType, - SubModelType, ) -from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel +from invokeai.backend.model_manager.load import AnyModelLoader from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore, UnknownMetadataException from ..shared.sqlite.sqlite_database import SqliteDatabase @@ -220,74 +217,6 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): model = ModelConfigFactory.make_config(json.loads(rows[0]), timestamp=rows[1]) return model - def load_model( - self, - key: str, - submodel: Optional[SubModelType], - context: Optional[InvocationContext] = None, - ) -> LoadedModel: - """ - Load the indicated model into memory and return a LoadedModel object. - - :param key: Key of model config to be fetched. - :param submodel: For main (pipeline models), the submodel to fetch. - :param context: Invocation context used for event reporting - - Exceptions: UnknownModelException -- model with this key not known - NotImplementedException -- a model loader was not provided at initialization time - """ - if not self._loader: - raise NotImplementedError(f"Class {self.__class__} was not initialized with a model loader") - # we can emit model loading events if we are executing with access to the invocation context - - model_config = self.get_model(key) - if context: - self._emit_load_event( - context=context, - model_config=model_config, - ) - loaded_model = self._loader.load_model(model_config, submodel) - if context: - self._emit_load_event( - context=context, - model_config=model_config, - loaded=True, - ) - return loaded_model - - def load_model_by_attr( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, - ) -> LoadedModel: - """ - Load the indicated model into memory and return a LoadedModel object. - - This is provided for API compatability with the get_model() method - in the original model manager. However, note that LoadedModel is - not the same as the original ModelInfo that ws returned. - - :param model_name: Key of model config to be fetched. - :param base_model: Base model - :param model_type: Type of the model - :param submodel: For main (pipeline models), the submodel to fetch - :param context: The invocation context. - - Exceptions: UnknownModelException -- model with this key not known - NotImplementedException -- a model loader was not provided at initialization time - ValueError -- more than one model matches this combination - """ - configs = self.search_by_attr(model_name, base_model, model_type) - if len(configs) == 0: - raise UnknownModelException(f"{base_model}/{model_type}/{model_name}: Unknown model") - elif len(configs) > 1: - raise ValueError(f"{base_model}/{model_type}/{model_name}: More than one model matches.") - else: - return self.load_model(configs[0].key, submodel) - def exists(self, key: str) -> bool: """ Return True if a model with the indicated key exists in the databse. @@ -476,29 +405,3 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): return PaginatedResults( page=page, pages=ceil(total / per_page), per_page=per_page, total=total, items=items ) - - def _emit_load_event( - self, - context: InvocationContext, - model_config: AnyModelConfig, - loaded: Optional[bool] = False, - ) -> None: - if context.services.queue.is_canceled(context.graph_execution_state_id): - raise CanceledException() - - if not loaded: - context.services.events.emit_model_load_started( - queue_id=context.queue_id, - queue_item_id=context.queue_item_id, - queue_batch_id=context.queue_batch_id, - graph_execution_state_id=context.graph_execution_state_id, - model_config=model_config, - ) - else: - context.services.events.emit_model_load_completed( - queue_id=context.queue_id, - queue_item_id=context.queue_item_id, - queue_batch_id=context.queue_batch_id, - graph_execution_state_id=context.graph_execution_state_id, - model_config=model_config, - ) diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py index b473444511..1f9ac56518 100644 --- a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py +++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py @@ -6,6 +6,7 @@ from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import class Migration6Callback: def __call__(self, cursor: sqlite3.Cursor) -> None: self._recreate_model_triggers(cursor) + self._delete_ip_adapters(cursor) def _recreate_model_triggers(self, cursor: sqlite3.Cursor) -> None: """ @@ -26,6 +27,22 @@ class Migration6Callback: """ ) + def _delete_ip_adapters(self, cursor: sqlite3.Cursor) -> None: + """ + Delete all the IP adapters. + + The model manager will automatically find and re-add them after the migration + is done. This allows the manager to add the correct image encoder to their + configuration records. + """ + + cursor.execute( + """--sql + DELETE FROM model_config + WHERE type='ip_adapter'; + """ + ) + def build_migration_6() -> Migration: """ @@ -33,6 +50,8 @@ def build_migration_6() -> Migration: This migration does the following: - Adds the model_config_updated_at trigger if it does not exist + - Delete all ip_adapter models so that the model prober can find and + update with the correct image processor model. """ migration_6 = Migration( from_version=5, diff --git a/invokeai/backend/embeddings/model_patcher.py b/invokeai/backend/embeddings/model_patcher.py index 4725181b8e..bee8909c31 100644 --- a/invokeai/backend/embeddings/model_patcher.py +++ b/invokeai/backend/embeddings/model_patcher.py @@ -64,7 +64,7 @@ class ModelPatcher: def apply_lora_unet( cls, unet: UNet2DConditionModel, - loras: List[Tuple[LoRAModelRaw, float]], + loras: Iterator[Tuple[LoRAModelRaw, float]], ) -> None: with cls.apply_lora(unet, loras, "lora_unet_"): yield @@ -307,7 +307,7 @@ class ONNXModelPatcher: def apply_lora_unet( cls, unet: OnnxRuntimeModel, - loras: List[Tuple[LoRAModelRaw, float]], + loras: Iterator[Tuple[LoRAModelRaw, float]], ) -> None: with cls.apply_lora(unet, loras, "lora_unet_"): yield diff --git a/invokeai/backend/image_util/safety_checker.py b/invokeai/backend/image_util/safety_checker.py index b9649925e1..92ddef5ecc 100644 --- a/invokeai/backend/image_util/safety_checker.py +++ b/invokeai/backend/image_util/safety_checker.py @@ -8,8 +8,8 @@ from PIL import Image import invokeai.backend.util.logging as logger from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend import SilenceWarnings from invokeai.backend.util.devices import choose_torch_device +from invokeai.backend.util.silence_warnings import SilenceWarnings config = InvokeAIAppConfig.get_config() diff --git a/invokeai/backend/ip_adapter/ip_adapter.py b/invokeai/backend/ip_adapter/ip_adapter.py index 9176bf1f49..b4706ea99c 100644 --- a/invokeai/backend/ip_adapter/ip_adapter.py +++ b/invokeai/backend/ip_adapter/ip_adapter.py @@ -8,7 +8,6 @@ from PIL import Image from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection from invokeai.backend.ip_adapter.ip_attention_weights import IPAttentionWeights -from invokeai.backend.model_management.models.base import calc_model_size_by_data from .resampler import Resampler @@ -124,6 +123,9 @@ class IPAdapter: self.attn_weights.to(device=self.device, dtype=self.dtype) def calc_size(self): + # workaround for circular import + from invokeai.backend.model_manager.load.model_util import calc_model_size_by_data + return calc_model_size_by_data(self._image_proj_model) + calc_model_size_by_data(self.attn_weights) def _init_image_proj_model(self, state_dict): diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 4534a4892f..9f0f774b49 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -21,7 +21,7 @@ Validation errors will raise an InvalidModelConfigException error. """ import time from enum import Enum -from typing import Literal, Optional, Type, Union, Class +from typing import Literal, Optional, Type, Union import torch from diffusers import ModelMixin @@ -335,7 +335,7 @@ class ModelConfigFactory(object): cls, model_data: Union[Dict[str, Any], AnyModelConfig], key: Optional[str] = None, - dest_class: Optional[Type[Class]] = None, + dest_class: Optional[Type[ModelConfigBase]] = None, timestamp: Optional[float] = None, ) -> AnyModelConfig: """ @@ -347,14 +347,17 @@ class ModelConfigFactory(object): :param dest_class: The config class to be returned. If not provided, will be selected automatically. """ + model: Optional[ModelConfigBase] = None if isinstance(model_data, ModelConfigBase): model = model_data elif dest_class: - model = dest_class.validate_python(model_data) + model = dest_class.model_validate(model_data) else: - model = AnyModelConfigValidator.validate_python(model_data) + # mypy doesn't typecheck TypeAdapters well? + model = AnyModelConfigValidator.validate_python(model_data) # type: ignore + assert model is not None if key: model.key = key if timestamp: model.last_modified = timestamp - return model + return model # type: ignore diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 9d98ee3053..3d026af226 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -18,8 +18,16 @@ from pathlib import Path from typing import Any, Callable, Dict, Optional, Tuple, Type from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType -from invokeai.backend.model_manager.config import AnyModel, VaeCheckpointConfig, VaeDiffusersConfig +from invokeai.backend.model_manager.config import ( + AnyModel, + AnyModelConfig, + BaseModelType, + ModelFormat, + ModelType, + SubModelType, + VaeCheckpointConfig, + VaeDiffusersConfig, +) from invokeai.backend.model_manager.load.convert_cache.convert_cache_base import ModelConvertCacheBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase from invokeai.backend.util.logging import InvokeAILogger @@ -32,7 +40,7 @@ class LoadedModel: config: AnyModelConfig locker: ModelLockerBase - def __enter__(self) -> AnyModel: # I think load_file() always returns a dict + def __enter__(self) -> AnyModel: """Context entry.""" self.locker.lock() return self.model @@ -171,6 +179,10 @@ class AnyModelLoader: def decorator(subclass: Type[ModelLoaderBase]) -> Type[ModelLoaderBase]: cls._logger.debug(f"Registering class {subclass.__name__} to load models of type {base}/{type}/{format}") key = cls._to_registry_key(base, type, format) + if key in cls._registry: + raise Exception( + f"{subclass.__name__} is trying to register as a loader for {base}/{type}/{format}, but this type of model has already been registered by {cls._registry[key].__name__}" + ) cls._registry[key] = subclass return subclass diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index c1dfe729af..df83c8320d 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -169,7 +169,7 @@ class ModelLoader(ModelLoaderBase): raise InvalidModelConfigException("An expected config.json file is missing from this model.") from e # This needs to be implemented in subclasses that handle checkpoints - def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Path) -> Path: raise NotImplementedError # This needs to be implemented in the subclass diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py index b1deb215b2..98d6f34cea 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -246,7 +246,7 @@ class ModelCache(ModelCacheBase[AnyModel]): def move_model_to_device(self, cache_entry: CacheRecord[AnyModel], target_device: torch.device) -> None: """Move model into the indicated device.""" - # These attributes are not in the base ModelMixin class but in derived classes. + # These attributes are not in the base ModelMixin class but in various derived classes. # Some models don't have these attributes, in which case they run in RAM/CPU. self.logger.debug(f"Called to move {cache_entry.key} to {target_device}") if not (hasattr(cache_entry.model, "device") and hasattr(cache_entry.model, "to")): diff --git a/invokeai/backend/model_manager/load/model_loaders/controlnet.py b/invokeai/backend/model_manager/load/model_loaders/controlnet.py index e61e2b46a6..d446d07933 100644 --- a/invokeai/backend/model_manager/load/model_loaders/controlnet.py +++ b/invokeai/backend/model_manager/load/model_loaders/controlnet.py @@ -35,28 +35,28 @@ class ControlnetLoader(GenericDiffusersLoader): else: return True - def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Path) -> Path: if config.base not in {BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2}: raise Exception(f"Vae conversion not supported for model type: {config.base}") else: assert hasattr(config, "config") config_file = config.config - if weights_path.suffix == ".safetensors": - checkpoint = safetensors.torch.load_file(weights_path, device="cpu") + if model_path.suffix == ".safetensors": + checkpoint = safetensors.torch.load_file(model_path, device="cpu") else: - checkpoint = torch.load(weights_path, map_location="cpu") + checkpoint = torch.load(model_path, map_location="cpu") # sometimes weights are hidden under "state_dict", and sometimes not if "state_dict" in checkpoint: checkpoint = checkpoint["state_dict"] convert_controlnet_to_diffusers( - weights_path, + model_path, output_path, original_config_file=self._app_config.root_path / config_file, image_size=512, scan_needed=True, - from_safetensors=weights_path.suffix == ".safetensors", + from_safetensors=model_path.suffix == ".safetensors", ) return output_path diff --git a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py index 03c26f3a0c..114e317f3c 100644 --- a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py +++ b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py @@ -12,8 +12,9 @@ from invokeai.backend.model_manager import ( ModelType, SubModelType, ) -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader + +from ..load_base import AnyModelLoader +from ..load_default import ModelLoader @AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) diff --git a/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py index a963e8403b..23b4e1fccd 100644 --- a/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py +++ b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py @@ -65,7 +65,7 @@ class StableDiffusionDiffusersModel(ModelLoader): else: return True - def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Path) -> Path: assert isinstance(config, MainCheckpointConfig) variant = config.variant base = config.base @@ -75,9 +75,9 @@ class StableDiffusionDiffusersModel(ModelLoader): config_file = config.config - self._logger.info(f"Converting {weights_path} to diffusers format") + self._logger.info(f"Converting {model_path} to diffusers format") convert_ckpt_to_diffusers( - weights_path, + model_path, output_path, model_type=self.model_base_to_model_type[base], model_version=base, @@ -86,7 +86,7 @@ class StableDiffusionDiffusersModel(ModelLoader): extract_ema=True, scan_needed=True, pipeline_class=pipeline_class, - from_safetensors=weights_path.suffix == ".safetensors", + from_safetensors=model_path.suffix == ".safetensors", precision=self._torch_dtype, load_safety_checker=False, ) diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py index 882ae05577..3983ea7595 100644 --- a/invokeai/backend/model_manager/load/model_loaders/vae.py +++ b/invokeai/backend/model_manager/load/model_loaders/vae.py @@ -37,7 +37,7 @@ class VaeLoader(GenericDiffusersLoader): else: return True - def _convert_model(self, config: AnyModelConfig, weights_path: Path, output_path: Path) -> Path: + def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Path) -> Path: # TO DO: check whether sdxl VAE models convert. if config.base not in {BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2}: raise Exception(f"Vae conversion not supported for model type: {config.base}") @@ -46,10 +46,10 @@ class VaeLoader(GenericDiffusersLoader): "v1-inference.yaml" if config.base == BaseModelType.StableDiffusion1 else "v2-inference-v.yaml" ) - if weights_path.suffix == ".safetensors": - checkpoint = safetensors.torch.load_file(weights_path, device="cpu") + if model_path.suffix == ".safetensors": + checkpoint = safetensors.torch.load_file(model_path, device="cpu") else: - checkpoint = torch.load(weights_path, map_location="cpu") + checkpoint = torch.load(model_path, map_location="cpu") # sometimes weights are hidden under "state_dict", and sometimes not if "state_dict" in checkpoint: diff --git a/invokeai/backend/model_manager/load/model_util.py b/invokeai/backend/model_manager/load/model_util.py index 3f2d22595e..c55eee48fa 100644 --- a/invokeai/backend/model_manager/load/model_util.py +++ b/invokeai/backend/model_manager/load/model_util.py @@ -65,7 +65,7 @@ def calc_model_size_by_fs(model_path: Path, subfolder: Optional[str] = None, var bit8_files = {f for f in all_files if ".8bit." in f.name or ".8bit-" in f.name} other_files = set(all_files) - fp16_files - bit8_files - if variant is None: + if not variant: # ModelRepoVariant.DEFAULT evaluates to empty string for compatability with HF files = other_files elif variant == "fp16": files = fp16_files diff --git a/invokeai/backend/model_manager/search.py b/invokeai/backend/model_manager/search.py index a54938fdd5..f7e1e1bed7 100644 --- a/invokeai/backend/model_manager/search.py +++ b/invokeai/backend/model_manager/search.py @@ -22,11 +22,12 @@ Example usage: import os from abc import ABC, abstractmethod +from logging import Logger from pathlib import Path from typing import Callable, Optional, Set, Union from pydantic import BaseModel, Field -from logging import Logger + from invokeai.backend.util.logging import InvokeAILogger default_logger: Logger = InvokeAILogger.get_logger() diff --git a/invokeai/backend/stable_diffusion/schedulers/__init__.py b/invokeai/backend/stable_diffusion/schedulers/__init__.py index a4e9dbf9da..0b780d3ee2 100644 --- a/invokeai/backend/stable_diffusion/schedulers/__init__.py +++ b/invokeai/backend/stable_diffusion/schedulers/__init__.py @@ -1 +1,3 @@ from .schedulers import SCHEDULER_MAP # noqa: F401 + +__all__ = ["SCHEDULER_MAP"] diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index 22b132370e..20b630dfc6 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -513,7 +513,7 @@ def select_and_download_models(opt: Namespace) -> None: """Prompt user for install/delete selections and execute.""" precision = "float32" if opt.full_precision else choose_precision(torch.device(choose_torch_device())) # unsure how to avoid a typing complaint in the next line: config.precision is an enumerated Literal - config.precision = precision # type: ignore + config.precision = precision install_helper = InstallHelper(config, logger) installer = install_helper.installer diff --git a/tests/aa_nodes/test_graph_execution_state.py b/tests/aa_nodes/test_graph_execution_state.py index 27d2d2230a..f839a4a878 100644 --- a/tests/aa_nodes/test_graph_execution_state.py +++ b/tests/aa_nodes/test_graph_execution_state.py @@ -62,9 +62,7 @@ def mock_services() -> InvocationServices: invocation_cache=MemoryInvocationCache(max_cache_size=0), logger=logging, # type: ignore model_manager=None, # type: ignore - model_records=None, # type: ignore download_queue=None, # type: ignore - model_install=None, # type: ignore names=None, # type: ignore performance_statistics=InvocationStatsService(), processor=DefaultInvocationProcessor(), diff --git a/tests/aa_nodes/test_invoker.py b/tests/aa_nodes/test_invoker.py index 437ea0f00d..774f7501dc 100644 --- a/tests/aa_nodes/test_invoker.py +++ b/tests/aa_nodes/test_invoker.py @@ -65,9 +65,7 @@ def mock_services() -> InvocationServices: invocation_cache=MemoryInvocationCache(max_cache_size=0), logger=logging, # type: ignore model_manager=None, # type: ignore - model_records=None, # type: ignore download_queue=None, # type: ignore - model_install=None, # type: ignore names=None, # type: ignore performance_statistics=InvocationStatsService(), processor=DefaultInvocationProcessor(), diff --git a/tests/backend/model_manager_2/model_loading/test_model_load.py b/tests/backend/model_manager_2/model_loading/test_model_load.py new file mode 100644 index 0000000000..a7a64e91ac --- /dev/null +++ b/tests/backend/model_manager_2/model_loading/test_model_load.py @@ -0,0 +1,22 @@ +""" +Test model loading +""" + +from pathlib import Path + +from invokeai.app.services.model_install import ModelInstallServiceBase +from invokeai.backend.embeddings.textual_inversion import TextualInversionModelRaw +from invokeai.backend.model_manager.load import AnyModelLoader +from tests.backend.model_manager_2.model_manager_2_fixtures import * # noqa F403 + + +def test_loading(mm2_installer: ModelInstallServiceBase, mm2_loader: AnyModelLoader, embedding_file: Path): + store = mm2_installer.record_store + matches = store.search_by_attr(model_name="test_embedding") + assert len(matches) == 0 + key = mm2_installer.register_path(embedding_file) + loaded_model = mm2_loader.load_model(store.get_model(key)) + assert loaded_model is not None + assert loaded_model.config.key == key + with loaded_model as model: + assert isinstance(model, TextualInversionModelRaw) diff --git a/tests/backend/model_manager_2/model_manager_2_fixtures.py b/tests/backend/model_manager_2/model_manager_2_fixtures.py index d6d091befe..d85eab67dd 100644 --- a/tests/backend/model_manager_2/model_manager_2_fixtures.py +++ b/tests/backend/model_manager_2/model_manager_2_fixtures.py @@ -20,6 +20,7 @@ from invokeai.backend.model_manager.config import ( ModelFormat, ModelType, ) +from invokeai.backend.model_manager.load import AnyModelLoader, ModelCache, ModelConvertCache from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.util.logging import InvokeAILogger from tests.backend.model_manager_2.model_metadata.metadata_examples import ( @@ -89,6 +90,16 @@ def mm2_app_config(mm2_root_dir: Path) -> InvokeAIAppConfig: return app_config +@pytest.fixture +def mm2_loader(mm2_app_config: InvokeAIAppConfig, mm2_record_store: ModelRecordServiceSQL) -> AnyModelLoader: + logger = InvokeAILogger.get_logger(config=mm2_app_config) + ram_cache = ModelCache( + logger=logger, max_cache_size=mm2_app_config.ram_cache_size, max_vram_cache_size=mm2_app_config.vram_cache_size + ) + convert_cache = ModelConvertCache(mm2_app_config.models_convert_cache_path) + return AnyModelLoader(app_config=mm2_app_config, logger=logger, ram_cache=ram_cache, convert_cache=convert_cache) + + @pytest.fixture def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceSQL: logger = InvokeAILogger.get_logger(config=mm2_app_config) From 4027e845d47437e371446f2f520cc8895fdeab25 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 11 Feb 2024 23:37:49 -0500 Subject: [PATCH 121/411] add back the `heuristic_import()` method and extend repo_ids to arbitrary file paths --- docs/contributing/MODEL_MANAGER.md | 52 ++++++++++++-- invokeai/app/api/routers/model_manager_v2.py | 70 ++++++++++++++++++- invokeai/app/api_app.py | 1 - .../model_install/model_install_base.py | 39 ++++++++++- .../model_install/model_install_default.py | 43 +++++++++++- .../model_manager/util/select_hf_files.py | 6 ++ 6 files changed, 199 insertions(+), 12 deletions(-) diff --git a/docs/contributing/MODEL_MANAGER.md b/docs/contributing/MODEL_MANAGER.md index 39220f4ba8..959b7f9733 100644 --- a/docs/contributing/MODEL_MANAGER.md +++ b/docs/contributing/MODEL_MANAGER.md @@ -446,6 +446,44 @@ required parameters: Once initialized, the installer will provide the following methods: +#### install_job = installer.heuristic_import(source, [config], [access_token]) + +This is a simplified interface to the installer which takes a source +string, an optional model configuration dictionary and an optional +access token. + +The `source` is a string that can be any of these forms + +1. A path on the local filesystem (`C:\\users\\fred\\model.safetensors`) +2. A Url pointing to a single downloadable model file (`https://civitai.com/models/58390/detail-tweaker-lora-lora`) +3. A HuggingFace repo_id with any of the following formats: + - `model/name` -- entire model + - `model/name:fp32` -- entire model, using the fp32 variant + - `model/name:fp16:vae` -- vae submodel, using the fp16 variant + - `model/name::vae` -- vae submodel, using default precision + - `model/name:fp16:path/to/model.safetensors` -- an individual model file, fp16 variant + - `model/name::path/to/model.safetensors` -- an individual model file, default variant + +Note that by specifying a relative path to the top of the HuggingFace +repo, you can download and install arbitrary models files. + +The variant, if not provided, will be automatically filled in with +`fp32` if the user has requested full precision, and `fp16` +otherwise. If a variant that does not exist is requested, then the +method will install whatever HuggingFace returns as its default +revision. + +`config` is an optional dict of values that will override the +autoprobed values for model type, base, scheduler prediction type, and +so forth. See [Model configuration and +probing](#Model-configuration-and-probing) for details. + +`access_token` is an optional access token for accessing resources +that need authentication. + +The method will return a `ModelInstallJob`. This object is discussed +at length in the following section. + #### install_job = installer.import_model() The `import_model()` method is the core of the installer. The @@ -464,9 +502,10 @@ source2 = LocalModelSource(path='/opt/models/sushi_diffusers') # a local dif source3 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5') # a repo_id source4 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5', subfolder='vae') # a subfolder within a repo_id source5 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5', variant='fp16') # a named variant of a HF model +source6 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5', subfolder='OrangeMix/OrangeMix1.ckpt') # path to an individual model file -source6 = URLModelSource(url='https://civitai.com/api/download/models/63006') # model located at a URL -source7 = URLModelSource(url='https://civitai.com/api/download/models/63006', access_token='letmein') # with an access token +source7 = URLModelSource(url='https://civitai.com/api/download/models/63006') # model located at a URL +source8 = URLModelSource(url='https://civitai.com/api/download/models/63006', access_token='letmein') # with an access token for source in [source1, source2, source3, source4, source5, source6, source7]: install_job = installer.install_model(source) @@ -522,7 +561,6 @@ can be passed to `import_model()`. attributes returned by the model prober. See the section below for details. - #### LocalModelSource This is used for a model that is located on a locally-accessible Posix @@ -715,7 +753,7 @@ and `cancelled`, as well as `in_terminal_state`. The last will return True if the job is in the complete, errored or cancelled states. -#### Model confguration and probing +#### Model configuration and probing The install service uses the `invokeai.backend.model_manager.probe` module during import to determine the model's type, base type, and @@ -1106,7 +1144,7 @@ job = queue.create_download_job( event_handlers=[my_handler1, my_handler2], # if desired start=True, ) - ``` +``` The `filename` argument forces the downloader to use the specified name for the file rather than the name provided by the remote source, @@ -1427,9 +1465,9 @@ set of keys to the corresponding model config objects. Find all model metadata records that have the given author and return a set of keys to the corresponding model config objects. -# The remainder of this documentation is provisional, pending implementation of the Load service +*** -## Let's get loaded, the lowdown on ModelLoadService +## The Lowdown on the ModelLoadService The `ModelLoadService` is responsible for loading a named model into memory so that it can be used for inference. Despite the fact that it diff --git a/invokeai/app/api/routers/model_manager_v2.py b/invokeai/app/api/routers/model_manager_v2.py index 4fc785e4f7..4482edfa0f 100644 --- a/invokeai/app/api/routers/model_manager_v2.py +++ b/invokeai/app/api/routers/model_manager_v2.py @@ -251,9 +251,75 @@ async def add_model_record( return result +@model_manager_v2_router.post( + "/heuristic_import", + operation_id="heuristic_import_model", + responses={ + 201: {"description": "The model imported successfully"}, + 415: {"description": "Unrecognized file/folder format"}, + 424: {"description": "The model appeared to import successfully, but could not be found in the model manager"}, + 409: {"description": "There is already a model corresponding to this path or repo_id"}, + }, + status_code=201, +) +async def heuristic_import( + source: str, + config: Optional[Dict[str, Any]] = Body( + description="Dict of fields that override auto-probed values in the model config record, such as name, description and prediction_type ", + default=None, + ), + access_token: Optional[str] = None, +) -> ModelInstallJob: + """Install a model using a string identifier. + + `source` can be any of the following. + + 1. A path on the local filesystem ('C:\\users\\fred\\model.safetensors') + 2. A Url pointing to a single downloadable model file + 3. A HuggingFace repo_id with any of the following formats: + - model/name + - model/name:fp16:vae + - model/name::vae -- use default precision + - model/name:fp16:path/to/model.safetensors + - model/name::path/to/model.safetensors + + `config` is an optional dict containing model configuration values that will override + the ones that are probed automatically. + + `access_token` is an optional access token for use with Urls that require + authentication. + + Models will be downloaded, probed, configured and installed in a + series of background threads. The return object has `status` attribute + that can be used to monitor progress. + + See the documentation for `import_model_record` for more information on + interpreting the job information returned by this route. + """ + logger = ApiDependencies.invoker.services.logger + + try: + installer = ApiDependencies.invoker.services.model_manager.install + result: ModelInstallJob = installer.heuristic_import( + source=source, + config=config, + ) + logger.info(f"Started installation of {source}") + except UnknownModelException as e: + logger.error(str(e)) + raise HTTPException(status_code=424, detail=str(e)) + except InvalidModelException as e: + logger.error(str(e)) + raise HTTPException(status_code=415) + except ValueError as e: + logger.error(str(e)) + raise HTTPException(status_code=409, detail=str(e)) + return result + + @model_manager_v2_router.post( "/import", - operation_id="import_model_record", + operation_id="import_model", responses={ 201: {"description": "The model imported successfully"}, 415: {"description": "Unrecognized file/folder format"}, @@ -269,7 +335,7 @@ async def import_model( default=None, ), ) -> ModelInstallJob: - """Add a model using its local path, repo_id, or remote URL. + """Install a model using its local path, repo_id, or remote URL. Models will be downloaded, probed, configured and installed in a series of background threads. The return object has `status` attribute diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 851cbc8160..1831b54c13 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -49,7 +49,6 @@ if True: # hack to make flake8 happy with imports coming after setting up the c download_queue, images, model_manager_v2, - models, session_queue, sessions, utilities, diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py index 635cb154d6..943cdf1157 100644 --- a/invokeai/app/services/model_install/model_install_base.py +++ b/invokeai/app/services/model_install/model_install_base.py @@ -127,8 +127,8 @@ class HFModelSource(StringLikeSource): def __str__(self) -> str: """Return string version of repoid when string rep needed.""" base: str = self.repo_id + base += f":{self.variant or ''}" base += f":{self.subfolder}" if self.subfolder else "" - base += f" ({self.variant})" if self.variant else "" return base @@ -324,6 +324,43 @@ class ModelInstallServiceBase(ABC): :returns id: The string ID of the registered model. """ + @abstractmethod + def heuristic_import( + self, + source: str, + config: Optional[Dict[str, Any]] = None, + access_token: Optional[str] = None, + ) -> ModelInstallJob: + r"""Install the indicated model using heuristics to interpret user intentions. + + :param source: String source + :param config: Optional dict. Any fields in this dict + will override corresponding autoassigned probe fields in the + model's config record as described in `import_model()`. + :param access_token: Optional access token for remote sources. + + The source can be: + 1. A local file path in posix() format (`/foo/bar` or `C:\foo\bar`) + 2. An http or https URL (`https://foo.bar/foo`) + 3. A HuggingFace repo_id (`foo/bar`, `foo/bar:fp16`, `foo/bar:fp16:vae`) + + We extend the HuggingFace repo_id syntax to include the variant and the + subfolder or path. The following are acceptable alternatives: + stabilityai/stable-diffusion-v4 + stabilityai/stable-diffusion-v4:fp16 + stabilityai/stable-diffusion-v4:fp16:vae + stabilityai/stable-diffusion-v4::/checkpoints/sd4.safetensors + stabilityai/stable-diffusion-v4:onnx:vae + + Because a local file path can look like a huggingface repo_id, the logic + first checks whether the path exists on disk, and if not, it is treated as + a parseable huggingface repo. + + The previous support for recursing into a local folder and loading all model-like files + has been removed. + """ + pass + @abstractmethod def import_model( self, diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index d32af4a513..df73fcb8cb 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -50,6 +50,7 @@ from .model_install_base import ( ModelInstallJob, ModelInstallServiceBase, ModelSource, + StringLikeSource, URLModelSource, ) @@ -177,6 +178,34 @@ class ModelInstallService(ModelInstallServiceBase): info, ) + def heuristic_import( + self, + source: str, + config: Optional[Dict[str, Any]] = None, + access_token: Optional[str] = None, + ) -> ModelInstallJob: + variants = "|".join(ModelRepoVariant.__members__.values()) + hf_repoid_re = f"^([^/:]+/[^/:]+)(?::({variants})?(?::/?([^:]+))?)?$" + source_obj: Optional[StringLikeSource] = None + + if Path(source).exists(): # A local file or directory + source_obj = LocalModelSource(path=Path(source)) + elif match := re.match(hf_repoid_re, source): + source_obj = HFModelSource( + repo_id=match.group(1), + variant=match.group(2) if match.group(2) else None, # pass None rather than '' + subfolder=Path(match.group(3)) if match.group(3) else None, + access_token=access_token, + ) + elif re.match(r"^https?://[^/]+", source): + source_obj = URLModelSource( + url=AnyHttpUrl(source), + access_token=access_token, + ) + else: + raise ValueError(f"Unsupported model source: '{source}'") + return self.import_model(source_obj, config) + def import_model(self, source: ModelSource, config: Optional[Dict[str, Any]] = None) -> ModelInstallJob: # noqa D102 similar_jobs = [x for x in self.list_jobs() if x.source == source and not x.in_terminal_state] if similar_jobs: @@ -571,6 +600,8 @@ class ModelInstallService(ModelInstallServiceBase): # TODO: Replace with tempfile.tmpdir() when multithreading is cleaned up. # Currently the tmpdir isn't automatically removed at exit because it is # being held in a daemon thread. + if len(remote_files) == 0: + raise ValueError(f"{source}: No downloadable files found") tmpdir = Path( mkdtemp( dir=self._app_config.models_path, @@ -586,6 +617,16 @@ class ModelInstallService(ModelInstallServiceBase): bytes=0, total_bytes=0, ) + # In the event that there is a subfolder specified in the source, + # we need to remove it from the destination path in order to avoid + # creating unwanted subfolders + if hasattr(source, "subfolder") and source.subfolder: + root = Path(remote_files[0].path.parts[0]) + subfolder = root / source.subfolder + else: + root = Path(".") + subfolder = Path(".") + # we remember the path up to the top of the tmpdir so that it may be # removed safely at the end of the install process. install_job._install_tmpdir = tmpdir @@ -595,7 +636,7 @@ class ModelInstallService(ModelInstallServiceBase): self._logger.debug(f"remote_files={remote_files}") for model_file in remote_files: url = model_file.url - path = model_file.path + path = root / model_file.path.relative_to(subfolder) self._logger.info(f"Downloading {url} => {path}") install_job.total_bytes += model_file.size assert hasattr(source, "access_token") diff --git a/invokeai/backend/model_manager/util/select_hf_files.py b/invokeai/backend/model_manager/util/select_hf_files.py index a894d915de..2fd7a3721a 100644 --- a/invokeai/backend/model_manager/util/select_hf_files.py +++ b/invokeai/backend/model_manager/util/select_hf_files.py @@ -36,6 +36,11 @@ def filter_files( """ variant = variant or ModelRepoVariant.DEFAULT paths: List[Path] = [] + root = files[0].parts[0] + + # if the subfolder is a single file, then bypass the selection and just return it + if subfolder and subfolder.suffix in [".safetensors", ".bin", ".onnx", ".xml", ".pth", ".pt", ".ckpt", ".msgpack"]: + return [root / subfolder] # Start by filtering on model file extensions, discarding images, docs, etc for file in files: @@ -61,6 +66,7 @@ def filter_files( # limit search to subfolder if requested if subfolder: + subfolder = root / subfolder paths = [x for x in paths if x.parent == Path(subfolder)] # _filter_by_variant uniquifies the paths and returns a set From a2cc4047f9f026d48218145bd3ff925a7936949c Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 12 Feb 2024 14:27:17 -0500 Subject: [PATCH 122/411] add a JIT download_and_cache() call to the model installer --- docs/contributing/MODEL_MANAGER.md | 40 +++++++++++++++ .../app/services/download/download_base.py | 13 +++++ .../app/services/download/download_default.py | 15 +++++- .../model_install/model_install_base.py | 34 ++++++++++++- .../model_install/model_install_default.py | 49 ++++++++++++++++++- .../convert_cache/convert_cache_default.py | 8 ++- 6 files changed, 154 insertions(+), 5 deletions(-) diff --git a/docs/contributing/MODEL_MANAGER.md b/docs/contributing/MODEL_MANAGER.md index 959b7f9733..b711c654de 100644 --- a/docs/contributing/MODEL_MANAGER.md +++ b/docs/contributing/MODEL_MANAGER.md @@ -792,6 +792,14 @@ returns a list of completed jobs. The optional `timeout` argument will return from the call if jobs aren't completed in the specified time. An argument of 0 (the default) will block indefinitely. +#### jobs = installer.wait_for_job(job, [timeout]) + +Like `wait_for_installs()`, but block until a specific job has +completed or errored, and then return the job. The optional `timeout` +argument will return from the call if the job doesn't complete in the +specified time. An argument of 0 (the default) will block +indefinitely. + #### jobs = installer.list_jobs() Return a list of all active and complete `ModelInstallJobs`. @@ -854,6 +862,31 @@ This method is similar to `unregister()`, but also unconditionally deletes the corresponding model weights file(s), regardless of whether they are inside or outside the InvokeAI models hierarchy. + +#### path = installer.download_and_cache(remote_source, [access_token], [timeout]) + +This utility routine will download the model file located at source, +cache it, and return the path to the cached file. It does not attempt +to determine the model type, probe its configuration values, or +register it with the models database. + +You may provide an access token if the remote source requires +authorization. The call will block indefinitely until the file is +completely downloaded, cancelled or raises an error of some sort. If +you provide a timeout (in seconds), the call will raise a +`TimeoutError` exception if the download hasn't completed in the +specified period. + +You may use this mechanism to request any type of file, not just a +model. The file will be stored in a subdirectory of +`INVOKEAI_ROOT/models/.cache`. If the requested file is found in the +cache, its path will be returned without redownloading it. + +Be aware that the models cache is cleared of infrequently-used files +and directories at regular intervals when the size of the cache +exceeds the value specified in Invoke's `convert_cache` configuration +variable. + #### List[str]=installer.scan_directory(scan_dir: Path, install: bool) This method will recursively scan the directory indicated in @@ -1187,6 +1220,13 @@ queue or was not created by this queue. This method will block until all the active jobs in the queue have reached a terminal state (completed, errored or cancelled). +#### queue.wait_for_job(job, [timeout]) + +This method will block until the indicated job has reached a terminal +state (completed, errored or cancelled). If the optional timeout is +provided, the call will block for at most timeout seconds, and raise a +TimeoutError otherwise. + #### jobs = queue.list_jobs() This will return a list of all jobs, including ones that have not yet diff --git a/invokeai/app/services/download/download_base.py b/invokeai/app/services/download/download_base.py index f854f64f58..2ac13b825f 100644 --- a/invokeai/app/services/download/download_base.py +++ b/invokeai/app/services/download/download_base.py @@ -260,3 +260,16 @@ class DownloadQueueServiceBase(ABC): def join(self) -> None: """Wait until all jobs are off the queue.""" pass + + @abstractmethod + def wait_for_job(self, job: DownloadJob, timeout: int = 0) -> DownloadJob: + """Wait until the indicated download job has reached a terminal state. + + This will block until the indicated install job has completed, + been cancelled, or errored out. + + :param job: The job to wait on. + :param timeout: Wait up to indicated number of seconds. Raise a TimeoutError if + the job hasn't completed within the indicated time. + """ + pass diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py index 7613c0893f..f740c50087 100644 --- a/invokeai/app/services/download/download_default.py +++ b/invokeai/app/services/download/download_default.py @@ -4,6 +4,7 @@ import os import re import threading +import time import traceback from pathlib import Path from queue import Empty, PriorityQueue @@ -52,6 +53,7 @@ class DownloadQueueService(DownloadQueueServiceBase): self._next_job_id = 0 self._queue = PriorityQueue() self._stop_event = threading.Event() + self._job_completed_event = threading.Event() self._worker_pool = set() self._lock = threading.Lock() self._logger = InvokeAILogger.get_logger("DownloadQueueService") @@ -188,6 +190,16 @@ class DownloadQueueService(DownloadQueueServiceBase): if not job.in_terminal_state: self.cancel_job(job) + def wait_for_job(self, job: DownloadJob, timeout: int = 0) -> DownloadJob: + """Block until the indicated job has reached terminal state, or when timeout limit reached.""" + start = time.time() + while not job.in_terminal_state: + if self._job_completed_event.wait(timeout=5): # in case we miss an event + self._job_completed_event.clear() + if timeout > 0 and time.time() - start > timeout: + raise TimeoutError("Timeout exceeded") + return job + def _start_workers(self, max_workers: int) -> None: """Start the requested number of worker threads.""" self._stop_event.clear() @@ -223,6 +235,7 @@ class DownloadQueueService(DownloadQueueServiceBase): finally: job.job_ended = get_iso_timestamp() + self._job_completed_event.set() # signal a change to terminal state self._queue.task_done() self._logger.debug(f"Download queue worker thread {threading.current_thread().name} exiting.") @@ -407,7 +420,7 @@ class DownloadQueueService(DownloadQueueServiceBase): # Example on_progress event handler to display a TQDM status bar # Activate with: -# download_service.download('http://foo.bar/baz', '/tmp', on_progress=TqdmProgress().job_update +# download_service.download(DownloadJob('http://foo.bar/baz', '/tmp', on_progress=TqdmProgress().update)) class TqdmProgress(object): """TQDM-based progress bar object to use in on_progress handlers.""" diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py index 943cdf1157..39ea8c4a0d 100644 --- a/invokeai/app/services/model_install/model_install_base.py +++ b/invokeai/app/services/model_install/model_install_base.py @@ -422,6 +422,18 @@ class ModelInstallServiceBase(ABC): def cancel_job(self, job: ModelInstallJob) -> None: """Cancel the indicated job.""" + @abstractmethod + def wait_for_job(self, job: ModelInstallJob, timeout: int = 0) -> ModelInstallJob: + """Wait for the indicated job to reach a terminal state. + + This will block until the indicated install job has completed, + been cancelled, or errored out. + + :param job: The job to wait on. + :param timeout: Wait up to indicated number of seconds. Raise a TimeoutError if + the job hasn't completed within the indicated time. + """ + @abstractmethod def wait_for_installs(self, timeout: int = 0) -> List[ModelInstallJob]: """ @@ -431,7 +443,8 @@ class ModelInstallServiceBase(ABC): completed, been cancelled, or errored out. :param timeout: Wait up to indicated number of seconds. Raise an Exception('timeout') if - installs do not complete within the indicated time. + installs do not complete within the indicated time. A timeout of zero (the default) + will block indefinitely until the installs complete. """ @abstractmethod @@ -447,3 +460,22 @@ class ModelInstallServiceBase(ABC): @abstractmethod def sync_to_config(self) -> None: """Synchronize models on disk to those in the model record database.""" + + @abstractmethod + def download_and_cache(self, source: Union[str, AnyHttpUrl], access_token: Optional[str] = None) -> Path: + """ + Download the model file located at source to the models cache and return its Path. + + :param source: A Url or a string that can be converted into one. + :param access_token: Optional access token to access restricted resources. + + The model file will be downloaded into the system-wide model cache + (`models/.cache`) if it isn't already there. Note that the model cache + is periodically cleared of infrequently-used entries when the model + converter runs. + + Note that this doesn't automaticallly install or register the model, but is + intended for use by nodes that need access to models that aren't directly + supported by InvokeAI. The downloading process takes advantage of the download queue + to avoid interrupting other operations. + """ diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index df73fcb8cb..414e300715 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -17,7 +17,7 @@ from pydantic.networks import AnyHttpUrl from requests import Session from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.download import DownloadJob, DownloadQueueServiceBase +from invokeai.app.services.download import DownloadJob, DownloadQueueServiceBase, TqdmProgress from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker from invokeai.app.services.model_records import DuplicateModelException, ModelRecordServiceBase, ModelRecordServiceSQL @@ -87,6 +87,7 @@ class ModelInstallService(ModelInstallServiceBase): self._lock = threading.Lock() self._stop_event = threading.Event() self._downloads_changed_event = threading.Event() + self._install_completed_event = threading.Event() self._download_queue = download_queue self._download_cache: Dict[AnyHttpUrl, ModelInstallJob] = {} self._running = False @@ -241,6 +242,17 @@ class ModelInstallService(ModelInstallServiceBase): assert isinstance(jobs[0], ModelInstallJob) return jobs[0] + def wait_for_job(self, job: ModelInstallJob, timeout: int = 0) -> ModelInstallJob: + """Block until the indicated job has reached terminal state, or when timeout limit reached.""" + start = time.time() + while not job.in_terminal_state: + if self._install_completed_event.wait(timeout=5): # in case we miss an event + self._install_completed_event.clear() + if timeout > 0 and time.time() - start > timeout: + raise TimeoutError("Timeout exceeded") + return job + + # TODO: Better name? Maybe wait_for_jobs()? Maybe too easily confused with above def wait_for_installs(self, timeout: int = 0) -> List[ModelInstallJob]: # noqa D102 """Block until all installation jobs are done.""" start = time.time() @@ -248,7 +260,7 @@ class ModelInstallService(ModelInstallServiceBase): if self._downloads_changed_event.wait(timeout=5): # in case we miss an event self._downloads_changed_event.clear() if timeout > 0 and time.time() - start > timeout: - raise Exception("Timeout exceeded") + raise TimeoutError("Timeout exceeded") self._install_queue.join() return self._install_jobs @@ -302,6 +314,38 @@ class ModelInstallService(ModelInstallServiceBase): path.unlink() self.unregister(key) + def download_and_cache( + self, + source: Union[str, AnyHttpUrl], + access_token: Optional[str] = None, + timeout: int = 0, + ) -> Path: + """Download the model file located at source to the models cache and return its Path.""" + model_hash = sha256(str(source).encode("utf-8")).hexdigest()[0:32] + model_path = self._app_config.models_convert_cache_path / model_hash + + # We expect the cache directory to contain one and only one downloaded file. + # We don't know the file's name in advance, as it is set by the download + # content-disposition header. + if model_path.exists(): + contents = [x for x in model_path.iterdir() if x.is_file()] + if len(contents) > 0: + return contents[0] + + model_path.mkdir(parents=True, exist_ok=True) + job = self._download_queue.download( + source=AnyHttpUrl(str(source)), + dest=model_path, + access_token=access_token, + on_progress=TqdmProgress().update, + ) + self._download_queue.wait_for_job(job, timeout) + if job.complete: + assert job.download_path is not None + return job.download_path + else: + raise Exception(job.error) + # -------------------------------------------------------------------------------------------- # Internal functions that manage the installer threads # -------------------------------------------------------------------------------------------- @@ -365,6 +409,7 @@ class ModelInstallService(ModelInstallServiceBase): # if this is an install of a remote file, then clean up the temporary directory if job._install_tmpdir is not None: rmtree(job._install_tmpdir) + self._install_completed_event.set() self._install_queue.task_done() self._logger.info("Install thread exiting") diff --git a/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py b/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py index 4c361258d9..84f4f76299 100644 --- a/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py +++ b/invokeai/backend/model_manager/load/convert_cache/convert_cache_default.py @@ -53,7 +53,13 @@ class ModelConvertCache(ModelConvertCacheBase): sentinel = path / config if sentinel.exists(): return sentinel.stat().st_atime - return 0.0 + + # no sentinel file found! - pick the most recent file in the directory + try: + atimes = sorted([x.stat().st_atime for x in path.iterdir() if x.is_file()], reverse=True) + return atimes[0] + except IndexError: + return 0.0 # sort by last access time - least accessed files will be at the end lru_models = sorted(self._cache_path.iterdir(), key=by_atime, reverse=True) From ff6e94f828ab2f5549de93b4b1b4b379417e2020 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 12 Feb 2024 21:25:42 -0500 Subject: [PATCH 123/411] add route for model conversion from safetensors to diffusers - Begin to add SwaggerUI documentation for AnyModelConfig and other discriminated Unions. --- invokeai/app/api/routers/model_manager_v2.py | 80 ++++++++++++++++++- .../model_install/model_install_default.py | 6 +- .../services/model_load/model_load_base.py | 14 +++- .../services/model_load/model_load_default.py | 12 ++- .../model_records/model_records_base.py | 7 -- .../model_records/model_records_sql.py | 10 +-- .../backend/model_manager/load/load_base.py | 5 ++ 7 files changed, 113 insertions(+), 21 deletions(-) diff --git a/invokeai/app/api/routers/model_manager_v2.py b/invokeai/app/api/routers/model_manager_v2.py index 4482edfa0f..8d31c6f286 100644 --- a/invokeai/app/api/routers/model_manager_v2.py +++ b/invokeai/app/api/routers/model_manager_v2.py @@ -2,6 +2,7 @@ """FastAPI route for model configuration records.""" import pathlib +import shutil from hashlib import sha1 from random import randbytes from typing import Any, Dict, List, Optional, Set @@ -24,8 +25,10 @@ from invokeai.app.services.shared.pagination import PaginatedResults from invokeai.backend.model_manager.config import ( AnyModelConfig, BaseModelType, + MainCheckpointConfig, ModelFormat, ModelType, + SubModelType, ) from invokeai.backend.model_manager.merge import MergeInterpolationMethod, ModelMerger from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata @@ -318,7 +321,7 @@ async def heuristic_import( @model_manager_v2_router.post( - "/import", + "/install", operation_id="import_model", responses={ 201: {"description": "The model imported successfully"}, @@ -490,6 +493,81 @@ async def sync_models_to_config() -> Response: return Response(status_code=204) +@model_manager_v2_router.put( + "/convert/{key}", + operation_id="convert_model", + responses={ + 200: {"description": "Model converted successfully"}, + 400: {"description": "Bad request"}, + 404: {"description": "Model not found"}, + 409: {"description": "There is already a model registered at this location"}, + }, +) +async def convert_model( + key: str = Path(description="Unique key of the safetensors main model to convert to diffusers format."), +) -> AnyModelConfig: + """ + Permanently convert a model into diffusers format, replacing the safetensors version. + Note that the key and model hash will change. Use the model configuration record returned + by this call to get the new values. + """ + logger = ApiDependencies.invoker.services.logger + loader = ApiDependencies.invoker.services.model_manager.load + store = ApiDependencies.invoker.services.model_manager.store + installer = ApiDependencies.invoker.services.model_manager.install + + try: + model_config = store.get_model(key) + except UnknownModelException as e: + logger.error(str(e)) + raise HTTPException(status_code=424, detail=str(e)) + + if not isinstance(model_config, MainCheckpointConfig): + logger.error(f"The model with key {key} is not a main checkpoint model.") + raise HTTPException(400, f"The model with key {key} is not a main checkpoint model.") + + # loading the model will convert it into a cached diffusers file + loader.load_model_by_config(model_config, submodel_type=SubModelType.Scheduler) + + # Get the path of the converted model from the loader + cache_path = loader.convert_cache.cache_path(key) + assert cache_path.exists() + + # temporarily rename the original safetensors file so that there is no naming conflict + original_name = model_config.name + model_config.name = f"{original_name}.DELETE" + store.update_model(key, config=model_config) + + # install the diffusers + try: + new_key = installer.install_path( + cache_path, + config={ + "name": original_name, + "description": model_config.description, + "original_hash": model_config.original_hash, + "source": model_config.source, + }, + ) + except DuplicateModelException as e: + logger.error(str(e)) + raise HTTPException(status_code=409, detail=str(e)) + + # get the original metadata + if orig_metadata := store.get_metadata(key): + store.metadata_store.add_metadata(new_key, orig_metadata) + + # delete the original safetensors file + installer.delete(key) + + # delete the cached version + shutil.rmtree(cache_path) + + # return the config record for the new diffusers directory + new_config: AnyModelConfig = store.get_model(new_key) + return new_config + + @model_manager_v2_router.put( "/merge", operation_id="merge", diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 414e300715..20a85a82a1 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -162,8 +162,10 @@ class ModelInstallService(ModelInstallServiceBase): config["source"] = model_path.resolve().as_posix() info: AnyModelConfig = self._probe_model(Path(model_path), config) - old_hash = info.original_hash - dest_path = self.app_config.models_path / info.base.value / info.type.value / model_path.name + old_hash = info.current_hash + dest_path = ( + self.app_config.models_path / info.base.value / info.type.value / (config.get("name") or model_path.name) + ) try: new_path = self._copy_model(model_path, dest_path) except FileExistsError as excp: diff --git a/invokeai/app/services/model_load/model_load_base.py b/invokeai/app/services/model_load/model_load_base.py index f298d98ce6..45eaf4652f 100644 --- a/invokeai/app/services/model_load/model_load_base.py +++ b/invokeai/app/services/model_load/model_load_base.py @@ -5,8 +5,10 @@ from abc import ABC, abstractmethod from typing import Optional from invokeai.app.invocations.baseinvocation import InvocationContext -from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, ModelType, SubModelType +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import LoadedModel +from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase class ModelLoadServiceBase(ABC): @@ -70,3 +72,13 @@ class ModelLoadServiceBase(ABC): NotImplementedException -- a model loader was not provided at initialization time ValueError -- more than one model matches this combination """ + + @property + @abstractmethod + def ram_cache(self) -> ModelCacheBase[AnyModel]: + """Return the RAM cache used by this loader.""" + + @property + @abstractmethod + def convert_cache(self) -> ModelConvertCacheBase: + """Return the checkpoint convert cache used by this loader.""" diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index 67107cada6..a6ccd5afbc 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -10,7 +10,7 @@ from invokeai.app.services.model_records import ModelRecordServiceBase, UnknownM from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel, ModelCache, ModelConvertCache from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.load.model_cache import ModelCacheBase +from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase from invokeai.backend.util.logging import InvokeAILogger from .model_load_base import ModelLoadServiceBase @@ -46,6 +46,16 @@ class ModelLoadService(ModelLoadServiceBase): ), ) + @property + def ram_cache(self) -> ModelCacheBase[AnyModel]: + """Return the RAM cache used by this loader.""" + return self._any_loader.ram_cache + + @property + def convert_cache(self) -> ModelConvertCacheBase: + """Return the checkpoint convert cache used by this loader.""" + return self._any_loader.convert_cache + def load_model_by_key( self, key: str, diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py index e2e98c7e89..b2eacc524b 100644 --- a/invokeai/app/services/model_records/model_records_base.py +++ b/invokeai/app/services/model_records/model_records_base.py @@ -17,7 +17,6 @@ from invokeai.backend.model_manager import ( ModelFormat, ModelType, ) -from invokeai.backend.model_manager.load import AnyModelLoader from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore @@ -195,12 +194,6 @@ class ModelRecordServiceBase(ABC): """ pass - @property - @abstractmethod - def loader(self) -> Optional[AnyModelLoader]: - """Return the model loader used by this instance.""" - pass - def all_models(self) -> List[AnyModelConfig]: """Return all the model configs in the database.""" return self.search_by_attr() diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index f48175351d..84a1412383 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -54,7 +54,6 @@ from invokeai.backend.model_manager.config import ( ModelFormat, ModelType, ) -from invokeai.backend.model_manager.load import AnyModelLoader from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore, UnknownMetadataException from ..shared.sqlite.sqlite_database import SqliteDatabase @@ -70,28 +69,21 @@ from .model_records_base import ( class ModelRecordServiceSQL(ModelRecordServiceBase): """Implementation of the ModelConfigStore ABC using a SQL database.""" - def __init__(self, db: SqliteDatabase, loader: Optional[AnyModelLoader] = None): + def __init__(self, db: SqliteDatabase): """ Initialize a new object from preexisting sqlite3 connection and threading lock objects. :param db: Sqlite connection object - :param loader: Initialized model loader object (optional) """ super().__init__() self._db = db self._cursor = db.conn.cursor() - self._loader = loader @property def db(self) -> SqliteDatabase: """Return the underlying database.""" return self._db - @property - def loader(self) -> Optional[AnyModelLoader]: - """Return the model loader used by this instance.""" - return self._loader - def add_model(self, key: str, config: Union[Dict[str, Any], AnyModelConfig]) -> AnyModelConfig: """ Add a model to the database. diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 3d026af226..5f392ada75 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -117,6 +117,11 @@ class AnyModelLoader: """Return the RAM cache associated used by the loaders.""" return self._ram_cache + @property + def convert_cache(self) -> ModelConvertCacheBase: + """Return the convert cache associated used by the loaders.""" + return self._convert_cache + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Return a model given its configuration. From 3e330d7d9dce1b1f4cb1b3ede864bf143d207680 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Tue, 13 Feb 2024 00:26:49 -0500 Subject: [PATCH 124/411] fix a number of typechecking errors --- invokeai/app/api/routers/download_queue.py | 6 +- invokeai/app/api/routers/model_manager_v2.py | 69 ++++++++++++++++--- invokeai/app/invocations/ip_adapter.py | 4 +- invokeai/app/invocations/model.py | 8 +-- invokeai/app/services/config/config_base.py | 11 +-- invokeai/app/services/config/config_common.py | 2 +- .../app/services/download/download_default.py | 12 ++-- invokeai/app/util/misc.py | 8 +-- .../backend/model_manager/load/load_base.py | 13 ++-- .../load/model_cache/model_cache_default.py | 4 +- .../metadata/fetch/fetch_base.py | 4 +- invokeai/backend/model_manager/probe.py | 2 +- invokeai/backend/model_manager/search.py | 6 +- 13 files changed, 101 insertions(+), 48 deletions(-) diff --git a/invokeai/app/api/routers/download_queue.py b/invokeai/app/api/routers/download_queue.py index 2dba376c18..a6e53c7a5c 100644 --- a/invokeai/app/api/routers/download_queue.py +++ b/invokeai/app/api/routers/download_queue.py @@ -36,7 +36,7 @@ async def list_downloads() -> List[DownloadJob]: 400: {"description": "Bad request"}, }, ) -async def prune_downloads(): +async def prune_downloads() -> Response: """Prune completed and errored jobs.""" queue = ApiDependencies.invoker.services.download_queue queue.prune_jobs() @@ -87,7 +87,7 @@ async def get_download_job( ) async def cancel_download_job( id: int = Path(description="ID of the download job to cancel."), -): +) -> Response: """Cancel a download job using its ID.""" try: queue = ApiDependencies.invoker.services.download_queue @@ -105,7 +105,7 @@ async def cancel_download_job( 204: {"description": "Download jobs have been cancelled"}, }, ) -async def cancel_all_download_jobs(): +async def cancel_all_download_jobs() -> Response: """Cancel all download jobs.""" ApiDependencies.invoker.services.download_queue.cancel_all_jobs() return Response(status_code=204) diff --git a/invokeai/app/api/routers/model_manager_v2.py b/invokeai/app/api/routers/model_manager_v2.py index 8d31c6f286..029c620707 100644 --- a/invokeai/app/api/routers/model_manager_v2.py +++ b/invokeai/app/api/routers/model_manager_v2.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Set from fastapi import Body, Path, Query, Response from fastapi.routing import APIRouter -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from starlette.exceptions import HTTPException from typing_extensions import Annotated @@ -37,6 +37,35 @@ from ..dependencies import ApiDependencies model_manager_v2_router = APIRouter(prefix="/v2/models", tags=["model_manager_v2"]) +example_model_output = { + "path": "sd-1/main/openjourney", + "name": "openjourney", + "base": "sd-1", + "type": "main", + "format": "diffusers", + "key": "3a0e45ff858926fd4a63da630688b1e1", + "original_hash": "1c12f18fb6e403baef26fb9d720fbd2f", + "current_hash": "1c12f18fb6e403baef26fb9d720fbd2f", + "description": "sd-1 main model openjourney", + "source": "/opt/invokeai/models/sd-1/main/openjourney", + "last_modified": 1707794711, + "vae": "/opt/invokeai/models/sd-1/vae/vae-ft-mse-840000-ema-pruned_fp16.safetensors", + "variant": "normal", + "prediction_type": "epsilon", + "repo_variant": "fp16", +} + +example_model_input = { + "path": "base/type/name", + "name": "model_name", + "base": "sd-1", + "type": "main", + "format": "diffusers", + "description": "Model description", + "vae": None, + "variant": "normal", +} + class ModelsList(BaseModel): """Return list of configs.""" @@ -88,7 +117,10 @@ async def list_model_records( "/i/{key}", operation_id="get_model_record", responses={ - 200: {"description": "Success"}, + 200: { + "description": "The model configuration was retrieved successfully", + "content": {"application/json": {"example": example_model_output}}, + }, 400: {"description": "Bad request"}, 404: {"description": "The model could not be found"}, }, @@ -165,18 +197,22 @@ async def search_by_metadata_tags( "/i/{key}", operation_id="update_model_record", responses={ - 200: {"description": "The model was updated successfully"}, + 200: { + "description": "The model was updated successfully", + "content": {"application/json": {"example": example_model_output}}, + }, 400: {"description": "Bad request"}, 404: {"description": "The model could not be found"}, 409: {"description": "There is already a model corresponding to the new name"}, }, status_code=200, - response_model=AnyModelConfig, ) async def update_model_record( key: Annotated[str, Path(description="Unique key of model")], - info: Annotated[AnyModelConfig, Body(description="Model config", discriminator="type")], -) -> AnyModelConfig: + info: Annotated[ + AnyModelConfig, Body(description="Model config", discriminator="type", example=example_model_input) + ], +) -> Annotated[AnyModelConfig, Field(example="this is neat")]: """Update model contents with a new config. If the model name or base fields are changed, then the model is renamed.""" logger = ApiDependencies.invoker.services.logger record_store = ApiDependencies.invoker.services.model_manager.store @@ -225,7 +261,10 @@ async def del_model_record( "/i/", operation_id="add_model_record", responses={ - 201: {"description": "The model added successfully"}, + 201: { + "description": "The model added successfully", + "content": {"application/json": {"example": example_model_output}}, + }, 409: {"description": "There is already a model corresponding to this path or repo_id"}, 415: {"description": "Unrecognized file/folder format"}, }, @@ -270,6 +309,7 @@ async def heuristic_import( config: Optional[Dict[str, Any]] = Body( description="Dict of fields that override auto-probed values in the model config record, such as name, description and prediction_type ", default=None, + example={"name": "modelT", "description": "antique cars"}, ), access_token: Optional[str] = None, ) -> ModelInstallJob: @@ -497,7 +537,10 @@ async def sync_models_to_config() -> Response: "/convert/{key}", operation_id="convert_model", responses={ - 200: {"description": "Model converted successfully"}, + 200: { + "description": "Model converted successfully", + "content": {"application/json": {"example": example_model_output}}, + }, 400: {"description": "Bad request"}, 404: {"description": "Model not found"}, 409: {"description": "There is already a model registered at this location"}, @@ -571,6 +614,15 @@ async def convert_model( @model_manager_v2_router.put( "/merge", operation_id="merge", + responses={ + 200: { + "description": "Model converted successfully", + "content": {"application/json": {"example": example_model_output}}, + }, + 400: {"description": "Bad request"}, + 404: {"description": "Model not found"}, + 409: {"description": "There is already a model registered at this location"}, + }, ) async def merge( keys: List[str] = Body(description="Keys for two to three models to merge", min_length=2, max_length=3), @@ -596,7 +648,6 @@ async def merge( interp: Interpolation method. One of "weighted_sum", "sigmoid", "inv_sigmoid" or "add_difference" [weighted_sum] merge_dest_directory: Specify a directory to store the merged model in [models directory] """ - print(f"here i am, keys={keys}") logger = ApiDependencies.invoker.services.logger try: logger.info(f"Merging models: {keys} into {merge_dest_directory or ''}/{merged_model_name}") diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index f64b3266bb..01124f62f3 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -90,10 +90,10 @@ class IPAdapterInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> IPAdapterOutput: # Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model. - ip_adapter_info = context.services.model_records.get_model(self.ip_adapter_model.key) + ip_adapter_info = context.services.model_manager.store.get_model(self.ip_adapter_model.key) image_encoder_model_id = ip_adapter_info.image_encoder_model_id image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip() - image_encoder_models = context.services.model_records.search_by_attr( + image_encoder_models = context.services.model_manager.store.search_by_attr( model_name=image_encoder_model_name, base_model=BaseModelType.Any, model_type=ModelType.CLIPVision ) assert len(image_encoder_models) == 1 diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index fa6e8b98da..f78425c6ee 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -103,7 +103,7 @@ class MainModelLoaderInvocation(BaseInvocation): key = self.model.key # TODO: not found exceptions - if not context.services.model_records.exists(key): + if not context.services.model_manager.store.exists(key): raise Exception(f"Unknown model {key}") return ModelLoaderOutput( @@ -172,7 +172,7 @@ class LoraLoaderInvocation(BaseInvocation): lora_key = self.lora.key - if not context.services.model_records.exists(lora_key): + if not context.services.model_manager.store.exists(lora_key): raise Exception(f"Unkown lora: {lora_key}!") if self.unet is not None and any(lora.key == lora_key for lora in self.unet.loras): @@ -252,7 +252,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): lora_key = self.lora.key - if not context.services.model_records.exists(lora_key): + if not context.services.model_manager.store.exists(lora_key): raise Exception(f"Unknown lora: {lora_key}!") if self.unet is not None and any(lora.key == lora_key for lora in self.unet.loras): @@ -318,7 +318,7 @@ class VaeLoaderInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> VAEOutput: key = self.vae_model.key - if not context.services.model_records.exists(key): + if not context.services.model_manager.store.exists(key): raise Exception(f"Unkown vae: {key}!") return VAEOutput(vae=VaeField(vae=ModelInfo(key=key))) diff --git a/invokeai/app/services/config/config_base.py b/invokeai/app/services/config/config_base.py index a304b38a95..983df6b468 100644 --- a/invokeai/app/services/config/config_base.py +++ b/invokeai/app/services/config/config_base.py @@ -27,11 +27,11 @@ class InvokeAISettings(BaseSettings): """Runtime configuration settings in which default values are read from an omegaconf .yaml file.""" initconf: ClassVar[Optional[DictConfig]] = None - argparse_groups: ClassVar[Dict] = {} + argparse_groups: ClassVar[Dict[str, Any]] = {} model_config = SettingsConfigDict(env_file_encoding="utf-8", arbitrary_types_allowed=True, case_sensitive=True) - def parse_args(self, argv: Optional[list] = sys.argv[1:]): + def parse_args(self, argv: Optional[List[str]] = sys.argv[1:]) -> None: """Call to parse command-line arguments.""" parser = self.get_parser() opt, unknown_opts = parser.parse_known_args(argv) @@ -68,7 +68,7 @@ class InvokeAISettings(BaseSettings): return OmegaConf.to_yaml(conf) @classmethod - def add_parser_arguments(cls, parser): + def add_parser_arguments(cls, parser) -> None: """Dynamically create arguments for a settings parser.""" if "type" in get_type_hints(cls): settings_stanza = get_args(get_type_hints(cls)["type"])[0] @@ -117,7 +117,8 @@ class InvokeAISettings(BaseSettings): """Return the category of a setting.""" hints = get_type_hints(cls) if command_field in hints: - return get_args(hints[command_field])[0] + result: str = get_args(hints[command_field])[0] + return result else: return "Uncategorized" @@ -158,7 +159,7 @@ class InvokeAISettings(BaseSettings): ] @classmethod - def add_field_argument(cls, command_parser, name: str, field, default_override=None): + def add_field_argument(cls, command_parser, name: str, field, default_override=None) -> None: """Add the argparse arguments for a setting parser.""" field_type = get_type_hints(cls).get(name) default = ( diff --git a/invokeai/app/services/config/config_common.py b/invokeai/app/services/config/config_common.py index d11bcabcf9..27a0f859c2 100644 --- a/invokeai/app/services/config/config_common.py +++ b/invokeai/app/services/config/config_common.py @@ -21,7 +21,7 @@ class PagingArgumentParser(argparse.ArgumentParser): It also supports reading defaults from an init file. """ - def print_help(self, file=None): + def print_help(self, file=None) -> None: text = self.format_help() pydoc.pager(text) diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py index f740c50087..7008f8ed74 100644 --- a/invokeai/app/services/download/download_default.py +++ b/invokeai/app/services/download/download_default.py @@ -8,12 +8,12 @@ import time import traceback from pathlib import Path from queue import Empty, PriorityQueue -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set import requests from pydantic.networks import AnyHttpUrl from requests import HTTPError -from tqdm import tqdm +from tqdm import tqdm, std from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.util.misc import get_iso_timestamp @@ -49,12 +49,12 @@ class DownloadQueueService(DownloadQueueServiceBase): :param max_parallel_dl: Number of simultaneous downloads allowed [5]. :param requests_session: Optional requests.sessions.Session object, for unit tests. """ - self._jobs = {} + self._jobs: Dict[int, DownloadJob] = {} self._next_job_id = 0 - self._queue = PriorityQueue() + self._queue: PriorityQueue[DownloadJob] = PriorityQueue() self._stop_event = threading.Event() self._job_completed_event = threading.Event() - self._worker_pool = set() + self._worker_pool: Set[threading.Thread] = set() self._lock = threading.Lock() self._logger = InvokeAILogger.get_logger("DownloadQueueService") self._event_bus = event_bus @@ -424,7 +424,7 @@ class DownloadQueueService(DownloadQueueServiceBase): class TqdmProgress(object): """TQDM-based progress bar object to use in on_progress handlers.""" - _bars: Dict[int, tqdm] # the tqdm object + _bars: Dict[int, tqdm] # type: ignore _last: Dict[int, int] # last bytes downloaded def __init__(self) -> None: # noqa D107 diff --git a/invokeai/app/util/misc.py b/invokeai/app/util/misc.py index 910b05d8dd..da431929db 100644 --- a/invokeai/app/util/misc.py +++ b/invokeai/app/util/misc.py @@ -5,7 +5,7 @@ import uuid import numpy as np -def get_timestamp(): +def get_timestamp() -> int: return int(datetime.datetime.now(datetime.timezone.utc).timestamp()) @@ -20,16 +20,16 @@ def get_datetime_from_iso_timestamp(iso_timestamp: str) -> datetime.datetime: SEED_MAX = np.iinfo(np.uint32).max -def get_random_seed(): +def get_random_seed() -> int: rng = np.random.default_rng(seed=None) return int(rng.integers(0, SEED_MAX)) -def uuid_string(): +def uuid_string() -> str: res = uuid.uuid4() return str(res) -def is_optional(value: typing.Any): +def is_optional(value: typing.Any) -> bool: """Checks if a value is typed as Optional. Note that Optional is sugar for Union[x, None].""" return typing.get_origin(value) is typing.Union and type(None) in typing.get_args(value) diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 5f392ada75..7649dee762 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -22,6 +22,7 @@ from invokeai.backend.model_manager.config import ( AnyModel, AnyModelConfig, BaseModelType, + ModelConfigBase, ModelFormat, ModelType, SubModelType, @@ -70,7 +71,7 @@ class ModelLoaderBase(ABC): pass @abstractmethod - def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + def load_model(self, model_config: ModelConfigBase, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Return a model given its confguration. @@ -122,7 +123,7 @@ class AnyModelLoader: """Return the convert cache associated used by the loaders.""" return self._convert_cache - def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + def load_model(self, model_config: ModelConfigBase, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Return a model given its configuration. @@ -144,8 +145,8 @@ class AnyModelLoader: @classmethod def get_implementation( - cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] - ) -> Tuple[Type[ModelLoaderBase], AnyModelConfig, Optional[SubModelType]]: + cls, config: ModelConfigBase, submodel_type: Optional[SubModelType] + ) -> Tuple[Type[ModelLoaderBase], ModelConfigBase, Optional[SubModelType]]: """Get subclass of ModelLoaderBase registered to handle base and type.""" # We have to handle VAE overrides here because this will change the model type and the corresponding implementation returned conf2, submodel_type = cls._handle_subtype_overrides(config, submodel_type) @@ -161,8 +162,8 @@ class AnyModelLoader: @classmethod def _handle_subtype_overrides( - cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] - ) -> Tuple[AnyModelConfig, Optional[SubModelType]]: + cls, config: ModelConfigBase, submodel_type: Optional[SubModelType] + ) -> Tuple[ModelConfigBase, Optional[SubModelType]]: if submodel_type == SubModelType.Vae and hasattr(config, "vae") and config.vae is not None: model_path = Path(config.vae) config_class = ( diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py index 98d6f34cea..786396062c 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -34,8 +34,8 @@ from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, from invokeai.backend.util.devices import choose_torch_device from invokeai.backend.util.logging import InvokeAILogger -from .model_cache_base import CacheRecord, CacheStats, ModelCacheBase -from .model_locker import ModelLocker, ModelLockerBase +from .model_cache_base import CacheRecord, CacheStats, ModelCacheBase, ModelLockerBase +from .model_locker import ModelLocker if choose_torch_device() == torch.device("mps"): from torch import mps diff --git a/invokeai/backend/model_manager/metadata/fetch/fetch_base.py b/invokeai/backend/model_manager/metadata/fetch/fetch_base.py index d628ab5c17..5d75493b92 100644 --- a/invokeai/backend/model_manager/metadata/fetch/fetch_base.py +++ b/invokeai/backend/model_manager/metadata/fetch/fetch_base.py @@ -20,7 +20,7 @@ from requests.sessions import Session from invokeai.backend.model_manager import ModelRepoVariant -from ..metadata_base import AnyModelRepoMetadata, AnyModelRepoMetadataValidator +from ..metadata_base import AnyModelRepoMetadata, AnyModelRepoMetadataValidator, BaseMetadata class ModelMetadataFetchBase(ABC): @@ -62,5 +62,5 @@ class ModelMetadataFetchBase(ABC): @classmethod def from_json(cls, json: str) -> AnyModelRepoMetadata: """Given the JSON representation of the metadata, return the corresponding Pydantic object.""" - metadata = AnyModelRepoMetadataValidator.validate_json(json) + metadata: BaseMetadata = AnyModelRepoMetadataValidator.validate_json(json) # type: ignore return metadata diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index e7d21c578f..2c2066d7c5 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -166,7 +166,7 @@ class ModelProbe(object): fields["original_hash"] = fields.get("original_hash") or hash fields["current_hash"] = fields.get("current_hash") or hash - if format_type == ModelFormat.Diffusers: + if format_type == ModelFormat.Diffusers and hasattr(probe, "get_repo_variant"): fields["repo_variant"] = fields.get("repo_variant") or probe.get_repo_variant() # additional fields needed for main and controlnet models diff --git a/invokeai/backend/model_manager/search.py b/invokeai/backend/model_manager/search.py index f7e1e1bed7..0ead22b743 100644 --- a/invokeai/backend/model_manager/search.py +++ b/invokeai/backend/model_manager/search.py @@ -116,9 +116,9 @@ class ModelSearch(ModelSearchBase): # returns all models that have 'anime' in the path """ - models_found: Set[Path] = Field(default=None) - scanned_dirs: Set[Path] = Field(default=None) - pruned_paths: Set[Path] = Field(default=None) + models_found: Optional[Set[Path]] = Field(default=None) + scanned_dirs: Optional[Set[Path]] = Field(default=None) + pruned_paths: Optional[Set[Path]] = Field(default=None) def search_started(self) -> None: self.models_found = set() From b0835db47d25b7d563cc462dbcfcae317d623c3f Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 14 Feb 2024 11:10:50 -0500 Subject: [PATCH 125/411] improve swagger documentation --- invokeai/app/api/routers/model_manager_v2.py | 216 ++++++++++++------ .../app/services/download/download_default.py | 2 +- invokeai/backend/model_manager/config.py | 16 +- 3 files changed, 160 insertions(+), 74 deletions(-) diff --git a/invokeai/app/api/routers/model_manager_v2.py b/invokeai/app/api/routers/model_manager_v2.py index 029c620707..2471e0d8c9 100644 --- a/invokeai/app/api/routers/model_manager_v2.py +++ b/invokeai/app/api/routers/model_manager_v2.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Set from fastapi import Body, Path, Query, Response from fastapi.routing import APIRouter -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict from starlette.exceptions import HTTPException from typing_extensions import Annotated @@ -37,35 +37,6 @@ from ..dependencies import ApiDependencies model_manager_v2_router = APIRouter(prefix="/v2/models", tags=["model_manager_v2"]) -example_model_output = { - "path": "sd-1/main/openjourney", - "name": "openjourney", - "base": "sd-1", - "type": "main", - "format": "diffusers", - "key": "3a0e45ff858926fd4a63da630688b1e1", - "original_hash": "1c12f18fb6e403baef26fb9d720fbd2f", - "current_hash": "1c12f18fb6e403baef26fb9d720fbd2f", - "description": "sd-1 main model openjourney", - "source": "/opt/invokeai/models/sd-1/main/openjourney", - "last_modified": 1707794711, - "vae": "/opt/invokeai/models/sd-1/vae/vae-ft-mse-840000-ema-pruned_fp16.safetensors", - "variant": "normal", - "prediction_type": "epsilon", - "repo_variant": "fp16", -} - -example_model_input = { - "path": "base/type/name", - "name": "model_name", - "base": "sd-1", - "type": "main", - "format": "diffusers", - "description": "Model description", - "vae": None, - "variant": "normal", -} - class ModelsList(BaseModel): """Return list of configs.""" @@ -84,6 +55,86 @@ class ModelTagSet(BaseModel): tags: Set[str] +############################################################################## +# These are example inputs and outputs that are used in places where Swagger +# is unable to generate a correct example. +############################################################################## +example_model_config = { + "path": "string", + "name": "string", + "base": "sd-1", + "type": "main", + "format": "checkpoint", + "config": "string", + "key": "string", + "original_hash": "string", + "current_hash": "string", + "description": "string", + "source": "string", + "last_modified": 0, + "vae": "string", + "variant": "normal", + "prediction_type": "epsilon", + "repo_variant": "fp16", + "upcast_attention": False, + "ztsnr_training": False, +} + +example_model_input = { + "path": "/path/to/model", + "name": "model_name", + "base": "sd-1", + "type": "main", + "format": "checkpoint", + "config": "configs/stable-diffusion/v1-inference.yaml", + "description": "Model description", + "vae": None, + "variant": "normal", +} + +example_model_metadata = { + "name": "ip_adapter_sd_image_encoder", + "author": "InvokeAI", + "tags": [ + "transformers", + "safetensors", + "clip_vision_model", + "endpoints_compatible", + "region:us", + "has_space", + "license:apache-2.0", + ], + "files": [ + { + "url": "https://huggingface.co/InvokeAI/ip_adapter_sd_image_encoder/resolve/main/README.md", + "path": "ip_adapter_sd_image_encoder/README.md", + "size": 628, + "sha256": None, + }, + { + "url": "https://huggingface.co/InvokeAI/ip_adapter_sd_image_encoder/resolve/main/config.json", + "path": "ip_adapter_sd_image_encoder/config.json", + "size": 560, + "sha256": None, + }, + { + "url": "https://huggingface.co/InvokeAI/ip_adapter_sd_image_encoder/resolve/main/model.safetensors", + "path": "ip_adapter_sd_image_encoder/model.safetensors", + "size": 2528373448, + "sha256": "6ca9667da1ca9e0b0f75e46bb030f7e011f44f86cbfb8d5a36590fcd7507b030", + }, + ], + "type": "huggingface", + "id": "InvokeAI/ip_adapter_sd_image_encoder", + "tag_dict": {"license": "apache-2.0"}, + "last_modified": "2023-09-23T17:33:25Z", +} + +############################################################################## +# ROUTES +############################################################################## + + @model_manager_v2_router.get( "/", operation_id="list_model_records", @@ -119,7 +170,7 @@ async def list_model_records( responses={ 200: { "description": "The model configuration was retrieved successfully", - "content": {"application/json": {"example": example_model_output}}, + "content": {"application/json": {"example": example_model_config}}, }, 400: {"description": "Bad request"}, 404: {"description": "The model could not be found"}, @@ -137,7 +188,7 @@ async def get_model_record( raise HTTPException(status_code=404, detail=str(e)) -@model_manager_v2_router.get("/meta", operation_id="list_model_summary") +@model_manager_v2_router.get("/summary", operation_id="list_model_summary") async def list_model_summary( page: int = Query(default=0, description="The page to get"), per_page: int = Query(default=10, description="The number of models per page"), @@ -153,7 +204,10 @@ async def list_model_summary( "/meta/i/{key}", operation_id="get_model_metadata", responses={ - 200: {"description": "Success"}, + 200: { + "description": "The model metadata was retrieved successfully", + "content": {"application/json": {"example": example_model_metadata}}, + }, 400: {"description": "Bad request"}, 404: {"description": "No metadata available"}, }, @@ -199,7 +253,7 @@ async def search_by_metadata_tags( responses={ 200: { "description": "The model was updated successfully", - "content": {"application/json": {"example": example_model_output}}, + "content": {"application/json": {"example": example_model_config}}, }, 400: {"description": "Bad request"}, 404: {"description": "The model could not be found"}, @@ -212,7 +266,7 @@ async def update_model_record( info: Annotated[ AnyModelConfig, Body(description="Model config", discriminator="type", example=example_model_input) ], -) -> Annotated[AnyModelConfig, Field(example="this is neat")]: +) -> AnyModelConfig: """Update model contents with a new config. If the model name or base fields are changed, then the model is renamed.""" logger = ApiDependencies.invoker.services.logger record_store = ApiDependencies.invoker.services.model_manager.store @@ -263,7 +317,7 @@ async def del_model_record( responses={ 201: { "description": "The model added successfully", - "content": {"application/json": {"example": example_model_output}}, + "content": {"application/json": {"example": example_model_config}}, }, 409: {"description": "There is already a model corresponding to this path or repo_id"}, 415: {"description": "Unrecognized file/folder format"}, @@ -271,7 +325,9 @@ async def del_model_record( status_code=201, ) async def add_model_record( - config: Annotated[AnyModelConfig, Body(description="Model config", discriminator="type")], + config: Annotated[ + AnyModelConfig, Body(description="Model config", discriminator="type", example=example_model_input) + ], ) -> AnyModelConfig: """Add a model using the configuration information appropriate for its type.""" logger = ApiDependencies.invoker.services.logger @@ -389,32 +445,38 @@ async def import_model( appropriate value: * To install a local path using LocalModelSource, pass a source of form: - `{ + ``` + { "type": "local", "path": "/path/to/model", "inplace": false - }` - The "inplace" flag, if true, will register the model in place in its - current filesystem location. Otherwise, the model will be copied - into the InvokeAI models directory. + } + ``` + The "inplace" flag, if true, will register the model in place in its + current filesystem location. Otherwise, the model will be copied + into the InvokeAI models directory. * To install a HuggingFace repo_id using HFModelSource, pass a source of form: - `{ + ``` + { "type": "hf", "repo_id": "stabilityai/stable-diffusion-2.0", "variant": "fp16", "subfolder": "vae", "access_token": "f5820a918aaf01" - }` - The `variant`, `subfolder` and `access_token` fields are optional. + } + ``` + The `variant`, `subfolder` and `access_token` fields are optional. * To install a remote model using an arbitrary URL, pass: - `{ + ``` + { "type": "url", "url": "http://www.civitai.com/models/123456", "access_token": "f5820a918aaf01" - }` - The `access_token` field is optonal + } + ``` + The `access_token` field is optonal The model's configuration record will be probed and filled in automatically. To override the default guesses, pass "metadata" @@ -423,9 +485,9 @@ async def import_model( Installation occurs in the background. Either use list_model_install_jobs() to poll for completion, or listen on the event bus for the following events: - "model_install_running" - "model_install_completed" - "model_install_error" + * "model_install_running" + * "model_install_completed" + * "model_install_error" On successful completion, the event's payload will contain the field "key" containing the installed ID of the model. On an error, the event's payload @@ -459,7 +521,25 @@ async def import_model( operation_id="list_model_install_jobs", ) async def list_model_install_jobs() -> List[ModelInstallJob]: - """Return list of model install jobs.""" + """Return the list of model install jobs. + + Install jobs have a numeric `id`, a `status`, and other fields that provide information on + the nature of the job and its progress. The `status` is one of: + + * "waiting" -- Job is waiting in the queue to run + * "downloading" -- Model file(s) are downloading + * "running" -- Model has downloaded and the model probing and registration process is running + * "completed" -- Installation completed successfully + * "error" -- An error occurred. Details will be in the "error_type" and "error" fields. + * "cancelled" -- Job was cancelled before completion. + + Once completed, information about the model such as its size, base + model, type, and metadata can be retrieved from the `config_out` + field. For multi-file models such as diffusers, information on individual files + can be retrieved from `download_parts`. + + See the example and schema below for more information. + """ jobs: List[ModelInstallJob] = ApiDependencies.invoker.services.model_manager.install.list_jobs() return jobs @@ -473,7 +553,10 @@ async def list_model_install_jobs() -> List[ModelInstallJob]: }, ) async def get_model_install_job(id: int = Path(description="Model install id")) -> ModelInstallJob: - """Return model install job corresponding to the given source.""" + """ + Return model install job corresponding to the given source. See the documentation for 'List Model Install Jobs' + for information on the format of the return value. + """ try: result: ModelInstallJob = ApiDependencies.invoker.services.model_manager.install.get_job_by_id(id) return result @@ -539,7 +622,7 @@ async def sync_models_to_config() -> Response: responses={ 200: { "description": "Model converted successfully", - "content": {"application/json": {"example": example_model_output}}, + "content": {"application/json": {"example": example_model_config}}, }, 400: {"description": "Bad request"}, 404: {"description": "Model not found"}, @@ -551,8 +634,8 @@ async def convert_model( ) -> AnyModelConfig: """ Permanently convert a model into diffusers format, replacing the safetensors version. - Note that the key and model hash will change. Use the model configuration record returned - by this call to get the new values. + Note that during the conversion process the key and model hash will change. + The return value is the model configuration for the converted model. """ logger = ApiDependencies.invoker.services.logger loader = ApiDependencies.invoker.services.model_manager.load @@ -617,7 +700,7 @@ async def convert_model( responses={ 200: { "description": "Model converted successfully", - "content": {"application/json": {"example": example_model_output}}, + "content": {"application/json": {"example": example_model_config}}, }, 400: {"description": "Bad request"}, 404: {"description": "Model not found"}, @@ -639,14 +722,17 @@ async def merge( ), ) -> AnyModelConfig: """ - Merge diffusers models. - - keys: List of 2-3 model keys to merge together. All models must use the same base type. - merged_model_name: Name for the merged model [Concat model names] - alpha: Alpha value (0.0-1.0). Higher values give more weight to the second model [0.5] - force: If true, force the merge even if the models were generated by different versions of the diffusers library [False] - interp: Interpolation method. One of "weighted_sum", "sigmoid", "inv_sigmoid" or "add_difference" [weighted_sum] - merge_dest_directory: Specify a directory to store the merged model in [models directory] + Merge diffusers models. The process is controlled by a set parameters provided in the body of the request. + ``` + Argument Description [default] + -------- ---------------------- + keys List of 2-3 model keys to merge together. All models must use the same base type. + merged_model_name Name for the merged model [Concat model names] + alpha Alpha value (0.0-1.0). Higher values give more weight to the second model [0.5] + force If true, force the merge even if the models were generated by different versions of the diffusers library [False] + interp Interpolation method. One of "weighted_sum", "sigmoid", "inv_sigmoid" or "add_difference" [weighted_sum] + merge_dest_directory Specify a directory to store the merged model in [models directory] + ``` """ logger = ApiDependencies.invoker.services.logger try: diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py index 7008f8ed74..6d5cedbcad 100644 --- a/invokeai/app/services/download/download_default.py +++ b/invokeai/app/services/download/download_default.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Set import requests from pydantic.networks import AnyHttpUrl from requests import HTTPError -from tqdm import tqdm, std +from tqdm import tqdm from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.util.misc import get_iso_timestamp diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 9f0f774b49..42921f0b32 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -123,11 +123,11 @@ class ModelRepoVariant(str, Enum): class ModelConfigBase(BaseModel): """Base class for model configuration information.""" - path: str - name: str - base: BaseModelType - type: ModelType - format: ModelFormat + path: str = Field(description="filesystem path to the model file or directory") + name: str = Field(description="model name") + base: BaseModelType = Field(description="base model") + type: ModelType = Field(description="type of the model") + format: ModelFormat = Field(description="model format") key: str = Field(description="unique key for model", default="") original_hash: Optional[str] = Field( description="original fasthash of model contents", default=None @@ -135,9 +135,9 @@ class ModelConfigBase(BaseModel): current_hash: Optional[str] = Field( description="current fasthash of model contents", default=None ) # if model is converted or otherwise modified, this will hold updated hash - description: Optional[str] = Field(default=None) - source: Optional[str] = Field(description="Model download source (URL or repo_id)", default=None) - last_modified: Optional[float] = Field(description="Timestamp for modification time", default_factory=time.time) + description: Optional[str] = Field(description="human readable description of the model", default=None) + source: Optional[str] = Field(description="model original source (path, URL or repo_id)", default=None) + last_modified: Optional[float] = Field(description="timestamp for modification time", default_factory=time.time) model_config = ConfigDict( use_enum_values=False, From 35e8a33dfd4bdf374f1db526484ad8801203b642 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Wed, 14 Feb 2024 09:36:30 -0500 Subject: [PATCH 126/411] Remove references to model_records service, change submodel property on ModelInfo to submodel_type to support new params in model manager --- docs/contributing/MODEL_MANAGER.md | 2 +- invokeai/app/invocations/latent.py | 2 +- invokeai/app/invocations/model.py | 22 +++++++-------- invokeai/app/invocations/sdxl.py | 28 +++++++++---------- .../backend/model_management/model_manager.py | 2 +- pyproject.toml | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/contributing/MODEL_MANAGER.md b/docs/contributing/MODEL_MANAGER.md index b711c654de..b19699de73 100644 --- a/docs/contributing/MODEL_MANAGER.md +++ b/docs/contributing/MODEL_MANAGER.md @@ -1627,7 +1627,7 @@ payload=dict( queue_batch_id=queue_batch_id, graph_execution_state_id=graph_execution_state_id, model_key=model_key, - submodel=submodel, + submodel_type=submodel, hash=model_info.hash, location=str(model_info.location), precision=str(model_info.precision), diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 289da2dd73..c3de521940 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -812,7 +812,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): ) with set_seamless(vae_info.model, self.vae.seamless_axes), vae_info as vae: - assert isinstance(vae, torch.Tensor) + assert isinstance(vae, torch.nn.Module) latents = latents.to(vae.device) if self.fp32: vae.to(dtype=torch.float32) diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index f78425c6ee..71a71a63c8 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -18,7 +18,7 @@ from .baseinvocation import ( class ModelInfo(BaseModel): key: str = Field(description="Key of model as returned by ModelRecordServiceBase.get_model()") - submodel: Optional[SubModelType] = Field(default=None, description="Info to load submodel") + submodel_type: Optional[SubModelType] = Field(default=None, description="Info to load submodel") class LoraInfo(ModelInfo): @@ -110,22 +110,22 @@ class MainModelLoaderInvocation(BaseInvocation): unet=UNetField( unet=ModelInfo( key=key, - submodel=SubModelType.UNet, + submodel_type=SubModelType.UNet, ), scheduler=ModelInfo( key=key, - submodel=SubModelType.Scheduler, + submodel_type=SubModelType.Scheduler, ), loras=[], ), clip=ClipField( tokenizer=ModelInfo( key=key, - submodel=SubModelType.Tokenizer, + submodel_type=SubModelType.Tokenizer, ), text_encoder=ModelInfo( key=key, - submodel=SubModelType.TextEncoder, + submodel_type=SubModelType.TextEncoder, ), loras=[], skipped_layers=0, @@ -133,7 +133,7 @@ class MainModelLoaderInvocation(BaseInvocation): vae=VaeField( vae=ModelInfo( key=key, - submodel=SubModelType.Vae, + submodel_type=SubModelType.Vae, ), ), ) @@ -188,7 +188,7 @@ class LoraLoaderInvocation(BaseInvocation): output.unet.loras.append( LoraInfo( key=lora_key, - submodel=None, + submodel_type=None, weight=self.weight, ) ) @@ -198,7 +198,7 @@ class LoraLoaderInvocation(BaseInvocation): output.clip.loras.append( LoraInfo( key=lora_key, - submodel=None, + submodel_type=None, weight=self.weight, ) ) @@ -271,7 +271,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): output.unet.loras.append( LoraInfo( key=lora_key, - submodel=None, + submodel_type=None, weight=self.weight, ) ) @@ -281,7 +281,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): output.clip.loras.append( LoraInfo( key=lora_key, - submodel=None, + submodel_type=None, weight=self.weight, ) ) @@ -291,7 +291,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): output.clip2.loras.append( LoraInfo( key=lora_key, - submodel=None, + submodel_type=None, weight=self.weight, ) ) diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 633a6477fd..85e6fb787f 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -43,29 +43,29 @@ class SDXLModelLoaderInvocation(BaseInvocation): model_key = self.model.key # TODO: not found exceptions - if not context.services.model_records.exists(model_key): + if not context.services.model_manager.store.exists(model_key): raise Exception(f"Unknown model: {model_key}") return SDXLModelLoaderOutput( unet=UNetField( unet=ModelInfo( key=model_key, - submodel=SubModelType.UNet, + submodel_type=SubModelType.UNet, ), scheduler=ModelInfo( key=model_key, - submodel=SubModelType.Scheduler, + submodel_type=SubModelType.Scheduler, ), loras=[], ), clip=ClipField( tokenizer=ModelInfo( key=model_key, - submodel=SubModelType.Tokenizer, + submodel_type=SubModelType.Tokenizer, ), text_encoder=ModelInfo( key=model_key, - submodel=SubModelType.TextEncoder, + submodel_type=SubModelType.TextEncoder, ), loras=[], skipped_layers=0, @@ -73,11 +73,11 @@ class SDXLModelLoaderInvocation(BaseInvocation): clip2=ClipField( tokenizer=ModelInfo( key=model_key, - submodel=SubModelType.Tokenizer2, + submodel_type=SubModelType.Tokenizer2, ), text_encoder=ModelInfo( key=model_key, - submodel=SubModelType.TextEncoder2, + submodel_type=SubModelType.TextEncoder2, ), loras=[], skipped_layers=0, @@ -85,7 +85,7 @@ class SDXLModelLoaderInvocation(BaseInvocation): vae=VaeField( vae=ModelInfo( key=model_key, - submodel=SubModelType.Vae, + submodel_type=SubModelType.Vae, ), ), ) @@ -112,29 +112,29 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation): model_key = self.model.key # TODO: not found exceptions - if not context.services.model_records.exists(model_key): + if not context.services.model_manager.store.exists(model_key): raise Exception(f"Unknown model: {model_key}") return SDXLRefinerModelLoaderOutput( unet=UNetField( unet=ModelInfo( key=model_key, - submodel=SubModelType.UNet, + submodel_type=SubModelType.UNet, ), scheduler=ModelInfo( key=model_key, - submodel=SubModelType.Scheduler, + submodel_type=SubModelType.Scheduler, ), loras=[], ), clip2=ClipField( tokenizer=ModelInfo( key=model_key, - submodel=SubModelType.Tokenizer2, + submodel_type=SubModelType.Tokenizer2, ), text_encoder=ModelInfo( key=model_key, - submodel=SubModelType.TextEncoder2, + submodel_type=SubModelType.TextEncoder2, ), loras=[], skipped_layers=0, @@ -142,7 +142,7 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation): vae=VaeField( vae=ModelInfo( key=model_key, - submodel=SubModelType.Vae, + submodel_type=SubModelType.Vae, ), ), ) diff --git a/invokeai/backend/model_management/model_manager.py b/invokeai/backend/model_management/model_manager.py index da74ca3fb5..84d93f15fa 100644 --- a/invokeai/backend/model_management/model_manager.py +++ b/invokeai/backend/model_management/model_manager.py @@ -499,7 +499,7 @@ class ModelManager(object): model_class=model_class, base_model=base_model, model_type=model_type, - submodel=submodel_type, + submodel_type=submodel_type, ) if model_key not in self.cache_keys: diff --git a/pyproject.toml b/pyproject.toml index 2958e3629a..f57607bc0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,7 +245,7 @@ module = [ "invokeai.app.services.invocation_stats.invocation_stats_default", "invokeai.app.services.model_manager.model_manager_base", "invokeai.app.services.model_manager.model_manager_default", - "invokeai.app.services.model_records.model_records_sql", + "invokeai.app.services.model_manager.store.model_records_sql", "invokeai.app.util.controlnet_utils", "invokeai.backend.image_util.txt2mask", "invokeai.backend.image_util.safety_checker", From 262cbaacdd36a50681bb6bc8f4541dac4911e6c5 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Wed, 14 Feb 2024 09:51:11 -0500 Subject: [PATCH 127/411] References to context.services.model_manager.store.get_model can only accept keys, remove invalid assertion --- invokeai/app/invocations/latent.py | 4 ++-- .../load/model_cache/model_cache_default.py | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index c3de521940..05293fdfee 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -681,7 +681,7 @@ class DenoiseLatentsInvocation(BaseInvocation): source_node_id = graph_execution_state.prepared_source_mapping[self.id] # get the unet's config so that we can pass the base to dispatch_progress() - unet_config = context.services.model_manager.store.get_model(**self.unet.unet.model_dump()) + unet_config = context.services.model_manager.store.get_model(self.unet.unet.key) def step_callback(state: PipelineIntermediateState) -> None: self.dispatch_progress(context, source_node_id, state, unet_config.base) @@ -709,7 +709,7 @@ class DenoiseLatentsInvocation(BaseInvocation): # Apply the LoRA after unet has been moved to its target device for faster patching. ModelPatcher.apply_lora_unet(unet, _lora_loader()), ): - assert isinstance(unet, torch.Tensor) + assert isinstance(unet, UNet2DConditionModel) latents = latents.to(device=unet.device, dtype=unet.dtype) if noise is not None: noise = noise.to(device=unet.device, dtype=unet.dtype) diff --git a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py index 786396062c..02ce1266c7 100644 --- a/invokeai/backend/model_manager/load/model_cache/model_cache_default.py +++ b/invokeai/backend/model_manager/load/model_cache/model_cache_default.py @@ -303,18 +303,18 @@ class ModelCache(ModelCacheBase[AnyModel]): in_vram_models = 0 locked_in_vram_models = 0 for cache_record in self._cached_models.values(): - assert hasattr(cache_record.model, "device") - if cache_record.model.device == self.storage_device: - in_ram_models += 1 - else: - in_vram_models += 1 - if cache_record.locked: - locked_in_vram_models += 1 + if hasattr(cache_record.model, "device"): + if cache_record.model.device == self.storage_device: + in_ram_models += 1 + else: + in_vram_models += 1 + if cache_record.locked: + locked_in_vram_models += 1 - self.logger.debug( - f"Current VRAM/RAM usage: {vram}/{ram}; models_in_ram/models_in_vram(locked) =" - f" {in_ram_models}/{in_vram_models}({locked_in_vram_models})" - ) + self.logger.debug( + f"Current VRAM/RAM usage: {vram}/{ram}; models_in_ram/models_in_vram(locked) =" + f" {in_ram_models}/{in_vram_models}({locked_in_vram_models})" + ) def make_room(self, model_size: int) -> None: """Make enough room in the cache to accommodate a new model of indicated size.""" From 4c6e34b216e442e2f3a3582fa576fbb0c8904e31 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Wed, 14 Feb 2024 13:07:11 -0500 Subject: [PATCH 128/411] Update _get_hf_load_class to support clipvision models --- invokeai/backend/model_manager/load/load_default.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index df83c8320d..9ed0ccb2d3 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -163,8 +163,12 @@ class ModelLoader(ModelLoaderBase): else: try: config = self._load_diffusers_config(model_path, config_name="config.json") - class_name = config["_class_name"] - return self._hf_definition_to_type(module="diffusers", class_name=class_name) + class_name = config.get("_class_name", None) + if class_name: + return self._hf_definition_to_type(module="diffusers", class_name=class_name) + if config.get("model_type", None) == "clip_vision_model": + class_name = config.get("architectures")[0] + return self._hf_definition_to_type(module="transformers", class_name=class_name) except KeyError as e: raise InvalidModelConfigException("An expected config.json file is missing from this model.") from e From 88d6de4101ad7ddbaa0a52e0b8ea5096dffa0d6f Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Wed, 14 Feb 2024 13:16:15 -0500 Subject: [PATCH 129/411] Raise InvalidModelConfigException when unable to detect load class in ModelLoader --- invokeai/backend/model_manager/load/load_default.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index 9ed0ccb2d3..1dac121a30 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -169,6 +169,8 @@ class ModelLoader(ModelLoaderBase): if config.get("model_type", None) == "clip_vision_model": class_name = config.get("architectures")[0] return self._hf_definition_to_type(module="transformers", class_name=class_name) + if not class_name: + raise InvalidModelConfigException("Unable to decifer Load Class based on given config.json") except KeyError as e: raise InvalidModelConfigException("An expected config.json file is missing from this model.") from e From 539570cc7a3cc486055ef61d3a279344dd81b978 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:43:41 +1100 Subject: [PATCH 130/411] feat(nodes): update invocation context for mm2, update nodes model usage --- invokeai/app/invocations/compel.py | 40 ++------ invokeai/app/invocations/ip_adapter.py | 7 +- invokeai/app/invocations/latent.py | 71 +++----------- invokeai/app/invocations/model.py | 8 +- invokeai/app/invocations/sdxl.py | 4 +- .../services/model_load/model_load_base.py | 14 +-- .../services/model_load/model_load_default.py | 48 +++++----- .../app/services/shared/invocation_context.py | 94 ++++++++++++++----- invokeai/app/util/step_callback.py | 2 +- 9 files changed, 141 insertions(+), 147 deletions(-) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 3850fb6cc3..5159d5b89c 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -69,20 +69,12 @@ class CompelInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> ConditioningOutput: - tokenizer_info = context.services.model_manager.load.load_model_by_key( - **self.clip.tokenizer.model_dump(), - context=context, - ) - text_encoder_info = context.services.model_manager.load.load_model_by_key( - **self.clip.text_encoder.model_dump(), - context=context, - ) + tokenizer_info = context.models.load(**self.clip.tokenizer.model_dump()) + text_encoder_info = context.models.load(**self.clip.text_encoder.model_dump()) def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in self.clip.loras: - lora_info = context.services.model_manager.load.load_model_by_key( - **lora.model_dump(exclude={"weight"}), context=context - ) + lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) assert isinstance(lora_info.model, LoRAModelRaw) yield (lora_info.model, lora.weight) del lora_info @@ -94,10 +86,7 @@ class CompelInvocation(BaseInvocation): for trigger in extract_ti_triggers_from_prompt(self.prompt): name = trigger[1:-1] try: - loaded_model = context.services.model_manager.load.load_model_by_key( - **self.clip.text_encoder.model_dump(), - context=context, - ).model + loaded_model = context.models.load(**self.clip.text_encoder.model_dump()).model assert isinstance(loaded_model, TextualInversionModelRaw) ti_list.append((name, loaded_model)) except UnknownModelException: @@ -165,14 +154,8 @@ class SDXLPromptInvocationBase: lora_prefix: str, zero_on_empty: bool, ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[ExtraConditioningInfo]]: - tokenizer_info = context.services.model_manager.load.load_model_by_key( - **clip_field.tokenizer.model_dump(), - context=context, - ) - text_encoder_info = context.services.model_manager.load.load_model_by_key( - **clip_field.text_encoder.model_dump(), - context=context, - ) + tokenizer_info = context.models.load(**clip_field.tokenizer.model_dump()) + text_encoder_info = context.models.load(**clip_field.text_encoder.model_dump()) # return zero on empty if prompt == "" and zero_on_empty: @@ -197,9 +180,7 @@ class SDXLPromptInvocationBase: def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in clip_field.loras: - lora_info = context.services.model_manager.load.load_model_by_key( - **lora.model_dump(exclude={"weight"}), context=context - ) + lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) lora_model = lora_info.model assert isinstance(lora_model, LoRAModelRaw) yield (lora_model, lora.weight) @@ -212,11 +193,8 @@ class SDXLPromptInvocationBase: for trigger in extract_ti_triggers_from_prompt(prompt): name = trigger[1:-1] try: - ti_model = context.services.model_manager.load.load_model_by_attr( - model_name=name, - base_model=text_encoder_info.config.base, - model_type=ModelType.TextualInversion, - context=context, + ti_model = context.models.load_by_attrs( + model_name=name, base_model=text_encoder_info.config.base, model_type=ModelType.TextualInversion ).model assert isinstance(ti_model, TextualInversionModelRaw) ti_list.append((name, ti_model)) diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py index 01124f62f3..15e254010b 100644 --- a/invokeai/app/invocations/ip_adapter.py +++ b/invokeai/app/invocations/ip_adapter.py @@ -14,8 +14,7 @@ from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField from invokeai.app.invocations.primitives import ImageField from invokeai.app.invocations.util import validate_begin_end_step, validate_weights from invokeai.app.services.shared.invocation_context import InvocationContext -from invokeai.backend.model_management.models.base import BaseModelType, ModelType -from invokeai.backend.model_management.models.ip_adapter import get_ip_adapter_image_encoder_model_id +from invokeai.backend.model_manager.config import BaseModelType, ModelType # LS: Consider moving these two classes into model.py @@ -90,10 +89,10 @@ class IPAdapterInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> IPAdapterOutput: # Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model. - ip_adapter_info = context.services.model_manager.store.get_model(self.ip_adapter_model.key) + ip_adapter_info = context.models.get_config(self.ip_adapter_model.key) image_encoder_model_id = ip_adapter_info.image_encoder_model_id image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip() - image_encoder_models = context.services.model_manager.store.search_by_attr( + image_encoder_models = context.models.search_by_attrs( model_name=image_encoder_model_name, base_model=BaseModelType.Any, model_type=ModelType.CLIPVision ) assert len(image_encoder_models) == 1 diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 05293fdfee..5dd0eb074d 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -141,7 +141,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation): @torch.no_grad() def invoke(self, context: InvocationContext) -> DenoiseMaskOutput: if self.image is not None: - image = context.services.images.get_pil_image(self.image.image_name) + image = context.images.get_pil(self.image.image_name) image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB")) if image_tensor.dim() == 3: image_tensor = image_tensor.unsqueeze(0) @@ -153,10 +153,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation): ) if image_tensor is not None: - vae_info = context.services.model_manager.load.load_model_by_key( - **self.vae.vae.model_dump(), - context=context, - ) + vae_info = context.models.load(**self.vae.vae.model_dump()) img_mask = tv_resize(mask, image_tensor.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) masked_image = image_tensor * torch.where(img_mask < 0.5, 0.0, 1.0) @@ -182,10 +179,7 @@ def get_scheduler( seed: int, ) -> Scheduler: scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP["ddim"]) - orig_scheduler_info = context.services.model_manager.load.load_model_by_key( - **scheduler_info.model_dump(), - context=context, - ) + orig_scheduler_info = context.models.load(**scheduler_info.model_dump()) with orig_scheduler_info as orig_scheduler: scheduler_config = orig_scheduler.config @@ -399,12 +393,7 @@ class DenoiseLatentsInvocation(BaseInvocation): # and if weight is None, populate with default 1.0? controlnet_data = [] for control_info in control_list: - control_model = exit_stack.enter_context( - context.services.model_manager.load.load_model_by_key( - key=control_info.control_model.key, - context=context, - ) - ) + control_model = exit_stack.enter_context(context.models.load(key=control_info.control_model.key)) # control_models.append(control_model) control_image_field = control_info.image @@ -466,25 +455,17 @@ class DenoiseLatentsInvocation(BaseInvocation): conditioning_data.ip_adapter_conditioning = [] for single_ip_adapter in ip_adapter: ip_adapter_model: Union[IPAdapter, IPAdapterPlus] = exit_stack.enter_context( - context.services.model_manager.load.load_model_by_key( - key=single_ip_adapter.ip_adapter_model.key, - context=context, - ) + context.models.load(key=single_ip_adapter.ip_adapter_model.key) ) - image_encoder_model_info = context.services.model_manager.load.load_model_by_key( - key=single_ip_adapter.image_encoder_model.key, - context=context, - ) + image_encoder_model_info = context.models.load(key=single_ip_adapter.image_encoder_model.key) # `single_ip_adapter.image` could be a list or a single ImageField. Normalize to a list here. single_ipa_image_fields = single_ip_adapter.image if not isinstance(single_ipa_image_fields, list): single_ipa_image_fields = [single_ipa_image_fields] - single_ipa_images = [ - context.services.images.get_pil_image(image.image_name) for image in single_ipa_image_fields - ] + single_ipa_images = [context.images.get_pil(image.image_name) for image in single_ipa_image_fields] # TODO(ryand): With some effort, the step of running the CLIP Vision encoder could be done before any other # models are needed in memory. This would help to reduce peak memory utilization in low-memory environments. @@ -528,10 +509,7 @@ class DenoiseLatentsInvocation(BaseInvocation): t2i_adapter_data = [] for t2i_adapter_field in t2i_adapter: - t2i_adapter_model_info = context.services.model_manager.load.load_model_by_key( - key=t2i_adapter_field.t2i_adapter_model.key, - context=context, - ) + t2i_adapter_model_info = context.models.load(key=t2i_adapter_field.t2i_adapter_model.key) image = context.images.get_pil(t2i_adapter_field.image.image_name) # The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally. @@ -676,30 +654,20 @@ class DenoiseLatentsInvocation(BaseInvocation): do_classifier_free_guidance=True, ) - # Get the source node id (we are invoking the prepared node) - graph_execution_state = context.services.graph_execution_manager.get(context.graph_execution_state_id) - source_node_id = graph_execution_state.prepared_source_mapping[self.id] - # get the unet's config so that we can pass the base to dispatch_progress() - unet_config = context.services.model_manager.store.get_model(self.unet.unet.key) + unet_config = context.models.get_config(self.unet.unet.key) def step_callback(state: PipelineIntermediateState) -> None: - self.dispatch_progress(context, source_node_id, state, unet_config.base) + context.util.sd_step_callback(state, unet_config.base) def _lora_loader() -> Iterator[Tuple[LoRAModelRaw, float]]: for lora in self.unet.loras: - lora_info = context.services.model_manager.load.load_model_by_key( - **lora.model_dump(exclude={"weight"}), - context=context, - ) + lora_info = context.models.load(**lora.model_dump(exclude={"weight"})) yield (lora_info.model, lora.weight) del lora_info return - unet_info = context.services.model_manager.load.load_model_by_key( - **self.unet.unet.model_dump(), - context=context, - ) + unet_info = context.models.load(**self.unet.unet.model_dump()) assert isinstance(unet_info.model, UNet2DConditionModel) with ( ExitStack() as exit_stack, @@ -806,10 +774,7 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard): def invoke(self, context: InvocationContext) -> ImageOutput: latents = context.tensors.load(self.latents.latents_name) - vae_info = context.services.model_manager.load.load_model_by_key( - **self.vae.vae.model_dump(), - context=context, - ) + vae_info = context.models.load(**self.vae.vae.model_dump()) with set_seamless(vae_info.model, self.vae.seamless_axes), vae_info as vae: assert isinstance(vae, torch.nn.Module) @@ -1032,10 +997,7 @@ class ImageToLatentsInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> LatentsOutput: image = context.images.get_pil(self.image.image_name) - vae_info = context.services.model_manager.load.load_model_by_key( - **self.vae.vae.model_dump(), - context=context, - ) + vae_info = context.models.load(**self.vae.vae.model_dump()) image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB")) if image_tensor.dim() == 3: @@ -1239,10 +1201,7 @@ class IdealSizeInvocation(BaseInvocation): return tuple((x - x % multiple_of) for x in args) def invoke(self, context: InvocationContext) -> IdealSizeOutput: - unet_config = context.services.model_manager.load.load_model_by_key( - **self.unet.unet.model_dump(), - context=context, - ) + unet_config = context.models.get_config(**self.unet.unet.model_dump()) aspect = self.width / self.height dimension: float = 512 if unet_config.base == BaseModelType.StableDiffusion2: diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py index 71a71a63c8..6087bc82db 100644 --- a/invokeai/app/invocations/model.py +++ b/invokeai/app/invocations/model.py @@ -103,7 +103,7 @@ class MainModelLoaderInvocation(BaseInvocation): key = self.model.key # TODO: not found exceptions - if not context.services.model_manager.store.exists(key): + if not context.models.exists(key): raise Exception(f"Unknown model {key}") return ModelLoaderOutput( @@ -172,7 +172,7 @@ class LoraLoaderInvocation(BaseInvocation): lora_key = self.lora.key - if not context.services.model_manager.store.exists(lora_key): + if not context.models.exists(lora_key): raise Exception(f"Unkown lora: {lora_key}!") if self.unet is not None and any(lora.key == lora_key for lora in self.unet.loras): @@ -252,7 +252,7 @@ class SDXLLoraLoaderInvocation(BaseInvocation): lora_key = self.lora.key - if not context.services.model_manager.store.exists(lora_key): + if not context.models.exists(lora_key): raise Exception(f"Unknown lora: {lora_key}!") if self.unet is not None and any(lora.key == lora_key for lora in self.unet.loras): @@ -318,7 +318,7 @@ class VaeLoaderInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> VAEOutput: key = self.vae_model.key - if not context.services.model_manager.store.exists(key): + if not context.models.exists(key): raise Exception(f"Unkown vae: {key}!") return VAEOutput(vae=VaeField(vae=ModelInfo(key=key))) diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py index 85e6fb787f..0df27c0011 100644 --- a/invokeai/app/invocations/sdxl.py +++ b/invokeai/app/invocations/sdxl.py @@ -43,7 +43,7 @@ class SDXLModelLoaderInvocation(BaseInvocation): model_key = self.model.key # TODO: not found exceptions - if not context.services.model_manager.store.exists(model_key): + if not context.models.exists(model_key): raise Exception(f"Unknown model: {model_key}") return SDXLModelLoaderOutput( @@ -112,7 +112,7 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation): model_key = self.model.key # TODO: not found exceptions - if not context.services.model_manager.store.exists(model_key): + if not context.models.exists(model_key): raise Exception(f"Unknown model: {model_key}") return SDXLRefinerModelLoaderOutput( diff --git a/invokeai/app/services/model_load/model_load_base.py b/invokeai/app/services/model_load/model_load_base.py index 45eaf4652f..f4dd905135 100644 --- a/invokeai/app/services/model_load/model_load_base.py +++ b/invokeai/app/services/model_load/model_load_base.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import Optional -from invokeai.app.invocations.baseinvocation import InvocationContext +from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import LoadedModel from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase @@ -19,14 +19,14 @@ class ModelLoadServiceBase(ABC): self, key: str, submodel_type: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> LoadedModel: """ Given a model's key, load it and return the LoadedModel object. :param key: Key of model config to be fetched. :param submodel: For main (pipeline models), the submodel to fetch. - :param context: Invocation context used for event reporting + :param context_data: Invocation context data used for event reporting """ pass @@ -35,14 +35,14 @@ class ModelLoadServiceBase(ABC): self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> LoadedModel: """ Given a model's configuration, load it and return the LoadedModel object. :param model_config: Model configuration record (as returned by ModelRecordBase.get_model()) :param submodel: For main (pipeline models), the submodel to fetch. - :param context: Invocation context used for event reporting + :param context_data: Invocation context data used for event reporting """ pass @@ -53,7 +53,7 @@ class ModelLoadServiceBase(ABC): base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> LoadedModel: """ Given a model's attributes, search the database for it, and if found, load and return the LoadedModel object. @@ -66,7 +66,7 @@ class ModelLoadServiceBase(ABC): :param base_model: Base model :param model_type: Type of the model :param submodel: For main (pipeline models), the submodel to fetch - :param context: The invocation context. + :param context_data: The invocation context data. Exceptions: UnknownModelException -- model with these attributes not known NotImplementedException -- a model loader was not provided at initialization time diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index a6ccd5afbc..29b297c814 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -3,10 +3,11 @@ from typing import Optional -from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException +from invokeai.app.services.invoker import Invoker from invokeai.app.services.model_records import ModelRecordServiceBase, UnknownModelException +from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel, ModelCache, ModelConvertCache from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase @@ -46,6 +47,9 @@ class ModelLoadService(ModelLoadServiceBase): ), ) + def start(self, invoker: Invoker) -> None: + self._invoker = invoker + @property def ram_cache(self) -> ModelCacheBase[AnyModel]: """Return the RAM cache used by this loader.""" @@ -60,7 +64,7 @@ class ModelLoadService(ModelLoadServiceBase): self, key: str, submodel_type: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> LoadedModel: """ Given a model's key, load it and return the LoadedModel object. @@ -70,7 +74,7 @@ class ModelLoadService(ModelLoadServiceBase): :param context: Invocation context used for event reporting """ config = self._store.get_model(key) - return self.load_model_by_config(config, submodel_type, context) + return self.load_model_by_config(config, submodel_type, context_data) def load_model_by_attr( self, @@ -78,7 +82,7 @@ class ModelLoadService(ModelLoadServiceBase): base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> LoadedModel: """ Given a model's attributes, search the database for it, and if found, load and return the LoadedModel object. @@ -109,7 +113,7 @@ class ModelLoadService(ModelLoadServiceBase): self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None, - context: Optional[InvocationContext] = None, + context_data: Optional[InvocationContextData] = None, ) -> LoadedModel: """ Given a model's configuration, load it and return the LoadedModel object. @@ -118,15 +122,15 @@ class ModelLoadService(ModelLoadServiceBase): :param submodel: For main (pipeline models), the submodel to fetch. :param context: Invocation context used for event reporting """ - if context: + if context_data: self._emit_load_event( - context=context, + context_data=context_data, model_config=model_config, ) loaded_model = self._any_loader.load_model(model_config, submodel_type) - if context: + if context_data: self._emit_load_event( - context=context, + context_data=context_data, model_config=model_config, loaded=True, ) @@ -134,26 +138,28 @@ class ModelLoadService(ModelLoadServiceBase): def _emit_load_event( self, - context: InvocationContext, + context_data: InvocationContextData, model_config: AnyModelConfig, loaded: Optional[bool] = False, ) -> None: - if context.services.queue.is_canceled(context.graph_execution_state_id): + if not self._invoker: + return + if self._invoker.services.queue.is_canceled(context_data.session_id): raise CanceledException() if not loaded: - context.services.events.emit_model_load_started( - queue_id=context.queue_id, - queue_item_id=context.queue_item_id, - queue_batch_id=context.queue_batch_id, - graph_execution_state_id=context.graph_execution_state_id, + self._invoker.services.events.emit_model_load_started( + queue_id=context_data.queue_id, + queue_item_id=context_data.queue_item_id, + queue_batch_id=context_data.batch_id, + graph_execution_state_id=context_data.session_id, model_config=model_config, ) else: - context.services.events.emit_model_load_completed( - queue_id=context.queue_id, - queue_item_id=context.queue_item_id, - queue_batch_id=context.queue_batch_id, - graph_execution_state_id=context.graph_execution_state_id, + self._invoker.services.events.emit_model_load_completed( + queue_id=context_data.queue_id, + queue_item_id=context_data.queue_item_id, + queue_batch_id=context_data.batch_id, + graph_execution_state_id=context_data.session_id, model_config=model_config, ) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index c68dc1140b..089d09f825 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from pathlib import Path from typing import TYPE_CHECKING, Optional from PIL.Image import Image @@ -12,8 +13,9 @@ from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.step_callback import stable_diffusion_step_callback -from invokeai.backend.model_management.model_manager import LoadedModelInfo -from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType +from invokeai.backend.model_manager.config import AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType +from invokeai.backend.model_manager.load.load_base import LoadedModel +from invokeai.backend.model_manager.metadata.metadata_base import AnyModelRepoMetadata from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData @@ -259,45 +261,95 @@ class ConditioningInterface(InvocationContextInterface): class ModelsInterface(InvocationContextInterface): - def exists(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> bool: + def exists(self, key: str) -> bool: """ Checks if a model exists. - :param model_name: The name of the model to check. - :param base_model: The base model of the model to check. - :param model_type: The type of the model to check. + :param key: The key of the model. """ - return self._services.model_manager.model_exists(model_name, base_model, model_type) + return self._services.model_manager.store.exists(key) - def load( - self, model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None - ) -> LoadedModelInfo: + def load(self, key: str, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Loads a model. - :param model_name: The name of the model to get. - :param base_model: The base model of the model to get. - :param model_type: The type of the model to get. - :param submodel: The submodel of the model to get. + :param key: The key of the model. + :param submodel_type: The submodel of the model to get. :returns: An object representing the loaded model. """ # The model manager emits events as it loads the model. It needs the context data to build # the event payloads. - return self._services.model_manager.get_model( - model_name, base_model, model_type, submodel, context_data=self._context_data + return self._services.model_manager.load.load_model_by_key( + key=key, submodel_type=submodel_type, context_data=self._context_data ) - def get_info(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> dict: + def load_by_attrs( + self, model_name: str, base_model: BaseModelType, model_type: ModelType, submodel: Optional[SubModelType] = None + ) -> LoadedModel: + """ + Loads a model by its attributes. + + :param model_name: Name of to be fetched. + :param base_model: Base model + :param model_type: Type of the model + :param submodel: For main (pipeline models), the submodel to fetch + """ + return self._services.model_manager.load.load_model_by_attr( + model_name=model_name, + base_model=base_model, + model_type=model_type, + submodel=submodel, + context_data=self._context_data, + ) + + def get_config(self, key: str) -> AnyModelConfig: """ Gets a model's info, an dict-like object. - :param model_name: The name of the model to get. - :param base_model: The base model of the model to get. - :param model_type: The type of the model to get. + :param key: The key of the model. """ - return self._services.model_manager.model_info(model_name, base_model, model_type) + return self._services.model_manager.store.get_model(key=key) + + def get_metadata(self, key: str) -> Optional[AnyModelRepoMetadata]: + """ + Gets a model's metadata, if it has any. + + :param key: The key of the model. + """ + return self._services.model_manager.store.get_metadata(key=key) + + def search_by_path(self, path: Path) -> list[AnyModelConfig]: + """ + Searches for models by path. + + :param path: The path to search for. + """ + return self._services.model_manager.store.search_by_path(path) + + def search_by_attrs( + self, + model_name: Optional[str] = None, + base_model: Optional[BaseModelType] = None, + model_type: Optional[ModelType] = None, + model_format: Optional[ModelFormat] = None, + ) -> list[AnyModelConfig]: + """ + Searches for models by attributes. + + :param model_name: Name of to be fetched. + :param base_model: Base model + :param model_type: Type of the model + :param submodel: For main (pipeline models), the submodel to fetch + """ + + return self._services.model_manager.store.search_by_attr( + model_name=model_name, + base_model=base_model, + model_type=model_type, + model_format=model_format, + ) class ConfigInterface(InvocationContextInterface): diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index d83b380d95..33d00ca366 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -4,8 +4,8 @@ import torch from PIL import Image from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException, ProgressImage +from invokeai.backend.model_manager.config import BaseModelType -from ...backend.model_management.models import BaseModelType from ...backend.stable_diffusion import PipelineIntermediateState from ...backend.util.util import image_to_dataURL From c80987eb8a3cd36398a195bc3532d72abaa9d19a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:50:47 +1100 Subject: [PATCH 131/411] chore: ruff --- invokeai/app/invocations/controlnet_image_processors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 580ee08562..8542134fff 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -39,7 +39,6 @@ from invokeai.app.invocations.util import validate_begin_end_step, validate_weig from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.image_util.depth_anything import DepthAnythingDetector from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector -from invokeai.backend.model_management.models.base import BaseModelType from .baseinvocation import ( BaseInvocation, From 68f53460f05f686df3c7cf8df36805adc1ce9f27 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:52:44 +1100 Subject: [PATCH 132/411] chore: lint --- invokeai/frontend/web/src/features/nodes/types/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/error.ts b/invokeai/frontend/web/src/features/nodes/types/error.ts index c3da136c7a..82bc0f86e0 100644 --- a/invokeai/frontend/web/src/features/nodes/types/error.ts +++ b/invokeai/frontend/web/src/features/nodes/types/error.ts @@ -60,4 +60,4 @@ export class FieldParseError extends Error { export class UnableToExtractSchemaNameFromRefError extends FieldParseError {} export class UnsupportedArrayItemType extends FieldParseError {} export class UnsupportedUnionError extends FieldParseError {} -export class UnsupportedPrimitiveTypeError extends FieldParseError {} \ No newline at end of file +export class UnsupportedPrimitiveTypeError extends FieldParseError {} From 651ac56b2c47c6301cfa4092946bfbc9b1264569 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:53:41 +1100 Subject: [PATCH 133/411] fix(ui): fix type issues --- .../nodes/components/sidePanel/viewMode/WorkflowField.tsx | 4 ++-- .../src/features/nodes/util/schema/parseFieldType.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx index 0e5857933a..e707dd4f54 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx @@ -16,7 +16,7 @@ type Props = { const WorkflowField = ({ nodeId, fieldName }: Props) => { const label = useFieldLabel(nodeId, fieldName); - const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'input'); + const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, 'inputs'); const { isValueChanged, onReset } = useFieldOriginalValue(nodeId, fieldName); return ( @@ -36,7 +36,7 @@ const WorkflowField = ({ nodeId, fieldName }: Props) => { /> )} } + label={} openDelay={HANDLE_TOOLTIP_OPEN_DELAY} placement="top" > diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts index 2f4ce48a32..d7011ad6f8 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/parseFieldType.test.ts @@ -284,13 +284,13 @@ describe('refObjectToSchemaName', async () => { }); describe.concurrent('parseFieldType', async () => { - it.each(primitiveTypes)('parses primitive types ($name)', ({ schema, expected }) => { + it.each(primitiveTypes)('parses primitive types ($name)', ({ schema, expected }: ParseFieldTypeTestCase) => { expect(parseFieldType(schema)).toEqual(expected); }); - it.each(complexTypes)('parses complex types ($name)', ({ schema, expected }) => { + it.each(complexTypes)('parses complex types ($name)', ({ schema, expected }: ParseFieldTypeTestCase) => { expect(parseFieldType(schema)).toEqual(expected); }); - it.each(specialCases)('parses special case types ($name)', ({ schema, expected }) => { + it.each(specialCases)('parses special case types ($name)', ({ schema, expected }: ParseFieldTypeTestCase) => { expect(parseFieldType(schema)).toEqual(expected); }); From fab30b5a11fc1f0d7704c31a75f30be7567a4721 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:16:25 +1100 Subject: [PATCH 134/411] feat(ui): export components type --- .../frontend/web/src/services/api/types.ts | 228 +++++++++--------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 55ff808b40..f9a1decf65 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -3,7 +3,7 @@ import type { EntityState } from '@reduxjs/toolkit'; import type { components, paths } from 'services/api/schema'; import type { O } from 'ts-toolbelt'; -type s = components['schemas']; +export type S = components['schemas']; export type ImageCache = EntityState; @@ -23,60 +23,60 @@ export type BatchConfig = export type EnqueueBatchResult = components['schemas']['EnqueueBatchResult']; -export type InputFieldJSONSchemaExtra = s['InputFieldJSONSchemaExtra']; -export type OutputFieldJSONSchemaExtra = s['OutputFieldJSONSchemaExtra']; -export type InvocationJSONSchemaExtra = s['UIConfigBase']; +export type InputFieldJSONSchemaExtra = S['InputFieldJSONSchemaExtra']; +export type OutputFieldJSONSchemaExtra = S['OutputFieldJSONSchemaExtra']; +export type InvocationJSONSchemaExtra = S['UIConfigBase']; // App Info -export type AppVersion = s['AppVersion']; -export type AppConfig = s['AppConfig']; -export type AppDependencyVersions = s['AppDependencyVersions']; +export type AppVersion = S['AppVersion']; +export type AppConfig = S['AppConfig']; +export type AppDependencyVersions = S['AppDependencyVersions']; // Images -export type ImageDTO = s['ImageDTO']; -export type BoardDTO = s['BoardDTO']; -export type BoardChanges = s['BoardChanges']; -export type ImageChanges = s['ImageRecordChanges']; -export type ImageCategory = s['ImageCategory']; -export type ResourceOrigin = s['ResourceOrigin']; -export type ImageField = s['ImageField']; -export type OffsetPaginatedResults_BoardDTO_ = s['OffsetPaginatedResults_BoardDTO_']; -export type OffsetPaginatedResults_ImageDTO_ = s['OffsetPaginatedResults_ImageDTO_']; +export type ImageDTO = S['ImageDTO']; +export type BoardDTO = S['BoardDTO']; +export type BoardChanges = S['BoardChanges']; +export type ImageChanges = S['ImageRecordChanges']; +export type ImageCategory = S['ImageCategory']; +export type ResourceOrigin = S['ResourceOrigin']; +export type ImageField = S['ImageField']; +export type OffsetPaginatedResults_BoardDTO_ = S['OffsetPaginatedResults_BoardDTO_']; +export type OffsetPaginatedResults_ImageDTO_ = S['OffsetPaginatedResults_ImageDTO_']; // Models -export type ModelType = s['invokeai__backend__model_management__models__base__ModelType']; -export type SubModelType = s['SubModelType']; -export type BaseModelType = s['invokeai__backend__model_management__models__base__BaseModelType']; -export type MainModelField = s['MainModelField']; -export type VAEModelField = s['VAEModelField']; -export type LoRAModelField = s['LoRAModelField']; -export type LoRAModelFormat = s['LoRAModelFormat']; -export type ControlNetModelField = s['ControlNetModelField']; -export type IPAdapterModelField = s['IPAdapterModelField']; -export type T2IAdapterModelField = s['T2IAdapterModelField']; -export type ModelsList = s['invokeai__app__api__routers__models__ModelsList']; -export type ControlField = s['ControlField']; -export type IPAdapterField = s['IPAdapterField']; +export type ModelType = S['ModelType']; +export type SubModelType = S['SubModelType']; +export type BaseModelType = S['BaseModelType']; +export type MainModelField = S['MainModelField']; +export type VAEModelField = S['VAEModelField']; +export type LoRAModelField = S['LoRAModelField']; +export type LoRAModelFormat = S['LoRAModelFormat']; +export type ControlNetModelField = S['ControlNetModelField']; +export type IPAdapterModelField = S['IPAdapterModelField']; +export type T2IAdapterModelField = S['T2IAdapterModelField']; +export type ModelsList = S['invokeai__app__api__routers__models__ModelsList']; +export type ControlField = S['ControlField']; +export type IPAdapterField = S['IPAdapterField']; // Model Configs -export type LoRAModelConfig = s['LoRAModelConfig']; -export type VaeModelConfig = s['VaeModelConfig']; -export type ControlNetModelCheckpointConfig = s['ControlNetModelCheckpointConfig']; -export type ControlNetModelDiffusersConfig = s['ControlNetModelDiffusersConfig']; +export type LoRAModelConfig = S['LoRAModelConfig']; +export type VaeModelConfig = S['VaeModelConfig']; +export type ControlNetModelCheckpointConfig = S['ControlNetModelCheckpointConfig']; +export type ControlNetModelDiffusersConfig = S['ControlNetModelDiffusersConfig']; export type ControlNetModelConfig = ControlNetModelCheckpointConfig | ControlNetModelDiffusersConfig; -export type IPAdapterModelInvokeAIConfig = s['IPAdapterModelInvokeAIConfig']; +export type IPAdapterModelInvokeAIConfig = S['IPAdapterModelInvokeAIConfig']; export type IPAdapterModelConfig = IPAdapterModelInvokeAIConfig; -export type T2IAdapterModelDiffusersConfig = s['T2IAdapterModelDiffusersConfig']; +export type T2IAdapterModelDiffusersConfig = S['T2IAdapterModelDiffusersConfig']; export type T2IAdapterModelConfig = T2IAdapterModelDiffusersConfig; -export type TextualInversionModelConfig = s['TextualInversionModelConfig']; +export type TextualInversionModelConfig = S['TextualInversionModelConfig']; export type DiffusersModelConfig = - | s['StableDiffusion1ModelDiffusersConfig'] - | s['StableDiffusion2ModelDiffusersConfig'] - | s['StableDiffusionXLModelDiffusersConfig']; + | S['StableDiffusion1ModelDiffusersConfig'] + | S['StableDiffusion2ModelDiffusersConfig'] + | S['StableDiffusionXLModelDiffusersConfig']; export type CheckpointModelConfig = - | s['StableDiffusion1ModelCheckpointConfig'] - | s['StableDiffusion2ModelCheckpointConfig'] - | s['StableDiffusionXLModelCheckpointConfig']; + | S['StableDiffusion1ModelCheckpointConfig'] + | S['StableDiffusion2ModelCheckpointConfig'] + | S['StableDiffusionXLModelCheckpointConfig']; export type MainModelConfig = DiffusersModelConfig | CheckpointModelConfig; export type AnyModelConfig = | LoRAModelConfig @@ -87,87 +87,87 @@ export type AnyModelConfig = | TextualInversionModelConfig | MainModelConfig; -export type MergeModelConfig = s['Body_merge_models']; -export type ImportModelConfig = s['Body_import_model']; +export type MergeModelConfig = S['Body_merge_models']; +export type ImportModelConfig = S['Body_import_model']; // Graphs -export type Graph = s['Graph']; +export type Graph = S['Graph']; export type NonNullableGraph = O.Required; -export type Edge = s['Edge']; -export type GraphExecutionState = s['GraphExecutionState']; -export type Batch = s['Batch']; -export type SessionQueueItemDTO = s['SessionQueueItemDTO']; -export type SessionQueueItem = s['SessionQueueItem']; -export type WorkflowRecordOrderBy = s['WorkflowRecordOrderBy']; -export type SQLiteDirection = s['SQLiteDirection']; -export type WorkflowDTO = s['WorkflowRecordDTO']; -export type WorkflowRecordListItemDTO = s['WorkflowRecordListItemDTO']; +export type Edge = S['Edge']; +export type GraphExecutionState = S['GraphExecutionState']; +export type Batch = S['Batch']; +export type SessionQueueItemDTO = S['SessionQueueItemDTO']; +export type SessionQueueItem = S['SessionQueueItem']; +export type WorkflowRecordOrderBy = S['WorkflowRecordOrderBy']; +export type SQLiteDirection = S['SQLiteDirection']; +export type WorkflowDTO = S['WorkflowRecordDTO']; +export type WorkflowRecordListItemDTO = S['WorkflowRecordListItemDTO']; // General nodes -export type CollectInvocation = s['CollectInvocation']; -export type IterateInvocation = s['IterateInvocation']; -export type RangeInvocation = s['RangeInvocation']; -export type RandomRangeInvocation = s['RandomRangeInvocation']; -export type RangeOfSizeInvocation = s['RangeOfSizeInvocation']; -export type ImageResizeInvocation = s['ImageResizeInvocation']; -export type ImageBlurInvocation = s['ImageBlurInvocation']; -export type ImageScaleInvocation = s['ImageScaleInvocation']; -export type InfillPatchMatchInvocation = s['InfillPatchMatchInvocation']; -export type InfillTileInvocation = s['InfillTileInvocation']; -export type CreateDenoiseMaskInvocation = s['CreateDenoiseMaskInvocation']; -export type MaskEdgeInvocation = s['MaskEdgeInvocation']; -export type RandomIntInvocation = s['RandomIntInvocation']; -export type CompelInvocation = s['CompelInvocation']; -export type DynamicPromptInvocation = s['DynamicPromptInvocation']; -export type NoiseInvocation = s['NoiseInvocation']; -export type DenoiseLatentsInvocation = s['DenoiseLatentsInvocation']; -export type SDXLLoraLoaderInvocation = s['SDXLLoraLoaderInvocation']; -export type ImageToLatentsInvocation = s['ImageToLatentsInvocation']; -export type LatentsToImageInvocation = s['LatentsToImageInvocation']; -export type ImageCollectionInvocation = s['ImageCollectionInvocation']; -export type MainModelLoaderInvocation = s['MainModelLoaderInvocation']; -export type LoraLoaderInvocation = s['LoraLoaderInvocation']; -export type ESRGANInvocation = s['ESRGANInvocation']; -export type DivideInvocation = s['DivideInvocation']; -export type ImageNSFWBlurInvocation = s['ImageNSFWBlurInvocation']; -export type ImageWatermarkInvocation = s['ImageWatermarkInvocation']; -export type SeamlessModeInvocation = s['SeamlessModeInvocation']; -export type MetadataInvocation = s['MetadataInvocation']; -export type CoreMetadataInvocation = s['CoreMetadataInvocation']; -export type MetadataItemInvocation = s['MetadataItemInvocation']; -export type MergeMetadataInvocation = s['MergeMetadataInvocation']; -export type IPAdapterMetadataField = s['IPAdapterMetadataField']; -export type T2IAdapterField = s['T2IAdapterField']; -export type LoRAMetadataField = s['LoRAMetadataField']; +export type CollectInvocation = S['CollectInvocation']; +export type IterateInvocation = S['IterateInvocation']; +export type RangeInvocation = S['RangeInvocation']; +export type RandomRangeInvocation = S['RandomRangeInvocation']; +export type RangeOfSizeInvocation = S['RangeOfSizeInvocation']; +export type ImageResizeInvocation = S['ImageResizeInvocation']; +export type ImageBlurInvocation = S['ImageBlurInvocation']; +export type ImageScaleInvocation = S['ImageScaleInvocation']; +export type InfillPatchMatchInvocation = S['InfillPatchMatchInvocation']; +export type InfillTileInvocation = S['InfillTileInvocation']; +export type CreateDenoiseMaskInvocation = S['CreateDenoiseMaskInvocation']; +export type MaskEdgeInvocation = S['MaskEdgeInvocation']; +export type RandomIntInvocation = S['RandomIntInvocation']; +export type CompelInvocation = S['CompelInvocation']; +export type DynamicPromptInvocation = S['DynamicPromptInvocation']; +export type NoiseInvocation = S['NoiseInvocation']; +export type DenoiseLatentsInvocation = S['DenoiseLatentsInvocation']; +export type SDXLLoraLoaderInvocation = S['SDXLLoraLoaderInvocation']; +export type ImageToLatentsInvocation = S['ImageToLatentsInvocation']; +export type LatentsToImageInvocation = S['LatentsToImageInvocation']; +export type ImageCollectionInvocation = S['ImageCollectionInvocation']; +export type MainModelLoaderInvocation = S['MainModelLoaderInvocation']; +export type LoraLoaderInvocation = S['LoraLoaderInvocation']; +export type ESRGANInvocation = S['ESRGANInvocation']; +export type DivideInvocation = S['DivideInvocation']; +export type ImageNSFWBlurInvocation = S['ImageNSFWBlurInvocation']; +export type ImageWatermarkInvocation = S['ImageWatermarkInvocation']; +export type SeamlessModeInvocation = S['SeamlessModeInvocation']; +export type MetadataInvocation = S['MetadataInvocation']; +export type CoreMetadataInvocation = S['CoreMetadataInvocation']; +export type MetadataItemInvocation = S['MetadataItemInvocation']; +export type MergeMetadataInvocation = S['MergeMetadataInvocation']; +export type IPAdapterMetadataField = S['IPAdapterMetadataField']; +export type T2IAdapterField = S['T2IAdapterField']; +export type LoRAMetadataField = S['LoRAMetadataField']; // ControlNet Nodes -export type ControlNetInvocation = s['ControlNetInvocation']; -export type T2IAdapterInvocation = s['T2IAdapterInvocation']; -export type IPAdapterInvocation = s['IPAdapterInvocation']; -export type CannyImageProcessorInvocation = s['CannyImageProcessorInvocation']; -export type ColorMapImageProcessorInvocation = s['ColorMapImageProcessorInvocation']; -export type ContentShuffleImageProcessorInvocation = s['ContentShuffleImageProcessorInvocation']; -export type DepthAnythingImageProcessorInvocation = s['DepthAnythingImageProcessorInvocation']; -export type HedImageProcessorInvocation = s['HedImageProcessorInvocation']; -export type LineartAnimeImageProcessorInvocation = s['LineartAnimeImageProcessorInvocation']; -export type LineartImageProcessorInvocation = s['LineartImageProcessorInvocation']; -export type MediapipeFaceProcessorInvocation = s['MediapipeFaceProcessorInvocation']; -export type MidasDepthImageProcessorInvocation = s['MidasDepthImageProcessorInvocation']; -export type MlsdImageProcessorInvocation = s['MlsdImageProcessorInvocation']; -export type NormalbaeImageProcessorInvocation = s['NormalbaeImageProcessorInvocation']; -export type DWOpenposeImageProcessorInvocation = s['DWOpenposeImageProcessorInvocation']; -export type PidiImageProcessorInvocation = s['PidiImageProcessorInvocation']; -export type ZoeDepthImageProcessorInvocation = s['ZoeDepthImageProcessorInvocation']; +export type ControlNetInvocation = S['ControlNetInvocation']; +export type T2IAdapterInvocation = S['T2IAdapterInvocation']; +export type IPAdapterInvocation = S['IPAdapterInvocation']; +export type CannyImageProcessorInvocation = S['CannyImageProcessorInvocation']; +export type ColorMapImageProcessorInvocation = S['ColorMapImageProcessorInvocation']; +export type ContentShuffleImageProcessorInvocation = S['ContentShuffleImageProcessorInvocation']; +export type DepthAnythingImageProcessorInvocation = S['DepthAnythingImageProcessorInvocation']; +export type HedImageProcessorInvocation = S['HedImageProcessorInvocation']; +export type LineartAnimeImageProcessorInvocation = S['LineartAnimeImageProcessorInvocation']; +export type LineartImageProcessorInvocation = S['LineartImageProcessorInvocation']; +export type MediapipeFaceProcessorInvocation = S['MediapipeFaceProcessorInvocation']; +export type MidasDepthImageProcessorInvocation = S['MidasDepthImageProcessorInvocation']; +export type MlsdImageProcessorInvocation = S['MlsdImageProcessorInvocation']; +export type NormalbaeImageProcessorInvocation = S['NormalbaeImageProcessorInvocation']; +export type DWOpenposeImageProcessorInvocation = S['DWOpenposeImageProcessorInvocation']; +export type PidiImageProcessorInvocation = S['PidiImageProcessorInvocation']; +export type ZoeDepthImageProcessorInvocation = S['ZoeDepthImageProcessorInvocation']; // Node Outputs -export type ImageOutput = s['ImageOutput']; -export type StringOutput = s['StringOutput']; -export type FloatOutput = s['FloatOutput']; -export type IntegerOutput = s['IntegerOutput']; -export type IterateInvocationOutput = s['IterateInvocationOutput']; -export type CollectInvocationOutput = s['CollectInvocationOutput']; -export type LatentsOutput = s['LatentsOutput']; -export type GraphInvocationOutput = s['GraphInvocationOutput']; +export type ImageOutput = S['ImageOutput']; +export type StringOutput = S['StringOutput']; +export type FloatOutput = S['FloatOutput']; +export type IntegerOutput = S['IntegerOutput']; +export type IterateInvocationOutput = S['IterateInvocationOutput']; +export type CollectInvocationOutput = S['CollectInvocationOutput']; +export type LatentsOutput = S['LatentsOutput']; +export type GraphInvocationOutput = S['GraphInvocationOutput']; // Post-image upload actions, controls workflows when images are uploaded From 7996d43af90433bcd99c2b76b5ab25e4c021a490 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:15:21 +1100 Subject: [PATCH 135/411] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 2012 +++++++---------- 1 file changed, 841 insertions(+), 1171 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 1599b310c9..3393e74d48 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -19,80 +19,14 @@ export type paths = { */ post: operations["parse_dynamicprompts"]; }; - "/api/v1/models/": { - /** - * List Models - * @description Gets a list of models - */ - get: operations["list_models"]; - }; - "/api/v1/models/{base_model}/{model_type}/{model_name}": { - /** - * Delete Model - * @description Delete Model - */ - delete: operations["del_model"]; - /** - * Update Model - * @description Update model contents with a new config. If the model name or base fields are changed, then the model is renamed. - */ - patch: operations["update_model"]; - }; - "/api/v1/models/import": { - /** - * Import Model - * @description Add a model using its local path, repo_id, or remote URL. Model characteristics will be probed and configured automatically - */ - post: operations["import_model"]; - }; - "/api/v1/models/add": { - /** - * Add Model - * @description Add a model using the configuration information appropriate for its type. Only local models can be added by path - */ - post: operations["add_model"]; - }; - "/api/v1/models/convert/{base_model}/{model_type}/{model_name}": { - /** - * Convert Model - * @description Convert a checkpoint model into a diffusers model, optionally saving to the indicated destination directory, or `models` if none. - */ - put: operations["convert_model"]; - }; - "/api/v1/models/search": { - /** Search For Models */ - get: operations["search_for_models"]; - }; - "/api/v1/models/ckpt_confs": { - /** - * List Ckpt Configs - * @description Return a list of the legacy checkpoint configuration files stored in `ROOT/configs/stable-diffusion`, relative to ROOT. - */ - get: operations["list_ckpt_configs"]; - }; - "/api/v1/models/sync": { - /** - * Sync To Config - * @description Call after making changes to models.yaml, autoimport directories or models directory to synchronize - * in-memory data structures with disk data structures. - */ - post: operations["sync_to_config"]; - }; - "/api/v1/models/merge/{base_model}": { - /** - * Merge Models - * @description Convert a checkpoint model into a diffusers model - */ - put: operations["merge_models"]; - }; - "/api/v1/model/record/": { + "/api/v2/models/": { /** * List Model Records * @description Get a list of models. */ get: operations["list_model_records"]; }; - "/api/v1/model/record/i/{key}": { + "/api/v2/models/i/{key}": { /** * Get Model Record * @description Get a model record @@ -112,50 +46,76 @@ export type paths = { */ patch: operations["update_model_record"]; }; - "/api/v1/model/record/meta": { + "/api/v2/models/summary": { /** * List Model Summary * @description Gets a page of model summary data. */ get: operations["list_model_summary"]; }; - "/api/v1/model/record/meta/i/{key}": { + "/api/v2/models/meta/i/{key}": { /** * Get Model Metadata * @description Get a model metadata object. */ get: operations["get_model_metadata"]; }; - "/api/v1/model/record/tags": { + "/api/v2/models/tags": { /** * List Tags * @description Get a unique set of all the model tags. */ get: operations["list_tags"]; }; - "/api/v1/model/record/tags/search": { + "/api/v2/models/tags/search": { /** * Search By Metadata Tags * @description Get a list of models. */ get: operations["search_by_metadata_tags"]; }; - "/api/v1/model/record/i/": { + "/api/v2/models/i/": { /** * Add Model Record * @description Add a model using the configuration information appropriate for its type. */ post: operations["add_model_record"]; }; - "/api/v1/model/record/import": { + "/api/v2/models/heuristic_import": { /** - * List Model Install Jobs - * @description Return list of model install jobs. + * Heuristic Import + * @description Install a model using a string identifier. + * + * `source` can be any of the following. + * + * 1. A path on the local filesystem ('C:\users\fred\model.safetensors') + * 2. A Url pointing to a single downloadable model file + * 3. A HuggingFace repo_id with any of the following formats: + * - model/name + * - model/name:fp16:vae + * - model/name::vae -- use default precision + * - model/name:fp16:path/to/model.safetensors + * - model/name::path/to/model.safetensors + * + * `config` is an optional dict containing model configuration values that will override + * the ones that are probed automatically. + * + * `access_token` is an optional access token for use with Urls that require + * authentication. + * + * Models will be downloaded, probed, configured and installed in a + * series of background threads. The return object has `status` attribute + * that can be used to monitor progress. + * + * See the documentation for `import_model_record` for more information on + * interpreting the job information returned by this route. */ - get: operations["list_model_install_jobs"]; + post: operations["heuristic_import_model"]; + }; + "/api/v2/models/install": { /** * Import Model - * @description Add a model using its local path, repo_id, or remote URL. + * @description Install a model using its local path, repo_id, or remote URL. * * Models will be downloaded, probed, configured and installed in a * series of background threads. The return object has `status` attribute @@ -166,32 +126,38 @@ export type paths = { * appropriate value: * * * To install a local path using LocalModelSource, pass a source of form: - * `{ + * ``` + * { * "type": "local", * "path": "/path/to/model", * "inplace": false - * }` - * The "inplace" flag, if true, will register the model in place in its - * current filesystem location. Otherwise, the model will be copied - * into the InvokeAI models directory. + * } + * ``` + * The "inplace" flag, if true, will register the model in place in its + * current filesystem location. Otherwise, the model will be copied + * into the InvokeAI models directory. * * * To install a HuggingFace repo_id using HFModelSource, pass a source of form: - * `{ + * ``` + * { * "type": "hf", * "repo_id": "stabilityai/stable-diffusion-2.0", * "variant": "fp16", * "subfolder": "vae", * "access_token": "f5820a918aaf01" - * }` - * The `variant`, `subfolder` and `access_token` fields are optional. + * } + * ``` + * The `variant`, `subfolder` and `access_token` fields are optional. * * * To install a remote model using an arbitrary URL, pass: - * `{ + * ``` + * { * "type": "url", * "url": "http://www.civitai.com/models/123456", * "access_token": "f5820a918aaf01" - * }` - * The `access_token` field is optonal + * } + * ``` + * The `access_token` field is optonal * * The model's configuration record will be probed and filled in * automatically. To override the default guesses, pass "metadata" @@ -200,26 +166,51 @@ export type paths = { * Installation occurs in the background. Either use list_model_install_jobs() * to poll for completion, or listen on the event bus for the following events: * - * "model_install_running" - * "model_install_completed" - * "model_install_error" + * * "model_install_running" + * * "model_install_completed" + * * "model_install_error" * * On successful completion, the event's payload will contain the field "key" * containing the installed ID of the model. On an error, the event's payload * will contain the fields "error_type" and "error" describing the nature of the * error and its traceback, respectively. */ - post: operations["import_model_record"]; + post: operations["import_model"]; + }; + "/api/v2/models/import": { + /** + * List Model Install Jobs + * @description Return the list of model install jobs. + * + * Install jobs have a numeric `id`, a `status`, and other fields that provide information on + * the nature of the job and its progress. The `status` is one of: + * + * * "waiting" -- Job is waiting in the queue to run + * * "downloading" -- Model file(s) are downloading + * * "running" -- Model has downloaded and the model probing and registration process is running + * * "completed" -- Installation completed successfully + * * "error" -- An error occurred. Details will be in the "error_type" and "error" fields. + * * "cancelled" -- Job was cancelled before completion. + * + * Once completed, information about the model such as its size, base + * model, type, and metadata can be retrieved from the `config_out` + * field. For multi-file models such as diffusers, information on individual files + * can be retrieved from `download_parts`. + * + * See the example and schema below for more information. + */ + get: operations["list_model_install_jobs"]; /** * Prune Model Install Jobs * @description Prune all completed and errored jobs from the install job list. */ patch: operations["prune_model_install_jobs"]; }; - "/api/v1/model/record/import/{id}": { + "/api/v2/models/import/{id}": { /** * Get Model Install Job - * @description Return model install job corresponding to the given source. + * @description Return model install job corresponding to the given source. See the documentation for 'List Model Install Jobs' + * for information on the format of the return value. */ get: operations["get_model_install_job"]; /** @@ -228,7 +219,7 @@ export type paths = { */ delete: operations["cancel_model_install_job"]; }; - "/api/v1/model/record/sync": { + "/api/v2/models/sync": { /** * Sync Models To Config * @description Traverse the models and autoimport directories. @@ -238,17 +229,29 @@ export type paths = { */ patch: operations["sync_models_to_config"]; }; - "/api/v1/model/record/merge": { + "/api/v2/models/convert/{key}": { + /** + * Convert Model + * @description Permanently convert a model into diffusers format, replacing the safetensors version. + * Note that during the conversion process the key and model hash will change. + * The return value is the model configuration for the converted model. + */ + put: operations["convert_model"]; + }; + "/api/v2/models/merge": { /** * Merge - * @description Merge diffusers models. - * - * keys: List of 2-3 model keys to merge together. All models must use the same base type. - * merged_model_name: Name for the merged model [Concat model names] - * alpha: Alpha value (0.0-1.0). Higher values give more weight to the second model [0.5] - * force: If true, force the merge even if the models were generated by different versions of the diffusers library [False] - * interp: Interpolation method. One of "weighted_sum", "sigmoid", "inv_sigmoid" or "add_difference" [weighted_sum] - * merge_dest_directory: Specify a directory to store the merged model in [models directory] + * @description Merge diffusers models. The process is controlled by a set parameters provided in the body of the request. + * ``` + * Argument Description [default] + * -------- ---------------------- + * keys List of 2-3 model keys to merge together. All models must use the same base type. + * merged_model_name Name for the merged model [Concat model names] + * alpha Alpha value (0.0-1.0). Higher values give more weight to the second model [0.5] + * force If true, force the merge even if the models were generated by different versions of the diffusers library [False] + * interp Interpolation method. One of "weighted_sum", "sigmoid", "inv_sigmoid" or "add_difference" [weighted_sum] + * merge_dest_directory Specify a directory to store the merged model in [models directory] + * ``` */ put: operations["merge"]; }; @@ -815,6 +818,12 @@ export type components = { */ type?: "basemetadata"; }; + /** + * BaseModelType + * @description Base model type. + * @enum {string} + */ + BaseModelType: "any" | "sd-1" | "sd-2" | "sdxl" | "sdxl-refiner"; /** Batch */ Batch: { /** @@ -1163,19 +1172,6 @@ export type components = { }; /** Body_import_model */ Body_import_model: { - /** - * Location - * @description A model path, repo_id or URL to import - */ - location: string; - /** - * Prediction Type - * @description Prediction type for SDv2 checkpoints and rare SDv1 checkpoints - */ - prediction_type?: ("v_prediction" | "epsilon" | "sample") | null; - }; - /** Body_import_model_record */ - Body_import_model_record: { /** Source */ source: components["schemas"]["LocalModelSource"] | components["schemas"]["HFModelSource"] | components["schemas"]["CivitaiModelSource"] | components["schemas"]["URLModelSource"]; /** @@ -1216,11 +1212,6 @@ export type components = { */ merge_dest_directory?: string | null; }; - /** Body_merge_models */ - Body_merge_models: { - /** @description Model configuration */ - body: components["schemas"]["MergeModelsBody"]; - }; /** Body_parse_dynamicprompts */ Body_parse_dynamicprompts: { /** @@ -1412,11 +1403,18 @@ export type components = { * @description Model config for ClipVision. */ CLIPVisionDiffusersConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default clip_vision @@ -1444,45 +1442,29 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; - }; - /** CLIPVisionModelDiffusersConfig */ - CLIPVisionModelDiffusersConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; /** - * Model Type - * @default clip_vision - * @constant + * Last Modified + * @description timestamp for modification time */ - model_type: "clip_vision"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "diffusers"; - error?: components["schemas"]["ModelError"] | null; + last_modified?: number | null; }; /** CLIPVisionModelField */ CLIPVisionModelField: { /** - * Model Name - * @description Name of the CLIP Vision image encoder model + * Key + * @description Key to the CLIP Vision image encoder model */ - model_name: string; - /** @description Base model (usually 'Any') */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + key: string; }; /** * CV2 Infill @@ -2538,11 +2520,18 @@ export type components = { * @description Model config for ControlNet models (diffusers version). */ ControlNetCheckpointConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default controlnet @@ -2571,13 +2560,21 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; /** * Config * @description path to the checkpoint model config file @@ -2589,11 +2586,18 @@ export type components = { * @description Model config for ControlNet models (diffusers version). */ ControlNetDiffusersConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default controlnet @@ -2622,13 +2626,23 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; + /** @default */ + repo_variant?: components["schemas"]["ModelRepoVariant"] | null; }; /** * ControlNet @@ -2695,64 +2709,16 @@ export type components = { */ type: "controlnet"; }; - /** ControlNetModelCheckpointConfig */ - ControlNetModelCheckpointConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default controlnet - * @constant - */ - model_type: "controlnet"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "checkpoint"; - error?: components["schemas"]["ModelError"] | null; - /** Config */ - config: string; - }; - /** ControlNetModelDiffusersConfig */ - ControlNetModelDiffusersConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default controlnet - * @constant - */ - model_type: "controlnet"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "diffusers"; - error?: components["schemas"]["ModelError"] | null; - }; /** * ControlNetModelField * @description ControlNet model field */ ControlNetModelField: { /** - * Model Name - * @description Name of the ControlNet model + * Key + * @description Model config record key for the ControlNet model */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + key: string; }; /** * ControlOutput @@ -4246,7 +4212,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["ImageCropInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["IdealSizeInvocation"]; + [key: string]: components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"]; }; /** * Edges @@ -4283,7 +4249,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["MetadataOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ClipSkipInvocationOutput"]; + [key: string]: components["schemas"]["ImageCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["CLIPOutput"]; }; /** * Errors @@ -4477,11 +4443,18 @@ export type components = { * @description Model config for IP Adaptor format models. */ IPAdapterConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default ip_adapter @@ -4509,13 +4482,23 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; + /** Image Encoder Model Id */ + image_encoder_model_id: string; }; /** IPAdapterField */ IPAdapterField: { @@ -4632,34 +4615,10 @@ export type components = { /** IPAdapterModelField */ IPAdapterModelField: { /** - * Model Name - * @description Name of the IP-Adapter model + * Key + * @description Key to the IP-Adapter model */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - }; - /** IPAdapterModelInvokeAIConfig */ - IPAdapterModelInvokeAIConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default ip_adapter - * @constant - */ - model_type: "ip_adapter"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "invokeai"; - error?: components["schemas"]["ModelError"] | null; + key: string; }; /** IPAdapterOutput */ IPAdapterOutput: { @@ -6562,11 +6521,18 @@ export type components = { * @description Model config for LoRA/Lycoris models. */ LoRAConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default lora @@ -6594,13 +6560,21 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; }; /** * LoRAMetadataField @@ -6615,42 +6589,17 @@ export type components = { */ weight: number; }; - /** LoRAModelConfig */ - LoRAModelConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default lora - * @constant - */ - model_type: "lora"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - model_format: components["schemas"]["LoRAModelFormat"]; - error?: components["schemas"]["ModelError"] | null; - }; /** * LoRAModelField * @description LoRA model field */ LoRAModelField: { /** - * Model Name - * @description Name of the LoRA model + * Key + * @description LoRA model key */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + key: string; }; - /** - * LoRAModelFormat - * @enum {string} - */ - LoRAModelFormat: "lycoris" | "diffusers"; /** * LocalModelSource * @description A local file or directory path. @@ -6678,16 +6627,12 @@ export type components = { /** LoraInfo */ LoraInfo: { /** - * Model Name - * @description Info to load submodel + * Key + * @description Key of model as returned by ModelRecordServiceBase.get_model() */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + key: string; /** @description Info to load submodel */ - model_type: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"]; - /** @description Info to load submodel */ - submodel?: components["schemas"]["SubModelType"] | null; + submodel_type?: components["schemas"]["SubModelType"] | null; /** * Weight * @description Lora's weight which to use when apply to model @@ -6771,11 +6716,18 @@ export type components = { * @description Model config for main checkpoint models. */ MainCheckpointConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default main @@ -6804,17 +6756,32 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; /** Vae */ vae?: string | null; /** @default normal */ - variant?: components["schemas"]["invokeai__backend__model_manager__config__ModelVariantType"]; + variant?: components["schemas"]["ModelVariantType"]; + /** @default epsilon */ + prediction_type?: components["schemas"]["SchedulerPredictionType"]; + /** + * Upcast Attention + * @default false + */ + upcast_attention?: boolean; /** * Ztsnr Training * @default false @@ -6831,11 +6798,18 @@ export type components = { * @description Model config for main diffusers models. */ MainDiffusersConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default main @@ -6864,29 +6838,39 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; /** Vae */ vae?: string | null; /** @default normal */ - variant?: components["schemas"]["invokeai__backend__model_manager__config__ModelVariantType"]; - /** - * Ztsnr Training - * @default false - */ - ztsnr_training?: boolean; + variant?: components["schemas"]["ModelVariantType"]; /** @default epsilon */ - prediction_type?: components["schemas"]["invokeai__backend__model_manager__config__SchedulerPredictionType"]; + prediction_type?: components["schemas"]["SchedulerPredictionType"]; /** * Upcast Attention * @default false */ upcast_attention?: boolean; + /** + * Ztsnr Training + * @default false + */ + ztsnr_training?: boolean; + /** @default */ + repo_variant?: components["schemas"]["ModelRepoVariant"] | null; }; /** * MainModelField @@ -6894,14 +6878,10 @@ export type components = { */ MainModelField: { /** - * Model Name - * @description Name of the model + * Key + * @description Model key */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** @description Model Type */ - model_type: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"]; + key: string; }; /** * Main Model @@ -7153,38 +7133,6 @@ export type components = { */ type: "merge_metadata"; }; - /** MergeModelsBody */ - MergeModelsBody: { - /** - * Model Names - * @description model name - */ - model_names: string[]; - /** - * Merged Model Name - * @description Name of destination model - */ - merged_model_name: string | null; - /** - * Alpha - * @description Alpha weighting strength to apply to 2d and 3d models - * @default 0.5 - */ - alpha?: number | null; - /** @description Interpolation method */ - interp: components["schemas"]["MergeInterpolationMethod"] | null; - /** - * Force - * @description Force merging of models created with different versions of diffusers - * @default false - */ - force?: boolean | null; - /** - * Merge Dest Directory - * @description Save the merged model to the designated directory (with 'merged_model_name' appended) - */ - merge_dest_directory?: string | null; - }; /** * Merge Tiles to Image * @description Merge multiple tile images into a single image. @@ -7459,11 +7407,6 @@ export type components = { */ type: "mlsd_image_processor"; }; - /** - * ModelError - * @constant - */ - ModelError: "not_found"; /** * ModelFormat * @description Storage format of model. @@ -7473,16 +7416,12 @@ export type components = { /** ModelInfo */ ModelInfo: { /** - * Model Name - * @description Info to load submodel + * Key + * @description Key of model as returned by ModelRecordServiceBase.get_model() */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + key: string; /** @description Info to load submodel */ - model_type: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"]; - /** @description Info to load submodel */ - submodel?: components["schemas"]["SubModelType"] | null; + submodel_type?: components["schemas"]["SubModelType"] | null; }; /** * ModelInstallJob @@ -7508,7 +7447,7 @@ export type components = { * Config Out * @description After successful installation, this will hold the configuration object. */ - config_out?: (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"] | null; + config_out?: (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"] | null; /** * Inplace * @description Leave model in its current location; otherwise install under models directory @@ -7587,7 +7526,7 @@ export type components = { * @description Various hugging face variants on the diffusers format. * @enum {string} */ - ModelRepoVariant: "default" | "fp16" | "fp32" | "onnx" | "openvino" | "flax"; + ModelRepoVariant: "" | "fp16" | "fp32" | "onnx" | "openvino" | "flax"; /** * ModelSummary * @description A short summary of models for UI listing purposes. @@ -7599,9 +7538,9 @@ export type components = { */ key: string; /** @description model type */ - type: components["schemas"]["invokeai__backend__model_manager__config__ModelType"]; + type: components["schemas"]["ModelType"]; /** @description base model */ - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + base: components["schemas"]["BaseModelType"]; /** @description model format */ format: components["schemas"]["ModelFormat"]; /** @@ -7620,6 +7559,26 @@ export type components = { */ tags: string[]; }; + /** + * ModelType + * @description Model type. + * @enum {string} + */ + ModelType: "onnx" | "main" | "vae" | "lora" | "controlnet" | "embedding" | "ip_adapter" | "clip_vision" | "t2i_adapter"; + /** + * ModelVariantType + * @description Variant type. + * @enum {string} + */ + ModelVariantType: "normal" | "inpaint" | "depth"; + /** + * ModelsList + * @description Return list of configs. + */ + ModelsList: { + /** Models */ + models: ((components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"])[]; + }; /** * Multiply Integers * @description Multiplies two numbers @@ -7808,9 +7767,15 @@ export type components = { * @description Model config for ONNX format models based on sd-1. */ ONNXSD1Config: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; /** * Base @@ -7845,38 +7810,52 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; /** Vae */ vae?: string | null; /** @default normal */ - variant?: components["schemas"]["invokeai__backend__model_manager__config__ModelVariantType"]; - /** - * Ztsnr Training - * @default false - */ - ztsnr_training?: boolean; + variant?: components["schemas"]["ModelVariantType"]; /** @default epsilon */ - prediction_type?: components["schemas"]["invokeai__backend__model_manager__config__SchedulerPredictionType"]; + prediction_type?: components["schemas"]["SchedulerPredictionType"]; /** * Upcast Attention * @default false */ upcast_attention?: boolean; + /** + * Ztsnr Training + * @default false + */ + ztsnr_training?: boolean; }; /** * ONNXSD2Config * @description Model config for ONNX format models based on sd-2. */ ONNXSD2Config: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; /** * Base @@ -7911,78 +7890,117 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; /** Vae */ vae?: string | null; /** @default normal */ - variant?: components["schemas"]["invokeai__backend__model_manager__config__ModelVariantType"]; - /** - * Ztsnr Training - * @default false - */ - ztsnr_training?: boolean; + variant?: components["schemas"]["ModelVariantType"]; /** @default v_prediction */ - prediction_type?: components["schemas"]["invokeai__backend__model_manager__config__SchedulerPredictionType"]; + prediction_type?: components["schemas"]["SchedulerPredictionType"]; /** * Upcast Attention * @default true */ upcast_attention?: boolean; - }; - /** ONNXStableDiffusion1ModelConfig */ - ONNXStableDiffusion1ModelConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; /** - * Model Type + * Ztsnr Training + * @default false + */ + ztsnr_training?: boolean; + }; + /** + * ONNXSDXLConfig + * @description Model config for ONNX format models based on sdxl. + */ + ONNXSDXLConfig: { + /** + * Path + * @description filesystem path to the model file or directory + */ + path: string; + /** + * Name + * @description model name + */ + name: string; + /** + * Base + * @default sdxl + * @constant + */ + base?: "sdxl"; + /** + * Type * @default onnx * @constant */ - model_type: "onnx"; - /** Path */ - path: string; - /** Description */ + type?: "onnx"; + /** + * Format + * @enum {string} + */ + format: "onnx" | "olive"; + /** + * Key + * @description unique key for model + * @default + */ + key?: string; + /** + * Original Hash + * @description original fasthash of model contents + */ + original_hash?: string | null; + /** + * Current Hash + * @description current fasthash of model contents + */ + current_hash?: string | null; + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** - * Model Format - * @constant + * Source + * @description model original source (path, URL or repo_id) */ - model_format: "onnx"; - error?: components["schemas"]["ModelError"] | null; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - }; - /** ONNXStableDiffusion2ModelConfig */ - ONNXStableDiffusion2ModelConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + source?: string | null; /** - * Model Type - * @default onnx - * @constant + * Last Modified + * @description timestamp for modification time */ - model_type: "onnx"; - /** Path */ - path: string; - /** Description */ - description?: string | null; + last_modified?: number | null; + /** Vae */ + vae?: string | null; + /** @default normal */ + variant?: components["schemas"]["ModelVariantType"]; + /** @default v_prediction */ + prediction_type?: components["schemas"]["SchedulerPredictionType"]; /** - * Model Format - * @constant + * Upcast Attention + * @default false */ - model_format: "onnx"; - error?: components["schemas"]["ModelError"] | null; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - prediction_type: components["schemas"]["invokeai__backend__model_management__models__base__SchedulerPredictionType"]; - /** Upcast Attention */ - upcast_attention: boolean; + upcast_attention?: boolean; + /** + * Ztsnr Training + * @default false + */ + ztsnr_training?: boolean; }; /** OffsetPaginatedResults[BoardDTO] */ OffsetPaginatedResults_BoardDTO_: { @@ -9119,6 +9137,12 @@ export type components = { */ type: "scheduler_output"; }; + /** + * SchedulerPredictionType + * @description Scheduler prediction type. + * @enum {string} + */ + SchedulerPredictionType: "epsilon" | "v_prediction" | "sample"; /** * Seamless * @description Applies the seamless transformation to the Model UNet and VAE. @@ -9468,162 +9492,6 @@ export type components = { */ type: "show_image"; }; - /** StableDiffusion1ModelCheckpointConfig */ - StableDiffusion1ModelCheckpointConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default main - * @constant - */ - model_type: "main"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "checkpoint"; - error?: components["schemas"]["ModelError"] | null; - /** Vae */ - vae?: string | null; - /** Config */ - config: string; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - }; - /** StableDiffusion1ModelDiffusersConfig */ - StableDiffusion1ModelDiffusersConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default main - * @constant - */ - model_type: "main"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "diffusers"; - error?: components["schemas"]["ModelError"] | null; - /** Vae */ - vae?: string | null; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - }; - /** StableDiffusion2ModelCheckpointConfig */ - StableDiffusion2ModelCheckpointConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default main - * @constant - */ - model_type: "main"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "checkpoint"; - error?: components["schemas"]["ModelError"] | null; - /** Vae */ - vae?: string | null; - /** Config */ - config: string; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - }; - /** StableDiffusion2ModelDiffusersConfig */ - StableDiffusion2ModelDiffusersConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default main - * @constant - */ - model_type: "main"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "diffusers"; - error?: components["schemas"]["ModelError"] | null; - /** Vae */ - vae?: string | null; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - }; - /** StableDiffusionXLModelCheckpointConfig */ - StableDiffusionXLModelCheckpointConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default main - * @constant - */ - model_type: "main"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "checkpoint"; - error?: components["schemas"]["ModelError"] | null; - /** Vae */ - vae?: string | null; - /** Config */ - config: string; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - }; - /** StableDiffusionXLModelDiffusersConfig */ - StableDiffusionXLModelDiffusersConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default main - * @constant - */ - model_type: "main"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "diffusers"; - error?: components["schemas"]["ModelError"] | null; - /** Vae */ - vae?: string | null; - variant: components["schemas"]["invokeai__backend__model_management__models__base__ModelVariantType"]; - }; /** * Step Param Easing * @description Experimental per-step parameter easing for denoising steps @@ -10079,6 +9947,7 @@ export type components = { }; /** * SubModelType + * @description Submodel type. * @enum {string} */ SubModelType: "unet" | "text_encoder" | "text_encoder_2" | "tokenizer" | "tokenizer_2" | "vae" | "vae_decoder" | "vae_encoder" | "scheduler" | "safety_checker"; @@ -10216,37 +10085,13 @@ export type components = { */ type: "t2i_adapter"; }; - /** T2IAdapterModelDiffusersConfig */ - T2IAdapterModelDiffusersConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default t2i_adapter - * @constant - */ - model_type: "t2i_adapter"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** - * Model Format - * @constant - */ - model_format: "diffusers"; - error?: components["schemas"]["ModelError"] | null; - }; /** T2IAdapterModelField */ T2IAdapterModelField: { /** - * Model Name - * @description Name of the T2I-Adapter model + * Key + * @description Model record key for the T2I-Adapter model */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + key: string; }; /** T2IAdapterOutput */ T2IAdapterOutput: { @@ -10267,11 +10112,18 @@ export type components = { * @description Model config for T2I. */ T2IConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default t2i_adapter @@ -10299,13 +10151,21 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; }; /** TBLR */ TBLR: { @@ -10323,11 +10183,18 @@ export type components = { * @description Model config for textual inversion embeddings. */ TextualInversionConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default embedding @@ -10355,32 +10222,21 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; - }; - /** TextualInversionModelConfig */ - TextualInversionModelConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; /** - * Model Type - * @default embedding - * @constant + * Last Modified + * @description timestamp for modification time */ - model_type: "embedding"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - /** Model Format */ - model_format: null; - error?: components["schemas"]["ModelError"] | null; + last_modified?: number | null; }; /** Tile */ Tile: { @@ -10546,7 +10402,7 @@ export type components = { }; /** * UNetOutput - * @description Base class for invocations that output a UNet field + * @description Base class for invocations that output a UNet field. */ UNetOutput: { /** @@ -10646,12 +10502,10 @@ export type components = { */ VAEModelField: { /** - * Model Name - * @description Name of the model + * Key + * @description Model's key */ - model_name: string; - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; + key: string; }; /** * VAEOutput @@ -10675,11 +10529,18 @@ export type components = { * @description Model config for standalone VAE models. */ VaeCheckpointConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default vae @@ -10708,24 +10569,39 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; }; /** * VaeDiffusersConfig * @description Model config for standalone VAE models (diffusers version). */ VaeDiffusersConfig: { - /** Path */ + /** + * Path + * @description filesystem path to the model file or directory + */ path: string; - /** Name */ + /** + * Name + * @description model name + */ name: string; - base: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"]; + /** @description base model */ + base: components["schemas"]["BaseModelType"]; /** * Type * @default vae @@ -10754,13 +10630,21 @@ export type components = { * @description current fasthash of model contents */ current_hash?: string | null; - /** Description */ + /** + * Description + * @description human readable description of the model + */ description?: string | null; /** * Source - * @description Model download source (URL or repo_id) + * @description model original source (path, URL or repo_id) */ source?: string | null; + /** + * Last Modified + * @description timestamp for modification time + */ + last_modified?: number | null; }; /** VaeField */ VaeField: { @@ -10806,29 +10690,6 @@ export type components = { */ type: "vae_loader"; }; - /** VaeModelConfig */ - VaeModelConfig: { - /** Model Name */ - model_name: string; - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** - * Model Type - * @default vae - * @constant - */ - model_type: "vae"; - /** Path */ - path: string; - /** Description */ - description?: string | null; - model_format: components["schemas"]["VaeModelFormat"]; - error?: components["schemas"]["ModelError"] | null; - }; - /** - * VaeModelFormat - * @enum {string} - */ - VaeModelFormat: "checkpoint" | "diffusers"; /** ValidationError */ ValidationError: { /** Location */ @@ -11085,63 +10946,6 @@ export type components = { */ type: "zoe_depth_image_processor"; }; - /** - * ModelsList - * @description Return list of configs. - */ - invokeai__app__api__routers__model_records__ModelsList: { - /** Models */ - models: ((components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"])[]; - }; - /** ModelsList */ - invokeai__app__api__routers__models__ModelsList: { - /** Models */ - models: (components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"])[]; - }; - /** - * BaseModelType - * @enum {string} - */ - invokeai__backend__model_management__models__base__BaseModelType: "any" | "sd-1" | "sd-2" | "sdxl" | "sdxl-refiner"; - /** - * ModelType - * @enum {string} - */ - invokeai__backend__model_management__models__base__ModelType: "onnx" | "main" | "vae" | "lora" | "controlnet" | "embedding" | "ip_adapter" | "clip_vision" | "t2i_adapter"; - /** - * ModelVariantType - * @enum {string} - */ - invokeai__backend__model_management__models__base__ModelVariantType: "normal" | "inpaint" | "depth"; - /** - * SchedulerPredictionType - * @enum {string} - */ - invokeai__backend__model_management__models__base__SchedulerPredictionType: "epsilon" | "v_prediction" | "sample"; - /** - * BaseModelType - * @description Base model type. - * @enum {string} - */ - invokeai__backend__model_manager__config__BaseModelType: "any" | "sd-1" | "sd-2" | "sdxl" | "sdxl-refiner"; - /** - * ModelType - * @description Model type. - * @enum {string} - */ - invokeai__backend__model_manager__config__ModelType: "onnx" | "main" | "vae" | "lora" | "controlnet" | "embedding" | "ip_adapter" | "clip_vision" | "t2i_adapter"; - /** - * ModelVariantType - * @description Variant type. - * @enum {string} - */ - invokeai__backend__model_manager__config__ModelVariantType: "normal" | "inpaint" | "depth"; - /** - * SchedulerPredictionType - * @description Scheduler prediction type. - * @enum {string} - */ - invokeai__backend__model_manager__config__SchedulerPredictionType: "epsilon" | "v_prediction" | "sample"; /** * Classification * @description The classification of an Invocation. @@ -11309,53 +11113,65 @@ export type components = { */ UIType: "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_MainModel" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; /** - * StableDiffusionXLModelFormat + * VaeModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; - /** - * T2IAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - T2IAdapterModelFormat: "diffusers"; - /** - * CLIPVisionModelFormat - * @description An enumeration. - * @enum {string} - */ - CLIPVisionModelFormat: "diffusers"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion1ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * IPAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - IPAdapterModelFormat: "invokeai"; + VaeModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusion2ModelFormat * @description An enumeration. * @enum {string} */ StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + /** + * CLIPVisionModelFormat + * @description An enumeration. + * @enum {string} + */ + CLIPVisionModelFormat: "diffusers"; + /** + * StableDiffusionXLModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion1ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; + /** + * ControlNetModelFormat + * @description An enumeration. + * @enum {string} + */ + ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. * @enum {string} */ StableDiffusionOnnxModelFormat: "olive" | "onnx"; + /** + * T2IAdapterModelFormat + * @description An enumeration. + * @enum {string} + */ + T2IAdapterModelFormat: "diffusers"; + /** + * LoRAModelFormat + * @description An enumeration. + * @enum {string} + */ + LoRAModelFormat: "lycoris" | "diffusers"; + /** + * IPAdapterModelFormat + * @description An enumeration. + * @enum {string} + */ + IPAdapterModelFormat: "invokeai"; }; responses: never; parameters: never; @@ -11425,328 +11241,6 @@ export type operations = { }; }; }; - /** - * List Models - * @description Gets a list of models - */ - list_models: { - parameters: { - query?: { - /** @description Base models to include */ - base_models?: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"][] | null; - /** @description The type of model to get */ - model_type?: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"] | null; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - content: { - "application/json": components["schemas"]["invokeai__app__api__routers__models__ModelsList"]; - }; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - /** - * Delete Model - * @description Delete Model - */ - del_model: { - parameters: { - path: { - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** @description The type of model */ - model_type: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"]; - /** @description model name */ - model_name: string; - }; - }; - responses: { - /** @description Model deleted successfully */ - 204: { - content: never; - }; - /** @description Model not found */ - 404: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - /** - * Update Model - * @description Update model contents with a new config. If the model name or base fields are changed, then the model is renamed. - */ - update_model: { - parameters: { - path: { - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** @description The type of model */ - model_type: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"]; - /** @description model name */ - model_name: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"]; - }; - }; - responses: { - /** @description The model was updated successfully */ - 200: { - content: { - "application/json": components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"]; - }; - }; - /** @description Bad request */ - 400: { - content: never; - }; - /** @description The model could not be found */ - 404: { - content: never; - }; - /** @description There is already a model corresponding to the new name */ - 409: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - /** - * Import Model - * @description Add a model using its local path, repo_id, or remote URL. Model characteristics will be probed and configured automatically - */ - import_model: { - requestBody: { - content: { - "application/json": components["schemas"]["Body_import_model"]; - }; - }; - responses: { - /** @description The model imported successfully */ - 201: { - content: { - "application/json": components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"]; - }; - }; - /** @description The model could not be found */ - 404: { - content: never; - }; - /** @description There is already a model corresponding to this path or repo_id */ - 409: { - content: never; - }; - /** @description Unrecognized file/folder format */ - 415: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - /** @description The model appeared to import successfully, but could not be found in the model manager */ - 424: { - content: never; - }; - }; - }; - /** - * Add Model - * @description Add a model using the configuration information appropriate for its type. Only local models can be added by path - */ - add_model: { - requestBody: { - content: { - "application/json": components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"]; - }; - }; - responses: { - /** @description The model added successfully */ - 201: { - content: { - "application/json": components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"]; - }; - }; - /** @description The model could not be found */ - 404: { - content: never; - }; - /** @description There is already a model corresponding to this path or repo_id */ - 409: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - /** @description The model appeared to add successfully, but could not be found in the model manager */ - 424: { - content: never; - }; - }; - }; - /** - * Convert Model - * @description Convert a checkpoint model into a diffusers model, optionally saving to the indicated destination directory, or `models` if none. - */ - convert_model: { - parameters: { - query?: { - /** @description Save the converted model to the designated directory */ - convert_dest_directory?: string | null; - }; - path: { - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - /** @description The type of model */ - model_type: components["schemas"]["invokeai__backend__model_management__models__base__ModelType"]; - /** @description model name */ - model_name: string; - }; - }; - responses: { - /** @description Model converted successfully */ - 200: { - content: { - "application/json": components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"]; - }; - }; - /** @description Bad request */ - 400: { - content: never; - }; - /** @description Model not found */ - 404: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - /** Search For Models */ - search_for_models: { - parameters: { - query: { - /** @description Directory path to search for models */ - search_path: string; - }; - }; - responses: { - /** @description Directory searched successfully */ - 200: { - content: { - "application/json": string[]; - }; - }; - /** @description Invalid directory path */ - 404: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - /** - * List Ckpt Configs - * @description Return a list of the legacy checkpoint configuration files stored in `ROOT/configs/stable-diffusion`, relative to ROOT. - */ - list_ckpt_configs: { - responses: { - /** @description paths retrieved successfully */ - 200: { - content: { - "application/json": string[]; - }; - }; - }; - }; - /** - * Sync To Config - * @description Call after making changes to models.yaml, autoimport directories or models directory to synchronize - * in-memory data structures with disk data structures. - */ - sync_to_config: { - responses: { - /** @description synchronization successful */ - 201: { - content: { - "application/json": boolean; - }; - }; - }; - }; - /** - * Merge Models - * @description Convert a checkpoint model into a diffusers model - */ - merge_models: { - parameters: { - path: { - /** @description Base model */ - base_model: components["schemas"]["invokeai__backend__model_management__models__base__BaseModelType"]; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["Body_merge_models"]; - }; - }; - responses: { - /** @description Model converted successfully */ - 200: { - content: { - "application/json": components["schemas"]["ONNXStableDiffusion1ModelConfig"] | components["schemas"]["StableDiffusion1ModelCheckpointConfig"] | components["schemas"]["StableDiffusion1ModelDiffusersConfig"] | components["schemas"]["VaeModelConfig"] | components["schemas"]["LoRAModelConfig"] | components["schemas"]["ControlNetModelCheckpointConfig"] | components["schemas"]["ControlNetModelDiffusersConfig"] | components["schemas"]["TextualInversionModelConfig"] | components["schemas"]["IPAdapterModelInvokeAIConfig"] | components["schemas"]["CLIPVisionModelDiffusersConfig"] | components["schemas"]["T2IAdapterModelDiffusersConfig"] | components["schemas"]["ONNXStableDiffusion2ModelConfig"] | components["schemas"]["StableDiffusion2ModelCheckpointConfig"] | components["schemas"]["StableDiffusion2ModelDiffusersConfig"] | components["schemas"]["StableDiffusionXLModelCheckpointConfig"] | components["schemas"]["StableDiffusionXLModelDiffusersConfig"]; - }; - }; - /** @description Incompatible models */ - 400: { - content: never; - }; - /** @description One or more models not found */ - 404: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; /** * List Model Records * @description Get a list of models. @@ -11755,9 +11249,9 @@ export type operations = { parameters: { query?: { /** @description Base models to include */ - base_models?: components["schemas"]["invokeai__backend__model_manager__config__BaseModelType"][] | null; + base_models?: components["schemas"]["BaseModelType"][] | null; /** @description The type of model to get */ - model_type?: components["schemas"]["invokeai__backend__model_manager__config__ModelType"] | null; + model_type?: components["schemas"]["ModelType"] | null; /** @description Exact match on the name of the model */ model_name?: string | null; /** @description Exact match on the format of the model (e.g. 'diffusers') */ @@ -11768,7 +11262,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["invokeai__app__api__routers__model_records__ModelsList"]; + "application/json": components["schemas"]["ModelsList"]; }; }; /** @description Validation Error */ @@ -11791,10 +11285,10 @@ export type operations = { }; }; responses: { - /** @description Success */ + /** @description The model configuration was retrieved successfully */ 200: { content: { - "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; }; }; /** @description Bad request */ @@ -11857,14 +11351,26 @@ export type operations = { }; requestBody: { content: { - "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + /** + * @example { + * "path": "/path/to/model", + * "name": "model_name", + * "base": "sd-1", + * "type": "main", + * "format": "checkpoint", + * "config": "configs/stable-diffusion/v1-inference.yaml", + * "description": "Model description", + * "variant": "normal" + * } + */ + "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; }; }; responses: { /** @description The model was updated successfully */ 200: { content: { - "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; }; }; /** @description Bad request */ @@ -11929,7 +11435,7 @@ export type operations = { }; }; responses: { - /** @description Success */ + /** @description The model metadata was retrieved successfully */ 200: { content: { "application/json": (components["schemas"]["BaseMetadata"] | components["schemas"]["HuggingFaceMetadata"] | components["schemas"]["CivitaiMetadata"]) | null; @@ -11980,7 +11486,7 @@ export type operations = { /** @description Successful Response */ 200: { content: { - "application/json": components["schemas"]["invokeai__app__api__routers__model_records__ModelsList"]; + "application/json": components["schemas"]["ModelsList"]; }; }; /** @description Validation Error */ @@ -11998,14 +11504,26 @@ export type operations = { add_model_record: { requestBody: { content: { - "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + /** + * @example { + * "path": "/path/to/model", + * "name": "model_name", + * "base": "sd-1", + * "type": "main", + * "format": "checkpoint", + * "config": "configs/stable-diffusion/v1-inference.yaml", + * "description": "Model description", + * "variant": "normal" + * } + */ + "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; }; }; responses: { /** @description The model added successfully */ 201: { content: { - "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; }; }; /** @description There is already a model corresponding to this path or repo_id */ @@ -12025,79 +11543,49 @@ export type operations = { }; }; /** - * List Model Install Jobs - * @description Return list of model install jobs. - */ - list_model_install_jobs: { - responses: { - /** @description Successful Response */ - 200: { - content: { - "application/json": components["schemas"]["ModelInstallJob"][]; - }; - }; - }; - }; - /** - * Import Model - * @description Add a model using its local path, repo_id, or remote URL. + * Heuristic Import + * @description Install a model using a string identifier. + * + * `source` can be any of the following. + * + * 1. A path on the local filesystem ('C:\users\fred\model.safetensors') + * 2. A Url pointing to a single downloadable model file + * 3. A HuggingFace repo_id with any of the following formats: + * - model/name + * - model/name:fp16:vae + * - model/name::vae -- use default precision + * - model/name:fp16:path/to/model.safetensors + * - model/name::path/to/model.safetensors + * + * `config` is an optional dict containing model configuration values that will override + * the ones that are probed automatically. + * + * `access_token` is an optional access token for use with Urls that require + * authentication. * * Models will be downloaded, probed, configured and installed in a * series of background threads. The return object has `status` attribute * that can be used to monitor progress. * - * The source object is a discriminated Union of LocalModelSource, - * HFModelSource and URLModelSource. Set the "type" field to the - * appropriate value: - * - * * To install a local path using LocalModelSource, pass a source of form: - * `{ - * "type": "local", - * "path": "/path/to/model", - * "inplace": false - * }` - * The "inplace" flag, if true, will register the model in place in its - * current filesystem location. Otherwise, the model will be copied - * into the InvokeAI models directory. - * - * * To install a HuggingFace repo_id using HFModelSource, pass a source of form: - * `{ - * "type": "hf", - * "repo_id": "stabilityai/stable-diffusion-2.0", - * "variant": "fp16", - * "subfolder": "vae", - * "access_token": "f5820a918aaf01" - * }` - * The `variant`, `subfolder` and `access_token` fields are optional. - * - * * To install a remote model using an arbitrary URL, pass: - * `{ - * "type": "url", - * "url": "http://www.civitai.com/models/123456", - * "access_token": "f5820a918aaf01" - * }` - * The `access_token` field is optonal - * - * The model's configuration record will be probed and filled in - * automatically. To override the default guesses, pass "metadata" - * with a Dict containing the attributes you wish to override. - * - * Installation occurs in the background. Either use list_model_install_jobs() - * to poll for completion, or listen on the event bus for the following events: - * - * "model_install_running" - * "model_install_completed" - * "model_install_error" - * - * On successful completion, the event's payload will contain the field "key" - * containing the installed ID of the model. On an error, the event's payload - * will contain the fields "error_type" and "error" describing the nature of the - * error and its traceback, respectively. + * See the documentation for `import_model_record` for more information on + * interpreting the job information returned by this route. */ - import_model_record: { - requestBody: { + heuristic_import_model: { + parameters: { + query: { + source: string; + access_token?: string | null; + }; + }; + requestBody?: { content: { - "application/json": components["schemas"]["Body_import_model_record"]; + /** + * @example { + * "name": "modelT", + * "description": "antique cars" + * } + */ + "application/json": Record | null; }; }; responses: { @@ -12127,6 +11615,132 @@ export type operations = { }; }; }; + /** + * Import Model + * @description Install a model using its local path, repo_id, or remote URL. + * + * Models will be downloaded, probed, configured and installed in a + * series of background threads. The return object has `status` attribute + * that can be used to monitor progress. + * + * The source object is a discriminated Union of LocalModelSource, + * HFModelSource and URLModelSource. Set the "type" field to the + * appropriate value: + * + * * To install a local path using LocalModelSource, pass a source of form: + * ``` + * { + * "type": "local", + * "path": "/path/to/model", + * "inplace": false + * } + * ``` + * The "inplace" flag, if true, will register the model in place in its + * current filesystem location. Otherwise, the model will be copied + * into the InvokeAI models directory. + * + * * To install a HuggingFace repo_id using HFModelSource, pass a source of form: + * ``` + * { + * "type": "hf", + * "repo_id": "stabilityai/stable-diffusion-2.0", + * "variant": "fp16", + * "subfolder": "vae", + * "access_token": "f5820a918aaf01" + * } + * ``` + * The `variant`, `subfolder` and `access_token` fields are optional. + * + * * To install a remote model using an arbitrary URL, pass: + * ``` + * { + * "type": "url", + * "url": "http://www.civitai.com/models/123456", + * "access_token": "f5820a918aaf01" + * } + * ``` + * The `access_token` field is optonal + * + * The model's configuration record will be probed and filled in + * automatically. To override the default guesses, pass "metadata" + * with a Dict containing the attributes you wish to override. + * + * Installation occurs in the background. Either use list_model_install_jobs() + * to poll for completion, or listen on the event bus for the following events: + * + * * "model_install_running" + * * "model_install_completed" + * * "model_install_error" + * + * On successful completion, the event's payload will contain the field "key" + * containing the installed ID of the model. On an error, the event's payload + * will contain the fields "error_type" and "error" describing the nature of the + * error and its traceback, respectively. + */ + import_model: { + requestBody: { + content: { + "application/json": components["schemas"]["Body_import_model"]; + }; + }; + responses: { + /** @description The model imported successfully */ + 201: { + content: { + "application/json": components["schemas"]["ModelInstallJob"]; + }; + }; + /** @description There is already a model corresponding to this path or repo_id */ + 409: { + content: never; + }; + /** @description Unrecognized file/folder format */ + 415: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + /** @description The model appeared to import successfully, but could not be found in the model manager */ + 424: { + content: never; + }; + }; + }; + /** + * List Model Install Jobs + * @description Return the list of model install jobs. + * + * Install jobs have a numeric `id`, a `status`, and other fields that provide information on + * the nature of the job and its progress. The `status` is one of: + * + * * "waiting" -- Job is waiting in the queue to run + * * "downloading" -- Model file(s) are downloading + * * "running" -- Model has downloaded and the model probing and registration process is running + * * "completed" -- Installation completed successfully + * * "error" -- An error occurred. Details will be in the "error_type" and "error" fields. + * * "cancelled" -- Job was cancelled before completion. + * + * Once completed, information about the model such as its size, base + * model, type, and metadata can be retrieved from the `config_out` + * field. For multi-file models such as diffusers, information on individual files + * can be retrieved from `download_parts`. + * + * See the example and schema below for more information. + */ + list_model_install_jobs: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["ModelInstallJob"][]; + }; + }; + }; + }; /** * Prune Model Install Jobs * @description Prune all completed and errored jobs from the install job list. @@ -12151,7 +11765,8 @@ export type operations = { }; /** * Get Model Install Job - * @description Return model install job corresponding to the given source. + * @description Return model install job corresponding to the given source. See the documentation for 'List Model Install Jobs' + * for information on the format of the return value. */ get_model_install_job: { parameters: { @@ -12234,16 +11849,59 @@ export type operations = { }; }; }; + /** + * Convert Model + * @description Permanently convert a model into diffusers format, replacing the safetensors version. + * Note that during the conversion process the key and model hash will change. + * The return value is the model configuration for the converted model. + */ + convert_model: { + parameters: { + path: { + /** @description Unique key of the safetensors main model to convert to diffusers format. */ + key: string; + }; + }; + responses: { + /** @description Model converted successfully */ + 200: { + content: { + "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + }; + }; + /** @description Bad request */ + 400: { + content: never; + }; + /** @description Model not found */ + 404: { + content: never; + }; + /** @description There is already a model registered at this location */ + 409: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; /** * Merge - * @description Merge diffusers models. - * - * keys: List of 2-3 model keys to merge together. All models must use the same base type. - * merged_model_name: Name for the merged model [Concat model names] - * alpha: Alpha value (0.0-1.0). Higher values give more weight to the second model [0.5] - * force: If true, force the merge even if the models were generated by different versions of the diffusers library [False] - * interp: Interpolation method. One of "weighted_sum", "sigmoid", "inv_sigmoid" or "add_difference" [weighted_sum] - * merge_dest_directory: Specify a directory to store the merged model in [models directory] + * @description Merge diffusers models. The process is controlled by a set parameters provided in the body of the request. + * ``` + * Argument Description [default] + * -------- ---------------------- + * keys List of 2-3 model keys to merge together. All models must use the same base type. + * merged_model_name Name for the merged model [Concat model names] + * alpha Alpha value (0.0-1.0). Higher values give more weight to the second model [0.5] + * force If true, force the merge even if the models were generated by different versions of the diffusers library [False] + * interp Interpolation method. One of "weighted_sum", "sigmoid", "inv_sigmoid" or "add_difference" [weighted_sum] + * merge_dest_directory Specify a directory to store the merged model in [models directory] + * ``` */ merge: { requestBody: { @@ -12252,12 +11910,24 @@ export type operations = { }; }; responses: { - /** @description Successful Response */ + /** @description Model converted successfully */ 200: { content: { - "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; }; }; + /** @description Bad request */ + 400: { + content: never; + }; + /** @description Model not found */ + 404: { + content: never; + }; + /** @description There is already a model registered at this location */ + 409: { + content: never; + }; /** @description Validation Error */ 422: { content: { From cb804e75ed51ff7762be315e5857211f7fe44e35 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:16:11 +1100 Subject: [PATCH 136/411] tests(ui): enable vitest type testing This is useful for the zod schemas and types we have created to match the backend. --- invokeai/frontend/web/.gitignore | 3 +++ invokeai/frontend/web/package.json | 1 + invokeai/frontend/web/pnpm-lock.yaml | 7 +++++++ invokeai/frontend/web/vite.config.mts | 5 ++++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/.gitignore b/invokeai/frontend/web/.gitignore index 8e7ebc76a1..3e8a372bc7 100644 --- a/invokeai/frontend/web/.gitignore +++ b/invokeai/frontend/web/.gitignore @@ -41,3 +41,6 @@ stats.html # Yalc .yalc yalc.lock + +# vitest +tsconfig.vitest-temp.json \ No newline at end of file diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index b2838e538c..cea13350d2 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -154,6 +154,7 @@ "rollup-plugin-visualizer": "^5.12.0", "storybook": "^7.6.10", "ts-toolbelt": "^9.6.0", + "tsafe": "^1.6.6", "typescript": "^5.3.3", "vite": "^5.0.12", "vite-plugin-css-injected-by-js": "^3.3.1", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index f3bf68cf1d..0ec2e47a0c 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -300,6 +300,9 @@ devDependencies: ts-toolbelt: specifier: ^9.6.0 version: 9.6.0 + tsafe: + specifier: ^1.6.6 + version: 1.6.6 typescript: specifier: ^5.3.3 version: 5.3.3 @@ -13505,6 +13508,10 @@ packages: resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} dev: true + /tsafe@1.6.6: + resolution: {integrity: sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==} + dev: true + /tsconfck@3.0.1(typescript@5.3.3): resolution: {integrity: sha512-7ppiBlF3UEddCLeI1JRx5m2Ryq+xk4JrZuq4EuYXykipebaq1dV0Fhgr1hb7CkmHt32QSgOZlcqVLEtHBG4/mg==} engines: {node: ^18 || >=20} diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index 325c6467de..f4dbae7123 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -84,7 +84,10 @@ export default defineConfig(({ mode }) => { }, }, test: { - // + typecheck: { + enabled: true, + ignoreSourceErrors: true, + }, }, }; }); From fc107ed7113f1eca1ace560e2b1bcede17d65770 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:16:55 +1100 Subject: [PATCH 137/411] tests(ui): add type tests --- .../src/features/nodes/types/common.test-d.ts | 69 +++++++++++++++++++ .../features/nodes/types/workflow.test-d.ts | 18 +++++ 2 files changed, 87 insertions(+) create mode 100644 invokeai/frontend/web/src/features/nodes/types/common.test-d.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/workflow.test-d.ts diff --git a/invokeai/frontend/web/src/features/nodes/types/common.test-d.ts b/invokeai/frontend/web/src/features/nodes/types/common.test-d.ts new file mode 100644 index 0000000000..7f28e864a1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/common.test-d.ts @@ -0,0 +1,69 @@ +import type { + BaseModel, + BoardField, + Classification, + CLIPField, + ColorField, + ControlField, + ControlNetModelField, + ImageField, + ImageOutput, + IPAdapterField, + IPAdapterModelField, + LoraInfo, + LoRAModelField, + MainModelField, + ModelInfo, + ModelType, + ProgressImage, + SchedulerField, + SDXLRefinerModelField, + SubModelType, + T2IAdapterField, + T2IAdapterModelField, + UNetField, + VAEField, +} from 'features/nodes/types/common'; +import type { S } from 'services/api/types'; +import type { Equals, Extends } from 'tsafe'; +import { assert } from 'tsafe'; +import { describe, test } from 'vitest'; + +/** + * These types originate from the server and are recreated as zod schemas manually, for use at runtime. + * The tests ensure that the types are correctly recreated. + */ + +describe('Common types', () => { + // Complex field types + test('ImageField', () => assert>()); + test('BoardField', () => assert>()); + test('ColorField', () => assert>()); + test('SchedulerField', () => assert>>()); + test('UNetField', () => assert>()); + test('CLIPField', () => assert>()); + test('MainModelField', () => assert>()); + test('SDXLRefinerModelField', () => assert>()); + test('VAEField', () => assert>()); + test('ControlField', () => assert>()); + // @ts-expect-error TODO(psyche): fix types + test('IPAdapterField', () => assert>()); + test('T2IAdapterField', () => assert>()); + test('LoRAModelField', () => assert>()); + test('ControlNetModelField', () => assert>()); + test('IPAdapterModelField', () => assert>()); + test('T2IAdapterModelField', () => assert>()); + + // Model component types + test('BaseModel', () => assert>()); + test('ModelType', () => assert>()); + test('SubModelType', () => assert>()); + test('ModelInfo', () => assert>()); + + // Misc types + test('LoraInfo', () => assert>()); + // @ts-expect-error TODO(psyche): There is no `ProgressImage` in the server types yet + test('ProgressImage', () => assert>()); + test('ImageOutput', () => assert>()); + test('Classification', () => assert>()); +}); diff --git a/invokeai/frontend/web/src/features/nodes/types/workflow.test-d.ts b/invokeai/frontend/web/src/features/nodes/types/workflow.test-d.ts new file mode 100644 index 0000000000..7cb1ea230c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/workflow.test-d.ts @@ -0,0 +1,18 @@ +import type { WorkflowCategory, WorkflowV3, XYPosition } from 'features/nodes/types/workflow'; +import type * as ReactFlow from 'reactflow'; +import type { S } from 'services/api/types'; +import type { Equals, Extends } from 'tsafe'; +import { assert } from 'tsafe'; +import { describe, test } from 'vitest'; + +/** + * These types originate from the server and are recreated as zod schemas manually, for use at runtime. + * The tests ensure that the types are correctly recreated. + */ + +describe('Workflow types', () => { + test('XYPosition', () => assert>()); + test('WorkflowCategory', () => assert>()); + // @ts-expect-error TODO(psyche): Need to revise server types! + test('WorkflowV3', () => assert>()); +}); From b7ba65fef4f29c2e47e680c97d2d4552a0bfc90a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:17:16 +1100 Subject: [PATCH 138/411] fix(ui): update model types --- .../web/src/features/nodes/types/common.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index b524474379..ef579fce8c 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -52,27 +52,29 @@ export type SchedulerField = z.infer; // #region Model-related schemas export const zBaseModel = z.enum(['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']); -export const zModelType = z.enum(['main', 'vae', 'lora', 'controlnet', 'embedding']); +export const zModelType = z.enum([ + 'main', + 'vae', + 'lora', + 'controlnet', + 'embedding', + 'ip_adapter', + 'clip_vision', + 't2i_adapter', + 'onnx', // TODO(psyche): Remove this when removed from backend +]); export const zModelName = z.string().min(3); export const zModelIdentifier = z.object({ - model_name: zModelName, - base_model: zBaseModel, + key: z.string().min(1), }); export type BaseModel = z.infer; export type ModelType = z.infer; export type ModelIdentifier = z.infer; -export const zMainModelField = z.object({ - model_name: zModelName, - base_model: zBaseModel, - model_type: z.literal('main'), -}); -export const zSDXLRefinerModelField = z.object({ - model_name: z.string().min(1), - base_model: z.literal('sdxl-refiner'), - model_type: z.literal('main'), -}); +export const zMainModelField = zModelIdentifier; export type MainModelField = z.infer; + +export const zSDXLRefinerModelField = zModelIdentifier; export type SDXLRefinerModelField = z.infer; export const zSubModelType = z.enum([ @@ -92,8 +94,7 @@ export type SubModelType = z.infer; export const zVAEModelField = zModelIdentifier; export const zModelInfo = zModelIdentifier.extend({ - model_type: zModelType, - submodel: zSubModelType.optional(), + submodel_type: zSubModelType.nullish(), }); export type ModelInfo = z.infer; From 6df3c450e8955a4112b074eb6b9ef20d23e8350a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:51:47 +1100 Subject: [PATCH 139/411] fix(nodes): fix t2i adapter model loading --- invokeai/app/invocations/latent.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 5dd0eb074d..1f21b539dc 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -509,19 +509,20 @@ class DenoiseLatentsInvocation(BaseInvocation): t2i_adapter_data = [] for t2i_adapter_field in t2i_adapter: - t2i_adapter_model_info = context.models.load(key=t2i_adapter_field.t2i_adapter_model.key) + t2i_adapter_model_config = context.models.get_config(key=t2i_adapter_field.t2i_adapter_model.key) + t2i_adapter_loaded_model = context.models.load(key=t2i_adapter_field.t2i_adapter_model.key) image = context.images.get_pil(t2i_adapter_field.image.image_name) # The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally. - if t2i_adapter_model_info.base == BaseModelType.StableDiffusion1: + if t2i_adapter_model_config.base == BaseModelType.StableDiffusion1: max_unet_downscale = 8 - elif t2i_adapter_model_info.base == BaseModelType.StableDiffusionXL: + elif t2i_adapter_model_config.base == BaseModelType.StableDiffusionXL: max_unet_downscale = 4 else: - raise ValueError(f"Unexpected T2I-Adapter base model type: '{t2i_adapter_model_info.base}'.") + raise ValueError(f"Unexpected T2I-Adapter base model type: '{t2i_adapter_model_config.base}'.") t2i_adapter_model: T2IAdapter - with t2i_adapter_model_info as t2i_adapter_model: + with t2i_adapter_loaded_model as t2i_adapter_model: total_downscale_factor = t2i_adapter_model.total_downscale_factor # Resize the T2I-Adapter input image. From dab939f7d17a139a6652c0c525af61d5a900ef4c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:56:02 +1100 Subject: [PATCH 140/411] feat(ui): update model identifier to be key (wip) - Update most model identifiers to be `{key: string}` instead of name/base/type. Doesn't change the model select components yet. - Update model _parameters_, stored in redux, to be `{key: string, base: BaseModel}` - we need to store the base model to be able to check model compatibility. May want to store the whole config? Not sure... --- .../frontend/web/.storybook/ReduxInit.tsx | 8 +- .../listeners/enqueueRequestedLinear.ts | 2 +- .../listeners/modelSelected.ts | 10 +- .../listeners/modelsLoaded.ts | 43 +-- .../common/hooks/useGroupedModelCombobox.ts | 6 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 2 +- .../web/src/common/hooks/useModelCombobox.ts | 6 +- .../src/features/canvas/store/canvasSlice.ts | 2 +- .../parameters/ParamControlAdapterModel.tsx | 17 +- .../hooks/useAddControlAdapter.ts | 4 +- .../store/controlAdaptersSlice.ts | 6 +- .../features/embedding/EmbeddingSelect.tsx | 8 +- .../ImageMetadataActions.tsx | 14 +- .../src/features/lora/components/LoRACard.tsx | 2 +- .../src/features/lora/components/LoRAList.tsx | 2 +- .../features/lora/components/LoRASelect.tsx | 6 +- .../web/src/features/lora/store/loraSlice.ts | 35 ++- .../subpanels/ModelManagerPanel.tsx | 10 +- .../ModelManagerPanel/CheckpointModelEdit.tsx | 4 +- .../ModelManagerPanel/DiffusersModelEdit.tsx | 4 +- .../ModelManagerPanel/LoRAModelEdit.tsx | 14 +- .../subpanels/ModelManagerPanel/ModelList.tsx | 6 +- .../ModelManagerPanel/ModelListItem.tsx | 4 +- .../ControlNetModelFieldInputComponent.tsx | 4 +- .../IPAdapterModelFieldInputComponent.tsx | 4 +- .../inputs/LoRAModelFieldInputComponent.tsx | 4 +- .../inputs/MainModelFieldInputComponent.tsx | 4 +- .../RefinerModelFieldInputComponent.tsx | 4 +- .../SDXLMainModelFieldInputComponent.tsx | 4 +- .../T2IAdapterModelFieldInputComponent.tsx | 4 +- .../inputs/VAEModelFieldInputComponent.tsx | 4 +- .../web/src/features/nodes/types/common.ts | 16 +- .../util/graph/addControlNetToLinearGraph.ts | 2 +- .../util/graph/addIPAdapterToLinearGraph.ts | 2 +- .../nodes/util/graph/addLoRAsToGraph.ts | 9 +- .../nodes/util/graph/addSDXLLoRAstoGraph.ts | 9 +- .../util/graph/addT2IAdapterToLinearGraph.ts | 2 +- .../nodes/util/graph/buildCanvasGraph.ts | 8 +- .../util/graph/buildLinearBatchConfig.ts | 2 +- .../components/Advanced/ParamClipSkip.tsx | 6 +- .../components/Core/ParamPositivePrompt.tsx | 2 +- .../MainModel/ParamMainModelSelect.tsx | 4 +- .../VAEModel/ParamVAEModelSelect.tsx | 6 +- .../parameters/hooks/useRecallParameters.ts | 29 +- .../parameters/store/generationSlice.ts | 6 +- .../parameters/types/parameterSchemas.ts | 15 +- .../parameters/util/optimalDimension.ts | 6 +- .../ParamSDXLRefinerModelSelect.tsx | 6 +- .../AdvancedSettingsAccordion.tsx | 3 +- .../GenerationSettingsAccordion.tsx | 5 +- .../ImageSettingsAccordion.tsx | 2 +- .../ui/components/ParametersPanel.tsx | 2 +- .../web/src/services/api/endpoints/models.ts | 284 +++++------------- .../frontend/web/src/services/api/types.ts | 47 ++- 54 files changed, 267 insertions(+), 453 deletions(-) diff --git a/invokeai/frontend/web/.storybook/ReduxInit.tsx b/invokeai/frontend/web/.storybook/ReduxInit.tsx index 55d0132242..7d3f8e0d2b 100644 --- a/invokeai/frontend/web/.storybook/ReduxInit.tsx +++ b/invokeai/frontend/web/.storybook/ReduxInit.tsx @@ -10,13 +10,7 @@ export const ReduxInit = memo((props: PropsWithChildren) => { const dispatch = useAppDispatch(); useGlobalModifiersInit(); useEffect(() => { - dispatch( - modelChanged({ - model_name: 'test_model', - base_model: 'sd-1', - model_type: 'main', - }) - ); + dispatch(modelChanged({ key: 'test_model', base: 'sd-1' })); }, []); return props.children; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts index e1e13fadbe..d1cb692c98 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear.ts @@ -19,7 +19,7 @@ export const addEnqueueRequestedLinear = () => { let graph; - if (model && model.base_model === 'sdxl') { + if (model && model.base === 'sdxl') { if (action.payload.tabName === 'txt2img') { graph = buildLinearSDXLTextToImageGraph(state); } else { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts index 7638c5522a..35e2ad5f9b 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelSelected.ts @@ -30,8 +30,8 @@ export const addModelSelectedListener = () => { const newModel = result.data; - const newBaseModel = newModel.base_model; - const didBaseModelChange = state.generation.model?.base_model !== newBaseModel; + const newBaseModel = newModel.base; + const didBaseModelChange = state.generation.model?.base !== newBaseModel; if (didBaseModelChange) { // we may need to reset some incompatible submodels @@ -39,7 +39,7 @@ export const addModelSelectedListener = () => { // handle incompatible loras forEach(state.lora.loras, (lora, id) => { - if (lora.base_model !== newBaseModel) { + if (lora.base !== newBaseModel) { dispatch(loraRemoved(id)); modelsCleared += 1; } @@ -47,14 +47,14 @@ export const addModelSelectedListener = () => { // handle incompatible vae const { vae } = state.generation; - if (vae && vae.base_model !== newBaseModel) { + if (vae && vae.base !== newBaseModel) { dispatch(vaeSelected(null)); modelsCleared += 1; } // handle incompatible controlnets selectControlAdapterAll(state.controlAdapters).forEach((ca) => { - if (ca.model?.base_model !== newBaseModel) { + if (ca.model?.base !== newBaseModel) { dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false })); modelsCleared += 1; } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 0ffe88cd07..366644fa68 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -34,14 +34,7 @@ export const addModelsLoadedListener = () => { return; } - const isCurrentModelAvailable = currentModel - ? models.some( - (m) => - m.model_name === currentModel.model_name && - m.base_model === currentModel.base_model && - m.model_type === currentModel.model_type - ) - : false; + const isCurrentModelAvailable = currentModel ? models.some((m) => m.key === currentModel.key) : false; if (isCurrentModelAvailable) { return; @@ -74,14 +67,7 @@ export const addModelsLoadedListener = () => { return; } - const isCurrentModelAvailable = currentModel - ? models.some( - (m) => - m.model_name === currentModel.model_name && - m.base_model === currentModel.base_model && - m.model_type === currentModel.model_type - ) - : false; + const isCurrentModelAvailable = currentModel ? models.some((m) => m.key === currentModel.key) : false; if (!isCurrentModelAvailable) { dispatch(refinerModelChanged(null)); @@ -103,10 +89,7 @@ export const addModelsLoadedListener = () => { return; } - const isCurrentVAEAvailable = some( - action.payload.entities, - (m) => m?.model_name === currentVae?.model_name && m?.base_model === currentVae?.base_model - ); + const isCurrentVAEAvailable = some(action.payload.entities, (m) => m?.key === currentVae?.key); if (isCurrentVAEAvailable) { return; @@ -140,10 +123,7 @@ export const addModelsLoadedListener = () => { const loras = getState().lora.loras; forEach(loras, (lora, id) => { - const isLoRAAvailable = some( - action.payload.entities, - (m) => m?.model_name === lora?.model_name && m?.base_model === lora?.base_model - ); + const isLoRAAvailable = some(action.payload.entities, (m) => m?.key === lora?.key); if (isLoRAAvailable) { return; @@ -161,10 +141,7 @@ export const addModelsLoadedListener = () => { log.info({ models: action.payload.entities }, `ControlNet models loaded (${action.payload.ids.length})`); selectAllControlNets(getState().controlAdapters).forEach((ca) => { - const isModelAvailable = some( - action.payload.entities, - (m) => m?.model_name === ca?.model?.model_name && m?.base_model === ca?.model?.base_model - ); + const isModelAvailable = some(action.payload.entities, (m) => m?.key === ca?.model?.key); if (isModelAvailable) { return; @@ -182,10 +159,7 @@ export const addModelsLoadedListener = () => { log.info({ models: action.payload.entities }, `T2I Adapter models loaded (${action.payload.ids.length})`); selectAllT2IAdapters(getState().controlAdapters).forEach((ca) => { - const isModelAvailable = some( - action.payload.entities, - (m) => m?.model_name === ca?.model?.model_name && m?.base_model === ca?.model?.base_model - ); + const isModelAvailable = some(action.payload.entities, (m) => m?.key === ca?.model?.key); if (isModelAvailable) { return; @@ -203,10 +177,7 @@ export const addModelsLoadedListener = () => { log.info({ models: action.payload.entities }, `IP Adapter models loaded (${action.payload.ids.length})`); selectAllIPAdapters(getState().controlAdapters).forEach((ca) => { - const isModelAvailable = some( - action.payload.entities, - (m) => m?.model_name === ca?.model?.model_name && m?.base_model === ca?.model?.base_model - ); + const isModelAvailable = some(action.payload.entities, (m) => m?.key === ca?.model?.key); if (isModelAvailable) { return; diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index eb55db79ca..875ce1f1c4 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -5,10 +5,10 @@ import type { GroupBase } from 'chakra-react-select'; import { groupBy, map, reduce } from 'lodash-es'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { AnyModelConfigEntity } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/endpoints/models'; import { getModelId } from 'services/api/endpoints/models'; -type UseGroupedModelComboboxArg = { +type UseGroupedModelComboboxArg = { modelEntities: EntityState | undefined; selectedModel?: Pick | null; onChange: (value: T | null) => void; @@ -24,7 +24,7 @@ type UseGroupedModelComboboxReturn = { noOptionsMessage: () => string; }; -export const useGroupedModelCombobox = ( +export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index baa704e75c..b31efed970 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -105,7 +105,7 @@ const selector = createMemoizedSelector( number: i + 1, }) ); - } else if (ca.model.base_model !== model?.base_model) { + } else if (ca.model.base !== model?.base) { // This should never happen, just a sanity check reasons.push( i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { diff --git a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts index 880b316379..341fed1e47 100644 --- a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts @@ -3,10 +3,10 @@ import type { EntityState } from '@reduxjs/toolkit'; import { map } from 'lodash-es'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { AnyModelConfigEntity } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/endpoints/models'; import { getModelId } from 'services/api/endpoints/models'; -type UseModelComboboxArg = { +type UseModelComboboxArg = { modelEntities: EntityState | undefined; selectedModel?: Pick | null; onChange: (value: T | null) => void; @@ -23,7 +23,7 @@ type UseModelComboboxReturn = { noOptionsMessage: () => string; }; -export const useModelCombobox = ( +export const useModelCombobox = ( arg: UseModelComboboxArg ): UseModelComboboxReturn => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index cd734d3f00..f50d52c1bf 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -626,7 +626,7 @@ export const canvasSlice = createSlice({ }, extraReducers: (builder) => { builder.addCase(modelChanged, (state, action) => { - if (action.meta.previousModel?.base_model === action.payload?.base_model) { + if (action.meta.previousModel?.base === action.payload?.base) { // The base model hasn't changed, we don't need to optimize the size return; } diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx index 13851b143c..a320238445 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx @@ -11,12 +11,7 @@ import { selectGenerationSlice } from 'features/parameters/store/generationSlice import { pick } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { - ControlNetModelConfigEntity, - IPAdapterModelConfigEntity, - T2IAdapterModelConfigEntity, -} from 'services/api/endpoints/models'; -import type { AnyModelConfig } from 'services/api/types'; +import type { AnyModelConfig, ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'services/api/types'; type ParamControlAdapterModelProps = { id: string; @@ -29,21 +24,21 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { const controlAdapterType = useControlAdapterType(id); const model = useControlAdapterModel(id); const dispatch = useAppDispatch(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base_model); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); const mainModel = useAppSelector(selectMainModel); const { t } = useTranslation(); const models = useControlAdapterModelEntities(controlAdapterType); const _onChange = useCallback( - (model: ControlNetModelConfigEntity | IPAdapterModelConfigEntity | T2IAdapterModelConfigEntity | null) => { + (model: ControlNetConfig | IPAdapterConfig | T2IAdapterConfig | null) => { if (!model) { return; } dispatch( controlAdapterModelChanged({ id, - model: pick(model, 'base_model', 'model_name'), + model: pick(model, 'base', 'key'), }) ); }, @@ -57,7 +52,7 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { const getIsDisabled = useCallback( (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base_model; + const isCompatible = currentBaseModel === model.base; const hasMainModel = Boolean(currentBaseModel); return !hasMainModel || !isCompatible; }, @@ -73,7 +68,7 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { return ( - + { - const baseModel = useAppSelector((s) => s.generation.model?.base_model); + const baseModel = useAppSelector((s) => s.generation.model?.base); const dispatch = useAppDispatch(); const models = useControlAdapterModels(type); const firstModel = useMemo(() => { // prefer to use a model that matches the base model - const firstCompatibleModel = models.filter((m) => (baseModel ? m.base_model === baseModel : true))[0]; + const firstCompatibleModel = models.filter((m) => (baseModel ? m.base === baseModel : true))[0]; if (firstCompatibleModel) { return firstCompatibleModel; diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts index 49b07f16a1..fce94ad019 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts @@ -236,7 +236,8 @@ export const controlAdaptersSlice = createSlice({ let processorType: ControlAdapterProcessorType | undefined = undefined; for (const modelSubstring in CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS) { - if (model.model_name.includes(modelSubstring)) { + // TODO(MM2): matching modelSubstring to the model key is no longer a valid way to figure out the default processorType + if (model.key.includes(modelSubstring)) { processorType = CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS[modelSubstring]; break; } @@ -359,7 +360,8 @@ export const controlAdaptersSlice = createSlice({ let processorType: ControlAdapterProcessorType | undefined = undefined; for (const modelSubstring in CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS) { - if (cn.model?.model_name.includes(modelSubstring)) { + // TODO(MM2): matching modelSubstring to the model key is no longer a valid way to figure out the default processorType + if (cn.model?.key.includes(modelSubstring)) { processorType = CONTROLADAPTER_MODEL_DEFAULT_PROCESSORS[modelSubstring]; break; } diff --git a/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx b/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx index ffe9d63360..426ddd21e2 100644 --- a/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx +++ b/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx @@ -6,18 +6,18 @@ import type { EmbeddingSelectProps } from 'features/embedding/types'; import { t } from 'i18next'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import type { TextualInversionModelConfigEntity } from 'services/api/endpoints/models'; import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; +import type { TextualInversionConfig } from 'services/api/types'; const noOptionsMessage = () => t('embedding.noMatchingEmbedding'); export const EmbeddingSelect = memo(({ onSelect, onClose }: EmbeddingSelectProps) => { const { t } = useTranslation(); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base_model); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); const getIsDisabled = useCallback( - (embedding: TextualInversionModelConfigEntity): boolean => { + (embedding: TextualInversionConfig): boolean => { const isCompatible = currentBaseModel === embedding.base_model; const hasMainModel = Boolean(currentBaseModel); return !hasMainModel || !isCompatible; @@ -27,7 +27,7 @@ export const EmbeddingSelect = memo(({ onSelect, onClose }: EmbeddingSelectProps const { data, isLoading } = useGetTextualInversionModelsQuery(); const _onChange = useCallback( - (embedding: TextualInversionModelConfigEntity | null) => { + (embedding: TextualInversionConfig | null) => { if (!embedding) { return; } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index e9a1461186..5907ba0700 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -208,8 +208,8 @@ const ImageMetadataActions = (props: Props) => { {metadata.seed !== undefined && metadata.seed !== null && ( )} - {metadata.model !== undefined && metadata.model !== null && metadata.model.model_name && ( - + {metadata.model !== undefined && metadata.model !== null && metadata.model.key && ( + )} {metadata.width && ( @@ -222,7 +222,7 @@ const ImageMetadataActions = (props: Props) => { )} {metadata.steps && ( @@ -269,7 +269,7 @@ const ImageMetadataActions = (props: Props) => { ); @@ -279,7 +279,7 @@ const ImageMetadataActions = (props: Props) => { ))} @@ -287,7 +287,7 @@ const ImageMetadataActions = (props: Props) => { ))} @@ -295,7 +295,7 @@ const ImageMetadataActions = (props: Props) => { ))} diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index 28bd8afe95..81e0027b2d 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -44,7 +44,7 @@ export const LoRACard = memo((props: LoRACardProps) => { - {lora.model_name} + {lora.key} diff --git a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx index 9f37454d16..7bcd537805 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRAList.tsx @@ -18,7 +18,7 @@ export const LoRAList = memo(() => { return ( {lorasArray.map((lora) => ( - + ))} ); diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index ed70a4d44a..069c557aef 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -7,7 +7,7 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { loraAdded, selectLoraSlice } from 'features/lora/store/loraSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { LoRAModelConfigEntity } from 'services/api/endpoints/models'; +import type { LoRAConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; const selectAddedLoRAs = createMemoizedSelector(selectLoraSlice, (lora) => lora.loras); @@ -19,7 +19,7 @@ const LoRASelect = () => { const addedLoRAs = useAppSelector(selectAddedLoRAs); const currentBaseModel = useAppSelector((s) => s.generation.model?.base_model); - const getIsDisabled = (lora: LoRAModelConfigEntity): boolean => { + const getIsDisabled = (lora: LoRAConfig): boolean => { const isCompatible = currentBaseModel === lora.base_model; const isAdded = Boolean(addedLoRAs[lora.id]); const hasMainModel = Boolean(currentBaseModel); @@ -27,7 +27,7 @@ const LoRASelect = () => { }; const _onChange = useCallback( - (lora: LoRAModelConfigEntity | null) => { + (lora: LoRAConfig | null) => { if (!lora) { return; } diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts index ab1b140a7c..dd455e12c3 100644 --- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts +++ b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts @@ -2,10 +2,9 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas'; -import type { LoRAModelConfigEntity } from 'services/api/endpoints/models'; +import type { LoRAConfig } from 'services/api/types'; export type LoRA = ParameterLoRAModel & { - id: string; weight: number; isEnabled?: boolean; }; @@ -29,40 +28,40 @@ export const loraSlice = createSlice({ name: 'lora', initialState: initialLoraState, reducers: { - loraAdded: (state, action: PayloadAction) => { - const { model_name, id, base_model } = action.payload; - state.loras[id] = { id, model_name, base_model, ...defaultLoRAConfig }; + loraAdded: (state, action: PayloadAction) => { + const { key, base } = action.payload; + state.loras[key] = { key, base, ...defaultLoRAConfig }; }, - loraRecalled: (state, action: PayloadAction) => { - const { model_name, id, base_model, weight } = action.payload; - state.loras[id] = { id, model_name, base_model, weight, isEnabled: true }; + loraRecalled: (state, action: PayloadAction) => { + const { key, base, weight } = action.payload; + state.loras[key] = { key, base, weight, isEnabled: true }; }, loraRemoved: (state, action: PayloadAction) => { - const id = action.payload; - delete state.loras[id]; + const key = action.payload; + delete state.loras[key]; }, lorasCleared: (state) => { state.loras = {}; }, - loraWeightChanged: (state, action: PayloadAction<{ id: string; weight: number }>) => { - const { id, weight } = action.payload; - const lora = state.loras[id]; + loraWeightChanged: (state, action: PayloadAction<{ key: string; weight: number }>) => { + const { key, weight } = action.payload; + const lora = state.loras[key]; if (!lora) { return; } lora.weight = weight; }, loraWeightReset: (state, action: PayloadAction) => { - const id = action.payload; - const lora = state.loras[id]; + const key = action.payload; + const lora = state.loras[key]; if (!lora) { return; } lora.weight = defaultLoRAConfig.weight; }, - loraIsEnabledChanged: (state, action: PayloadAction>) => { - const { id, isEnabled } = action.payload; - const lora = state.loras[id]; + loraIsEnabledChanged: (state, action: PayloadAction>) => { + const { key, isEnabled } = action.payload; + const lora = state.loras[key]; if (!lora) { return; } diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx index 6b9abdbfec..7501151ba4 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx @@ -3,9 +3,9 @@ import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ALL_BASE_MODELS } from 'services/api/constants'; import type { - DiffusersModelConfigEntity, - LoRAModelConfigEntity, - MainModelConfigEntity, + DiffusersModelConfig, + LoRAConfig, + MainModelConfig, } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery, useGetMainModelsQuery } from 'services/api/endpoints/models'; @@ -38,7 +38,7 @@ const ModelManagerPanel = () => { }; type ModelEditProps = { - model: MainModelConfigEntity | LoRAModelConfigEntity | undefined; + model: MainModelConfig | LoRAConfig | undefined; }; const ModelEdit = (props: ModelEditProps) => { @@ -50,7 +50,7 @@ const ModelEdit = (props: ModelEditProps) => { } if (model?.model_format === 'diffusers') { - return ; + return ; } if (model?.model_type === 'lora') { diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx index f4d271187d..43707308e0 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx @@ -21,14 +21,14 @@ import { memo, useCallback, useEffect, useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import type { CheckpointModelConfigEntity } from 'services/api/endpoints/models'; +import type { CheckpointModelConfig } from 'services/api/endpoints/models'; import { useGetCheckpointConfigsQuery, useUpdateMainModelsMutation } from 'services/api/endpoints/models'; import type { CheckpointModelConfig } from 'services/api/types'; import ModelConvert from './ModelConvert'; type CheckpointModelEditProps = { - model: CheckpointModelConfigEntity; + model: CheckpointModelConfig; }; const CheckpointModelEdit = (props: CheckpointModelEditProps) => { diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx index 4670f32157..bf6349234f 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx @@ -9,12 +9,12 @@ import { memo, useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import type { DiffusersModelConfigEntity } from 'services/api/endpoints/models'; +import type { DiffusersModelConfig } from 'services/api/endpoints/models'; import { useUpdateMainModelsMutation } from 'services/api/endpoints/models'; import type { DiffusersModelConfig } from 'services/api/types'; type DiffusersModelEditProps = { - model: DiffusersModelConfigEntity; + model: DiffusersModelConfig; }; const DiffusersModelEdit = (props: DiffusersModelEditProps) => { diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx index 2baf735bee..75151cd001 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx @@ -8,12 +8,12 @@ import { memo, useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import type { LoRAModelConfigEntity } from 'services/api/endpoints/models'; +import type { LoRAConfig } from 'services/api/endpoints/models'; import { useUpdateLoRAModelsMutation } from 'services/api/endpoints/models'; -import type { LoRAModelConfig } from 'services/api/types'; +import type { LoRAConfig } from 'services/api/types'; type LoRAModelEditProps = { - model: LoRAModelConfigEntity; + model: LoRAConfig; }; const LoRAModelEdit = (props: LoRAModelEditProps) => { @@ -30,7 +30,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { control, formState: { errors }, reset, - } = useForm({ + } = useForm({ defaultValues: { model_name: model.model_name ? model.model_name : '', base_model: model.base_model, @@ -42,7 +42,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { mode: 'onChange', }); - const onSubmit = useCallback>( + const onSubmit = useCallback>( (values) => { const responseBody = { base_model: model.base_model, @@ -53,7 +53,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { updateLoRAModel(responseBody) .unwrap() .then((payload) => { - reset(payload as LoRAModelConfig, { keepDefaultValues: true }); + reset(payload as LoRAConfig, { keepDefaultValues: true }); dispatch( addToast( makeToast({ @@ -106,7 +106,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { {t('modelManager.description')} - control={control} name="base_model" /> + control={control} name="base_model" /> {t('modelManager.modelLocation')} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx index 94db3d20c3..dd74bb0c23 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx @@ -5,7 +5,7 @@ import type { ChangeEvent, PropsWithChildren } from 'react'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ALL_BASE_MODELS } from 'services/api/constants'; -import type { LoRAModelConfigEntity, MainModelConfigEntity } from 'services/api/endpoints/models'; +import type { LoRAConfig, MainModelConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery, useGetMainModelsQuery } from 'services/api/endpoints/models'; import ModelListItem from './ModelListItem'; @@ -127,7 +127,7 @@ const ModelList = (props: ModelListProps) => { export default memo(ModelList); -const modelsFilter = ( +const modelsFilter = ( data: EntityState | undefined, model_type: ModelType, model_format: ModelFormat | undefined, @@ -163,7 +163,7 @@ StyledModelContainer.displayName = 'StyledModelContainer'; type ModelListWrapperProps = { title: string; - modelList: MainModelConfigEntity[] | LoRAModelConfigEntity[]; + modelList: MainModelConfig[] | LoRAConfig[]; selected: ModelListProps; }; diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx index fdd13e09f5..835499d25a 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -15,11 +15,11 @@ import { makeToast } from 'features/system/util/makeToast'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -import type { LoRAModelConfigEntity, MainModelConfigEntity } from 'services/api/endpoints/models'; +import type { LoRAConfig, MainModelConfig } from 'services/api/endpoints/models'; import { useDeleteLoRAModelsMutation, useDeleteMainModelsMutation } from 'services/api/endpoints/models'; type ModelListItemProps = { - model: MainModelConfigEntity | LoRAModelConfigEntity; + model: MainModelConfig | LoRAConfig; isSelected: boolean; setSelectedModelId: (v: string | undefined) => void; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx index 22024f3d3c..53d800e7b6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx @@ -4,7 +4,7 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldControlNetModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { ControlNetModelFieldInputInstance, ControlNetModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; -import type { ControlNetModelConfigEntity } from 'services/api/endpoints/models'; +import type { ControlNetConfig } from 'services/api/endpoints/models'; import { useGetControlNetModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -17,7 +17,7 @@ const ControlNetModelFieldInputComponent = (props: Props) => { const { data, isLoading } = useGetControlNetModelsQuery(); const _onChange = useCallback( - (value: ControlNetModelConfigEntity | null) => { + (value: ControlNetConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx index 2cde347247..3f195ceb32 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx @@ -4,7 +4,7 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldIPAdapterModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { IPAdapterModelFieldInputInstance, IPAdapterModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; -import type { IPAdapterModelConfigEntity } from 'services/api/endpoints/models'; +import type { IPAdapterConfig } from 'services/api/endpoints/models'; import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -17,7 +17,7 @@ const IPAdapterModelFieldInputComponent = ( const { data: ipAdapterModels } = useGetIPAdapterModelsQuery(); const _onChange = useCallback( - (value: IPAdapterModelConfigEntity | null) => { + (value: IPAdapterConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx index 96208d68d4..eeb07fa08e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx @@ -4,7 +4,7 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldLoRAModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { LoRAModelFieldInputInstance, LoRAModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; -import type { LoRAModelConfigEntity } from 'services/api/endpoints/models'; +import type { LoRAConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -16,7 +16,7 @@ const LoRAModelFieldInputComponent = (props: Props) => { const dispatch = useAppDispatch(); const { data, isLoading } = useGetLoRAModelsQuery(); const _onChange = useCallback( - (value: LoRAModelConfigEntity | null) => { + (value: LoRAConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx index 64c6970cae..7ddde08816 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx @@ -6,7 +6,7 @@ import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { MainModelFieldInputInstance, MainModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; import { NON_SDXL_MAIN_MODELS } from 'services/api/constants'; -import type { MainModelConfigEntity } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -18,7 +18,7 @@ const MainModelFieldInputComponent = (props: Props) => { const dispatch = useAppDispatch(); const { data, isLoading } = useGetMainModelsQuery(NON_SDXL_MAIN_MODELS); const _onChange = useCallback( - (value: MainModelConfigEntity | null) => { + (value: MainModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx index 98901af38b..9b5a1138d4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx @@ -9,7 +9,7 @@ import type { } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; import { REFINER_BASE_MODELS } from 'services/api/constants'; -import type { MainModelConfigEntity } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -21,7 +21,7 @@ const RefinerModelFieldInputComponent = (props: Props) => { const dispatch = useAppDispatch(); const { data, isLoading } = useGetMainModelsQuery(REFINER_BASE_MODELS); const _onChange = useCallback( - (value: MainModelConfigEntity | null) => { + (value: MainModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx index f5bc7ac3e4..cf353619e8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx @@ -6,7 +6,7 @@ import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { SDXLMainModelFieldInputInstance, SDXLMainModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; import { SDXL_MAIN_MODELS } from 'services/api/constants'; -import type { MainModelConfigEntity } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -18,7 +18,7 @@ const SDXLMainModelFieldInputComponent = (props: Props) => { const dispatch = useAppDispatch(); const { data, isLoading } = useGetMainModelsQuery(SDXL_MAIN_MODELS); const _onChange = useCallback( - (value: MainModelConfigEntity | null) => { + (value: MainModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx index 9baf0d2d61..8402c56343 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx @@ -4,7 +4,7 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldT2IAdapterModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { T2IAdapterModelFieldInputInstance, T2IAdapterModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; -import type { T2IAdapterModelConfigEntity } from 'services/api/endpoints/models'; +import type { T2IAdapterConfig } from 'services/api/endpoints/models'; import { useGetT2IAdapterModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -18,7 +18,7 @@ const T2IAdapterModelFieldInputComponent = ( const { data: t2iAdapterModels } = useGetT2IAdapterModelsQuery(); const _onChange = useCallback( - (value: T2IAdapterModelConfigEntity | null) => { + (value: T2IAdapterConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx index 070178f32a..af09f2d8f2 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx @@ -5,7 +5,7 @@ import { SyncModelsIconButton } from 'features/modelManager/components/SyncModel import { fieldVaeModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { VAEModelFieldInputInstance, VAEModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; -import type { VaeModelConfigEntity } from 'services/api/endpoints/models'; +import type { VAEConfig } from 'services/api/endpoints/models'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; import type { FieldComponentProps } from './types'; @@ -17,7 +17,7 @@ const VAEModelFieldInputComponent = (props: Props) => { const dispatch = useAppDispatch(); const { data, isLoading } = useGetVaeModelsQuery(); const _onChange = useCallback( - (value: VaeModelConfigEntity | null) => { + (value: VAEConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index ef579fce8c..891bd29bc8 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -67,11 +67,13 @@ export const zModelName = z.string().min(3); export const zModelIdentifier = z.object({ key: z.string().min(1), }); +export const zModelFieldBase = zModelIdentifier; +export const zModelIdentifierWithBase = zModelIdentifier.extend({ base: zBaseModel }); export type BaseModel = z.infer; export type ModelType = z.infer; export type ModelIdentifier = z.infer; - -export const zMainModelField = zModelIdentifier; +export type ModelIdentifierWithBase = z.infer; +export const zMainModelField = zModelFieldBase; export type MainModelField = z.infer; export const zSDXLRefinerModelField = zModelIdentifier; @@ -91,23 +93,23 @@ export const zSubModelType = z.enum([ ]); export type SubModelType = z.infer; -export const zVAEModelField = zModelIdentifier; +export const zVAEModelField = zModelFieldBase; export const zModelInfo = zModelIdentifier.extend({ submodel_type: zSubModelType.nullish(), }); export type ModelInfo = z.infer; -export const zLoRAModelField = zModelIdentifier; +export const zLoRAModelField = zModelFieldBase; export type LoRAModelField = z.infer; -export const zControlNetModelField = zModelIdentifier; +export const zControlNetModelField = zModelFieldBase; export type ControlNetModelField = z.infer; -export const zIPAdapterModelField = zModelIdentifier; +export const zIPAdapterModelField = zModelFieldBase; export type IPAdapterModelField = z.infer; -export const zT2IAdapterModelField = zModelIdentifier; +export const zT2IAdapterModelField = zModelFieldBase; export type T2IAdapterModelField = z.infer; export const zLoraInfo = zModelInfo.extend({ diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts index d862b0986e..1853d3722c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addControlNetToLinearGraph.ts @@ -14,7 +14,7 @@ import { upsertMetadata } from './metadata'; export const addControlNetToLinearGraph = (state: RootState, graph: NonNullableGraph, baseNodeId: string): void => { const validControlNets = selectValidControlNets(state.controlAdapters).filter( - (ca) => ca.model?.base_model === state.generation.model?.base_model + (ca) => ca.model?.base === state.generation.model?.base ); // const metadataAccumulator = graph.nodes[METADATA_ACCUMULATOR] as diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts index 3a79b78c6e..b51ac1bd52 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addIPAdapterToLinearGraph.ts @@ -14,7 +14,7 @@ import { upsertMetadata } from './metadata'; export const addIPAdapterToLinearGraph = (state: RootState, graph: NonNullableGraph, baseNodeId: string): void => { const validIPAdapters = selectValidIPAdapters(state.controlAdapters).filter( - (ca) => ca.model?.base_model === state.generation.model?.base_model + (ca) => ca.model?.base === state.generation.model?.base ); if (validIPAdapters.length) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts index 3ed71b7529..95bba9b441 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts @@ -28,6 +28,7 @@ export const addLoRAsToGraph = ( * So we need to inject a LoRA chain into the graph. */ + // TODO(MM2): check base model const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); const loraCount = size(enabledLoRAs); @@ -48,19 +49,19 @@ export const addLoRAsToGraph = ( const loraMetadata: CoreMetadataInvocation['loras'] = []; enabledLoRAs.forEach((lora) => { - const { model_name, base_model, weight } = lora; - const currentLoraNodeId = `${LORA_LOADER}_${model_name.replace('.', '_')}`; + const { key, weight } = lora; + const currentLoraNodeId = `${LORA_LOADER}_${key}`; const loraLoaderNode: LoraLoaderInvocation = { type: 'lora_loader', id: currentLoraNodeId, is_intermediate: true, - lora: { model_name, base_model }, + lora: { key }, weight, }; loraMetadata.push({ - lora: { model_name, base_model }, + lora: { key }, weight, }); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts index 9553568922..7874b059c9 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts @@ -31,6 +31,7 @@ export const addSDXLLoRAsToGraph = ( * So we need to inject a LoRA chain into the graph. */ + // TODO(MM2): check base model const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); const loraCount = size(enabledLoRAs); @@ -60,20 +61,20 @@ export const addSDXLLoRAsToGraph = ( let currentLoraIndex = 0; enabledLoRAs.forEach((lora) => { - const { model_name, base_model, weight } = lora; - const currentLoraNodeId = `${LORA_LOADER}_${model_name.replace('.', '_')}`; + const { key, weight } = lora; + const currentLoraNodeId = `${LORA_LOADER}_${key}`; const loraLoaderNode: SDXLLoraLoaderInvocation = { type: 'sdxl_lora_loader', id: currentLoraNodeId, is_intermediate: true, - lora: { model_name, base_model }, + lora: { key }, weight, }; loraMetadata.push( zLoRAMetadataItem.parse({ - lora: { model_name, base_model }, + lora: { key }, weight, }) ); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts index d35f72a2b4..84002337d7 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addT2IAdapterToLinearGraph.ts @@ -14,7 +14,7 @@ import { upsertMetadata } from './metadata'; export const addT2IAdaptersToLinearGraph = (state: RootState, graph: NonNullableGraph, baseNodeId: string): void => { const validT2IAdapters = selectValidT2IAdapters(state.controlAdapters).filter( - (ca) => ca.model?.base_model === state.generation.model?.base_model + (ca) => ca.model?.base === state.generation.model?.base ); if (validT2IAdapters.length) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasGraph.ts index 2b64f4898b..4ce2e4d673 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasGraph.ts @@ -19,7 +19,7 @@ export const buildCanvasGraph = ( let graph: NonNullableGraph; if (generationMode === 'txt2img') { - if (state.generation.model && state.generation.model.base_model === 'sdxl') { + if (state.generation.model && state.generation.model.base === 'sdxl') { graph = buildCanvasSDXLTextToImageGraph(state); } else { graph = buildCanvasTextToImageGraph(state); @@ -28,7 +28,7 @@ export const buildCanvasGraph = ( if (!canvasInitImage) { throw new Error('Missing canvas init image'); } - if (state.generation.model && state.generation.model.base_model === 'sdxl') { + if (state.generation.model && state.generation.model.base === 'sdxl') { graph = buildCanvasSDXLImageToImageGraph(state, canvasInitImage); } else { graph = buildCanvasImageToImageGraph(state, canvasInitImage); @@ -37,7 +37,7 @@ export const buildCanvasGraph = ( if (!canvasInitImage || !canvasMaskImage) { throw new Error('Missing canvas init and mask images'); } - if (state.generation.model && state.generation.model.base_model === 'sdxl') { + if (state.generation.model && state.generation.model.base === 'sdxl') { graph = buildCanvasSDXLInpaintGraph(state, canvasInitImage, canvasMaskImage); } else { graph = buildCanvasInpaintGraph(state, canvasInitImage, canvasMaskImage); @@ -46,7 +46,7 @@ export const buildCanvasGraph = ( if (!canvasInitImage) { throw new Error('Missing canvas init image'); } - if (state.generation.model && state.generation.model.base_model === 'sdxl') { + if (state.generation.model && state.generation.model.base === 'sdxl') { graph = buildCanvasSDXLOutpaintGraph(state, canvasInitImage, canvasMaskImage); } else { graph = buildCanvasOutpaintGraph(state, canvasInitImage, canvasMaskImage); diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts index d0e331fb46..9fcc6afaa0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearBatchConfig.ts @@ -105,7 +105,7 @@ export const prepareLinearUIBatch = (state: RootState, graph: NonNullableGraph, }); } - if (shouldConcatSDXLStylePrompt && model?.base_model === 'sdxl') { + if (shouldConcatSDXLStylePrompt && model?.base === 'sdxl') { if (graph.nodes[POSITIVE_CONDITIONING]) { firstBatchDatumList.push({ node_path: POSITIVE_CONDITIONING, diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx index 621ed56ef6..c23d541613 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -29,17 +29,17 @@ const ParamClipSkip = () => { if (!model) { return CLIP_SKIP_MAP['sd-1'].maxClip; } - return CLIP_SKIP_MAP[model.base_model].maxClip; + return CLIP_SKIP_MAP[model.base].maxClip; }, [model]); const sliderMarks = useMemo(() => { if (!model) { return CLIP_SKIP_MAP['sd-1'].markers; } - return CLIP_SKIP_MAP[model.base_model].markers; + return CLIP_SKIP_MAP[model.base].markers; }, [model]); - if (model?.base_model === 'sdxl') { + if (model?.base === 'sdxl') { return null; } diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index ae81f78fd1..a1852bfafe 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'; export const ParamPositivePrompt = memo(() => { const dispatch = useAppDispatch(); const prompt = useAppSelector((s) => s.generation.positivePrompt); - const baseModel = useAppSelector((s) => s.generation.model)?.base_model; + const baseModel = useAppSelector((s) => s.generation.model)?.base; const textareaRef = useRef(null); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx index c6c77b5fe9..18f780bdee 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx @@ -9,7 +9,7 @@ import { pick } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { NON_REFINER_BASE_MODELS } from 'services/api/constants'; -import type { MainModelConfigEntity } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/endpoints/models'; import { getModelId, mainModelsAdapterSelectors, useGetMainModelsQuery } from 'services/api/endpoints/models'; const selectModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); @@ -26,7 +26,7 @@ const ParamMainModelSelect = () => { return mainModelsAdapterSelectors.selectById(data, getModelId(model))?.description; }, [data, model]); const _onChange = useCallback( - (model: MainModelConfigEntity | null) => { + (model: MainModelConfig | null) => { if (!model) { return; } diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index f290378aa8..cc0164153d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -7,7 +7,7 @@ import { selectGenerationSlice, vaeSelected } from 'features/parameters/store/ge import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import type { VaeModelConfigEntity } from 'services/api/endpoints/models'; +import type { VAEConfig } from 'services/api/endpoints/models'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; const selector = createMemoizedSelector(selectGenerationSlice, (generation) => { @@ -21,7 +21,7 @@ const ParamVAEModelSelect = () => { const { model, vae } = useAppSelector(selector); const { data, isLoading } = useGetVaeModelsQuery(); const getIsDisabled = useCallback( - (vae: VaeModelConfigEntity): boolean => { + (vae: VAEConfig): boolean => { const isCompatible = model?.base_model === vae.base_model; const hasMainModel = Boolean(model?.base_model); return !hasMainModel || !isCompatible; @@ -29,7 +29,7 @@ const ParamVAEModelSelect = () => { [model?.base_model] ); const _onChange = useCallback( - (vae: VaeModelConfigEntity | null) => { + (vae: VAEConfig | null) => { dispatch(vaeSelected(vae ? pick(vae, 'base_model', 'model_name') : null)); }, [dispatch] diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts index 5a9fa6c66d..c8b17816bb 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts @@ -464,17 +464,15 @@ export const useRecallParameters = () => { return { lora: null, error: 'Invalid LoRA model' }; } - const { base_model, model_name } = loraMetadataItem.lora; + const { lora } = loraMetadataItem; - const matchingLoRA = loraModels - ? loraModelsAdapterSelectors.selectById(loraModels, `${base_model}/lora/${model_name}`) - : undefined; + const matchingLoRA = loraModels ? loraModelsAdapterSelectors.selectById(loraModels, lora.key) : undefined; if (!matchingLoRA) { return { lora: null, error: 'LoRA model is not installed' }; } - const isCompatibleBaseModel = matchingLoRA?.base_model === (newModel ?? model)?.base_model; + const isCompatibleBaseModel = matchingLoRA?.base === (newModel ?? model)?.base; if (!isCompatibleBaseModel) { return { @@ -520,17 +518,14 @@ export const useRecallParameters = () => { controlnetMetadataItem; const matchingControlNetModel = controlNetModels - ? controlNetModelsAdapterSelectors.selectById( - controlNetModels, - `${control_model.base_model}/controlnet/${control_model.model_name}` - ) + ? controlNetModelsAdapterSelectors.selectById(controlNetModels, control_model.key) : undefined; if (!matchingControlNetModel) { return { controlnet: null, error: 'ControlNet model is not installed' }; } - const isCompatibleBaseModel = matchingControlNetModel?.base_model === (newModel ?? model)?.base_model; + const isCompatibleBaseModel = matchingControlNetModel?.base === (newModel ?? model)?.base; if (!isCompatibleBaseModel) { return { @@ -597,17 +592,14 @@ export const useRecallParameters = () => { t2iAdapterMetadataItem; const matchingT2IAdapterModel = t2iAdapterModels - ? t2iAdapterModelsAdapterSelectors.selectById( - t2iAdapterModels, - `${t2i_adapter_model.base_model}/t2i_adapter/${t2i_adapter_model.model_name}` - ) + ? t2iAdapterModelsAdapterSelectors.selectById(t2iAdapterModels, t2i_adapter_model.key) : undefined; if (!matchingT2IAdapterModel) { return { controlnet: null, error: 'ControlNet model is not installed' }; } - const isCompatibleBaseModel = matchingT2IAdapterModel?.base_model === (newModel ?? model)?.base_model; + const isCompatibleBaseModel = matchingT2IAdapterModel?.base === (newModel ?? model)?.base; if (!isCompatibleBaseModel) { return { @@ -672,17 +664,14 @@ export const useRecallParameters = () => { const { image, ip_adapter_model, weight, begin_step_percent, end_step_percent } = ipAdapterMetadataItem; const matchingIPAdapterModel = ipAdapterModels - ? ipAdapterModelsAdapterSelectors.selectById( - ipAdapterModels, - `${ip_adapter_model.base_model}/ip_adapter/${ip_adapter_model.model_name}` - ) + ? ipAdapterModelsAdapterSelectors.selectById(ipAdapterModels, ip_adapter_model.key) : undefined; if (!matchingIPAdapterModel) { return { ipAdapter: null, error: 'IP Adapter model is not installed' }; } - const isCompatibleBaseModel = matchingIPAdapterModel?.base_model === (newModel ?? model)?.base_model; + const isCompatibleBaseModel = matchingIPAdapterModel?.base === (newModel ?? model)?.base; if (!isCompatibleBaseModel) { return { diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index df98943cd3..1666a34d6a 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -158,15 +158,15 @@ export const generationSlice = createSlice({ // Clamp ClipSkip Based On Selected Model // TODO(psyche): remove this special handling when https://github.com/invoke-ai/InvokeAI/issues/4583 is resolved // WIP PR here: https://github.com/invoke-ai/InvokeAI/pull/4624 - if (newModel.base_model === 'sdxl') { + if (newModel.base === 'sdxl') { // We don't support clip skip for SDXL yet - it's not in the graphs state.clipSkip = 0; } else { - const { maxClip } = CLIP_SKIP_MAP[newModel.base_model]; + const { maxClip } = CLIP_SKIP_MAP[newModel.base]; state.clipSkip = clamp(state.clipSkip, 0, maxClip); } - if (action.meta.previousModel?.base_model === newModel.base_model) { + if (action.meta.previousModel?.base === newModel.base) { // The base model hasn't changed, we don't need to optimize the size return; } diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts index 7a5efe9dcf..abd8ee2810 100644 --- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts @@ -1,5 +1,6 @@ import { NUMPY_RAND_MAX } from 'app/constants'; import { + zBaseModel, zControlNetModelField, zIPAdapterModelField, zLoRAModelField, @@ -104,48 +105,48 @@ export const isParameterAspectRatio = (val: unknown): val is ParameterAspectRati // #endregion // #region Model -export const zParameterModel = zMainModelField; +export const zParameterModel = zMainModelField.extend({ base: zBaseModel }); export type ParameterModel = z.infer; export const isParameterModel = (val: unknown): val is ParameterModel => zParameterModel.safeParse(val).success; // #endregion // #region SDXL Refiner Model -export const zParameterSDXLRefinerModel = zSDXLRefinerModelField; +export const zParameterSDXLRefinerModel = zSDXLRefinerModelField.extend({ base: zBaseModel }); export type ParameterSDXLRefinerModel = z.infer; export const isParameterSDXLRefinerModel = (val: unknown): val is ParameterSDXLRefinerModel => zParameterSDXLRefinerModel.safeParse(val).success; // #endregion // #region VAE Model -export const zParameterVAEModel = zVAEModelField; +export const zParameterVAEModel = zVAEModelField.extend({ base: zBaseModel }); export type ParameterVAEModel = z.infer; export const isParameterVAEModel = (val: unknown): val is ParameterVAEModel => zParameterVAEModel.safeParse(val).success; // #endregion // #region LoRA Model -export const zParameterLoRAModel = zLoRAModelField; +export const zParameterLoRAModel = zLoRAModelField.extend({ base: zBaseModel }); export type ParameterLoRAModel = z.infer; export const isParameterLoRAModel = (val: unknown): val is ParameterLoRAModel => zParameterLoRAModel.safeParse(val).success; // #endregion // #region ControlNet Model -export const zParameterControlNetModel = zControlNetModelField; +export const zParameterControlNetModel = zControlNetModelField.extend({ base: zBaseModel }); export type ParameterControlNetModel = z.infer; export const isParameterControlNetModel = (val: unknown): val is ParameterControlNetModel => zParameterControlNetModel.safeParse(val).success; // #endregion // #region IP Adapter Model -export const zParameterIPAdapterModel = zIPAdapterModelField; +export const zParameterIPAdapterModel = zIPAdapterModelField.extend({ base: zBaseModel }); export type ParameterIPAdapterModel = z.infer; export const isParameterIPAdapterModel = (val: unknown): val is ParameterIPAdapterModel => zParameterIPAdapterModel.safeParse(val).success; // #endregion // #region T2I Adapter Model -export const zParameterT2IAdapterModel = zT2IAdapterModelField; +export const zParameterT2IAdapterModel = zT2IAdapterModelField.extend({ base: zBaseModel }); export type ParameterT2IAdapterModel = z.infer; export const isParameterT2IAdapterModel = (val: unknown): val is ParameterT2IAdapterModel => zParameterT2IAdapterModel.safeParse(val).success; diff --git a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts index 1c550eb8a4..92b4f18272 100644 --- a/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts +++ b/invokeai/frontend/web/src/features/parameters/util/optimalDimension.ts @@ -1,12 +1,12 @@ -import type { ModelIdentifier } from 'features/nodes/types/common'; +import type { ModelIdentifierWithBase } from 'features/nodes/types/common'; /** * Gets the optimal dimension for a givel model, based on the model's base_model * @param model The model identifier * @returns The optimal dimension for the model */ -export const getOptimalDimension = (model?: ModelIdentifier | null): number => - model?.base_model === 'sdxl' ? 1024 : 512; +export const getOptimalDimension = (model?: ModelIdentifierWithBase | null): number => + model?.base === 'sdxl' ? 1024 : 512; const MIN_AREA_FACTOR = 0.8; const MAX_AREA_FACTOR = 1.2; diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx index 5559ec76b7..4c54251557 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx @@ -7,12 +7,12 @@ import { refinerModelChanged, selectSdxlSlice } from 'features/sdxl/store/sdxlSl import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { REFINER_BASE_MODELS } from 'services/api/constants'; -import type { MainModelConfigEntity } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; const selectModel = createMemoizedSelector(selectSdxlSlice, (sdxl) => sdxl.refinerModel); -const optionsFilter = (model: MainModelConfigEntity) => model.base_model === 'sdxl-refiner'; +const optionsFilter = (model: MainModelConfig) => model.base_model === 'sdxl-refiner'; const ParamSDXLRefinerModelSelect = () => { const dispatch = useAppDispatch(); @@ -20,7 +20,7 @@ const ParamSDXLRefinerModelSelect = () => { const { t } = useTranslation(); const { data, isLoading } = useGetMainModelsQuery(REFINER_BASE_MODELS); const _onChange = useCallback( - (model: MainModelConfigEntity | null) => { + (model: MainModelConfig | null) => { if (!model) { dispatch(refinerModelChanged(null)); return; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index bceee915cd..fc8c54576c 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -24,7 +24,8 @@ const formLabelProps2: FormLabelProps = { const selectBadges = createMemoizedSelector(selectGenerationSlice, (generation) => { const badges: (string | number)[] = []; if (generation.vae) { - let vaeBadge = generation.vae.model_name; + // TODO(MM2): Fetch the vae name + let vaeBadge = generation.vae.key; if (generation.vaePrecision === 'fp16') { vaeBadge += ` ${generation.vaePrecision}`; } 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 077875a8a7..cda7dcf6e9 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -35,9 +35,10 @@ const badgesSelector = createMemoizedSelector(selectLoraSlice, selectGenerationS const enabledLoRAsCount = filter(lora.loras, (l) => !!l.isEnabled).length; const loraTabBadges = enabledLoRAsCount ? [enabledLoRAsCount] : []; const accordionBadges: (string | number)[] = []; + // TODO(MM2): fetch model name if (generation.model) { - accordionBadges.push(generation.model.model_name); - accordionBadges.push(generation.model.base_model); + accordionBadges.push(generation.model.key); + accordionBadges.push(generation.model.base); } return { loraTabBadges, accordionBadges }; diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx index 2f778fe717..8f876850e8 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/ImageSettingsAccordion/ImageSettingsAccordion.tsx @@ -56,7 +56,7 @@ const selector = createMemoizedSelector( if (hrfEnabled) { badges.push('HiRes Fix'); } - return { badges, activeTabName, isSDXL: model?.base_model === 'sdxl' }; + return { badges, activeTabName, isSDXL: model?.base === 'sdxl' }; } ); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx index d52b2d9000..a74d132bd6 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx @@ -22,7 +22,7 @@ const overlayScrollbarsStyles: CSSProperties = { const ParametersPanel = () => { const activeTabName = useAppSelector(activeTabNameSelector); - const isSDXL = useAppSelector((s) => s.generation.model?.base_model === 'sdxl'); + const isSDXL = useAppSelector((s) => s.generation.model?.base === 'sdxl'); return ( diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index c11c8b45e5..97e221454d 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -1,64 +1,26 @@ -import type { EntityState } from '@reduxjs/toolkit'; +import type { EntityAdapter, EntityState } from '@reduxjs/toolkit'; import { createEntityAdapter } from '@reduxjs/toolkit'; import { getSelectorsOptions } from 'app/store/createMemoizedSelector'; -import { cloneDeep } from 'lodash-es'; import queryString from 'query-string'; import type { operations, paths } from 'services/api/schema'; import type { AnyModelConfig, BaseModelType, - CheckpointModelConfig, - ControlNetModelConfig, - DiffusersModelConfig, + ControlNetConfig, ImportModelConfig, - IPAdapterModelConfig, - LoRAModelConfig, + IPAdapterConfig, + LoRAConfig, MainModelConfig, MergeModelConfig, ModelType, - T2IAdapterModelConfig, - TextualInversionModelConfig, - VaeModelConfig, + T2IAdapterConfig, + TextualInversionConfig, + VAEConfig, } from 'services/api/types'; -import type { ApiTagDescription } from '..'; +import type { ApiTagDescription, tagTypes } from '..'; import { api, LIST_TAG } from '..'; -export type DiffusersModelConfigEntity = DiffusersModelConfig & { id: string }; -export type CheckpointModelConfigEntity = CheckpointModelConfig & { - id: string; -}; -export type MainModelConfigEntity = DiffusersModelConfigEntity | CheckpointModelConfigEntity; - -export type LoRAModelConfigEntity = LoRAModelConfig & { id: string }; - -export type ControlNetModelConfigEntity = ControlNetModelConfig & { - id: string; -}; - -export type IPAdapterModelConfigEntity = IPAdapterModelConfig & { - id: string; -}; - -export type T2IAdapterModelConfigEntity = T2IAdapterModelConfig & { - id: string; -}; - -export type TextualInversionModelConfigEntity = TextualInversionModelConfig & { - id: string; -}; - -export type VaeModelConfigEntity = VaeModelConfig & { id: string }; - -export type AnyModelConfigEntity = - | MainModelConfigEntity - | LoRAModelConfigEntity - | ControlNetModelConfigEntity - | IPAdapterModelConfigEntity - | T2IAdapterModelConfigEntity - | TextualInversionModelConfigEntity - | VaeModelConfigEntity; - type UpdateMainModelArg = { base_model: BaseModelType; model_name: string; @@ -68,11 +30,11 @@ type UpdateMainModelArg = { type UpdateLoRAModelArg = { base_model: BaseModelType; model_name: string; - body: LoRAModelConfig; + body: LoRAConfig; }; type UpdateMainModelResponse = - paths['/api/v1/models/{base_model}/{model_type}/{model_name}']['patch']['responses']['200']['content']['application/json']; + paths['/api/v2/models/i/{key}']['patch']['responses']['200']['content']['application/json']; type UpdateLoRAModelResponse = UpdateMainModelResponse; @@ -128,59 +90,71 @@ type CheckpointConfigsResponse = type SearchFolderArg = operations['search_for_models']['parameters']['query']; -export const mainModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), +export const mainModelsAdapter = createEntityAdapter({ + selectId: (entity) => entity.key, + sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const mainModelsAdapterSelectors = mainModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const loraModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), +export const loraModelsAdapter = createEntityAdapter({ + selectId: (entity) => entity.key, + sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const loraModelsAdapterSelectors = loraModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const controlNetModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), +export const controlNetModelsAdapter = createEntityAdapter({ + selectId: (entity) => entity.key, + sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const controlNetModelsAdapterSelectors = controlNetModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const ipAdapterModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), +export const ipAdapterModelsAdapter = createEntityAdapter({ + selectId: (entity) => entity.key, + sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const ipAdapterModelsAdapterSelectors = ipAdapterModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const t2iAdapterModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), +export const t2iAdapterModelsAdapter = createEntityAdapter({ + selectId: (entity) => entity.key, + sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const t2iAdapterModelsAdapterSelectors = t2iAdapterModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const textualInversionModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), +export const textualInversionModelsAdapter = createEntityAdapter({ + selectId: (entity) => entity.key, + sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const textualInversionModelsAdapterSelectors = textualInversionModelsAdapter.getSelectors( undefined, getSelectorsOptions ); -export const vaeModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), +export const vaeModelsAdapter = createEntityAdapter({ + selectId: (entity) => entity.key, + sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const vaeModelsAdapterSelectors = vaeModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const getModelId = ({ - base_model, - model_type, - model_name, -}: Pick) => `${base_model}/${model_type}/${model_name}`; +const buildProvidesTags = + (tagType: (typeof tagTypes)[number]) => + (result: EntityState | undefined) => { + const tags: ApiTagDescription[] = [{ type: tagType, id: LIST_TAG }, 'Model']; -const createModelEntities = (models: AnyModelConfig[]): T[] => { - const entityArray: T[] = []; - models.forEach((model) => { - const entity = { - ...cloneDeep(model), - id: getModelId(model), - } as T; - entityArray.push(entity); - }); - return entityArray; -}; + if (result) { + tags.push( + ...result.ids.map((id) => ({ + type: tagType, + id, + })) + ); + } + + return tags; + }; + +const buildTransformResponse = + (adapter: EntityAdapter) => + (response: { models: T[] }) => { + return adapter.setAll(adapter.getInitialState(), response.models); + }; export const modelsApi = api.injectEndpoints({ endpoints: (build) => ({ - getMainModels: build.query, BaseModelType[]>({ + getMainModels: build.query, BaseModelType[]>({ query: (base_models) => { const params = { model_type: 'main', @@ -190,24 +164,8 @@ export const modelsApi = api.injectEndpoints({ const query = queryString.stringify(params, { arrayFormat: 'none' }); return `models/?${query}`; }, - providesTags: (result) => { - const tags: ApiTagDescription[] = [{ type: 'MainModel', id: LIST_TAG }, 'Model']; - - if (result) { - tags.push( - ...result.ids.map((id) => ({ - type: 'MainModel' as const, - id, - })) - ); - } - - return tags; - }, - transformResponse: (response: { models: MainModelConfig[] }) => { - const entities = createModelEntities(response.models); - return mainModelsAdapter.setAll(mainModelsAdapter.getInitialState(), entities); - }, + providesTags: buildProvidesTags('MainModel'), + transformResponse: buildTransformResponse(mainModelsAdapter), }), updateMainModels: build.mutation({ query: ({ base_model, model_name, body }) => { @@ -277,26 +235,10 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: ['Model'], }), - getLoRAModels: build.query, void>({ + getLoRAModels: build.query, void>({ query: () => ({ url: 'models/', params: { model_type: 'lora' } }), - providesTags: (result) => { - const tags: ApiTagDescription[] = [{ type: 'LoRAModel', id: LIST_TAG }, 'Model']; - - if (result) { - tags.push( - ...result.ids.map((id) => ({ - type: 'LoRAModel' as const, - id, - })) - ); - } - - return tags; - }, - transformResponse: (response: { models: LoRAModelConfig[] }) => { - const entities = createModelEntities(response.models); - return loraModelsAdapter.setAll(loraModelsAdapter.getInitialState(), entities); - }, + providesTags: buildProvidesTags('LoRAModel'), + transformResponse: buildTransformResponse(loraModelsAdapter), }), updateLoRAModels: build.mutation({ query: ({ base_model, model_name, body }) => { @@ -317,110 +259,30 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: [{ type: 'LoRAModel', id: LIST_TAG }], }), - getControlNetModels: build.query, void>({ + getControlNetModels: build.query, void>({ query: () => ({ url: 'models/', params: { model_type: 'controlnet' } }), - providesTags: (result) => { - const tags: ApiTagDescription[] = [{ type: 'ControlNetModel', id: LIST_TAG }, 'Model']; - - if (result) { - tags.push( - ...result.ids.map((id) => ({ - type: 'ControlNetModel' as const, - id, - })) - ); - } - - return tags; - }, - transformResponse: (response: { models: ControlNetModelConfig[] }) => { - const entities = createModelEntities(response.models); - return controlNetModelsAdapter.setAll(controlNetModelsAdapter.getInitialState(), entities); - }, + providesTags: buildProvidesTags('ControlNetModel'), + transformResponse: buildTransformResponse(controlNetModelsAdapter), }), - getIPAdapterModels: build.query, void>({ + getIPAdapterModels: build.query, void>({ query: () => ({ url: 'models/', params: { model_type: 'ip_adapter' } }), - providesTags: (result) => { - const tags: ApiTagDescription[] = [{ type: 'IPAdapterModel', id: LIST_TAG }, 'Model']; - - if (result) { - tags.push( - ...result.ids.map((id) => ({ - type: 'IPAdapterModel' as const, - id, - })) - ); - } - - return tags; - }, - transformResponse: (response: { models: IPAdapterModelConfig[] }) => { - const entities = createModelEntities(response.models); - return ipAdapterModelsAdapter.setAll(ipAdapterModelsAdapter.getInitialState(), entities); - }, + providesTags: buildProvidesTags('IPAdapterModel'), + transformResponse: buildTransformResponse(ipAdapterModelsAdapter), }), - getT2IAdapterModels: build.query, void>({ + getT2IAdapterModels: build.query, void>({ query: () => ({ url: 'models/', params: { model_type: 't2i_adapter' } }), - providesTags: (result) => { - const tags: ApiTagDescription[] = [{ type: 'T2IAdapterModel', id: LIST_TAG }, 'Model']; - - if (result) { - tags.push( - ...result.ids.map((id) => ({ - type: 'T2IAdapterModel' as const, - id, - })) - ); - } - - return tags; - }, - transformResponse: (response: { models: T2IAdapterModelConfig[] }) => { - const entities = createModelEntities(response.models); - return t2iAdapterModelsAdapter.setAll(t2iAdapterModelsAdapter.getInitialState(), entities); - }, + providesTags: buildProvidesTags('T2IAdapterModel'), + transformResponse: buildTransformResponse(t2iAdapterModelsAdapter), }), - getVaeModels: build.query, void>({ + getVaeModels: build.query, void>({ query: () => ({ url: 'models/', params: { model_type: 'vae' } }), - providesTags: (result) => { - const tags: ApiTagDescription[] = [{ type: 'VaeModel', id: LIST_TAG }, 'Model']; - - if (result) { - tags.push( - ...result.ids.map((id) => ({ - type: 'VaeModel' as const, - id, - })) - ); - } - - return tags; - }, - transformResponse: (response: { models: VaeModelConfig[] }) => { - const entities = createModelEntities(response.models); - return vaeModelsAdapter.setAll(vaeModelsAdapter.getInitialState(), entities); - }, + providesTags: buildProvidesTags('VaeModel'), + transformResponse: buildTransformResponse(vaeModelsAdapter), }), - getTextualInversionModels: build.query, void>({ + getTextualInversionModels: build.query, void>({ query: () => ({ url: 'models/', params: { model_type: 'embedding' } }), - providesTags: (result) => { - const tags: ApiTagDescription[] = [{ type: 'TextualInversionModel', id: LIST_TAG }, 'Model']; - - if (result) { - tags.push( - ...result.ids.map((id) => ({ - type: 'TextualInversionModel' as const, - id, - })) - ); - } - - return tags; - }, - transformResponse: (response: { models: TextualInversionModelConfig[] }) => { - const entities = createModelEntities(response.models); - return textualInversionModelsAdapter.setAll(textualInversionModelsAdapter.getInitialState(), entities); - }, + providesTags: buildProvidesTags('TextualInversionModel'), + transformResponse: buildTransformResponse(textualInversionModelsAdapter), }), getModelsInFolder: build.query({ query: (arg) => { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index f9a1decf65..7a02cc5568 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -2,6 +2,7 @@ import type { UseToastOptions } from '@invoke-ai/ui-library'; import type { EntityState } from '@reduxjs/toolkit'; import type { components, paths } from 'services/api/schema'; import type { O } from 'ts-toolbelt'; +import type { SetRequired } from 'type-fest'; export type S = components['schemas']; @@ -54,40 +55,34 @@ export type LoRAModelFormat = S['LoRAModelFormat']; export type ControlNetModelField = S['ControlNetModelField']; export type IPAdapterModelField = S['IPAdapterModelField']; export type T2IAdapterModelField = S['T2IAdapterModelField']; -export type ModelsList = S['invokeai__app__api__routers__models__ModelsList']; export type ControlField = S['ControlField']; export type IPAdapterField = S['IPAdapterField']; // Model Configs -export type LoRAModelConfig = S['LoRAModelConfig']; -export type VaeModelConfig = S['VaeModelConfig']; -export type ControlNetModelCheckpointConfig = S['ControlNetModelCheckpointConfig']; -export type ControlNetModelDiffusersConfig = S['ControlNetModelDiffusersConfig']; -export type ControlNetModelConfig = ControlNetModelCheckpointConfig | ControlNetModelDiffusersConfig; -export type IPAdapterModelInvokeAIConfig = S['IPAdapterModelInvokeAIConfig']; -export type IPAdapterModelConfig = IPAdapterModelInvokeAIConfig; -export type T2IAdapterModelDiffusersConfig = S['T2IAdapterModelDiffusersConfig']; -export type T2IAdapterModelConfig = T2IAdapterModelDiffusersConfig; -export type TextualInversionModelConfig = S['TextualInversionModelConfig']; -export type DiffusersModelConfig = - | S['StableDiffusion1ModelDiffusersConfig'] - | S['StableDiffusion2ModelDiffusersConfig'] - | S['StableDiffusionXLModelDiffusersConfig']; -export type CheckpointModelConfig = - | S['StableDiffusion1ModelCheckpointConfig'] - | S['StableDiffusion2ModelCheckpointConfig'] - | S['StableDiffusionXLModelCheckpointConfig']; + +// TODO(MM2): Can we make key required in the pydantic model? +type KeyRequired = SetRequired; +export type LoRAConfig = KeyRequired; +// TODO(MM2): Can we rename this from Vae -> VAE +export type VAEConfig = KeyRequired | KeyRequired; +export type ControlNetConfig = KeyRequired | KeyRequired; +export type IPAdapterConfig = KeyRequired; +// TODO(MM2): Can we rename this to T2IAdapterConfig +export type T2IAdapterConfig = KeyRequired; +export type TextualInversionConfig = KeyRequired; +export type DiffusersModelConfig = KeyRequired; +export type CheckpointModelConfig = KeyRequired; export type MainModelConfig = DiffusersModelConfig | CheckpointModelConfig; export type AnyModelConfig = - | LoRAModelConfig - | VaeModelConfig - | ControlNetModelConfig - | IPAdapterModelConfig - | T2IAdapterModelConfig - | TextualInversionModelConfig + | LoRAConfig + | VAEConfig + | ControlNetConfig + | IPAdapterConfig + | T2IAdapterConfig + | TextualInversionConfig | MainModelConfig; -export type MergeModelConfig = S['Body_merge_models']; +export type MergeModelConfig = S['Body_merge']; export type ImportModelConfig = S['Body_import_model']; // Graphs From db363b517880e874370d633d88d0917606be8a5a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Feb 2024 21:57:30 +1100 Subject: [PATCH 141/411] refactor(ui): url builders for each router The MM2 router is at `api/v2/models`. URL builder utils make this a bit easier to manage. --- .../web/src/services/api/endpoints/appInfo.ts | 24 +++++--- .../web/src/services/api/endpoints/boards.ts | 20 +++++-- .../web/src/services/api/endpoints/images.ts | 56 ++++++++++++------- .../web/src/services/api/endpoints/models.ts | 53 +++++++++++------- .../web/src/services/api/endpoints/queue.ts | 40 +++++++------ .../src/services/api/endpoints/utilities.ts | 12 +++- .../src/services/api/endpoints/workflows.ts | 20 +++++-- .../frontend/web/src/services/api/index.ts | 3 + .../frontend/web/src/services/api/schema.ts | 36 ++++++------ .../frontend/web/src/services/api/types.ts | 6 +- .../frontend/web/src/services/api/util.ts | 3 +- invokeai/frontend/web/vite.config.mts | 6 +- 12 files changed, 177 insertions(+), 102 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts index c0916f568e..a7efaafcc8 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/appInfo.ts @@ -3,27 +3,35 @@ import type { OpenAPIV3_1 } from 'openapi-types'; import type { paths } from 'services/api/schema'; import type { AppConfig, AppDependencyVersions, AppVersion } from 'services/api/types'; -import { api } from '..'; +import { api, buildV1Url } from '..'; + +/** + * Builds an endpoint URL for the app router + * @example + * buildAppInfoUrl('some-path') + * // '/api/v1/app/some-path' + */ +const buildAppInfoUrl = (path: string = '') => buildV1Url(`app/${path}`); export const appInfoApi = api.injectEndpoints({ endpoints: (build) => ({ getAppVersion: build.query({ query: () => ({ - url: `app/version`, + url: buildAppInfoUrl('version'), method: 'GET', }), providesTags: ['FetchOnReconnect'], }), getAppDeps: build.query({ query: () => ({ - url: `app/app_deps`, + url: buildAppInfoUrl('app_deps'), method: 'GET', }), providesTags: ['FetchOnReconnect'], }), getAppConfig: build.query({ query: () => ({ - url: `app/config`, + url: buildAppInfoUrl('config'), method: 'GET', }), providesTags: ['FetchOnReconnect'], @@ -33,28 +41,28 @@ export const appInfoApi = api.injectEndpoints({ void >({ query: () => ({ - url: `app/invocation_cache/status`, + url: buildAppInfoUrl('invocation_cache/status'), method: 'GET', }), providesTags: ['InvocationCacheStatus', 'FetchOnReconnect'], }), clearInvocationCache: build.mutation({ query: () => ({ - url: `app/invocation_cache`, + url: buildAppInfoUrl('invocation_cache'), method: 'DELETE', }), invalidatesTags: ['InvocationCacheStatus'], }), enableInvocationCache: build.mutation({ query: () => ({ - url: `app/invocation_cache/enable`, + url: buildAppInfoUrl('invocation_cache/enable'), method: 'PUT', }), invalidatesTags: ['InvocationCacheStatus'], }), disableInvocationCache: build.mutation({ query: () => ({ - url: `app/invocation_cache/disable`, + url: buildAppInfoUrl('invocation_cache/disable'), method: 'PUT', }), invalidatesTags: ['InvocationCacheStatus'], diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts index 6977a2bd53..8efda86737 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts @@ -9,7 +9,15 @@ import type { import { getListImagesUrl } from 'services/api/util'; import type { ApiTagDescription } from '..'; -import { api, LIST_TAG } from '..'; +import { api, buildV1Url, LIST_TAG } from '..'; + +/** + * Builds an endpoint URL for the boards router + * @example + * buildBoardsUrl('some-path') + * // '/api/v1/boards/some-path' + */ +export const buildBoardsUrl = (path: string = '') => buildV1Url(`boards/${path}`); export const boardsApi = api.injectEndpoints({ endpoints: (build) => ({ @@ -17,7 +25,7 @@ export const boardsApi = api.injectEndpoints({ * Boards Queries */ listBoards: build.query({ - query: (arg) => ({ url: 'boards/', params: arg }), + query: (arg) => ({ url: buildBoardsUrl(), params: arg }), providesTags: (result) => { // any list of boards const tags: ApiTagDescription[] = [{ type: 'Board', id: LIST_TAG }, 'FetchOnReconnect']; @@ -38,7 +46,7 @@ export const boardsApi = api.injectEndpoints({ listAllBoards: build.query, void>({ query: () => ({ - url: 'boards/', + url: buildBoardsUrl(), params: { all: true }, }), providesTags: (result) => { @@ -61,7 +69,7 @@ export const boardsApi = api.injectEndpoints({ listAllImageNamesForBoard: build.query, string>({ query: (board_id) => ({ - url: `boards/${board_id}/image_names`, + url: buildBoardsUrl(`${board_id}/image_names`), }), providesTags: (result, error, arg) => [{ type: 'ImageNameList', id: arg }, 'FetchOnReconnect'], keepUnusedDataFor: 0, @@ -107,7 +115,7 @@ export const boardsApi = api.injectEndpoints({ createBoard: build.mutation({ query: (board_name) => ({ - url: `boards/`, + url: buildBoardsUrl(), method: 'POST', params: { board_name }, }), @@ -116,7 +124,7 @@ export const boardsApi = api.injectEndpoints({ updateBoard: build.mutation({ query: ({ board_id, changes }) => ({ - url: `boards/${board_id}`, + url: buildBoardsUrl(board_id), method: 'PATCH', body: changes, }), diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 181c8d23fc..49eb28390f 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -26,8 +26,24 @@ import { } from 'services/api/util'; import type { ApiTagDescription } from '..'; -import { api, LIST_TAG } from '..'; -import { boardsApi } from './boards'; +import { api, buildV1Url, LIST_TAG } from '..'; +import { boardsApi, buildBoardsUrl } from './boards'; + +/** + * Builds an endpoint URL for the images router + * @example + * buildImagesUrl('some-path') + * // '/api/v1/images/some-path' + */ +const buildImagesUrl = (path: string = '') => buildV1Url(`images/${path}`); + +/** + * Builds an endpoint URL for the board_images router + * @example + * buildBoardImagesUrl('some-path') + * // '/api/v1/board_images/some-path' + */ +const buildBoardImagesUrl = (path: string = '') => buildV1Url(`board_images/${path}`); export const imagesApi = api.injectEndpoints({ endpoints: (build) => ({ @@ -90,20 +106,20 @@ export const imagesApi = api.injectEndpoints({ keepUnusedDataFor: 86400, }), getIntermediatesCount: build.query({ - query: () => ({ url: 'images/intermediates' }), + query: () => ({ url: buildImagesUrl('intermediates') }), providesTags: ['IntermediatesCount', 'FetchOnReconnect'], }), clearIntermediates: build.mutation({ - query: () => ({ url: `images/intermediates`, method: 'DELETE' }), + query: () => ({ url: buildImagesUrl('intermediates'), method: 'DELETE' }), invalidatesTags: ['IntermediatesCount'], }), getImageDTO: build.query({ - query: (image_name) => ({ url: `images/i/${image_name}` }), + query: (image_name) => ({ url: buildImagesUrl(`i/${image_name}`) }), providesTags: (result, error, image_name) => [{ type: 'Image', id: image_name }], keepUnusedDataFor: 86400, // 24 hours }), getImageMetadata: build.query({ - query: (image_name) => ({ url: `images/i/${image_name}/metadata` }), + query: (image_name) => ({ url: buildImagesUrl(`i/${image_name}/metadata`) }), providesTags: (result, error, image_name) => [{ type: 'ImageMetadata', id: image_name }], transformResponse: ( response: paths['/api/v1/images/i/{image_name}/metadata']['get']['responses']['200']['content']['application/json'] @@ -130,7 +146,7 @@ export const imagesApi = api.injectEndpoints({ }), deleteImage: build.mutation({ query: ({ image_name }) => ({ - url: `images/i/${image_name}`, + url: buildImagesUrl(`i/${image_name}`), method: 'DELETE', }), async onQueryStarted(imageDTO, { dispatch, queryFulfilled }) { @@ -185,7 +201,7 @@ export const imagesApi = api.injectEndpoints({ query: ({ imageDTOs }) => { const image_names = imageDTOs.map((imageDTO) => imageDTO.image_name); return { - url: `images/delete`, + url: buildImagesUrl('delete'), method: 'POST', body: { image_names, @@ -258,7 +274,7 @@ export const imagesApi = api.injectEndpoints({ */ changeImageIsIntermediate: build.mutation({ query: ({ imageDTO, is_intermediate }) => ({ - url: `images/i/${imageDTO.image_name}`, + url: buildImagesUrl(`i/${imageDTO.image_name}`), method: 'PATCH', body: { is_intermediate }, }), @@ -380,7 +396,7 @@ export const imagesApi = api.injectEndpoints({ */ changeImageSessionId: build.mutation({ query: ({ imageDTO, session_id }) => ({ - url: `images/i/${imageDTO.image_name}`, + url: buildImagesUrl(`i/${imageDTO.image_name}`), method: 'PATCH', body: { session_id }, }), @@ -417,7 +433,7 @@ export const imagesApi = api.injectEndpoints({ { imageDTOs: ImageDTO[] } >({ query: ({ imageDTOs: images }) => ({ - url: `images/star`, + url: buildImagesUrl('star'), method: 'POST', body: { image_names: images.map((img) => img.image_name) }, }), @@ -511,7 +527,7 @@ export const imagesApi = api.injectEndpoints({ { imageDTOs: ImageDTO[] } >({ query: ({ imageDTOs: images }) => ({ - url: `images/unstar`, + url: buildImagesUrl('unstar'), method: 'POST', body: { image_names: images.map((img) => img.image_name) }, }), @@ -611,7 +627,7 @@ export const imagesApi = api.injectEndpoints({ const formData = new FormData(); formData.append('file', file); return { - url: `images/upload`, + url: buildImagesUrl('upload'), method: 'POST', body: formData, params: { @@ -674,7 +690,7 @@ export const imagesApi = api.injectEndpoints({ }), deleteBoard: build.mutation({ - query: (board_id) => ({ url: `boards/${board_id}`, method: 'DELETE' }), + query: (board_id) => ({ url: buildBoardsUrl(board_id), method: 'DELETE' }), invalidatesTags: () => [ { type: 'Board', id: LIST_TAG }, // invalidate the 'No Board' cache @@ -764,7 +780,7 @@ export const imagesApi = api.injectEndpoints({ deleteBoardAndImages: build.mutation({ query: (board_id) => ({ - url: `boards/${board_id}`, + url: buildBoardsUrl(board_id), method: 'DELETE', params: { include_images: true }, }), @@ -840,7 +856,7 @@ export const imagesApi = api.injectEndpoints({ query: ({ board_id, imageDTO }) => { const { image_name } = imageDTO; return { - url: `board_images/`, + url: buildBoardImagesUrl(), method: 'POST', body: { board_id, image_name }, }; @@ -961,7 +977,7 @@ export const imagesApi = api.injectEndpoints({ query: ({ imageDTO }) => { const { image_name } = imageDTO; return { - url: `board_images/`, + url: buildBoardImagesUrl(), method: 'DELETE', body: { image_name }, }; @@ -1080,7 +1096,7 @@ export const imagesApi = api.injectEndpoints({ } >({ query: ({ board_id, imageDTOs }) => ({ - url: `board_images/batch`, + url: buildBoardImagesUrl('batch'), method: 'POST', body: { image_names: imageDTOs.map((i) => i.image_name), @@ -1197,7 +1213,7 @@ export const imagesApi = api.injectEndpoints({ } >({ query: ({ imageDTOs }) => ({ - url: `board_images/batch/delete`, + url: buildBoardImagesUrl('batch/delete'), method: 'POST', body: { image_names: imageDTOs.map((i) => i.image_name), @@ -1321,7 +1337,7 @@ export const imagesApi = api.injectEndpoints({ components['schemas']['Body_download_images_from_list'] >({ query: ({ image_names, board_id }) => ({ - url: `images/download`, + url: buildImagesUrl('download'), method: 'POST', body: { image_names, diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 97e221454d..9a7f108056 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -19,7 +19,10 @@ import type { } from 'services/api/types'; import type { ApiTagDescription, tagTypes } from '..'; -import { api, LIST_TAG } from '..'; +import { api, buildV2Url, LIST_TAG } from '..'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const getModelId = (input: any): any => input; type UpdateMainModelArg = { base_model: BaseModelType; @@ -36,6 +39,8 @@ type UpdateLoRAModelArg = { type UpdateMainModelResponse = paths['/api/v2/models/i/{key}']['patch']['responses']['200']['content']['application/json']; +type ListModelsArg = NonNullable; + type UpdateLoRAModelResponse = UpdateMainModelResponse; type DeleteMainModelArg = { @@ -152,17 +157,25 @@ const buildTransformResponse = return adapter.setAll(adapter.getInitialState(), response.models); }; +/** + * Builds an endpoint URL for the models router + * @example + * buildModelsUrl('some-path') + * // '/api/v1/models/some-path' + */ +const buildModelsUrl = (path: string = '') => buildV2Url(`models/${path}`); + export const modelsApi = api.injectEndpoints({ endpoints: (build) => ({ getMainModels: build.query, BaseModelType[]>({ query: (base_models) => { - const params = { + const params: ListModelsArg = { model_type: 'main', base_models, }; const query = queryString.stringify(params, { arrayFormat: 'none' }); - return `models/?${query}`; + return buildModelsUrl(`?${query}`); }, providesTags: buildProvidesTags('MainModel'), transformResponse: buildTransformResponse(mainModelsAdapter), @@ -170,7 +183,7 @@ export const modelsApi = api.injectEndpoints({ updateMainModels: build.mutation({ query: ({ base_model, model_name, body }) => { return { - url: `models/${base_model}/main/${model_name}`, + url: buildModelsUrl(`${base_model}/main/${model_name}`), method: 'PATCH', body: body, }; @@ -180,7 +193,7 @@ export const modelsApi = api.injectEndpoints({ importMainModels: build.mutation({ query: ({ body }) => { return { - url: `models/import`, + url: buildModelsUrl('import'), method: 'POST', body: body, }; @@ -190,7 +203,7 @@ export const modelsApi = api.injectEndpoints({ addMainModels: build.mutation({ query: ({ body }) => { return { - url: `models/add`, + url: buildModelsUrl('add'), method: 'POST', body: body, }; @@ -200,7 +213,7 @@ export const modelsApi = api.injectEndpoints({ deleteMainModels: build.mutation({ query: ({ base_model, model_name, model_type }) => { return { - url: `models/${base_model}/${model_type}/${model_name}`, + url: buildModelsUrl(`${base_model}/${model_type}/${model_name}`), method: 'DELETE', }; }, @@ -209,7 +222,7 @@ export const modelsApi = api.injectEndpoints({ convertMainModels: build.mutation({ query: ({ base_model, model_name, convert_dest_directory }) => { return { - url: `models/convert/${base_model}/main/${model_name}`, + url: buildModelsUrl(`convert/${base_model}/main/${model_name}`), method: 'PUT', params: { convert_dest_directory }, }; @@ -219,7 +232,7 @@ export const modelsApi = api.injectEndpoints({ mergeMainModels: build.mutation({ query: ({ base_model, body }) => { return { - url: `models/merge/${base_model}`, + url: buildModelsUrl(`merge/${base_model}`), method: 'PUT', body: body, }; @@ -229,21 +242,21 @@ export const modelsApi = api.injectEndpoints({ syncModels: build.mutation({ query: () => { return { - url: `models/sync`, + url: buildModelsUrl('sync'), method: 'POST', }; }, invalidatesTags: ['Model'], }), getLoRAModels: build.query, void>({ - query: () => ({ url: 'models/', params: { model_type: 'lora' } }), + query: () => ({ url: buildModelsUrl(), params: { model_type: 'lora' } }), providesTags: buildProvidesTags('LoRAModel'), transformResponse: buildTransformResponse(loraModelsAdapter), }), updateLoRAModels: build.mutation({ query: ({ base_model, model_name, body }) => { return { - url: `models/${base_model}/lora/${model_name}`, + url: buildModelsUrl(`${base_model}/lora/${model_name}`), method: 'PATCH', body: body, }; @@ -253,34 +266,34 @@ export const modelsApi = api.injectEndpoints({ deleteLoRAModels: build.mutation({ query: ({ base_model, model_name }) => { return { - url: `models/${base_model}/lora/${model_name}`, + url: buildModelsUrl(`${base_model}/lora/${model_name}`), method: 'DELETE', }; }, invalidatesTags: [{ type: 'LoRAModel', id: LIST_TAG }], }), getControlNetModels: build.query, void>({ - query: () => ({ url: 'models/', params: { model_type: 'controlnet' } }), + query: () => ({ url: buildModelsUrl(), params: { model_type: 'controlnet' } }), providesTags: buildProvidesTags('ControlNetModel'), transformResponse: buildTransformResponse(controlNetModelsAdapter), }), getIPAdapterModels: build.query, void>({ - query: () => ({ url: 'models/', params: { model_type: 'ip_adapter' } }), + query: () => ({ url: buildModelsUrl(), params: { model_type: 'ip_adapter' } }), providesTags: buildProvidesTags('IPAdapterModel'), transformResponse: buildTransformResponse(ipAdapterModelsAdapter), }), getT2IAdapterModels: build.query, void>({ - query: () => ({ url: 'models/', params: { model_type: 't2i_adapter' } }), + query: () => ({ url: buildModelsUrl(), params: { model_type: 't2i_adapter' } }), providesTags: buildProvidesTags('T2IAdapterModel'), transformResponse: buildTransformResponse(t2iAdapterModelsAdapter), }), getVaeModels: build.query, void>({ - query: () => ({ url: 'models/', params: { model_type: 'vae' } }), + query: () => ({ url: buildModelsUrl(), params: { model_type: 'vae' } }), providesTags: buildProvidesTags('VaeModel'), transformResponse: buildTransformResponse(vaeModelsAdapter), }), getTextualInversionModels: build.query, void>({ - query: () => ({ url: 'models/', params: { model_type: 'embedding' } }), + query: () => ({ url: buildModelsUrl(), params: { model_type: 'embedding' } }), providesTags: buildProvidesTags('TextualInversionModel'), transformResponse: buildTransformResponse(textualInversionModelsAdapter), }), @@ -288,14 +301,14 @@ export const modelsApi = api.injectEndpoints({ query: (arg) => { const folderQueryStr = queryString.stringify(arg, {}); return { - url: `/models/search?${folderQueryStr}`, + url: buildModelsUrl(`search?${folderQueryStr}`), }; }, }), getCheckpointConfigs: build.query({ query: () => { return { - url: `/models/ckpt_confs`, + url: buildModelsUrl(`ckpt_confs`), }; }, }), diff --git a/invokeai/frontend/web/src/services/api/endpoints/queue.ts b/invokeai/frontend/web/src/services/api/endpoints/queue.ts index 6c0798a936..385aa8ad12 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/queue.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/queue.ts @@ -7,7 +7,15 @@ import queryString from 'query-string'; import type { components, paths } from 'services/api/schema'; import type { ApiTagDescription } from '..'; -import { api } from '..'; +import { api, buildV1Url } from '..'; + +/** + * Builds an endpoint URL for the queue router + * @example + * buildQueueUrl('some-path') + * // '/api/v1/queue/queue_id/some-path' + */ +const buildQueueUrl = (path: string = '') => buildV1Url(`queue/${$queueId.get()}/${path}`); const getListQueueItemsUrl = (queryArgs?: paths['/api/v1/queue/{queue_id}/list']['get']['parameters']['query']) => { const query = queryArgs @@ -17,10 +25,10 @@ const getListQueueItemsUrl = (queryArgs?: paths['/api/v1/queue/{queue_id}/list'] : undefined; if (query) { - return `queue/${$queueId.get()}/list?${query}`; + return buildQueueUrl(`list?${query}`); } - return `queue/${$queueId.get()}/list`; + return buildQueueUrl('list'); }; export type SessionQueueItemStatus = NonNullable< @@ -58,7 +66,7 @@ export const queueApi = api.injectEndpoints({ paths['/api/v1/queue/{queue_id}/enqueue_batch']['post']['requestBody']['content']['application/json'] >({ query: (arg) => ({ - url: `queue/${$queueId.get()}/enqueue_batch`, + url: buildQueueUrl('enqueue_batch'), body: arg, method: 'POST', }), @@ -78,7 +86,7 @@ export const queueApi = api.injectEndpoints({ void >({ query: () => ({ - url: `queue/${$queueId.get()}/processor/resume`, + url: buildQueueUrl('processor/resume'), method: 'PUT', }), invalidatesTags: ['CurrentSessionQueueItem', 'SessionQueueStatus'], @@ -88,7 +96,7 @@ export const queueApi = api.injectEndpoints({ void >({ query: () => ({ - url: `queue/${$queueId.get()}/processor/pause`, + url: buildQueueUrl('processor/pause'), method: 'PUT', }), invalidatesTags: ['CurrentSessionQueueItem', 'SessionQueueStatus'], @@ -98,7 +106,7 @@ export const queueApi = api.injectEndpoints({ void >({ query: () => ({ - url: `queue/${$queueId.get()}/prune`, + url: buildQueueUrl('prune'), method: 'PUT', }), invalidatesTags: ['SessionQueueStatus', 'BatchStatus'], @@ -117,7 +125,7 @@ export const queueApi = api.injectEndpoints({ void >({ query: () => ({ - url: `queue/${$queueId.get()}/clear`, + url: buildQueueUrl('clear'), method: 'PUT', }), invalidatesTags: [ @@ -142,7 +150,7 @@ export const queueApi = api.injectEndpoints({ void >({ query: () => ({ - url: `queue/${$queueId.get()}/current`, + url: buildQueueUrl('current'), method: 'GET', }), providesTags: (result) => { @@ -158,7 +166,7 @@ export const queueApi = api.injectEndpoints({ void >({ query: () => ({ - url: `queue/${$queueId.get()}/next`, + url: buildQueueUrl('next'), method: 'GET', }), providesTags: (result) => { @@ -174,7 +182,7 @@ export const queueApi = api.injectEndpoints({ void >({ query: () => ({ - url: `queue/${$queueId.get()}/status`, + url: buildQueueUrl('status'), method: 'GET', }), providesTags: ['SessionQueueStatus', 'FetchOnReconnect'], @@ -184,7 +192,7 @@ export const queueApi = api.injectEndpoints({ { batch_id: string } >({ query: ({ batch_id }) => ({ - url: `queue/${$queueId.get()}/b/${batch_id}/status`, + url: buildQueueUrl(`/b/${batch_id}/status`), method: 'GET', }), providesTags: (result) => { @@ -200,7 +208,7 @@ export const queueApi = api.injectEndpoints({ number >({ query: (item_id) => ({ - url: `queue/${$queueId.get()}/i/${item_id}`, + url: buildQueueUrl(`i/${item_id}`), method: 'GET', }), providesTags: (result) => { @@ -216,7 +224,7 @@ export const queueApi = api.injectEndpoints({ number >({ query: (item_id) => ({ - url: `queue/${$queueId.get()}/i/${item_id}/cancel`, + url: buildQueueUrl(`i/${item_id}/cancel`), method: 'PUT', }), onQueryStarted: async (item_id, { dispatch, queryFulfilled }) => { @@ -253,7 +261,7 @@ export const queueApi = api.injectEndpoints({ paths['/api/v1/queue/{queue_id}/cancel_by_batch_ids']['put']['requestBody']['content']['application/json'] >({ query: (body) => ({ - url: `queue/${$queueId.get()}/cancel_by_batch_ids`, + url: buildQueueUrl('cancel_by_batch_ids'), method: 'PUT', body, }), @@ -279,7 +287,7 @@ export const queueApi = api.injectEndpoints({ method: 'GET', }), serializeQueryArgs: () => { - return `queue/${$queueId.get()}/list`; + return buildQueueUrl('list'); }, transformResponse: (response: components['schemas']['CursorPaginatedResults_SessionQueueItemDTO_']) => queueItemsAdapter.addMany( diff --git a/invokeai/frontend/web/src/services/api/endpoints/utilities.ts b/invokeai/frontend/web/src/services/api/endpoints/utilities.ts index c08ee62dc9..309dd2dc79 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/utilities.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/utilities.ts @@ -1,6 +1,14 @@ import type { components } from 'services/api/schema'; -import { api } from '..'; +import { api, buildV1Url } from '..'; + +/** + * Builds an endpoint URL for the utilities router + * @example + * buildUtilitiesUrl('some-path') + * // '/api/v1/utilities/some-path' + */ +const buildUtilitiesUrl = (path: string = '') => buildV1Url(`utilities/${path}`); export const utilitiesApi = api.injectEndpoints({ endpoints: (build) => ({ @@ -9,7 +17,7 @@ export const utilitiesApi = api.injectEndpoints({ { prompt: string; max_prompts: number } >({ query: (arg) => ({ - url: 'utilities/dynamicprompts', + url: buildUtilitiesUrl('dynamicprompts'), body: arg, method: 'POST', }), diff --git a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts index c382f7e111..1e64809e5a 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts @@ -1,6 +1,14 @@ import type { paths } from 'services/api/schema'; -import { api, LIST_TAG } from '..'; +import { api, buildV1Url, LIST_TAG } from '..'; + +/** + * Builds an endpoint URL for the workflows router + * @example + * buildWorkflowsUrl('some-path') + * // '/api/v1/workflows/some-path' + */ +const buildWorkflowsUrl = (path: string = '') => buildV1Url(`workflows/${path}`); export const workflowsApi = api.injectEndpoints({ endpoints: (build) => ({ @@ -8,7 +16,7 @@ export const workflowsApi = api.injectEndpoints({ paths['/api/v1/workflows/i/{workflow_id}']['get']['responses']['200']['content']['application/json'], string >({ - query: (workflow_id) => `workflows/i/${workflow_id}`, + query: (workflow_id) => buildWorkflowsUrl(`i/${workflow_id}`), providesTags: (result, error, workflow_id) => [{ type: 'Workflow', id: workflow_id }, 'FetchOnReconnect'], onQueryStarted: async (arg, api) => { const { dispatch, queryFulfilled } = api; @@ -22,7 +30,7 @@ export const workflowsApi = api.injectEndpoints({ }), deleteWorkflow: build.mutation({ query: (workflow_id) => ({ - url: `workflows/i/${workflow_id}`, + url: buildWorkflowsUrl(`i/${workflow_id}`), method: 'DELETE', }), invalidatesTags: (result, error, workflow_id) => [ @@ -36,7 +44,7 @@ export const workflowsApi = api.injectEndpoints({ paths['/api/v1/workflows/']['post']['requestBody']['content']['application/json']['workflow'] >({ query: (workflow) => ({ - url: 'workflows/', + url: buildWorkflowsUrl(), method: 'POST', body: { workflow }, }), @@ -50,7 +58,7 @@ export const workflowsApi = api.injectEndpoints({ paths['/api/v1/workflows/i/{workflow_id}']['patch']['requestBody']['content']['application/json']['workflow'] >({ query: (workflow) => ({ - url: `workflows/i/${workflow.id}`, + url: buildWorkflowsUrl(`i/${workflow.id}`), method: 'PATCH', body: { workflow }, }), @@ -65,7 +73,7 @@ export const workflowsApi = api.injectEndpoints({ NonNullable >({ query: (params) => ({ - url: 'workflows/', + url: buildWorkflowsUrl(), params, }), providesTags: ['FetchOnReconnect', { type: 'Workflow', id: LIST_TAG }], diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 6a342bb72d..f32ae34cb7 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -112,3 +112,6 @@ function getCircularReplacer() { return value; }; } + +export const buildV1Url = (path: string): string => `api/v1/${path}`; +export const buildV2Url = (path: string): string => `api/v2/${path}`; diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 3393e74d48..40fc262be2 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -4212,7 +4212,7 @@ export type components = { * @description The nodes in this graph */ nodes?: { - [key: string]: components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"]; + [key: string]: components["schemas"]["ControlNetInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"]; }; /** * Edges @@ -4249,7 +4249,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["ImageCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["String2Output"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["CLIPOutput"]; + [key: string]: components["schemas"]["SchedulerOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["String2Output"] | components["schemas"]["IntegerOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["IterateInvocationOutput"]; }; /** * Errors @@ -11119,17 +11119,11 @@ export type components = { */ VaeModelFormat: "checkpoint" | "diffusers"; /** - * StableDiffusion2ModelFormat + * T2IAdapterModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * CLIPVisionModelFormat - * @description An enumeration. - * @enum {string} - */ - CLIPVisionModelFormat: "diffusers"; + T2IAdapterModelFormat: "diffusers"; /** * StableDiffusionXLModelFormat * @description An enumeration. @@ -11142,12 +11136,6 @@ export type components = { * @enum {string} */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. @@ -11155,17 +11143,29 @@ export type components = { */ StableDiffusionOnnxModelFormat: "olive" | "onnx"; /** - * T2IAdapterModelFormat + * ControlNetModelFormat * @description An enumeration. * @enum {string} */ - T2IAdapterModelFormat: "diffusers"; + ControlNetModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * LoRAModelFormat * @description An enumeration. * @enum {string} */ LoRAModelFormat: "lycoris" | "diffusers"; + /** + * CLIPVisionModelFormat + * @description An enumeration. + * @enum {string} + */ + CLIPVisionModelFormat: "diffusers"; /** * IPAdapterModelFormat * @description An enumeration. diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 7a02cc5568..4ae2f9b594 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -61,11 +61,13 @@ export type IPAdapterField = S['IPAdapterField']; // Model Configs // TODO(MM2): Can we make key required in the pydantic model? -type KeyRequired = SetRequired; +type KeyRequired = SetRequired; export type LoRAConfig = KeyRequired; // TODO(MM2): Can we rename this from Vae -> VAE export type VAEConfig = KeyRequired | KeyRequired; -export type ControlNetConfig = KeyRequired | KeyRequired; +export type ControlNetConfig = + | KeyRequired + | KeyRequired; export type IPAdapterConfig = KeyRequired; // TODO(MM2): Can we rename this to T2IAdapterConfig export type T2IAdapterConfig = KeyRequired; diff --git a/invokeai/frontend/web/src/services/api/util.ts b/invokeai/frontend/web/src/services/api/util.ts index f7f36f4630..a7a5d6451e 100644 --- a/invokeai/frontend/web/src/services/api/util.ts +++ b/invokeai/frontend/web/src/services/api/util.ts @@ -3,6 +3,7 @@ import { getSelectorsOptions } from 'app/store/createMemoizedSelector'; import { dateComparator } from 'common/util/dateComparator'; import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types'; import queryString from 'query-string'; +import { buildV1Url } from 'services/api'; import type { ImageCache, ImageDTO, ListImagesArgs } from './types'; @@ -79,4 +80,4 @@ export const imagesSelectors = imagesAdapter.getSelectors(undefined, getSelector // Helper to create the url for the listImages endpoint. Also we use it to create the cache key. export const getListImagesUrl = (queryArgs: ListImagesArgs) => - `images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`; + buildV1Url(`images/?${queryString.stringify(queryArgs, { arrayFormat: 'none' })}`); diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index f4dbae7123..32e3e1f64f 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -76,9 +76,9 @@ export default defineConfig(({ mode }) => { changeOrigin: true, }, // proxy nodes api - '/api/v1': { - target: 'http://127.0.0.1:9090/api/v1', - rewrite: (path) => path.replace(/^\/api\/v1/, ''), + '/api/': { + target: 'http://127.0.0.1:9090/api/', + rewrite: (path) => path.replace(/^\/api/, ''), changeOrigin: true, }, }, From e50b76571adf5ba65904edeb02938f95a5b450e3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:41:09 +1100 Subject: [PATCH 142/411] feat(ui): fix main model & control adapter model selects --- .../src/common/hooks/useModelCustomSelect.ts | 88 +++++++++++++++++++ .../parameters/ParamControlAdapterModel.tsx | 49 +++-------- .../hooks/useControlAdapterModelEntities.ts | 23 ----- .../hooks/useControlAdapterModelQuery.ts | 26 ++++++ .../hooks/useControlAdapterType.ts | 10 ++- .../subpanels/ModelManagerPanel.tsx | 6 +- .../MainModel/ParamMainModelSelect.tsx | 45 ++++------ .../src/features/parameters/store/actions.ts | 5 +- .../features/parameters/types/constants.ts | 4 +- 9 files changed, 154 insertions(+), 102 deletions(-) create mode 100644 invokeai/frontend/web/src/common/hooks/useModelCustomSelect.ts delete mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelEntities.ts create mode 100644 invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelQuery.ts diff --git a/invokeai/frontend/web/src/common/hooks/useModelCustomSelect.ts b/invokeai/frontend/web/src/common/hooks/useModelCustomSelect.ts new file mode 100644 index 0000000000..07ea98a274 --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useModelCustomSelect.ts @@ -0,0 +1,88 @@ +import type { Item } from '@invoke-ai/ui-library'; +import type { EntityState } from '@reduxjs/toolkit'; +import { EMPTY_ARRAY } from 'app/store/util'; +import type { ModelIdentifierWithBase } from 'features/nodes/types/common'; +import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants'; +import { filter } from 'lodash-es'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { AnyModelConfig } from 'services/api/types'; + +type UseModelCustomSelectArg = { + data: EntityState | undefined; + isLoading: boolean; + selectedModel?: ModelIdentifierWithBase | null; + onChange: (value: T | null) => void; + modelFilter?: (model: T) => boolean; + isModelDisabled?: (model: T) => boolean; +}; + +type UseModelCustomSelectReturn = { + selectedItem: Item | null; + items: Item[]; + onChange: (item: Item | null) => void; + placeholder: string; +}; + +const modelFilterDefault = () => true; +const isModelDisabledDefault = () => false; + +export const useModelCustomSelect = ({ + data, + isLoading, + selectedModel, + onChange, + modelFilter = modelFilterDefault, + isModelDisabled = isModelDisabledDefault, +}: UseModelCustomSelectArg): UseModelCustomSelectReturn => { + const { t } = useTranslation(); + + const items: Item[] = useMemo( + () => + data + ? filter(data.entities, modelFilter).map((m) => ({ + label: m.name, + value: m.key, + description: m.description, + group: MODEL_TYPE_SHORT_MAP[m.base], + isDisabled: isModelDisabled(m), + })) + : EMPTY_ARRAY, + [data, isModelDisabled, modelFilter] + ); + + const _onChange = useCallback( + (item: Item | null) => { + if (!item || !data) { + return; + } + const model = data.entities[item.value]; + if (!model) { + return; + } + onChange(model); + }, + [data, onChange] + ); + + const selectedItem = useMemo(() => items.find((o) => o.value === selectedModel?.key) ?? null, [selectedModel, items]); + + const placeholder = useMemo(() => { + if (isLoading) { + return t('common.loading'); + } + + if (items.length === 0) { + return t('models.noModelsAvailable'); + } + + return t('models.selectModel'); + }, [isLoading, items, t]); + + return { + items, + onChange: _onChange, + selectedItem, + placeholder, + }; +}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx index a320238445..696bf47b2a 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx @@ -1,34 +1,27 @@ -import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { CustomSelect, FormControl } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import { useModelCustomSelect } from 'common/hooks/useModelCustomSelect'; import { useControlAdapterIsEnabled } from 'features/controlAdapters/hooks/useControlAdapterIsEnabled'; import { useControlAdapterModel } from 'features/controlAdapters/hooks/useControlAdapterModel'; -import { useControlAdapterModelEntities } from 'features/controlAdapters/hooks/useControlAdapterModelEntities'; +import { useControlAdapterModelQuery } from 'features/controlAdapters/hooks/useControlAdapterModelQuery'; import { useControlAdapterType } from 'features/controlAdapters/hooks/useControlAdapterType'; import { controlAdapterModelChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { pick } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { AnyModelConfig, ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'services/api/types'; +import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'services/api/types'; type ParamControlAdapterModelProps = { id: string; }; -const selectMainModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); - const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { const isEnabled = useControlAdapterIsEnabled(id); const controlAdapterType = useControlAdapterType(id); const model = useControlAdapterModel(id); const dispatch = useAppDispatch(); const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const mainModel = useAppSelector(selectMainModel); - const { t } = useTranslation(); - const models = useControlAdapterModelEntities(controlAdapterType); + const { data, isLoading } = useControlAdapterModelQuery(controlAdapterType); const _onChange = useCallback( (model: ControlNetConfig | IPAdapterConfig | T2IAdapterConfig | null) => { @@ -50,34 +43,18 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { [controlAdapterType, model] ); - const getIsDisabled = useCallback( - (model: AnyModelConfig): boolean => { - const isCompatible = currentBaseModel === model.base; - const hasMainModel = Boolean(currentBaseModel); - return !hasMainModel || !isCompatible; - }, - [currentBaseModel] - ); - - const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ - modelEntities: models, - onChange: _onChange, + const { items, selectedItem, onChange, placeholder } = useModelCustomSelect({ + data, + isLoading, selectedModel, - getIsDisabled, + onChange: _onChange, + modelFilter: (model) => model.base === currentBaseModel, }); return ( - - - - - + + + ); }; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelEntities.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelEntities.ts deleted file mode 100644 index 0c8baaacc2..0000000000 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelEntities.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { ControlAdapterType } from 'features/controlAdapters/store/types'; -import { - useGetControlNetModelsQuery, - useGetIPAdapterModelsQuery, - useGetT2IAdapterModelsQuery, -} from 'services/api/endpoints/models'; - -export const useControlAdapterModelEntities = (type?: ControlAdapterType) => { - const { data: controlNetModelsData } = useGetControlNetModelsQuery(); - const { data: t2iAdapterModelsData } = useGetT2IAdapterModelsQuery(); - const { data: ipAdapterModelsData } = useGetIPAdapterModelsQuery(); - - if (type === 'controlnet') { - return controlNetModelsData; - } - if (type === 't2i_adapter') { - return t2iAdapterModelsData; - } - if (type === 'ip_adapter') { - return ipAdapterModelsData; - } - return; -}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelQuery.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelQuery.ts new file mode 100644 index 0000000000..1d092497af --- /dev/null +++ b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterModelQuery.ts @@ -0,0 +1,26 @@ +import type { ControlAdapterType } from 'features/controlAdapters/store/types'; +import { + useGetControlNetModelsQuery, + useGetIPAdapterModelsQuery, + useGetT2IAdapterModelsQuery, +} from 'services/api/endpoints/models'; + +export const useControlAdapterModelQuery = (type: ControlAdapterType) => { + const controlNetModelsQuery = useGetControlNetModelsQuery(); + const t2iAdapterModelsQuery = useGetT2IAdapterModelsQuery(); + const ipAdapterModelsQuery = useGetIPAdapterModelsQuery(); + + if (type === 'controlnet') { + return controlNetModelsQuery; + } + if (type === 't2i_adapter') { + return t2iAdapterModelsQuery; + } + if (type === 'ip_adapter') { + return ipAdapterModelsQuery; + } + + // Assert that the end of the function is not reachable. + const exhaustiveCheck: never = type; + return exhaustiveCheck; +}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts index 4e15dc9e64..fe818f3287 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/hooks/useControlAdapterType.ts @@ -5,14 +5,16 @@ import { selectControlAdaptersSlice, } from 'features/controlAdapters/store/controlAdaptersSlice'; import { useMemo } from 'react'; +import { assert } from 'tsafe'; export const useControlAdapterType = (id: string) => { const selector = useMemo( () => - createMemoizedSelector( - selectControlAdaptersSlice, - (controlAdapters) => selectControlAdapterById(controlAdapters, id)?.type - ), + createMemoizedSelector(selectControlAdaptersSlice, (controlAdapters) => { + const type = selectControlAdapterById(controlAdapters, id)?.type; + assert(type !== undefined, `Control adapter with id ${id} not found`); + return type; + }), [id] ); diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx index 7501151ba4..15149b339b 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx @@ -2,11 +2,7 @@ import { Flex, Text } from '@invoke-ai/ui-library'; import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ALL_BASE_MODELS } from 'services/api/constants'; -import type { - DiffusersModelConfig, - LoRAConfig, - MainModelConfig, -} from 'services/api/endpoints/models'; +import type { DiffusersModelConfig, LoRAConfig, MainModelConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery, useGetMainModelsQuery } from 'services/api/endpoints/models'; import CheckpointModelEdit from './ModelManagerPanel/CheckpointModelEdit'; diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx index 18f780bdee..0759020cc8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx @@ -1,62 +1,47 @@ -import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library'; +import { CustomSelect, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import { useModelCustomSelect } from 'common/hooks/useModelCustomSelect'; import { modelSelected } from 'features/parameters/store/actions'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; -import { pick } from 'lodash-es'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { NON_REFINER_BASE_MODELS } from 'services/api/constants'; -import type { MainModelConfig } from 'services/api/endpoints/models'; -import { getModelId, mainModelsAdapterSelectors, useGetMainModelsQuery } from 'services/api/endpoints/models'; +import { useGetMainModelsQuery } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/types'; const selectModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); const ParamMainModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const model = useAppSelector(selectModel); + const selectedModel = useAppSelector(selectModel); const { data, isLoading } = useGetMainModelsQuery(NON_REFINER_BASE_MODELS); - const tooltipLabel = useMemo(() => { - if (!data || !model) { - return; - } - return mainModelsAdapterSelectors.selectById(data, getModelId(model))?.description; - }, [data, model]); + const _onChange = useCallback( (model: MainModelConfig | null) => { if (!model) { return; } - dispatch(modelSelected(pick(model, ['base_model', 'model_name', 'model_type']))); + dispatch(modelSelected({ key: model.key, base: model.base })); }, [dispatch] ); - const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ - modelEntities: data, - onChange: _onChange, - selectedModel: model, + + const { items, selectedItem, onChange, placeholder } = useModelCustomSelect({ + data, isLoading, + selectedModel, + onChange: _onChange, }); return ( - + {t('modelManager.model')} - - - - - + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts index 0d4dda0e87..f7bf127c05 100644 --- a/invokeai/frontend/web/src/features/parameters/store/actions.ts +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -1,6 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; -import type { ImageDTO, MainModelField } from 'services/api/types'; +import type { ParameterModel } from 'features/parameters/types/parameterSchemas'; +import type { ImageDTO } from 'services/api/types'; export const initialImageSelected = createAction('generation/initialImageSelected'); -export const modelSelected = createAction('generation/modelSelected'); +export const modelSelected = createAction('generation/modelSelected'); diff --git a/invokeai/frontend/web/src/features/parameters/types/constants.ts b/invokeai/frontend/web/src/features/parameters/types/constants.ts index a74807a959..bd78759b39 100644 --- a/invokeai/frontend/web/src/features/parameters/types/constants.ts +++ b/invokeai/frontend/web/src/features/parameters/types/constants.ts @@ -17,8 +17,8 @@ export const MODEL_TYPE_MAP = { */ export const MODEL_TYPE_SHORT_MAP = { any: 'Any', - 'sd-1': 'SD1', - 'sd-2': 'SD2', + 'sd-1': 'SD1.X', + 'sd-2': 'SD2.X', sdxl: 'SDXL', 'sdxl-refiner': 'SDXLR', }; From f1597bd6da1bd411f5e2d7feb540cf68c1fe5db5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:42:15 +1100 Subject: [PATCH 143/411] chore(ui): lint --- invokeai/frontend/web/src/common/hooks/useModelCombobox.ts | 4 +--- invokeai/frontend/web/src/services/api/endpoints/workflows.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts index 341fed1e47..07e6aeb34c 100644 --- a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts @@ -23,9 +23,7 @@ type UseModelComboboxReturn = { noOptionsMessage: () => string; }; -export const useModelCombobox = ( - arg: UseModelComboboxArg -): UseModelComboboxReturn => { +export const useModelCombobox = (arg: UseModelComboboxArg): UseModelComboboxReturn => { const { t } = useTranslation(); const { modelEntities, selectedModel, getIsDisabled, onChange, isLoading, optionsFilter = () => true } = arg; const options = useMemo(() => { diff --git a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts index 1e64809e5a..0280e2ebc4 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts @@ -5,7 +5,7 @@ import { api, buildV1Url, LIST_TAG } from '..'; /** * Builds an endpoint URL for the workflows router * @example - * buildWorkflowsUrl('some-path') + * buildWorkflowsUrl('some-path') * // '/api/v1/workflows/some-path' */ const buildWorkflowsUrl = (path: string = '') => buildV1Url(`workflows/${path}`); From 996eb96b4e06fe4f82e672c482793cf8fe428506 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 15 Feb 2024 22:41:29 -0500 Subject: [PATCH 144/411] Fix issues identified during PR review by RyanjDick and brandonrising - ModelMetadataStoreService is now injected into ModelRecordStoreService (these two services are really joined at the hip, and should someday be merged) - ModelRecordStoreService is now injected into ModelManagerService - Reduced timeout value for the various installer and download wait*() methods - Introduced a Mock modelmanager for testing - Removed bare print() statement with _logger in the install helper backend. - Removed unused code from model loader init file - Made `locker` a private variable in the `LoadedModel` object. - Fixed up model merge frontend (will be deprecated anyway!) --- invokeai/app/api/dependencies.py | 8 +- .../app/services/download/download_default.py | 2 +- .../invocation_stats_default.py | 2 - .../model_install/model_install_base.py | 6 +- .../model_install/model_install_default.py | 15 +- .../model_manager/model_manager_default.py | 15 +- .../app/services/model_metadata/__init__.py | 9 + .../model_metadata/metadata_store_base.py | 65 +++++ .../model_metadata/metadata_store_sql.py | 222 ++++++++++++++++++ .../model_records/model_records_base.py | 6 +- .../model_records/model_records_sql.py | 18 +- invokeai/backend/install/install_helper.py | 13 +- .../backend/model_manager/load/__init__.py | 17 -- .../backend/model_manager/load/load_base.py | 8 +- .../model_manager/load/load_default.py | 2 +- invokeai/backend/model_manager/merge.py | 5 +- .../model_manager/metadata/__init__.py | 5 +- invokeai/frontend/merge/merge_diffusers.py | 133 +++++++---- tests/aa_nodes/test_invoker.py | 3 +- .../model_records/test_model_records_sql.py | 3 +- .../model_manager_2_fixtures.py | 15 +- .../model_metadata/test_model_metadata.py | 8 +- 22 files changed, 449 insertions(+), 131 deletions(-) create mode 100644 invokeai/app/services/model_metadata/__init__.py create mode 100644 invokeai/app/services/model_metadata/metadata_store_base.py create mode 100644 invokeai/app/services/model_metadata/metadata_store_sql.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 378961a055..8e79b26e2d 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -28,6 +28,8 @@ from ..services.invocation_services import InvocationServices from ..services.invocation_stats.invocation_stats_default import InvocationStatsService from ..services.invoker import Invoker from ..services.model_manager.model_manager_default import ModelManagerService +from ..services.model_metadata import ModelMetadataStoreSQL +from ..services.model_records import ModelRecordServiceSQL from ..services.names.names_default import SimpleNameService from ..services.session_processor.session_processor_default import DefaultSessionProcessor from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue @@ -94,8 +96,12 @@ class ApiDependencies: ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", ephemeral=True) ) download_queue_service = DownloadQueueService(event_bus=events) + model_metadata_service = ModelMetadataStoreSQL(db=db) model_manager = ModelManagerService.build_model_manager( - app_config=configuration, db=db, download_queue=download_queue_service, events=events + app_config=configuration, + model_record_service=ModelRecordServiceSQL(db=db, metadata_store=model_metadata_service), + download_queue=download_queue_service, + events=events, ) names = SimpleNameService() performance_statistics = InvocationStatsService() diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py index 6d5cedbcad..50cac80d09 100644 --- a/invokeai/app/services/download/download_default.py +++ b/invokeai/app/services/download/download_default.py @@ -194,7 +194,7 @@ class DownloadQueueService(DownloadQueueServiceBase): """Block until the indicated job has reached terminal state, or when timeout limit reached.""" start = time.time() while not job.in_terminal_state: - if self._job_completed_event.wait(timeout=5): # in case we miss an event + if self._job_completed_event.wait(timeout=0.25): # in case we miss an event self._job_completed_event.clear() if timeout > 0 and time.time() - start > timeout: raise TimeoutError("Timeout exceeded") diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index 6c893021de..486a1ca5b3 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -46,8 +46,6 @@ class InvocationStatsService(InvocationStatsServiceBase): # This is to handle case of the model manager not being initialized, which happens # during some tests. services = self._invoker.services - if services.model_manager is None or services.model_manager.load is None: - yield None if not self._stats.get(graph_execution_state_id): # First time we're seeing this graph_execution_state_id. self._stats[graph_execution_state_id] = GraphExecutionStats() diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py index 39ea8c4a0d..2f03db0af7 100644 --- a/invokeai/app/services/model_install/model_install_base.py +++ b/invokeai/app/services/model_install/model_install_base.py @@ -18,7 +18,9 @@ from invokeai.app.services.events import EventServiceBase from invokeai.app.services.invoker import Invoker from invokeai.app.services.model_records import ModelRecordServiceBase from invokeai.backend.model_manager import AnyModelConfig, ModelRepoVariant -from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore +from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata + +from ..model_metadata import ModelMetadataStoreBase class InstallStatus(str, Enum): @@ -243,7 +245,7 @@ class ModelInstallServiceBase(ABC): app_config: InvokeAIAppConfig, record_store: ModelRecordServiceBase, download_queue: DownloadQueueServiceBase, - metadata_store: ModelMetadataStore, + metadata_store: ModelMetadataStoreBase, event_bus: Optional["EventServiceBase"] = None, ): """ diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 20a85a82a1..7dee8bfd8c 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -20,7 +20,7 @@ from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.download import DownloadJob, DownloadQueueServiceBase, TqdmProgress from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker -from invokeai.app.services.model_records import DuplicateModelException, ModelRecordServiceBase, ModelRecordServiceSQL +from invokeai.app.services.model_records import DuplicateModelException, ModelRecordServiceBase from invokeai.backend.model_manager.config import ( AnyModelConfig, BaseModelType, @@ -33,7 +33,6 @@ from invokeai.backend.model_manager.metadata import ( AnyModelRepoMetadata, CivitaiMetadataFetch, HuggingFaceMetadataFetch, - ModelMetadataStore, ModelMetadataWithFiles, RemoteModelFile, ) @@ -65,7 +64,6 @@ class ModelInstallService(ModelInstallServiceBase): app_config: InvokeAIAppConfig, record_store: ModelRecordServiceBase, download_queue: DownloadQueueServiceBase, - metadata_store: Optional[ModelMetadataStore] = None, event_bus: Optional[EventServiceBase] = None, session: Optional[Session] = None, ): @@ -93,14 +91,7 @@ class ModelInstallService(ModelInstallServiceBase): self._running = False self._session = session self._next_job_id = 0 - # There may not necessarily be a metadata store initialized - # so we create one and initialize it with the same sql database - # used by the record store service. - if metadata_store: - self._metadata_store = metadata_store - else: - assert isinstance(record_store, ModelRecordServiceSQL) - self._metadata_store = ModelMetadataStore(record_store.db) + self._metadata_store = record_store.metadata_store # for convenience @property def app_config(self) -> InvokeAIAppConfig: # noqa D102 @@ -259,7 +250,7 @@ class ModelInstallService(ModelInstallServiceBase): """Block until all installation jobs are done.""" start = time.time() while len(self._download_cache) > 0: - if self._downloads_changed_event.wait(timeout=5): # in case we miss an event + if self._downloads_changed_event.wait(timeout=0.25): # in case we miss an event self._downloads_changed_event.clear() if timeout > 0 and time.time() - start > timeout: raise TimeoutError("Timeout exceeded") diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index 028d4af615..b96341be69 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -5,7 +5,6 @@ from typing_extensions import Self from invokeai.app.services.invoker import Invoker from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache -from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.util.logging import InvokeAILogger from ..config import InvokeAIAppConfig @@ -13,8 +12,7 @@ from ..download import DownloadQueueServiceBase from ..events.events_base import EventServiceBase from ..model_install import ModelInstallService, ModelInstallServiceBase from ..model_load import ModelLoadService, ModelLoadServiceBase -from ..model_records import ModelRecordServiceBase, ModelRecordServiceSQL -from ..shared.sqlite.sqlite_database import SqliteDatabase +from ..model_records import ModelRecordServiceBase from .model_manager_base import ModelManagerServiceBase @@ -64,7 +62,7 @@ class ModelManagerService(ModelManagerServiceBase): def build_model_manager( cls, app_config: InvokeAIAppConfig, - db: SqliteDatabase, + model_record_service: ModelRecordServiceBase, download_queue: DownloadQueueServiceBase, events: EventServiceBase, ) -> Self: @@ -82,19 +80,16 @@ class ModelManagerService(ModelManagerServiceBase): convert_cache = ModelConvertCache( cache_path=app_config.models_convert_cache_path, max_size=app_config.convert_cache_size ) - record_store = ModelRecordServiceSQL(db=db) loader = ModelLoadService( app_config=app_config, - record_store=record_store, + record_store=model_record_service, ram_cache=ram_cache, convert_cache=convert_cache, ) - record_store._loader = loader # yeah, there is a circular reference here installer = ModelInstallService( app_config=app_config, - record_store=record_store, + record_store=model_record_service, download_queue=download_queue, - metadata_store=ModelMetadataStore(db=db), event_bus=events, ) - return cls(store=record_store, install=installer, load=loader) + return cls(store=model_record_service, install=installer, load=loader) diff --git a/invokeai/app/services/model_metadata/__init__.py b/invokeai/app/services/model_metadata/__init__.py new file mode 100644 index 0000000000..981c96b709 --- /dev/null +++ b/invokeai/app/services/model_metadata/__init__.py @@ -0,0 +1,9 @@ +"""Init file for ModelMetadataStoreService module.""" + +from .metadata_store_base import ModelMetadataStoreBase +from .metadata_store_sql import ModelMetadataStoreSQL + +__all__ = [ + "ModelMetadataStoreBase", + "ModelMetadataStoreSQL", +] diff --git a/invokeai/app/services/model_metadata/metadata_store_base.py b/invokeai/app/services/model_metadata/metadata_store_base.py new file mode 100644 index 0000000000..e0e4381b09 --- /dev/null +++ b/invokeai/app/services/model_metadata/metadata_store_base.py @@ -0,0 +1,65 @@ +# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team +""" +Storage for Model Metadata +""" + +from abc import ABC, abstractmethod +from typing import List, Set, Tuple + +from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata + + +class ModelMetadataStoreBase(ABC): + """Store, search and fetch model metadata retrieved from remote repositories.""" + + @abstractmethod + def add_metadata(self, model_key: str, metadata: AnyModelRepoMetadata) -> None: + """ + Add a block of repo metadata to a model record. + + The model record config must already exist in the database with the + same key. Otherwise a FOREIGN KEY constraint exception will be raised. + + :param model_key: Existing model key in the `model_config` table + :param metadata: ModelRepoMetadata object to store + """ + + @abstractmethod + def get_metadata(self, model_key: str) -> AnyModelRepoMetadata: + """Retrieve the ModelRepoMetadata corresponding to model key.""" + + @abstractmethod + def list_all_metadata(self) -> List[Tuple[str, AnyModelRepoMetadata]]: # key, metadata + """Dump out all the metadata.""" + + @abstractmethod + def update_metadata(self, model_key: str, metadata: AnyModelRepoMetadata) -> AnyModelRepoMetadata: + """ + Update metadata corresponding to the model with the indicated key. + + :param model_key: Existing model key in the `model_config` table + :param metadata: ModelRepoMetadata object to update + """ + + @abstractmethod + def list_tags(self) -> Set[str]: + """Return all tags in the tags table.""" + + @abstractmethod + def search_by_tag(self, tags: Set[str]) -> Set[str]: + """Return the keys of models containing all of the listed tags.""" + + @abstractmethod + def search_by_author(self, author: str) -> Set[str]: + """Return the keys of models authored by the indicated author.""" + + @abstractmethod + def search_by_name(self, name: str) -> Set[str]: + """ + Return the keys of models with the indicated name. + + Note that this is the name of the model given to it by + the remote source. The user may have changed the local + name. The local name will be located in the model config + record object. + """ diff --git a/invokeai/app/services/model_metadata/metadata_store_sql.py b/invokeai/app/services/model_metadata/metadata_store_sql.py new file mode 100644 index 0000000000..afe9d2c8c6 --- /dev/null +++ b/invokeai/app/services/model_metadata/metadata_store_sql.py @@ -0,0 +1,222 @@ +# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team +""" +SQL Storage for Model Metadata +""" + +import sqlite3 +from typing import List, Optional, Set, Tuple + +from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase +from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, UnknownMetadataException +from invokeai.backend.model_manager.metadata.fetch import ModelMetadataFetchBase + +from .metadata_store_base import ModelMetadataStoreBase + + +class ModelMetadataStoreSQL(ModelMetadataStoreBase): + """Store, search and fetch model metadata retrieved from remote repositories.""" + + def __init__(self, db: SqliteDatabase): + """ + Initialize a new object from preexisting sqlite3 connection and threading lock objects. + + :param conn: sqlite3 connection object + :param lock: threading Lock object + """ + super().__init__() + self._db = db + self._cursor = self._db.conn.cursor() + + def add_metadata(self, model_key: str, metadata: AnyModelRepoMetadata) -> None: + """ + Add a block of repo metadata to a model record. + + The model record config must already exist in the database with the + same key. Otherwise a FOREIGN KEY constraint exception will be raised. + + :param model_key: Existing model key in the `model_config` table + :param metadata: ModelRepoMetadata object to store + """ + json_serialized = metadata.model_dump_json() + with self._db.lock: + try: + self._cursor.execute( + """--sql + INSERT INTO model_metadata( + id, + metadata + ) + VALUES (?,?); + """, + ( + model_key, + json_serialized, + ), + ) + self._update_tags(model_key, metadata.tags) + self._db.conn.commit() + except sqlite3.IntegrityError as excp: # FOREIGN KEY error: the key was not in model_config table + self._db.conn.rollback() + raise UnknownMetadataException from excp + except sqlite3.Error as excp: + self._db.conn.rollback() + raise excp + + def get_metadata(self, model_key: str) -> AnyModelRepoMetadata: + """Retrieve the ModelRepoMetadata corresponding to model key.""" + with self._db.lock: + self._cursor.execute( + """--sql + SELECT metadata FROM model_metadata + WHERE id=?; + """, + (model_key,), + ) + rows = self._cursor.fetchone() + if not rows: + raise UnknownMetadataException("model metadata not found") + return ModelMetadataFetchBase.from_json(rows[0]) + + def list_all_metadata(self) -> List[Tuple[str, AnyModelRepoMetadata]]: # key, metadata + """Dump out all the metadata.""" + with self._db.lock: + self._cursor.execute( + """--sql + SELECT id,metadata FROM model_metadata; + """, + (), + ) + rows = self._cursor.fetchall() + return [(x[0], ModelMetadataFetchBase.from_json(x[1])) for x in rows] + + def update_metadata(self, model_key: str, metadata: AnyModelRepoMetadata) -> AnyModelRepoMetadata: + """ + Update metadata corresponding to the model with the indicated key. + + :param model_key: Existing model key in the `model_config` table + :param metadata: ModelRepoMetadata object to update + """ + json_serialized = metadata.model_dump_json() # turn it into a json string. + with self._db.lock: + try: + self._cursor.execute( + """--sql + UPDATE model_metadata + SET + metadata=? + WHERE id=?; + """, + (json_serialized, model_key), + ) + if self._cursor.rowcount == 0: + raise UnknownMetadataException("model metadata not found") + self._update_tags(model_key, metadata.tags) + self._db.conn.commit() + except sqlite3.Error as e: + self._db.conn.rollback() + raise e + + return self.get_metadata(model_key) + + def list_tags(self) -> Set[str]: + """Return all tags in the tags table.""" + self._cursor.execute( + """--sql + select tag_text from tags; + """ + ) + return {x[0] for x in self._cursor.fetchall()} + + def search_by_tag(self, tags: Set[str]) -> Set[str]: + """Return the keys of models containing all of the listed tags.""" + with self._db.lock: + try: + matches: Optional[Set[str]] = None + for tag in tags: + self._cursor.execute( + """--sql + SELECT a.model_id FROM model_tags AS a, + tags AS b + WHERE a.tag_id=b.tag_id + AND b.tag_text=?; + """, + (tag,), + ) + model_keys = {x[0] for x in self._cursor.fetchall()} + if matches is None: + matches = model_keys + matches = matches.intersection(model_keys) + except sqlite3.Error as e: + raise e + return matches if matches else set() + + def search_by_author(self, author: str) -> Set[str]: + """Return the keys of models authored by the indicated author.""" + self._cursor.execute( + """--sql + SELECT id FROM model_metadata + WHERE author=?; + """, + (author,), + ) + return {x[0] for x in self._cursor.fetchall()} + + def search_by_name(self, name: str) -> Set[str]: + """ + Return the keys of models with the indicated name. + + Note that this is the name of the model given to it by + the remote source. The user may have changed the local + name. The local name will be located in the model config + record object. + """ + self._cursor.execute( + """--sql + SELECT id FROM model_metadata + WHERE name=?; + """, + (name,), + ) + return {x[0] for x in self._cursor.fetchall()} + + def _update_tags(self, model_key: str, tags: 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,), + ) + + 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/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py index b2eacc524b..d6014db448 100644 --- a/invokeai/app/services/model_records/model_records_base.py +++ b/invokeai/app/services/model_records/model_records_base.py @@ -17,7 +17,9 @@ from invokeai.backend.model_manager import ( ModelFormat, ModelType, ) -from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore +from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata + +from ..model_metadata import ModelMetadataStoreBase class DuplicateModelException(Exception): @@ -109,7 +111,7 @@ class ModelRecordServiceBase(ABC): @property @abstractmethod - def metadata_store(self) -> ModelMetadataStore: + def metadata_store(self) -> ModelMetadataStoreBase: """Return a ModelMetadataStore initialized on the same database.""" pass diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py index 84a1412383..dcd1114655 100644 --- a/invokeai/app/services/model_records/model_records_sql.py +++ b/invokeai/app/services/model_records/model_records_sql.py @@ -54,8 +54,9 @@ from invokeai.backend.model_manager.config import ( ModelFormat, ModelType, ) -from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, ModelMetadataStore, UnknownMetadataException +from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata, UnknownMetadataException +from ..model_metadata import ModelMetadataStoreBase, ModelMetadataStoreSQL from ..shared.sqlite.sqlite_database import SqliteDatabase from .model_records_base import ( DuplicateModelException, @@ -69,7 +70,7 @@ from .model_records_base import ( class ModelRecordServiceSQL(ModelRecordServiceBase): """Implementation of the ModelConfigStore ABC using a SQL database.""" - def __init__(self, db: SqliteDatabase): + def __init__(self, db: SqliteDatabase, metadata_store: ModelMetadataStoreBase): """ Initialize a new object from preexisting sqlite3 connection and threading lock objects. @@ -78,6 +79,7 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): super().__init__() self._db = db self._cursor = db.conn.cursor() + self._metadata_store = metadata_store @property def db(self) -> SqliteDatabase: @@ -157,7 +159,7 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): self._db.conn.rollback() raise e - def update_model(self, key: str, config: Union[dict, AnyModelConfig]) -> AnyModelConfig: + def update_model(self, key: str, config: Union[Dict[str, Any], AnyModelConfig]) -> AnyModelConfig: """ Update the model, returning the updated version. @@ -307,9 +309,9 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): return results @property - def metadata_store(self) -> ModelMetadataStore: + def metadata_store(self) -> ModelMetadataStoreBase: """Return a ModelMetadataStore initialized on the same database.""" - return ModelMetadataStore(self._db) + return self._metadata_store def get_metadata(self, key: str) -> Optional[AnyModelRepoMetadata]: """ @@ -330,18 +332,18 @@ class ModelRecordServiceSQL(ModelRecordServiceBase): :param tags: Set of tags to search for. All tags must be present. """ - store = ModelMetadataStore(self._db) + store = ModelMetadataStoreSQL(self._db) keys = store.search_by_tag(tags) return [self.get_model(x) for x in keys] def list_tags(self) -> Set[str]: """Return a unique set of all the model tags in the metadata database.""" - store = ModelMetadataStore(self._db) + store = ModelMetadataStoreSQL(self._db) return store.list_tags() def list_all_metadata(self) -> List[Tuple[str, AnyModelRepoMetadata]]: """List metadata for all models that have it.""" - store = ModelMetadataStore(self._db) + store = ModelMetadataStoreSQL(self._db) return store.list_all_metadata() def list_models( diff --git a/invokeai/backend/install/install_helper.py b/invokeai/backend/install/install_helper.py index 9c386c209c..3623b623a9 100644 --- a/invokeai/backend/install/install_helper.py +++ b/invokeai/backend/install/install_helper.py @@ -25,6 +25,7 @@ from invokeai.app.services.model_install import ( ModelSource, URLModelSource, ) +from invokeai.app.services.model_metadata import ModelMetadataStoreSQL from invokeai.app.services.model_records import ModelRecordServiceBase, ModelRecordServiceSQL from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.backend.model_manager import ( @@ -45,7 +46,7 @@ def initialize_record_store(app_config: InvokeAIAppConfig) -> ModelRecordService logger = InvokeAILogger.get_logger(config=app_config) image_files = DiskImageFileStorage(f"{app_config.output_path}/images") db = init_db(config=app_config, logger=logger, image_files=image_files) - obj: ModelRecordServiceBase = ModelRecordServiceSQL(db) + obj: ModelRecordServiceBase = ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) return obj @@ -54,12 +55,10 @@ def initialize_installer( ) -> ModelInstallServiceBase: """Return an initialized ModelInstallService object.""" record_store = initialize_record_store(app_config) - metadata_store = record_store.metadata_store download_queue = DownloadQueueService() installer = ModelInstallService( app_config=app_config, record_store=record_store, - metadata_store=metadata_store, download_queue=download_queue, event_bus=event_bus, ) @@ -287,14 +286,14 @@ class InstallHelper(object): model_name=model_name, ) if len(matches) > 1: - print( - f"{model_to_remove} is ambiguous. Please use model_base/model_type/model_name (e.g. sd-1/main/my_model) to disambiguate." + self._logger.error( + "{model_to_remove} is ambiguous. Please use model_base/model_type/model_name (e.g. sd-1/main/my_model) to disambiguate" ) elif not matches: - print(f"{model_to_remove}: unknown model") + self._logger.error(f"{model_to_remove}: unknown model") else: for m in matches: - print(f"Deleting {m.type}:{m.name}") + self._logger.info(f"Deleting {m.type}:{m.name}") installer.delete(m.key) installer.wait_for_installs() diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py index 966a739237..a3a840b625 100644 --- a/invokeai/backend/model_manager/load/__init__.py +++ b/invokeai/backend/model_manager/load/__init__.py @@ -4,10 +4,6 @@ Init file for the model loader. """ from importlib import import_module from pathlib import Path -from typing import Optional - -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.util.logging import InvokeAILogger from .convert_cache.convert_cache_default import ModelConvertCache from .load_base import AnyModelLoader, LoadedModel @@ -19,16 +15,3 @@ for module in loaders: import_module(f"{__package__}.model_loaders.{module}") __all__ = ["AnyModelLoader", "LoadedModel", "ModelCache", "ModelConvertCache"] - - -def get_standalone_loader(app_config: Optional[InvokeAIAppConfig]) -> AnyModelLoader: - app_config = app_config or InvokeAIAppConfig.get_config() - logger = InvokeAILogger.get_logger(config=app_config) - return AnyModelLoader( - app_config=app_config, - logger=logger, - ram_cache=ModelCache( - logger=logger, max_cache_size=app_config.ram_cache_size, max_vram_cache_size=app_config.vram_cache_size - ), - convert_cache=ModelConvertCache(app_config.models_convert_cache_path), - ) diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 7649dee762..4c5e899aa3 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -39,21 +39,21 @@ class LoadedModel: """Context manager object that mediates transfer from RAM<->VRAM.""" config: AnyModelConfig - locker: ModelLockerBase + _locker: ModelLockerBase def __enter__(self) -> AnyModel: """Context entry.""" - self.locker.lock() + self._locker.lock() return self.model def __exit__(self, *args: Any, **kwargs: Any) -> None: """Context exit.""" - self.locker.unlock() + self._locker.unlock() @property def model(self) -> AnyModel: """Return the model without locking it.""" - return self.locker.model + return self._locker.model class ModelLoaderBase(ABC): diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index 1dac121a30..79c9311de1 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -75,7 +75,7 @@ class ModelLoader(ModelLoaderBase): model_path = self._convert_if_needed(model_config, model_path, submodel_type) locker = self._load_if_needed(model_config, model_path, submodel_type) - return LoadedModel(config=model_config, locker=locker) + return LoadedModel(config=model_config, _locker=locker) def _get_model_path( self, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None diff --git a/invokeai/backend/model_manager/merge.py b/invokeai/backend/model_manager/merge.py index 2c94af4af3..108f1f0e6f 100644 --- a/invokeai/backend/model_manager/merge.py +++ b/invokeai/backend/model_manager/merge.py @@ -39,10 +39,7 @@ class ModelMerger(object): def __init__(self, installer: ModelInstallServiceBase): """ - Initialize a ModelMerger object. - - :param store: Underlying storage manager for the running process. - :param config: InvokeAIAppConfig object (if not provided, default will be selected). + Initialize a ModelMerger object with the model installer. """ self._installer = installer diff --git a/invokeai/backend/model_manager/metadata/__init__.py b/invokeai/backend/model_manager/metadata/__init__.py index 672e378c7f..a35e55f3d2 100644 --- a/invokeai/backend/model_manager/metadata/__init__.py +++ b/invokeai/backend/model_manager/metadata/__init__.py @@ -18,7 +18,7 @@ assert isinstance(data, CivitaiMetadata) if data.allow_commercial_use: print("Commercial use of this model is allowed") """ -from .fetch import CivitaiMetadataFetch, HuggingFaceMetadataFetch +from .fetch import CivitaiMetadataFetch, HuggingFaceMetadataFetch, ModelMetadataFetchBase from .metadata_base import ( AnyModelRepoMetadata, AnyModelRepoMetadataValidator, @@ -31,7 +31,6 @@ from .metadata_base import ( RemoteModelFile, UnknownMetadataException, ) -from .metadata_store import ModelMetadataStore __all__ = [ "AnyModelRepoMetadata", @@ -42,7 +41,7 @@ __all__ = [ "HuggingFaceMetadata", "HuggingFaceMetadataFetch", "LicenseRestrictions", - "ModelMetadataStore", + "ModelMetadataFetchBase", "BaseMetadata", "ModelMetadataWithFiles", "RemoteModelFile", diff --git a/invokeai/frontend/merge/merge_diffusers.py b/invokeai/frontend/merge/merge_diffusers.py index 92b98b52f9..5484040674 100644 --- a/invokeai/frontend/merge/merge_diffusers.py +++ b/invokeai/frontend/merge/merge_diffusers.py @@ -6,20 +6,40 @@ Copyright (c) 2023 Lincoln Stein and the InvokeAI Development Team """ import argparse import curses +import re import sys from argparse import Namespace from pathlib import Path -from typing import List +from typing import List, Optional, Tuple import npyscreen from npyscreen import widget -import invokeai.backend.util.logging as logger from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_management import BaseModelType, ModelManager, ModelMerger, ModelType +from invokeai.app.services.download import DownloadQueueService +from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage +from invokeai.app.services.model_install import ModelInstallService +from invokeai.app.services.model_metadata import ModelMetadataStoreSQL +from invokeai.app.services.model_records import ModelRecordServiceBase, ModelRecordServiceSQL +from invokeai.app.services.shared.sqlite.sqlite_util import init_db +from invokeai.backend.model_manager import ( + BaseModelType, + ModelFormat, + ModelType, + ModelVariantType, +) +from invokeai.backend.model_manager.merge import ModelMerger +from invokeai.backend.util.logging import InvokeAILogger from invokeai.frontend.install.widgets import FloatTitleSlider, SingleSelectColumns, TextBox config = InvokeAIAppConfig.get_config() +logger = InvokeAILogger.get_logger() + +BASE_TYPES = [ + (BaseModelType.StableDiffusion1, "Models Built on SD-1.x"), + (BaseModelType.StableDiffusion2, "Models Built on SD-2.x"), + (BaseModelType.StableDiffusionXL, "Models Built on SDXL"), +] def _parse_args() -> Namespace: @@ -48,7 +68,7 @@ def _parse_args() -> Namespace: parser.add_argument( "--base_model", type=str, - choices=[x.value for x in BaseModelType], + choices=[x[0].value for x in BASE_TYPES], help="The base model shared by the models to be merged", ) parser.add_argument( @@ -98,17 +118,17 @@ class mergeModelsForm(npyscreen.FormMultiPageAction): super().__init__(parentApp, name) @property - def model_manager(self): - return self.parentApp.model_manager + def record_store(self): + return self.parentApp.record_store def afterEditing(self): self.parentApp.setNextForm(None) def create(self): window_height, window_width = curses.initscr().getmaxyx() - - self.model_names = self.get_model_names() self.current_base = 0 + self.models = self.get_models(BASE_TYPES[self.current_base][0]) + self.model_names = [x[1] for x in self.models] max_width = max([len(x) for x in self.model_names]) max_width += 6 horizontal_layout = max_width * 3 < window_width @@ -128,11 +148,7 @@ class mergeModelsForm(npyscreen.FormMultiPageAction): self.nextrely += 1 self.base_select = self.add_widget_intelligent( SingleSelectColumns, - values=[ - "Models Built on SD-1.x", - "Models Built on SD-2.x", - "Models Built on SDXL", - ], + values=[x[1] for x in BASE_TYPES], value=[self.current_base], columns=4, max_height=2, @@ -263,21 +279,20 @@ class mergeModelsForm(npyscreen.FormMultiPageAction): sys.exit(0) def marshall_arguments(self) -> dict: - model_names = self.model_names + model_keys = [x[0] for x in self.models] models = [ - model_names[self.model1.value[0]], - model_names[self.model2.value[0]], + model_keys[self.model1.value[0]], + model_keys[self.model2.value[0]], ] if self.model3.value[0] > 0: - models.append(model_names[self.model3.value[0] - 1]) + models.append(model_keys[self.model3.value[0] - 1]) interp = "add_difference" else: interp = self.interpolations[self.merge_method.value[0]] - bases = ["sd-1", "sd-2", "sdxl"] args = { - "model_names": models, - "base_model": BaseModelType(bases[self.base_select.value[0]]), + "model_keys": models, + "base_model": tuple(BaseModelType)[self.base_select.value[0]], "alpha": self.alpha.value, "interp": interp, "force": self.force.value, @@ -311,18 +326,18 @@ class mergeModelsForm(npyscreen.FormMultiPageAction): else: return True - def get_model_names(self, base_model: BaseModelType = BaseModelType.StableDiffusion1) -> List[str]: - model_names = [ - info["model_name"] - for info in self.model_manager.list_models(model_type=ModelType.Main, base_model=base_model) - if info["model_format"] == "diffusers" + def get_models(self, base_model: Optional[BaseModelType] = None) -> List[Tuple[str, str]]: # key to name + models = [ + (x.key, x.name) + for x in self.record_store.search_by_attr(model_type=ModelType.Main, base_model=base_model) + if x.format == ModelFormat("diffusers") and x.variant == ModelVariantType("normal") ] - return sorted(model_names) + return sorted(models, key=lambda x: x[1]) - def _populate_models(self, value=None): - bases = ["sd-1", "sd-2", "sdxl"] - base_model = BaseModelType(bases[value[0]]) - self.model_names = self.get_model_names(base_model) + def _populate_models(self, value: List[int]): + base_model = BASE_TYPES[value[0]][0] + self.models = self.get_models(base_model) + self.model_names = [x[1] for x in self.models] models_plus_none = self.model_names.copy() models_plus_none.insert(0, "None") @@ -334,24 +349,24 @@ class mergeModelsForm(npyscreen.FormMultiPageAction): class Mergeapp(npyscreen.NPSAppManaged): - def __init__(self, model_manager: ModelManager): + def __init__(self, record_store: ModelRecordServiceBase): super().__init__() - self.model_manager = model_manager + self.record_store = record_store def onStart(self): npyscreen.setTheme(npyscreen.Themes.ElegantTheme) self.main = self.addForm("MAIN", mergeModelsForm, name="Merge Models Settings") -def run_gui(args: Namespace): - model_manager = ModelManager(config.model_conf_path) - mergeapp = Mergeapp(model_manager) +def run_gui(args: Namespace) -> None: + record_store: ModelRecordServiceBase = get_config_store() + mergeapp = Mergeapp(record_store) mergeapp.run() - args = mergeapp.merge_arguments - merger = ModelMerger(model_manager) + merger = get_model_merger(record_store) merger.merge_diffusion_models_and_save(**args) - logger.info(f'Models merged into new model: "{args["merged_model_name"]}".') + merged_model_name = args["merged_model_name"] + logger.info(f'Models merged into new model: "{merged_model_name}".') def run_cli(args: Namespace): @@ -364,20 +379,54 @@ def run_cli(args: Namespace): args.merged_model_name = "+".join(args.model_names) logger.info(f'No --merged_model_name provided. Defaulting to "{args.merged_model_name}"') - model_manager = ModelManager(config.model_conf_path) + record_store: ModelRecordServiceBase = get_config_store() assert ( - not model_manager.model_exists(args.merged_model_name, args.base_model, ModelType.Main) or args.clobber + len(record_store.search_by_attr(args.merged_model_name, args.base_model, ModelType.Main)) == 0 or args.clobber ), f'A model named "{args.merged_model_name}" already exists. Use --clobber to overwrite.' - merger = ModelMerger(model_manager) - merger.merge_diffusion_models_and_save(**vars(args)) + merger = get_model_merger(record_store) + model_keys = [] + for name in args.model_names: + if len(name) == 32 and re.match(r"^[0-9a-f]$", name): + model_keys.append(name) + else: + models = record_store.search_by_attr( + model_name=name, model_type=ModelType.Main, base_model=BaseModelType(args.base_model) + ) + assert len(models) > 0, f"{name}: Unknown model" + assert len(models) < 2, f"{name}: More than one model by this name. Please specify the model key instead." + model_keys.append(models[0].key) + + merger.merge_diffusion_models_and_save( + alpha=args.alpha, + model_keys=model_keys, + merged_model_name=args.merged_model_name, + interp=args.interp, + force=args.force, + ) logger.info(f'Models merged into new model: "{args.merged_model_name}".') +def get_config_store() -> ModelRecordServiceSQL: + output_path = config.output_path + assert output_path is not None + image_files = DiskImageFileStorage(output_path / "images") + db = init_db(config=config, logger=InvokeAILogger.get_logger(), image_files=image_files) + return ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) + + +def get_model_merger(record_store: ModelRecordServiceBase) -> ModelMerger: + installer = ModelInstallService(app_config=config, record_store=record_store, download_queue=DownloadQueueService()) + installer.start() + return ModelMerger(installer) + + def main(): args = _parse_args() if args.root_dir: config.parse_args(["--root", str(args.root_dir)]) + else: + config.parse_args([]) try: if args.front_end: diff --git a/tests/aa_nodes/test_invoker.py b/tests/aa_nodes/test_invoker.py index 774f7501dc..f67b5a2ac5 100644 --- a/tests/aa_nodes/test_invoker.py +++ b/tests/aa_nodes/test_invoker.py @@ -1,4 +1,5 @@ import logging +from unittest.mock import Mock import pytest @@ -64,7 +65,7 @@ def mock_services() -> InvocationServices: images=None, # type: ignore invocation_cache=MemoryInvocationCache(max_cache_size=0), logger=logging, # type: ignore - model_manager=None, # type: ignore + model_manager=Mock(), # type: ignore download_queue=None, # type: ignore names=None, # type: ignore performance_statistics=InvocationStatsService(), diff --git a/tests/app/services/model_records/test_model_records_sql.py b/tests/app/services/model_records/test_model_records_sql.py index 46afe0105b..852e1da979 100644 --- a/tests/app/services/model_records/test_model_records_sql.py +++ b/tests/app/services/model_records/test_model_records_sql.py @@ -8,6 +8,7 @@ from typing import Any import pytest from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.app.services.model_metadata import ModelMetadataStoreSQL from invokeai.app.services.model_records import ( DuplicateModelException, ModelRecordOrderBy, @@ -36,7 +37,7 @@ def store( config = InvokeAIAppConfig(root=datadir) logger = InvokeAILogger.get_logger(config=config) db = create_mock_sqlite_database(config, logger) - return ModelRecordServiceSQL(db) + return ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) def example_config() -> TextualInversionConfig: diff --git a/tests/backend/model_manager_2/model_manager_2_fixtures.py b/tests/backend/model_manager_2/model_manager_2_fixtures.py index d85eab67dd..ebdc9cb5cd 100644 --- a/tests/backend/model_manager_2/model_manager_2_fixtures.py +++ b/tests/backend/model_manager_2/model_manager_2_fixtures.py @@ -14,6 +14,7 @@ from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.download import DownloadQueueService from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.model_install import ModelInstallService, ModelInstallServiceBase +from invokeai.app.services.model_metadata import ModelMetadataStoreBase, ModelMetadataStoreSQL from invokeai.app.services.model_records import ModelRecordServiceSQL from invokeai.backend.model_manager.config import ( BaseModelType, @@ -21,7 +22,6 @@ from invokeai.backend.model_manager.config import ( ModelType, ) from invokeai.backend.model_manager.load import AnyModelLoader, ModelCache, ModelConvertCache -from invokeai.backend.model_manager.metadata import ModelMetadataStore from invokeai.backend.util.logging import InvokeAILogger from tests.backend.model_manager_2.model_metadata.metadata_examples import ( RepoCivitaiModelMetadata1, @@ -104,7 +104,7 @@ def mm2_loader(mm2_app_config: InvokeAIAppConfig, mm2_record_store: ModelRecordS def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceSQL: logger = InvokeAILogger.get_logger(config=mm2_app_config) db = create_mock_sqlite_database(mm2_app_config, logger) - store = ModelRecordServiceSQL(db) + store = ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) # add five simple config records to the database raw1 = { "path": "/tmp/foo1", @@ -163,15 +163,14 @@ def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceSQL @pytest.fixture -def mm2_metadata_store(mm2_record_store: ModelRecordServiceSQL) -> ModelMetadataStore: - db = mm2_record_store._db # to ensure we are sharing the same database - return ModelMetadataStore(db) +def mm2_metadata_store(mm2_record_store: ModelRecordServiceSQL) -> ModelMetadataStoreBase: + return mm2_record_store.metadata_store @pytest.fixture def mm2_session(embedding_file: Path, diffusers_dir: Path) -> Session: """This fixtures defines a series of mock URLs for testing download and installation.""" - sess = TestSession() + sess: Session = TestSession() sess.mount( "https://test.com/missing_model.safetensors", TestAdapter( @@ -258,8 +257,7 @@ def mm2_installer(mm2_app_config: InvokeAIAppConfig, mm2_session: Session) -> Mo logger = InvokeAILogger.get_logger() db = create_mock_sqlite_database(mm2_app_config, logger) events = DummyEventService() - store = ModelRecordServiceSQL(db) - metadata_store = ModelMetadataStore(db) + store = ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) download_queue = DownloadQueueService(requests_session=mm2_session) download_queue.start() @@ -268,7 +266,6 @@ def mm2_installer(mm2_app_config: InvokeAIAppConfig, mm2_session: Session) -> Mo app_config=mm2_app_config, record_store=store, download_queue=download_queue, - metadata_store=metadata_store, event_bus=events, session=mm2_session, ) diff --git a/tests/backend/model_manager_2/model_metadata/test_model_metadata.py b/tests/backend/model_manager_2/model_metadata/test_model_metadata.py index 5a2ec93767..f61eab1b5d 100644 --- a/tests/backend/model_manager_2/model_metadata/test_model_metadata.py +++ b/tests/backend/model_manager_2/model_metadata/test_model_metadata.py @@ -8,6 +8,7 @@ import pytest from pydantic.networks import HttpUrl from requests.sessions import Session +from invokeai.app.services.model_metadata import ModelMetadataStoreBase from invokeai.backend.model_manager.config import ModelRepoVariant from invokeai.backend.model_manager.metadata import ( CivitaiMetadata, @@ -15,14 +16,13 @@ from invokeai.backend.model_manager.metadata import ( CommercialUsage, HuggingFaceMetadata, HuggingFaceMetadataFetch, - ModelMetadataStore, UnknownMetadataException, ) from invokeai.backend.model_manager.util import select_hf_files from tests.backend.model_manager_2.model_manager_2_fixtures import * # noqa F403 -def test_metadata_store_put_get(mm2_metadata_store: ModelMetadataStore) -> None: +def test_metadata_store_put_get(mm2_metadata_store: ModelMetadataStoreBase) -> None: tags = {"text-to-image", "diffusers"} input_metadata = HuggingFaceMetadata( name="sdxl-vae", @@ -40,7 +40,7 @@ def test_metadata_store_put_get(mm2_metadata_store: ModelMetadataStore) -> None: assert mm2_metadata_store.list_tags() == tags -def test_metadata_store_update(mm2_metadata_store: ModelMetadataStore) -> None: +def test_metadata_store_update(mm2_metadata_store: ModelMetadataStoreBase) -> None: input_metadata = HuggingFaceMetadata( name="sdxl-vae", author="stabilityai", @@ -57,7 +57,7 @@ def test_metadata_store_update(mm2_metadata_store: ModelMetadataStore) -> None: assert input_metadata == output_metadata -def test_metadata_search(mm2_metadata_store: ModelMetadataStore) -> None: +def test_metadata_search(mm2_metadata_store: ModelMetadataStoreBase) -> None: metadata1 = HuggingFaceMetadata( name="sdxl-vae", author="stabilityai", From 5d612ec0958280f5709a22f9872c905abbab12ad Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 17 Feb 2024 11:45:32 -0500 Subject: [PATCH 145/411] Tidy names and locations of modules - Rename old "model_management" directory to "model_management_OLD" in order to catch dangling references to original model manager. - Caught and fixed most dangling references (still checking) - Rename lora, textual_inversion and model_patcher modules - Introduce a RawModel base class to simplfy the Union returned by the model loaders. - Tidy up the model manager 2-related tests. Add useful fixtures, and a finalizer to the queue and installer fixtures that will stop the services and release threads. --- invokeai/app/invocations/compel.py | 6 +- invokeai/app/invocations/latent.py | 4 +- .../services/model_load/model_load_default.py | 23 +- .../app/services/model_manager/__init__.py | 3 +- invokeai/backend/ip_adapter/ip_adapter.py | 3 +- invokeai/backend/{embeddings => }/lora.py | 7 +- .../README.md | 0 .../__init__.py | 0 .../convert_ckpt_to_diffusers.py | 0 .../detect_baked_in_vae.py | 0 .../lora.py | 0 .../memory_snapshot.py | 0 .../model_cache.py | 0 .../model_load_optimizations.py | 0 .../model_manager.py | 0 .../model_merge.py | 0 .../model_probe.py | 0 .../model_search.py | 0 .../models/__init__.py | 0 .../models/base.py | 0 .../models/clip_vision.py | 0 .../models/controlnet.py | 0 .../models/ip_adapter.py | 0 .../models/lora.py | 0 .../models/sdxl.py | 0 .../models/stable_diffusion.py | 0 .../models/stable_diffusion_onnx.py | 0 .../models/t2i_adapter.py | 0 .../models/textual_inversion.py | 0 .../models/vae.py | 0 .../seamless.py | 0 .../util.py | 0 invokeai/backend/model_manager/config.py | 9 +- .../libc_util.py | 0 .../model_manager/load/memory_snapshot.py | 4 +- .../model_manager/load/model_loaders/lora.py | 2 +- .../load/model_loaders/textual_inversion.py | 2 +- invokeai/backend/model_manager/probe.py | 9 +- .../backend/model_manager/util/libc_util.py | 75 ++ .../backend/model_manager/util/model_util.py | 129 +++ .../backend/{embeddings => }/model_patcher.py | 0 invokeai/backend/onnx/onnx_runtime.py | 1 + invokeai/backend/raw_model.py | 14 + .../{embeddings => }/textual_inversion.py | 6 +- invokeai/backend/util/test_utils.py | 45 +- invokeai/configs/INITIAL_MODELS.yaml.OLD | 153 ---- invokeai/configs/models.yaml.example | 47 - .../frontend/install/model_install.py.OLD | 845 ------------------ .../frontend/merge/merge_diffusers.py.OLD | 438 --------- .../model_install/test_model_install.py | 2 +- .../model_records/test_model_records_sql.py | 2 +- tests/backend/ip_adapter/test_ip_adapter.py | 2 +- .../data/invokeai_root/README | 0 .../stable-diffusion/v1-inference.yaml | 0 .../data/invokeai_root/databases/README | 0 .../data/invokeai_root/models/README | 0 .../test-diffusers-main/model_index.json | 0 .../scheduler/scheduler_config.json | 0 .../text_encoder/config.json | 0 .../text_encoder/model.fp16.safetensors | 0 .../text_encoder/model.safetensors | 0 .../text_encoder_2/config.json | 0 .../text_encoder_2/model.fp16.safetensors | 0 .../text_encoder_2/model.safetensors | 0 .../test-diffusers-main/tokenizer/merges.txt | 0 .../tokenizer/special_tokens_map.json | 0 .../tokenizer/tokenizer_config.json | 0 .../test-diffusers-main/tokenizer/vocab.json | 0 .../tokenizer_2/merges.txt | 0 .../tokenizer_2/special_tokens_map.json | 0 .../tokenizer_2/tokenizer_config.json | 0 .../tokenizer_2/vocab.json | 0 .../test-diffusers-main/unet/config.json | 0 .../diffusion_pytorch_model.fp16.safetensors | 0 .../unet/diffusion_pytorch_model.safetensors | 0 .../test-diffusers-main/vae/config.json | 0 .../diffusion_pytorch_model.fp16.safetensors | 0 .../vae/diffusion_pytorch_model.safetensors | 0 .../test_files/test_embedding.safetensors | Bin .../model_loading/test_model_load.py | 11 +- .../model_manager_fixtures.py} | 101 ++- .../model_metadata/metadata_examples.py | 0 .../model_metadata/test_model_metadata.py | 2 +- .../test_libc_util.py | 2 +- .../test_lora.py | 4 +- .../test_memory_snapshot.py | 6 +- .../test_model_load_optimization.py | 2 +- .../util/test_hf_model_select.py | 0 tests/conftest.py | 5 - 89 files changed, 355 insertions(+), 1609 deletions(-) rename invokeai/backend/{embeddings => }/lora.py (99%) rename invokeai/backend/{model_management => model_management_OLD}/README.md (100%) rename invokeai/backend/{model_management => model_management_OLD}/__init__.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/convert_ckpt_to_diffusers.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/detect_baked_in_vae.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/lora.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/memory_snapshot.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/model_cache.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/model_load_optimizations.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/model_manager.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/model_merge.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/model_probe.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/model_search.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/__init__.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/base.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/clip_vision.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/controlnet.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/ip_adapter.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/lora.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/sdxl.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/stable_diffusion.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/stable_diffusion_onnx.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/t2i_adapter.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/textual_inversion.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/models/vae.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/seamless.py (100%) rename invokeai/backend/{model_management => model_management_OLD}/util.py (100%) rename invokeai/backend/{model_management => model_manager}/libc_util.py (100%) create mode 100644 invokeai/backend/model_manager/util/libc_util.py create mode 100644 invokeai/backend/model_manager/util/model_util.py rename invokeai/backend/{embeddings => }/model_patcher.py (100%) create mode 100644 invokeai/backend/raw_model.py rename invokeai/backend/{embeddings => }/textual_inversion.py (97%) delete mode 100644 invokeai/configs/INITIAL_MODELS.yaml.OLD delete mode 100644 invokeai/configs/models.yaml.example delete mode 100644 invokeai/frontend/install/model_install.py.OLD delete mode 100644 invokeai/frontend/merge/merge_diffusers.py.OLD rename tests/backend/{model_manager_2 => model_manager}/data/invokeai_root/README (100%) rename tests/backend/{model_manager_2 => model_manager}/data/invokeai_root/configs/stable-diffusion/v1-inference.yaml (100%) rename tests/backend/{model_manager_2 => model_manager}/data/invokeai_root/databases/README (100%) rename tests/backend/{model_manager_2 => model_manager}/data/invokeai_root/models/README (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/model_index.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/scheduler/scheduler_config.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/text_encoder/config.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/text_encoder/model.fp16.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/text_encoder/model.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/text_encoder_2/config.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/text_encoder_2/model.fp16.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/text_encoder_2/model.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer/merges.txt (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer/special_tokens_map.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer/tokenizer_config.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer/vocab.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer_2/merges.txt (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer_2/special_tokens_map.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer_2/tokenizer_config.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/tokenizer_2/vocab.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/unet/config.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.fp16.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/vae/config.json (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.fp16.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/data/test_files/test_embedding.safetensors (100%) rename tests/backend/{model_manager_2 => model_manager}/model_loading/test_model_load.py (61%) rename tests/backend/{model_manager_2/model_manager_2_fixtures.py => model_manager/model_manager_fixtures.py} (80%) rename tests/backend/{model_manager_2 => model_manager}/model_metadata/metadata_examples.py (100%) rename tests/backend/{model_manager_2 => model_manager}/model_metadata/test_model_metadata.py (99%) rename tests/backend/{model_management => model_manager}/test_libc_util.py (88%) rename tests/backend/{model_management => model_manager}/test_lora.py (96%) rename tests/backend/{model_management => model_manager}/test_memory_snapshot.py (87%) rename tests/backend/{model_management => model_manager}/test_model_load_optimization.py (96%) rename tests/backend/{model_manager_2 => model_manager}/util/test_hf_model_select.py (100%) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 5159d5b89c..593121ba60 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -17,9 +17,9 @@ from invokeai.app.invocations.primitives import ConditioningOutput from invokeai.app.services.model_records import UnknownModelException from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.ti_utils import extract_ti_triggers_from_prompt -from invokeai.backend.embeddings.lora import LoRAModelRaw -from invokeai.backend.embeddings.model_patcher import ModelPatcher -from invokeai.backend.embeddings.textual_inversion import TextualInversionModelRaw +from invokeai.backend.lora import LoRAModelRaw +from invokeai.backend.model_patcher import ModelPatcher +from invokeai.backend.textual_inversion import TextualInversionModelRaw from invokeai.backend.model_manager import ModelType from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 1f21b539dc..bfe7255b62 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -50,10 +50,10 @@ from invokeai.app.invocations.primitives import ( from invokeai.app.invocations.t2i_adapter import T2IAdapterField from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.controlnet_utils import prepare_control_image -from invokeai.backend.embeddings.lora import LoRAModelRaw -from invokeai.backend.embeddings.model_patcher import ModelPatcher from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus +from invokeai.backend.lora import LoRAModelRaw from invokeai.backend.model_manager import BaseModelType, LoadedModel +from invokeai.backend.model_patcher import ModelPatcher from invokeai.backend.stable_diffusion import PipelineIntermediateState, set_seamless from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningData, IPAdapterConditioningInfo from invokeai.backend.util.silence_warnings import SilenceWarnings diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index 29b297c814..fa96a4672d 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -21,11 +21,11 @@ class ModelLoadService(ModelLoadServiceBase): """Wrapper around AnyModelLoader.""" def __init__( - self, - app_config: InvokeAIAppConfig, - record_store: ModelRecordServiceBase, - ram_cache: Optional[ModelCacheBase[AnyModel]] = None, - convert_cache: Optional[ModelConvertCacheBase] = None, + self, + app_config: InvokeAIAppConfig, + record_store: ModelRecordServiceBase, + ram_cache: ModelCacheBase[AnyModel], + convert_cache: ModelConvertCacheBase, ): """Initialize the model load service.""" logger = InvokeAILogger.get_logger(self.__class__.__name__) @@ -34,17 +34,8 @@ class ModelLoadService(ModelLoadServiceBase): self._any_loader = AnyModelLoader( app_config=app_config, logger=logger, - ram_cache=ram_cache - or ModelCache( - max_cache_size=app_config.ram_cache_size, - max_vram_cache_size=app_config.vram_cache_size, - logger=logger, - ), - convert_cache=convert_cache - or ModelConvertCache( - cache_path=app_config.models_convert_cache_path, - max_size=app_config.convert_cache_size, - ), + ram_cache=ram_cache, + convert_cache=convert_cache, ) def start(self, invoker: Invoker) -> None: diff --git a/invokeai/app/services/model_manager/__init__.py b/invokeai/app/services/model_manager/__init__.py index 5e281922a8..66707493f7 100644 --- a/invokeai/app/services/model_manager/__init__.py +++ b/invokeai/app/services/model_manager/__init__.py @@ -3,9 +3,10 @@ from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import LoadedModel -from .model_manager_default import ModelManagerService +from .model_manager_default import ModelManagerServiceBase, ModelManagerService __all__ = [ + "ModelManagerServiceBase", "ModelManagerService", "AnyModel", "AnyModelConfig", diff --git a/invokeai/backend/ip_adapter/ip_adapter.py b/invokeai/backend/ip_adapter/ip_adapter.py index b4706ea99c..3ba6fc5a23 100644 --- a/invokeai/backend/ip_adapter/ip_adapter.py +++ b/invokeai/backend/ip_adapter/ip_adapter.py @@ -10,6 +10,7 @@ from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection from invokeai.backend.ip_adapter.ip_attention_weights import IPAttentionWeights from .resampler import Resampler +from ..raw_model import RawModel class ImageProjModel(torch.nn.Module): @@ -91,7 +92,7 @@ class MLPProjModel(torch.nn.Module): return clip_extra_context_tokens -class IPAdapter: +class IPAdapter(RawModel): """IP-Adapter: https://arxiv.org/pdf/2308.06721.pdf""" def __init__( diff --git a/invokeai/backend/embeddings/lora.py b/invokeai/backend/lora.py similarity index 99% rename from invokeai/backend/embeddings/lora.py rename to invokeai/backend/lora.py index 3c7ef074ef..fb0c23067f 100644 --- a/invokeai/backend/embeddings/lora.py +++ b/invokeai/backend/lora.py @@ -10,8 +10,7 @@ from safetensors.torch import load_file from typing_extensions import Self from invokeai.backend.model_manager import BaseModelType - -from .embedding_base import EmbeddingModelRaw +from .raw_model import RawModel class LoRALayerBase: @@ -367,9 +366,7 @@ class IA3Layer(LoRALayerBase): AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer] - -# TODO: rename all methods used in model logic with Info postfix and remove here Raw postfix -class LoRAModelRaw(EmbeddingModelRaw): # (torch.nn.Module): +class LoRAModelRaw(RawModel): # (torch.nn.Module): _name: str layers: Dict[str, AnyLoRALayer] diff --git a/invokeai/backend/model_management/README.md b/invokeai/backend/model_management_OLD/README.md similarity index 100% rename from invokeai/backend/model_management/README.md rename to invokeai/backend/model_management_OLD/README.md diff --git a/invokeai/backend/model_management/__init__.py b/invokeai/backend/model_management_OLD/__init__.py similarity index 100% rename from invokeai/backend/model_management/__init__.py rename to invokeai/backend/model_management_OLD/__init__.py diff --git a/invokeai/backend/model_management/convert_ckpt_to_diffusers.py b/invokeai/backend/model_management_OLD/convert_ckpt_to_diffusers.py similarity index 100% rename from invokeai/backend/model_management/convert_ckpt_to_diffusers.py rename to invokeai/backend/model_management_OLD/convert_ckpt_to_diffusers.py diff --git a/invokeai/backend/model_management/detect_baked_in_vae.py b/invokeai/backend/model_management_OLD/detect_baked_in_vae.py similarity index 100% rename from invokeai/backend/model_management/detect_baked_in_vae.py rename to invokeai/backend/model_management_OLD/detect_baked_in_vae.py diff --git a/invokeai/backend/model_management/lora.py b/invokeai/backend/model_management_OLD/lora.py similarity index 100% rename from invokeai/backend/model_management/lora.py rename to invokeai/backend/model_management_OLD/lora.py diff --git a/invokeai/backend/model_management/memory_snapshot.py b/invokeai/backend/model_management_OLD/memory_snapshot.py similarity index 100% rename from invokeai/backend/model_management/memory_snapshot.py rename to invokeai/backend/model_management_OLD/memory_snapshot.py diff --git a/invokeai/backend/model_management/model_cache.py b/invokeai/backend/model_management_OLD/model_cache.py similarity index 100% rename from invokeai/backend/model_management/model_cache.py rename to invokeai/backend/model_management_OLD/model_cache.py diff --git a/invokeai/backend/model_management/model_load_optimizations.py b/invokeai/backend/model_management_OLD/model_load_optimizations.py similarity index 100% rename from invokeai/backend/model_management/model_load_optimizations.py rename to invokeai/backend/model_management_OLD/model_load_optimizations.py diff --git a/invokeai/backend/model_management/model_manager.py b/invokeai/backend/model_management_OLD/model_manager.py similarity index 100% rename from invokeai/backend/model_management/model_manager.py rename to invokeai/backend/model_management_OLD/model_manager.py diff --git a/invokeai/backend/model_management/model_merge.py b/invokeai/backend/model_management_OLD/model_merge.py similarity index 100% rename from invokeai/backend/model_management/model_merge.py rename to invokeai/backend/model_management_OLD/model_merge.py diff --git a/invokeai/backend/model_management/model_probe.py b/invokeai/backend/model_management_OLD/model_probe.py similarity index 100% rename from invokeai/backend/model_management/model_probe.py rename to invokeai/backend/model_management_OLD/model_probe.py diff --git a/invokeai/backend/model_management/model_search.py b/invokeai/backend/model_management_OLD/model_search.py similarity index 100% rename from invokeai/backend/model_management/model_search.py rename to invokeai/backend/model_management_OLD/model_search.py diff --git a/invokeai/backend/model_management/models/__init__.py b/invokeai/backend/model_management_OLD/models/__init__.py similarity index 100% rename from invokeai/backend/model_management/models/__init__.py rename to invokeai/backend/model_management_OLD/models/__init__.py diff --git a/invokeai/backend/model_management/models/base.py b/invokeai/backend/model_management_OLD/models/base.py similarity index 100% rename from invokeai/backend/model_management/models/base.py rename to invokeai/backend/model_management_OLD/models/base.py diff --git a/invokeai/backend/model_management/models/clip_vision.py b/invokeai/backend/model_management_OLD/models/clip_vision.py similarity index 100% rename from invokeai/backend/model_management/models/clip_vision.py rename to invokeai/backend/model_management_OLD/models/clip_vision.py diff --git a/invokeai/backend/model_management/models/controlnet.py b/invokeai/backend/model_management_OLD/models/controlnet.py similarity index 100% rename from invokeai/backend/model_management/models/controlnet.py rename to invokeai/backend/model_management_OLD/models/controlnet.py diff --git a/invokeai/backend/model_management/models/ip_adapter.py b/invokeai/backend/model_management_OLD/models/ip_adapter.py similarity index 100% rename from invokeai/backend/model_management/models/ip_adapter.py rename to invokeai/backend/model_management_OLD/models/ip_adapter.py diff --git a/invokeai/backend/model_management/models/lora.py b/invokeai/backend/model_management_OLD/models/lora.py similarity index 100% rename from invokeai/backend/model_management/models/lora.py rename to invokeai/backend/model_management_OLD/models/lora.py diff --git a/invokeai/backend/model_management/models/sdxl.py b/invokeai/backend/model_management_OLD/models/sdxl.py similarity index 100% rename from invokeai/backend/model_management/models/sdxl.py rename to invokeai/backend/model_management_OLD/models/sdxl.py diff --git a/invokeai/backend/model_management/models/stable_diffusion.py b/invokeai/backend/model_management_OLD/models/stable_diffusion.py similarity index 100% rename from invokeai/backend/model_management/models/stable_diffusion.py rename to invokeai/backend/model_management_OLD/models/stable_diffusion.py diff --git a/invokeai/backend/model_management/models/stable_diffusion_onnx.py b/invokeai/backend/model_management_OLD/models/stable_diffusion_onnx.py similarity index 100% rename from invokeai/backend/model_management/models/stable_diffusion_onnx.py rename to invokeai/backend/model_management_OLD/models/stable_diffusion_onnx.py diff --git a/invokeai/backend/model_management/models/t2i_adapter.py b/invokeai/backend/model_management_OLD/models/t2i_adapter.py similarity index 100% rename from invokeai/backend/model_management/models/t2i_adapter.py rename to invokeai/backend/model_management_OLD/models/t2i_adapter.py diff --git a/invokeai/backend/model_management/models/textual_inversion.py b/invokeai/backend/model_management_OLD/models/textual_inversion.py similarity index 100% rename from invokeai/backend/model_management/models/textual_inversion.py rename to invokeai/backend/model_management_OLD/models/textual_inversion.py diff --git a/invokeai/backend/model_management/models/vae.py b/invokeai/backend/model_management_OLD/models/vae.py similarity index 100% rename from invokeai/backend/model_management/models/vae.py rename to invokeai/backend/model_management_OLD/models/vae.py diff --git a/invokeai/backend/model_management/seamless.py b/invokeai/backend/model_management_OLD/seamless.py similarity index 100% rename from invokeai/backend/model_management/seamless.py rename to invokeai/backend/model_management_OLD/seamless.py diff --git a/invokeai/backend/model_management/util.py b/invokeai/backend/model_management_OLD/util.py similarity index 100% rename from invokeai/backend/model_management/util.py rename to invokeai/backend/model_management_OLD/util.py diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index 42921f0b32..bc4848b0a5 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -28,12 +28,11 @@ from diffusers import ModelMixin from pydantic import BaseModel, ConfigDict, Field, TypeAdapter from typing_extensions import Annotated, Any, Dict -from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel +from ..raw_model import RawModel -from ..embeddings.embedding_base import EmbeddingModelRaw -from ..ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus - -AnyModel = Union[ModelMixin, torch.nn.Module, IAIOnnxRuntimeModel, IPAdapter, IPAdapterPlus, EmbeddingModelRaw] +# ModelMixin is the base class for all diffusers and transformers models +# RawModel is the InvokeAI wrapper class for ip_adapters, loras, textual_inversion and onnx runtime +AnyModel = Union[ModelMixin, RawModel, torch.nn.Module] class InvalidModelConfigException(Exception): diff --git a/invokeai/backend/model_management/libc_util.py b/invokeai/backend/model_manager/libc_util.py similarity index 100% rename from invokeai/backend/model_management/libc_util.py rename to invokeai/backend/model_manager/libc_util.py diff --git a/invokeai/backend/model_manager/load/memory_snapshot.py b/invokeai/backend/model_manager/load/memory_snapshot.py index 346f5dc424..209d7166f3 100644 --- a/invokeai/backend/model_manager/load/memory_snapshot.py +++ b/invokeai/backend/model_manager/load/memory_snapshot.py @@ -5,7 +5,7 @@ import psutil import torch from typing_extensions import Self -from invokeai.backend.model_management.libc_util import LibcUtil, Struct_mallinfo2 +from ..util.libc_util import LibcUtil, Struct_mallinfo2 GB = 2**30 # 1 GB @@ -97,4 +97,4 @@ def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: O if snapshot_1.vram is not None and snapshot_2.vram is not None: msg += get_msg_line("VRAM", snapshot_1.vram, snapshot_2.vram) - return "\n" + msg if len(msg) > 0 else msg + return msg diff --git a/invokeai/backend/model_manager/load/model_loaders/lora.py b/invokeai/backend/model_manager/load/model_loaders/lora.py index d8e5f920e2..6ff2dcc918 100644 --- a/invokeai/backend/model_manager/load/model_loaders/lora.py +++ b/invokeai/backend/model_manager/load/model_loaders/lora.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Optional, Tuple from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.embeddings.lora import LoRAModelRaw +from invokeai.backend.lora import LoRAModelRaw from invokeai.backend.model_manager import ( AnyModel, AnyModelConfig, diff --git a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py index 6635f6b43f..9476747960 100644 --- a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py +++ b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Optional, Tuple -from invokeai.backend.embeddings.textual_inversion import TextualInversionModelRaw +from invokeai.backend.textual_inversion import TextualInversionModelRaw from invokeai.backend.model_manager import ( AnyModel, AnyModelConfig, diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index 2c2066d7c5..d511ffa875 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -8,9 +8,7 @@ import torch from picklescan.scanner import scan_file_path import invokeai.backend.util.logging as logger -from invokeai.backend.model_management.models.base import read_checkpoint_meta -from invokeai.backend.model_management.models.ip_adapter import IPAdapterModelFormat -from invokeai.backend.model_management.util import lora_token_vector_length +from .util.model_util import lora_token_vector_length, read_checkpoint_meta from invokeai.backend.util.util import SilenceWarnings from .config import ( @@ -55,7 +53,6 @@ LEGACY_CONFIGS: Dict[BaseModelType, Dict[ModelVariantType, Union[str, Dict[Sched }, } - class ProbeBase(object): """Base class for probes.""" @@ -653,8 +650,8 @@ class LoRAFolderProbe(FolderProbeBase): class IPAdapterFolderProbe(FolderProbeBase): - def get_format(self) -> IPAdapterModelFormat: - return IPAdapterModelFormat.InvokeAI.value + def get_format(self) -> ModelFormat: + return ModelFormat.InvokeAI def get_base_type(self) -> BaseModelType: model_file = self.model_path / "ip_adapter.bin" diff --git a/invokeai/backend/model_manager/util/libc_util.py b/invokeai/backend/model_manager/util/libc_util.py new file mode 100644 index 0000000000..1fbcae0a93 --- /dev/null +++ b/invokeai/backend/model_manager/util/libc_util.py @@ -0,0 +1,75 @@ +import ctypes + + +class Struct_mallinfo2(ctypes.Structure): + """A ctypes Structure that matches the libc mallinfo2 struct. + + Docs: + - https://man7.org/linux/man-pages/man3/mallinfo.3.html + - https://www.gnu.org/software/libc/manual/html_node/Statistics-of-Malloc.html + + struct mallinfo2 { + size_t arena; /* Non-mmapped space allocated (bytes) */ + size_t ordblks; /* Number of free chunks */ + size_t smblks; /* Number of free fastbin blocks */ + size_t hblks; /* Number of mmapped regions */ + size_t hblkhd; /* Space allocated in mmapped regions (bytes) */ + size_t usmblks; /* See below */ + size_t fsmblks; /* Space in freed fastbin blocks (bytes) */ + size_t uordblks; /* Total allocated space (bytes) */ + size_t fordblks; /* Total free space (bytes) */ + size_t keepcost; /* Top-most, releasable space (bytes) */ + }; + """ + + _fields_ = [ + ("arena", ctypes.c_size_t), + ("ordblks", ctypes.c_size_t), + ("smblks", ctypes.c_size_t), + ("hblks", ctypes.c_size_t), + ("hblkhd", ctypes.c_size_t), + ("usmblks", ctypes.c_size_t), + ("fsmblks", ctypes.c_size_t), + ("uordblks", ctypes.c_size_t), + ("fordblks", ctypes.c_size_t), + ("keepcost", ctypes.c_size_t), + ] + + def __str__(self): + s = "" + s += f"{'arena': <10}= {(self.arena/2**30):15.5f} # Non-mmapped space allocated (GB) (uordblks + fordblks)\n" + s += f"{'ordblks': <10}= {(self.ordblks): >15} # Number of free chunks\n" + s += f"{'smblks': <10}= {(self.smblks): >15} # Number of free fastbin blocks \n" + s += f"{'hblks': <10}= {(self.hblks): >15} # Number of mmapped regions \n" + s += f"{'hblkhd': <10}= {(self.hblkhd/2**30):15.5f} # Space allocated in mmapped regions (GB)\n" + s += f"{'usmblks': <10}= {(self.usmblks): >15} # Unused\n" + s += f"{'fsmblks': <10}= {(self.fsmblks/2**30):15.5f} # Space in freed fastbin blocks (GB)\n" + s += ( + f"{'uordblks': <10}= {(self.uordblks/2**30):15.5f} # Space used by in-use allocations (non-mmapped)" + " (GB)\n" + ) + s += f"{'fordblks': <10}= {(self.fordblks/2**30):15.5f} # Space in free blocks (non-mmapped) (GB)\n" + s += f"{'keepcost': <10}= {(self.keepcost/2**30):15.5f} # Top-most, releasable space (GB)\n" + return s + + +class LibcUtil: + """A utility class for interacting with the C Standard Library (`libc`) via ctypes. + + Note that this class will raise on __init__() if 'libc.so.6' can't be found. Take care to handle environments where + this shared library is not available. + + TODO: Improve cross-OS compatibility of this class. + """ + + def __init__(self): + self._libc = ctypes.cdll.LoadLibrary("libc.so.6") + + def mallinfo2(self) -> Struct_mallinfo2: + """Calls `libc` `mallinfo2`. + + Docs: https://man7.org/linux/man-pages/man3/mallinfo.3.html + """ + mallinfo2 = self._libc.mallinfo2 + mallinfo2.restype = Struct_mallinfo2 + return mallinfo2() diff --git a/invokeai/backend/model_manager/util/model_util.py b/invokeai/backend/model_manager/util/model_util.py new file mode 100644 index 0000000000..6847a40878 --- /dev/null +++ b/invokeai/backend/model_manager/util/model_util.py @@ -0,0 +1,129 @@ +"""Utilities for parsing model files, used mostly by probe.py""" + +import json +import torch +from typing import Union +from pathlib import Path +from picklescan.scanner import scan_file_path + +def _fast_safetensors_reader(path: str): + checkpoint = {} + device = torch.device("meta") + with open(path, "rb") as f: + definition_len = int.from_bytes(f.read(8), "little") + definition_json = f.read(definition_len) + definition = json.loads(definition_json) + + if "__metadata__" in definition and definition["__metadata__"].get("format", "pt") not in { + "pt", + "torch", + "pytorch", + }: + raise Exception("Supported only pytorch safetensors files") + definition.pop("__metadata__", None) + + for key, info in definition.items(): + dtype = { + "I8": torch.int8, + "I16": torch.int16, + "I32": torch.int32, + "I64": torch.int64, + "F16": torch.float16, + "F32": torch.float32, + "F64": torch.float64, + }[info["dtype"]] + + checkpoint[key] = torch.empty(info["shape"], dtype=dtype, device=device) + + return checkpoint + +def read_checkpoint_meta(path: Union[str, Path], scan: bool = False): + if str(path).endswith(".safetensors"): + try: + checkpoint = _fast_safetensors_reader(path) + except Exception: + # TODO: create issue for support "meta"? + checkpoint = safetensors.torch.load_file(path, device="cpu") + else: + if scan: + scan_result = scan_file_path(path) + if scan_result.infected_files != 0: + raise Exception(f'The model file "{path}" is potentially infected by malware. Aborting import.') + checkpoint = torch.load(path, map_location=torch.device("meta")) + return checkpoint + +def lora_token_vector_length(checkpoint: dict) -> int: + """ + Given a checkpoint in memory, return the lora token vector length + + :param checkpoint: The checkpoint + """ + + def _get_shape_1(key: str, tensor, checkpoint) -> int: + lora_token_vector_length = None + + if "." not in key: + return lora_token_vector_length # wrong key format + model_key, lora_key = key.split(".", 1) + + # check lora/locon + if lora_key == "lora_down.weight": + lora_token_vector_length = tensor.shape[1] + + # check loha (don't worry about hada_t1/hada_t2 as it used only in 4d shapes) + elif lora_key in ["hada_w1_b", "hada_w2_b"]: + lora_token_vector_length = tensor.shape[1] + + # check lokr (don't worry about lokr_t2 as it used only in 4d shapes) + elif "lokr_" in lora_key: + if model_key + ".lokr_w1" in checkpoint: + _lokr_w1 = checkpoint[model_key + ".lokr_w1"] + elif model_key + "lokr_w1_b" in checkpoint: + _lokr_w1 = checkpoint[model_key + ".lokr_w1_b"] + else: + return lora_token_vector_length # unknown format + + if model_key + ".lokr_w2" in checkpoint: + _lokr_w2 = checkpoint[model_key + ".lokr_w2"] + elif model_key + "lokr_w2_b" in checkpoint: + _lokr_w2 = checkpoint[model_key + ".lokr_w2_b"] + else: + return lora_token_vector_length # unknown format + + lora_token_vector_length = _lokr_w1.shape[1] * _lokr_w2.shape[1] + + elif lora_key == "diff": + lora_token_vector_length = tensor.shape[1] + + # ia3 can be detected only by shape[0] in text encoder + elif lora_key == "weight" and "lora_unet_" not in model_key: + lora_token_vector_length = tensor.shape[0] + + return lora_token_vector_length + + lora_token_vector_length = None + lora_te1_length = None + lora_te2_length = None + for key, tensor in checkpoint.items(): + if key.startswith("lora_unet_") and ("_attn2_to_k." in key or "_attn2_to_v." in key): + lora_token_vector_length = _get_shape_1(key, tensor, checkpoint) + elif key.startswith("lora_unet_") and ( + "time_emb_proj.lora_down" in key + ): # recognizes format at https://civitai.com/models/224641 + lora_token_vector_length = _get_shape_1(key, tensor, checkpoint) + elif key.startswith("lora_te") and "_self_attn_" in key: + tmp_length = _get_shape_1(key, tensor, checkpoint) + if key.startswith("lora_te_"): + lora_token_vector_length = tmp_length + elif key.startswith("lora_te1_"): + lora_te1_length = tmp_length + elif key.startswith("lora_te2_"): + lora_te2_length = tmp_length + + if lora_te1_length is not None and lora_te2_length is not None: + lora_token_vector_length = lora_te1_length + lora_te2_length + + if lora_token_vector_length is not None: + break + + return lora_token_vector_length diff --git a/invokeai/backend/embeddings/model_patcher.py b/invokeai/backend/model_patcher.py similarity index 100% rename from invokeai/backend/embeddings/model_patcher.py rename to invokeai/backend/model_patcher.py diff --git a/invokeai/backend/onnx/onnx_runtime.py b/invokeai/backend/onnx/onnx_runtime.py index f79fa01569..9b2096abdf 100644 --- a/invokeai/backend/onnx/onnx_runtime.py +++ b/invokeai/backend/onnx/onnx_runtime.py @@ -8,6 +8,7 @@ import numpy as np import onnx from onnx import numpy_helper from onnxruntime import InferenceSession, SessionOptions, get_available_providers +from ..raw_model import RawModel ONNX_WEIGHTS_NAME = "model.onnx" diff --git a/invokeai/backend/raw_model.py b/invokeai/backend/raw_model.py new file mode 100644 index 0000000000..2e224d538b --- /dev/null +++ b/invokeai/backend/raw_model.py @@ -0,0 +1,14 @@ +"""Base class for 'Raw' models. + +The RawModel class is the base class of LoRAModelRaw and TextualInversionModelRaw, +and is used for type checking of calls to the model patcher. Its main purpose +is to avoid a circular import issues when lora.py tries to import BaseModelType +from invokeai.backend.model_manager.config, and the latter tries to import LoRAModelRaw +from lora.py. + +The term 'raw' was introduced to describe a wrapper around a torch.nn.Module +that adds additional methods and attributes. +""" + +class RawModel: + """Base class for 'Raw' model wrappers.""" diff --git a/invokeai/backend/embeddings/textual_inversion.py b/invokeai/backend/textual_inversion.py similarity index 97% rename from invokeai/backend/embeddings/textual_inversion.py rename to invokeai/backend/textual_inversion.py index 389edff039..9a4fa0b540 100644 --- a/invokeai/backend/embeddings/textual_inversion.py +++ b/invokeai/backend/textual_inversion.py @@ -8,11 +8,9 @@ from compel.embeddings_provider import BaseTextualInversionManager from safetensors.torch import load_file from transformers import CLIPTokenizer from typing_extensions import Self +from .raw_model import RawModel -from .embedding_base import EmbeddingModelRaw - - -class TextualInversionModelRaw(EmbeddingModelRaw): +class TextualInversionModelRaw(RawModel): embedding: torch.Tensor # [n, 768]|[n, 1280] embedding_2: Optional[torch.Tensor] = None # [n, 768]|[n, 1280] - for SDXL models diff --git a/invokeai/backend/util/test_utils.py b/invokeai/backend/util/test_utils.py index 685603cedc..a3def182c8 100644 --- a/invokeai/backend/util/test_utils.py +++ b/invokeai/backend/util/test_utils.py @@ -5,10 +5,9 @@ from typing import Optional, Union import pytest import torch -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.backend.install.model_install_backend import ModelInstall -from invokeai.backend.model_management.model_manager import LoadedModelInfo -from invokeai.backend.model_management.models.base import BaseModelType, ModelNotFoundException, ModelType, SubModelType +from invokeai.app.services.model_manager import ModelManagerServiceBase +from invokeai.app.services.model_records import UnknownModelException +from invokeai.backend.model_manager import BaseModelType, LoadedModel, ModelType, SubModelType @pytest.fixture(scope="session") @@ -16,31 +15,20 @@ def torch_device(): return "cuda" if torch.cuda.is_available() else "cpu" -@pytest.fixture(scope="module") -def model_installer(): - """A global ModelInstall pytest fixture to be used by many tests.""" - # HACK(ryand): InvokeAIAppConfig.get_config() returns a singleton config object. This can lead to weird interactions - # between tests that need to alter the config. For example, some tests change the 'root' directory in the config, - # which can cause `install_and_load_model(...)` to re-download the model unnecessarily. As a temporary workaround, - # we pass a kwarg to get_config, which causes the config to be re-loaded. To fix this properly, we should stop using - # a singleton. - return ModelInstall(InvokeAIAppConfig.get_config(log_level="info")) - - def install_and_load_model( - model_installer: ModelInstall, + model_manager: ModelManagerServiceBase, model_path_id_or_url: Union[str, Path], model_name: str, base_model: BaseModelType, model_type: ModelType, submodel_type: Optional[SubModelType] = None, -) -> LoadedModelInfo: - """Install a model if it is not already installed, then get the LoadedModelInfo for that model. +) -> LoadedModel: + """Install a model if it is not already installed, then get the LoadedModel for that model. This is intended as a utility function for tests. Args: - model_installer (ModelInstall): The model installer. + mm2_model_manager (ModelManagerServiceBase): The model manager model_path_id_or_url (Union[str, Path]): The path, HF ID, URL, etc. where the model can be installed from if it is not already installed. model_name (str): The model name, forwarded to ModelManager.get_model(...). @@ -51,16 +39,23 @@ def install_and_load_model( Returns: LoadedModelInfo """ - # If the requested model is already installed, return its LoadedModelInfo. - with contextlib.suppress(ModelNotFoundException): - return model_installer.mgr.get_model(model_name, base_model, model_type, submodel_type) + # If the requested model is already installed, return its LoadedModel + with contextlib.suppress(UnknownModelException): + # TODO: Replace with wrapper call + loaded_model: LoadedModel = model_manager.load.load_model_by_attr( + model_name=model_name, base_model=base_model, model_type=model_type + ) + return loaded_model # Install the requested model. - model_installer.heuristic_import(model_path_id_or_url) + job = model_manager.install.heuristic_import(model_path_id_or_url) + model_manager.install.wait_for_job(job, timeout=10) + assert job.complete try: - return model_installer.mgr.get_model(model_name, base_model, model_type, submodel_type) - except ModelNotFoundException as e: + loaded_model = model_manager.load.load_model_by_config(job.config_out) + return loaded_model + except UnknownModelException as e: raise Exception( "Failed to get model info after installing it. There could be a mismatch between the requested model and" f" the installation id ('{model_path_id_or_url}'). Error: {e}" diff --git a/invokeai/configs/INITIAL_MODELS.yaml.OLD b/invokeai/configs/INITIAL_MODELS.yaml.OLD deleted file mode 100644 index c230665e3a..0000000000 --- a/invokeai/configs/INITIAL_MODELS.yaml.OLD +++ /dev/null @@ -1,153 +0,0 @@ -# This file predefines a few models that the user may want to install. -sd-1/main/stable-diffusion-v1-5: - description: Stable Diffusion version 1.5 diffusers model (4.27 GB) - repo_id: runwayml/stable-diffusion-v1-5 - recommended: True - default: True -sd-1/main/stable-diffusion-v1-5-inpainting: - description: RunwayML SD 1.5 model optimized for inpainting, diffusers version (4.27 GB) - repo_id: runwayml/stable-diffusion-inpainting - recommended: True -sd-2/main/stable-diffusion-2-1: - description: Stable Diffusion version 2.1 diffusers model, trained on 768 pixel images (5.21 GB) - repo_id: stabilityai/stable-diffusion-2-1 - recommended: False -sd-2/main/stable-diffusion-2-inpainting: - description: Stable Diffusion version 2.0 inpainting model (5.21 GB) - repo_id: stabilityai/stable-diffusion-2-inpainting - recommended: False -sdxl/main/stable-diffusion-xl-base-1-0: - description: Stable Diffusion XL base model (12 GB) - repo_id: stabilityai/stable-diffusion-xl-base-1.0 - recommended: True -sdxl-refiner/main/stable-diffusion-xl-refiner-1-0: - description: Stable Diffusion XL refiner model (12 GB) - repo_id: stabilityai/stable-diffusion-xl-refiner-1.0 - recommended: False -sdxl/vae/sdxl-1-0-vae-fix: - description: Fine tuned version of the SDXL-1.0 VAE - repo_id: madebyollin/sdxl-vae-fp16-fix - recommended: True -sd-1/main/Analog-Diffusion: - description: An SD-1.5 model trained on diverse analog photographs (2.13 GB) - repo_id: wavymulder/Analog-Diffusion - recommended: False -sd-1/main/Deliberate_v5: - description: Versatile model that produces detailed images up to 768px (4.27 GB) - path: https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate_v5.safetensors - recommended: False -sd-1/main/Dungeons-and-Diffusion: - description: Dungeons & Dragons characters (2.13 GB) - repo_id: 0xJustin/Dungeons-and-Diffusion - recommended: False -sd-1/main/dreamlike-photoreal-2: - description: A photorealistic model trained on 768 pixel images based on SD 1.5 (2.13 GB) - repo_id: dreamlike-art/dreamlike-photoreal-2.0 - recommended: False -sd-1/main/Inkpunk-Diffusion: - description: Stylized illustrations inspired by Gorillaz, FLCL and Shinkawa; prompt with "nvinkpunk" (4.27 GB) - repo_id: Envvi/Inkpunk-Diffusion - recommended: False -sd-1/main/openjourney: - description: An SD 1.5 model fine tuned on Midjourney; prompt with "mdjrny-v4 style" (2.13 GB) - repo_id: prompthero/openjourney - recommended: False -sd-1/main/seek.art_MEGA: - repo_id: coreco/seek.art_MEGA - description: A general use SD-1.5 "anything" model that supports multiple styles (2.1 GB) - recommended: False -sd-1/main/trinart_stable_diffusion_v2: - description: An SD-1.5 model finetuned with ~40K assorted high resolution manga/anime-style images (2.13 GB) - repo_id: naclbit/trinart_stable_diffusion_v2 - recommended: False -sd-1/controlnet/qrcode_monster: - repo_id: monster-labs/control_v1p_sd15_qrcode_monster - subfolder: v2 -sd-1/controlnet/canny: - repo_id: lllyasviel/control_v11p_sd15_canny - recommended: True -sd-1/controlnet/inpaint: - repo_id: lllyasviel/control_v11p_sd15_inpaint -sd-1/controlnet/mlsd: - repo_id: lllyasviel/control_v11p_sd15_mlsd -sd-1/controlnet/depth: - repo_id: lllyasviel/control_v11f1p_sd15_depth - recommended: True -sd-1/controlnet/normal_bae: - repo_id: lllyasviel/control_v11p_sd15_normalbae -sd-1/controlnet/seg: - repo_id: lllyasviel/control_v11p_sd15_seg -sd-1/controlnet/lineart: - repo_id: lllyasviel/control_v11p_sd15_lineart - recommended: True -sd-1/controlnet/lineart_anime: - repo_id: lllyasviel/control_v11p_sd15s2_lineart_anime -sd-1/controlnet/openpose: - repo_id: lllyasviel/control_v11p_sd15_openpose - recommended: True -sd-1/controlnet/scribble: - repo_id: lllyasviel/control_v11p_sd15_scribble - recommended: False -sd-1/controlnet/softedge: - repo_id: lllyasviel/control_v11p_sd15_softedge -sd-1/controlnet/shuffle: - repo_id: lllyasviel/control_v11e_sd15_shuffle -sd-1/controlnet/tile: - repo_id: lllyasviel/control_v11f1e_sd15_tile -sd-1/controlnet/ip2p: - repo_id: lllyasviel/control_v11e_sd15_ip2p -sd-1/t2i_adapter/canny-sd15: - repo_id: TencentARC/t2iadapter_canny_sd15v2 -sd-1/t2i_adapter/sketch-sd15: - repo_id: TencentARC/t2iadapter_sketch_sd15v2 -sd-1/t2i_adapter/depth-sd15: - repo_id: TencentARC/t2iadapter_depth_sd15v2 -sd-1/t2i_adapter/zoedepth-sd15: - repo_id: TencentARC/t2iadapter_zoedepth_sd15v1 -sdxl/t2i_adapter/canny-sdxl: - repo_id: TencentARC/t2i-adapter-canny-sdxl-1.0 -sdxl/t2i_adapter/zoedepth-sdxl: - repo_id: TencentARC/t2i-adapter-depth-zoe-sdxl-1.0 -sdxl/t2i_adapter/lineart-sdxl: - repo_id: TencentARC/t2i-adapter-lineart-sdxl-1.0 -sdxl/t2i_adapter/sketch-sdxl: - repo_id: TencentARC/t2i-adapter-sketch-sdxl-1.0 -sd-1/embedding/EasyNegative: - path: https://huggingface.co/embed/EasyNegative/resolve/main/EasyNegative.safetensors - recommended: True -sd-1/embedding/ahx-beta-453407d: - repo_id: sd-concepts-library/ahx-beta-453407d -sd-1/lora/Ink scenery: - path: https://civitai.com/api/download/models/83390 -sd-1/ip_adapter/ip_adapter_sd15: - repo_id: InvokeAI/ip_adapter_sd15 - recommended: True - requires: - - InvokeAI/ip_adapter_sd_image_encoder - description: IP-Adapter for SD 1.5 models -sd-1/ip_adapter/ip_adapter_plus_sd15: - repo_id: InvokeAI/ip_adapter_plus_sd15 - recommended: False - requires: - - InvokeAI/ip_adapter_sd_image_encoder - description: Refined IP-Adapter for SD 1.5 models -sd-1/ip_adapter/ip_adapter_plus_face_sd15: - repo_id: InvokeAI/ip_adapter_plus_face_sd15 - recommended: False - requires: - - InvokeAI/ip_adapter_sd_image_encoder - description: Refined IP-Adapter for SD 1.5 models, adapted for faces -sdxl/ip_adapter/ip_adapter_sdxl: - repo_id: InvokeAI/ip_adapter_sdxl - recommended: False - requires: - - InvokeAI/ip_adapter_sdxl_image_encoder - description: IP-Adapter for SDXL models -any/clip_vision/ip_adapter_sd_image_encoder: - repo_id: InvokeAI/ip_adapter_sd_image_encoder - recommended: False - description: Required model for using IP-Adapters with SD-1/2 models -any/clip_vision/ip_adapter_sdxl_image_encoder: - repo_id: InvokeAI/ip_adapter_sdxl_image_encoder - recommended: False - description: Required model for using IP-Adapters with SDXL models diff --git a/invokeai/configs/models.yaml.example b/invokeai/configs/models.yaml.example deleted file mode 100644 index 98f8f77e62..0000000000 --- a/invokeai/configs/models.yaml.example +++ /dev/null @@ -1,47 +0,0 @@ -# This file describes the alternative machine learning models -# available to InvokeAI script. -# -# To add a new model, follow the examples below. Each -# model requires a model config file, a weights file, -# and the width and height of the images it -# was trained on. -diffusers-1.4: - description: 🤗🧨 Stable Diffusion v1.4 - format: diffusers - repo_id: CompVis/stable-diffusion-v1-4 -diffusers-1.5: - description: 🤗🧨 Stable Diffusion v1.5 - format: diffusers - repo_id: runwayml/stable-diffusion-v1-5 - default: true -diffusers-1.5+mse: - description: 🤗🧨 Stable Diffusion v1.5 + MSE-finetuned VAE - format: diffusers - repo_id: runwayml/stable-diffusion-v1-5 - vae: - repo_id: stabilityai/sd-vae-ft-mse -diffusers-inpainting-1.5: - description: 🤗🧨 inpainting for Stable Diffusion v1.5 - format: diffusers - repo_id: runwayml/stable-diffusion-inpainting -stable-diffusion-1.5: - description: The newest Stable Diffusion version 1.5 weight file (4.27 GB) - weights: models/ldm/stable-diffusion-v1/v1-5-pruned-emaonly.ckpt - config: configs/stable-diffusion/v1-inference.yaml - width: 512 - height: 512 - vae: ./models/ldm/stable-diffusion-v1/vae-ft-mse-840000-ema-pruned.ckpt -stable-diffusion-1.4: - description: Stable Diffusion inference model version 1.4 - config: configs/stable-diffusion/v1-inference.yaml - weights: models/ldm/stable-diffusion-v1/sd-v1-4.ckpt - vae: models/ldm/stable-diffusion-v1/vae-ft-mse-840000-ema-pruned.ckpt - width: 512 - height: 512 -inpainting-1.5: - weights: models/ldm/stable-diffusion-v1/sd-v1-5-inpainting.ckpt - config: configs/stable-diffusion/v1-inpainting-inference.yaml - vae: models/ldm/stable-diffusion-v1/vae-ft-mse-840000-ema-pruned.ckpt - description: RunwayML SD 1.5 model optimized for inpainting - width: 512 - height: 512 diff --git a/invokeai/frontend/install/model_install.py.OLD b/invokeai/frontend/install/model_install.py.OLD deleted file mode 100644 index e23538ffd6..0000000000 --- a/invokeai/frontend/install/model_install.py.OLD +++ /dev/null @@ -1,845 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2022 Lincoln D. Stein (https://github.com/lstein) -# Before running stable-diffusion on an internet-isolated machine, -# run this script from one with internet connectivity. The -# two machines must share a common .cache directory. - -""" -This is the npyscreen frontend to the model installation application. -The work is actually done in backend code in model_install_backend.py. -""" - -import argparse -import curses -import logging -import sys -import textwrap -import traceback -from argparse import Namespace -from multiprocessing import Process -from multiprocessing.connection import Connection, Pipe -from pathlib import Path -from shutil import get_terminal_size -from typing import Optional - -import npyscreen -import torch -from npyscreen import widget - -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.install.model_install_backend import InstallSelections, ModelInstall, SchedulerPredictionType -from invokeai.backend.model_management import ModelManager, ModelType -from invokeai.backend.util import choose_precision, choose_torch_device -from invokeai.backend.util.logging import InvokeAILogger -from invokeai.frontend.install.widgets import ( - MIN_COLS, - MIN_LINES, - BufferBox, - CenteredTitleText, - CyclingForm, - MultiSelectColumns, - SingleSelectColumns, - TextBox, - WindowTooSmallException, - select_stable_diffusion_config_file, - set_min_terminal_size, -) - -config = InvokeAIAppConfig.get_config() -logger = InvokeAILogger.get_logger() - -# build a table mapping all non-printable characters to None -# for stripping control characters -# from https://stackoverflow.com/questions/92438/stripping-non-printable-characters-from-a-string-in-python -NOPRINT_TRANS_TABLE = {i: None for i in range(0, sys.maxunicode + 1) if not chr(i).isprintable()} - -# maximum number of installed models we can display before overflowing vertically -MAX_OTHER_MODELS = 72 - - -def make_printable(s: str) -> str: - """Replace non-printable characters in a string""" - return s.translate(NOPRINT_TRANS_TABLE) - - -class addModelsForm(CyclingForm, npyscreen.FormMultiPage): - # for responsive resizing set to False, but this seems to cause a crash! - FIX_MINIMUM_SIZE_WHEN_CREATED = True - - # for persistence - current_tab = 0 - - def __init__(self, parentApp, name, multipage=False, *args, **keywords): - self.multipage = multipage - self.subprocess = None - super().__init__(parentApp=parentApp, name=name, *args, **keywords) # noqa: B026 # TODO: maybe this is bad? - - def create(self): - self.keypress_timeout = 10 - self.counter = 0 - self.subprocess_connection = None - - if not config.model_conf_path.exists(): - with open(config.model_conf_path, "w") as file: - print("# InvokeAI model configuration file", file=file) - self.installer = ModelInstall(config) - self.all_models = self.installer.all_models() - self.starter_models = self.installer.starter_models() - self.model_labels = self._get_model_labels() - window_width, window_height = get_terminal_size() - - self.nextrely -= 1 - self.add_widget_intelligent( - npyscreen.FixedText, - value="Use ctrl-N and ctrl-P to move to the ext and

revious fields. Cursor keys navigate, and selects.", - editable=False, - color="CAUTION", - ) - self.nextrely += 1 - self.tabs = self.add_widget_intelligent( - SingleSelectColumns, - values=[ - "STARTERS", - "MAINS", - "CONTROLNETS", - "T2I-ADAPTERS", - "IP-ADAPTERS", - "LORAS", - "TI EMBEDDINGS", - ], - value=[self.current_tab], - columns=7, - max_height=2, - relx=8, - scroll_exit=True, - ) - self.tabs.on_changed = self._toggle_tables - - top_of_table = self.nextrely - self.starter_pipelines = self.add_starter_pipelines() - bottom_of_table = self.nextrely - - self.nextrely = top_of_table - self.pipeline_models = self.add_pipeline_widgets( - model_type=ModelType.Main, window_width=window_width, exclude=self.starter_models - ) - # self.pipeline_models['autoload_pending'] = True - bottom_of_table = max(bottom_of_table, self.nextrely) - - self.nextrely = top_of_table - self.controlnet_models = self.add_model_widgets( - model_type=ModelType.ControlNet, - window_width=window_width, - ) - bottom_of_table = max(bottom_of_table, self.nextrely) - - self.nextrely = top_of_table - self.t2i_models = self.add_model_widgets( - model_type=ModelType.T2IAdapter, - window_width=window_width, - ) - bottom_of_table = max(bottom_of_table, self.nextrely) - self.nextrely = top_of_table - self.ipadapter_models = self.add_model_widgets( - model_type=ModelType.IPAdapter, - window_width=window_width, - ) - bottom_of_table = max(bottom_of_table, self.nextrely) - - self.nextrely = top_of_table - self.lora_models = self.add_model_widgets( - model_type=ModelType.Lora, - window_width=window_width, - ) - bottom_of_table = max(bottom_of_table, self.nextrely) - - self.nextrely = top_of_table - self.ti_models = self.add_model_widgets( - model_type=ModelType.TextualInversion, - window_width=window_width, - ) - bottom_of_table = max(bottom_of_table, self.nextrely) - - self.nextrely = bottom_of_table + 1 - - self.monitor = self.add_widget_intelligent( - BufferBox, - name="Log Messages", - editable=False, - max_height=6, - ) - - self.nextrely += 1 - done_label = "APPLY CHANGES" - back_label = "BACK" - cancel_label = "CANCEL" - current_position = self.nextrely - if self.multipage: - self.back_button = self.add_widget_intelligent( - npyscreen.ButtonPress, - name=back_label, - when_pressed_function=self.on_back, - ) - else: - self.nextrely = current_position - self.cancel_button = self.add_widget_intelligent( - npyscreen.ButtonPress, name=cancel_label, when_pressed_function=self.on_cancel - ) - self.nextrely = current_position - self.ok_button = self.add_widget_intelligent( - npyscreen.ButtonPress, - name=done_label, - relx=(window_width - len(done_label)) // 2, - when_pressed_function=self.on_execute, - ) - - label = "APPLY CHANGES & EXIT" - self.nextrely = current_position - self.done = self.add_widget_intelligent( - npyscreen.ButtonPress, - name=label, - relx=window_width - len(label) - 15, - when_pressed_function=self.on_done, - ) - - # This restores the selected page on return from an installation - for _i in range(1, self.current_tab + 1): - self.tabs.h_cursor_line_down(1) - self._toggle_tables([self.current_tab]) - - ############# diffusers tab ########## - def add_starter_pipelines(self) -> dict[str, npyscreen.widget]: - """Add widgets responsible for selecting diffusers models""" - widgets = {} - models = self.all_models - starters = self.starter_models - starter_model_labels = self.model_labels - - self.installed_models = sorted([x for x in starters if models[x].installed]) - - widgets.update( - label1=self.add_widget_intelligent( - CenteredTitleText, - name="Select from a starter set of Stable Diffusion models from HuggingFace.", - editable=False, - labelColor="CAUTION", - ) - ) - - self.nextrely -= 1 - # if user has already installed some initial models, then don't patronize them - # by showing more recommendations - show_recommended = len(self.installed_models) == 0 - keys = [x for x in models.keys() if x in starters] - widgets.update( - models_selected=self.add_widget_intelligent( - MultiSelectColumns, - columns=1, - name="Install Starter Models", - values=[starter_model_labels[x] for x in keys], - value=[ - keys.index(x) - for x in keys - if (show_recommended and models[x].recommended) or (x in self.installed_models) - ], - max_height=len(starters) + 1, - relx=4, - scroll_exit=True, - ), - models=keys, - ) - - self.nextrely += 1 - return widgets - - ############# Add a set of model install widgets ######## - def add_model_widgets( - self, - model_type: ModelType, - window_width: int = 120, - install_prompt: str = None, - exclude: set = None, - ) -> dict[str, npyscreen.widget]: - """Generic code to create model selection widgets""" - if exclude is None: - exclude = set() - widgets = {} - model_list = [x for x in self.all_models if self.all_models[x].model_type == model_type and x not in exclude] - model_labels = [self.model_labels[x] for x in model_list] - - show_recommended = len(self.installed_models) == 0 - truncated = False - if len(model_list) > 0: - max_width = max([len(x) for x in model_labels]) - columns = window_width // (max_width + 8) # 8 characters for "[x] " and padding - columns = min(len(model_list), columns) or 1 - prompt = ( - install_prompt - or f"Select the desired {model_type.value.title()} models to install. Unchecked models will be purged from disk." - ) - - widgets.update( - label1=self.add_widget_intelligent( - CenteredTitleText, - name=prompt, - editable=False, - labelColor="CAUTION", - ) - ) - - if len(model_labels) > MAX_OTHER_MODELS: - model_labels = model_labels[0:MAX_OTHER_MODELS] - truncated = True - - widgets.update( - models_selected=self.add_widget_intelligent( - MultiSelectColumns, - columns=columns, - name=f"Install {model_type} Models", - values=model_labels, - value=[ - model_list.index(x) - for x in model_list - if (show_recommended and self.all_models[x].recommended) or self.all_models[x].installed - ], - max_height=len(model_list) // columns + 1, - relx=4, - scroll_exit=True, - ), - models=model_list, - ) - - if truncated: - widgets.update( - warning_message=self.add_widget_intelligent( - npyscreen.FixedText, - value=f"Too many models to display (max={MAX_OTHER_MODELS}). Some are not displayed.", - editable=False, - color="CAUTION", - ) - ) - - self.nextrely += 1 - widgets.update( - download_ids=self.add_widget_intelligent( - TextBox, - name="Additional URLs, or HuggingFace repo_ids to install (Space separated. Use shift-control-V to paste):", - max_height=4, - scroll_exit=True, - editable=True, - ) - ) - return widgets - - ### Tab for arbitrary diffusers widgets ### - def add_pipeline_widgets( - self, - model_type: ModelType = ModelType.Main, - window_width: int = 120, - **kwargs, - ) -> dict[str, npyscreen.widget]: - """Similar to add_model_widgets() but adds some additional widgets at the bottom - to support the autoload directory""" - widgets = self.add_model_widgets( - model_type=model_type, - window_width=window_width, - install_prompt=f"Installed {model_type.value.title()} models. Unchecked models in the InvokeAI root directory will be deleted. Enter URLs, paths or repo_ids to import.", - **kwargs, - ) - - return widgets - - def resize(self): - super().resize() - if s := self.starter_pipelines.get("models_selected"): - keys = [x for x in self.all_models.keys() if x in self.starter_models] - s.values = [self.model_labels[x] for x in keys] - - def _toggle_tables(self, value=None): - selected_tab = value[0] - widgets = [ - self.starter_pipelines, - self.pipeline_models, - self.controlnet_models, - self.t2i_models, - self.ipadapter_models, - self.lora_models, - self.ti_models, - ] - - for group in widgets: - for _k, v in group.items(): - try: - v.hidden = True - v.editable = False - except Exception: - pass - for _k, v in widgets[selected_tab].items(): - try: - v.hidden = False - if not isinstance(v, (npyscreen.FixedText, npyscreen.TitleFixedText, CenteredTitleText)): - v.editable = True - except Exception: - pass - self.__class__.current_tab = selected_tab # for persistence - self.display() - - def _get_model_labels(self) -> dict[str, str]: - window_width, window_height = get_terminal_size() - checkbox_width = 4 - spacing_width = 2 - - models = self.all_models - label_width = max([len(models[x].name) for x in models]) - description_width = window_width - label_width - checkbox_width - spacing_width - - result = {} - for x in models.keys(): - description = models[x].description - description = ( - description[0 : description_width - 3] + "..." - if description and len(description) > description_width - else description - if description - else "" - ) - result[x] = f"%-{label_width}s %s" % (models[x].name, description) - return result - - def _get_columns(self) -> int: - window_width, window_height = get_terminal_size() - cols = 4 if window_width > 240 else 3 if window_width > 160 else 2 if window_width > 80 else 1 - return min(cols, len(self.installed_models)) - - def confirm_deletions(self, selections: InstallSelections) -> bool: - remove_models = selections.remove_models - if len(remove_models) > 0: - mods = "\n".join([ModelManager.parse_key(x)[0] for x in remove_models]) - return npyscreen.notify_ok_cancel( - f"These unchecked models will be deleted from disk. Continue?\n---------\n{mods}" - ) - else: - return True - - def on_execute(self): - self.marshall_arguments() - app = self.parentApp - if not self.confirm_deletions(app.install_selections): - return - - self.monitor.entry_widget.buffer(["Processing..."], scroll_end=True) - self.ok_button.hidden = True - self.display() - - # TO DO: Spawn a worker thread, not a subprocess - parent_conn, child_conn = Pipe() - p = Process( - target=process_and_execute, - kwargs={ - "opt": app.program_opts, - "selections": app.install_selections, - "conn_out": child_conn, - }, - ) - p.start() - child_conn.close() - self.subprocess_connection = parent_conn - self.subprocess = p - app.install_selections = InstallSelections() - - def on_back(self): - self.parentApp.switchFormPrevious() - self.editing = False - - def on_cancel(self): - self.parentApp.setNextForm(None) - self.parentApp.user_cancelled = True - self.editing = False - - def on_done(self): - self.marshall_arguments() - if not self.confirm_deletions(self.parentApp.install_selections): - return - self.parentApp.setNextForm(None) - self.parentApp.user_cancelled = False - self.editing = False - - ########## This routine monitors the child process that is performing model installation and removal ##### - def while_waiting(self): - """Called during idle periods. Main task is to update the Log Messages box with messages - from the child process that does the actual installation/removal""" - c = self.subprocess_connection - if not c: - return - - monitor_widget = self.monitor.entry_widget - while c.poll(): - try: - data = c.recv_bytes().decode("utf-8") - data.strip("\n") - - # processing child is requesting user input to select the - # right configuration file - if data.startswith("*need v2 config"): - _, model_path, *_ = data.split(":", 2) - self._return_v2_config(model_path) - - # processing child is done - elif data == "*done*": - self._close_subprocess_and_regenerate_form() - break - - # update the log message box - else: - data = make_printable(data) - data = data.replace("[A", "") - monitor_widget.buffer( - textwrap.wrap( - data, - width=monitor_widget.width, - subsequent_indent=" ", - ), - scroll_end=True, - ) - self.display() - except (EOFError, OSError): - self.subprocess_connection = None - - def _return_v2_config(self, model_path: str): - c = self.subprocess_connection - model_name = Path(model_path).name - message = select_stable_diffusion_config_file(model_name=model_name) - c.send_bytes(message.encode("utf-8")) - - def _close_subprocess_and_regenerate_form(self): - app = self.parentApp - self.subprocess_connection.close() - self.subprocess_connection = None - self.monitor.entry_widget.buffer(["** Action Complete **"]) - self.display() - - # rebuild the form, saving and restoring some of the fields that need to be preserved. - saved_messages = self.monitor.entry_widget.values - - app.main_form = app.addForm( - "MAIN", - addModelsForm, - name="Install Stable Diffusion Models", - multipage=self.multipage, - ) - app.switchForm("MAIN") - - app.main_form.monitor.entry_widget.values = saved_messages - app.main_form.monitor.entry_widget.buffer([""], scroll_end=True) - # app.main_form.pipeline_models['autoload_directory'].value = autoload_dir - # app.main_form.pipeline_models['autoscan_on_startup'].value = autoscan - - def marshall_arguments(self): - """ - Assemble arguments and store as attributes of the application: - .starter_models: dict of model names to install from INITIAL_CONFIGURE.yaml - True => Install - False => Remove - .scan_directory: Path to a directory of models to scan and import - .autoscan_on_startup: True if invokeai should scan and import at startup time - .import_model_paths: list of URLs, repo_ids and file paths to import - """ - selections = self.parentApp.install_selections - all_models = self.all_models - - # Defined models (in INITIAL_CONFIG.yaml or models.yaml) to add/remove - ui_sections = [ - self.starter_pipelines, - self.pipeline_models, - self.controlnet_models, - self.t2i_models, - self.ipadapter_models, - self.lora_models, - self.ti_models, - ] - for section in ui_sections: - if "models_selected" not in section: - continue - selected = {section["models"][x] for x in section["models_selected"].value} - models_to_install = [x for x in selected if not self.all_models[x].installed] - models_to_remove = [x for x in section["models"] if x not in selected and self.all_models[x].installed] - selections.remove_models.extend(models_to_remove) - selections.install_models.extend( - all_models[x].path or all_models[x].repo_id - for x in models_to_install - if all_models[x].path or all_models[x].repo_id - ) - - # models located in the 'download_ids" section - for section in ui_sections: - if downloads := section.get("download_ids"): - selections.install_models.extend(downloads.value.split()) - - # NOT NEEDED - DONE IN BACKEND NOW - # # special case for the ipadapter_models. If any of the adapters are - # # chosen, then we add the corresponding encoder(s) to the install list. - # section = self.ipadapter_models - # if section.get("models_selected"): - # selected_adapters = [ - # self.all_models[section["models"][x]].name for x in section.get("models_selected").value - # ] - # encoders = [] - # if any(["sdxl" in x for x in selected_adapters]): - # encoders.append("ip_adapter_sdxl_image_encoder") - # if any(["sd15" in x for x in selected_adapters]): - # encoders.append("ip_adapter_sd_image_encoder") - # for encoder in encoders: - # key = f"any/clip_vision/{encoder}" - # repo_id = f"InvokeAI/{encoder}" - # if key not in self.all_models: - # selections.install_models.append(repo_id) - - -class AddModelApplication(npyscreen.NPSAppManaged): - def __init__(self, opt): - super().__init__() - self.program_opts = opt - self.user_cancelled = False - # self.autoload_pending = True - self.install_selections = InstallSelections() - - def onStart(self): - npyscreen.setTheme(npyscreen.Themes.DefaultTheme) - self.main_form = self.addForm( - "MAIN", - addModelsForm, - name="Install Stable Diffusion Models", - cycle_widgets=False, - ) - - -class StderrToMessage: - def __init__(self, connection: Connection): - self.connection = connection - - def write(self, data: str): - self.connection.send_bytes(data.encode("utf-8")) - - def flush(self): - pass - - -# -------------------------------------------------------- -def ask_user_for_prediction_type(model_path: Path, tui_conn: Connection = None) -> SchedulerPredictionType: - if tui_conn: - logger.debug("Waiting for user response...") - return _ask_user_for_pt_tui(model_path, tui_conn) - else: - return _ask_user_for_pt_cmdline(model_path) - - -def _ask_user_for_pt_cmdline(model_path: Path) -> Optional[SchedulerPredictionType]: - choices = [SchedulerPredictionType.Epsilon, SchedulerPredictionType.VPrediction, None] - print( - f""" -Please select the scheduler prediction type of the checkpoint named {model_path.name}: -[1] "epsilon" - most v1.5 models and v2 models trained on 512 pixel images -[2] "vprediction" - v2 models trained on 768 pixel images and a few v1.5 models -[3] Accept the best guess; you can fix it in the Web UI later -""" - ) - choice = None - ok = False - while not ok: - try: - choice = input("select [3]> ").strip() - if not choice: - return None - choice = choices[int(choice) - 1] - ok = True - except (ValueError, IndexError): - print(f"{choice} is not a valid choice") - except EOFError: - return - return choice - - -def _ask_user_for_pt_tui(model_path: Path, tui_conn: Connection) -> SchedulerPredictionType: - tui_conn.send_bytes(f"*need v2 config for:{model_path}".encode("utf-8")) - # note that we don't do any status checking here - response = tui_conn.recv_bytes().decode("utf-8") - if response is None: - return None - elif response == "epsilon": - return SchedulerPredictionType.epsilon - elif response == "v": - return SchedulerPredictionType.VPrediction - elif response == "guess": - return None - else: - return None - - -# -------------------------------------------------------- -def process_and_execute( - opt: Namespace, - selections: InstallSelections, - conn_out: Connection = None, -): - # need to reinitialize config in subprocess - config = InvokeAIAppConfig.get_config() - args = ["--root", opt.root] if opt.root else [] - config.parse_args(args) - - # set up so that stderr is sent to conn_out - if conn_out: - translator = StderrToMessage(conn_out) - sys.stderr = translator - sys.stdout = translator - logger = InvokeAILogger.get_logger() - logger.handlers.clear() - logger.addHandler(logging.StreamHandler(translator)) - - installer = ModelInstall(config, prediction_type_helper=lambda x: ask_user_for_prediction_type(x, conn_out)) - installer.install(selections) - - if conn_out: - conn_out.send_bytes("*done*".encode("utf-8")) - conn_out.close() - - -# -------------------------------------------------------- -def select_and_download_models(opt: Namespace): - precision = "float32" if opt.full_precision else choose_precision(torch.device(choose_torch_device())) - config.precision = precision - installer = ModelInstall(config, prediction_type_helper=ask_user_for_prediction_type) - if opt.list_models: - installer.list_models(opt.list_models) - elif opt.add or opt.delete: - selections = InstallSelections(install_models=opt.add or [], remove_models=opt.delete or []) - installer.install(selections) - elif opt.default_only: - selections = InstallSelections(install_models=installer.default_model()) - installer.install(selections) - elif opt.yes_to_all: - selections = InstallSelections(install_models=installer.recommended_models()) - installer.install(selections) - - # this is where the TUI is called - else: - # needed to support the probe() method running under a subprocess - torch.multiprocessing.set_start_method("spawn") - - if not set_min_terminal_size(MIN_COLS, MIN_LINES): - raise WindowTooSmallException( - "Could not increase terminal size. Try running again with a larger window or smaller font size." - ) - - installApp = AddModelApplication(opt) - try: - installApp.run() - except KeyboardInterrupt as e: - if hasattr(installApp, "main_form"): - if installApp.main_form.subprocess and installApp.main_form.subprocess.is_alive(): - logger.info("Terminating subprocesses") - installApp.main_form.subprocess.terminate() - installApp.main_form.subprocess = None - raise e - process_and_execute(opt, installApp.install_selections) - - -# ------------------------------------- -def main(): - parser = argparse.ArgumentParser(description="InvokeAI model downloader") - parser.add_argument( - "--add", - nargs="*", - help="List of URLs, local paths or repo_ids of models to install", - ) - parser.add_argument( - "--delete", - nargs="*", - help="List of names of models to idelete", - ) - parser.add_argument( - "--full-precision", - dest="full_precision", - action=argparse.BooleanOptionalAction, - type=bool, - default=False, - help="use 32-bit weights instead of faster 16-bit weights", - ) - parser.add_argument( - "--yes", - "-y", - dest="yes_to_all", - action="store_true", - help='answer "yes" to all prompts', - ) - parser.add_argument( - "--default_only", - action="store_true", - help="Only install the default model", - ) - parser.add_argument( - "--list-models", - choices=[x.value for x in ModelType], - help="list installed models", - ) - parser.add_argument( - "--config_file", - "-c", - dest="config_file", - type=str, - default=None, - help="path to configuration file to create", - ) - parser.add_argument( - "--root_dir", - dest="root", - type=str, - default=None, - help="path to root of install directory", - ) - opt = parser.parse_args() - - invoke_args = [] - if opt.root: - invoke_args.extend(["--root", opt.root]) - if opt.full_precision: - invoke_args.extend(["--precision", "float32"]) - config.parse_args(invoke_args) - logger = InvokeAILogger().get_logger(config=config) - - if not config.model_conf_path.exists(): - logger.info("Your InvokeAI root directory is not set up. Calling invokeai-configure.") - from invokeai.frontend.install.invokeai_configure import invokeai_configure - - invokeai_configure() - sys.exit(0) - - try: - select_and_download_models(opt) - except AssertionError as e: - logger.error(e) - sys.exit(-1) - except KeyboardInterrupt: - curses.nocbreak() - curses.echo() - curses.endwin() - logger.info("Goodbye! Come back soon.") - except WindowTooSmallException as e: - logger.error(str(e)) - except widget.NotEnoughSpaceForWidget as e: - if str(e).startswith("Height of 1 allocated"): - logger.error("Insufficient vertical space for the interface. Please make your window taller and try again") - input("Press any key to continue...") - except Exception as e: - if str(e).startswith("addwstr"): - logger.error( - "Insufficient horizontal space for the interface. Please make your window wider and try again." - ) - else: - print(f"An exception has occurred: {str(e)} Details:") - print(traceback.format_exc(), file=sys.stderr) - input("Press any key to continue...") - - -# ------------------------------------- -if __name__ == "__main__": - main() diff --git a/invokeai/frontend/merge/merge_diffusers.py.OLD b/invokeai/frontend/merge/merge_diffusers.py.OLD deleted file mode 100644 index b365198f87..0000000000 --- a/invokeai/frontend/merge/merge_diffusers.py.OLD +++ /dev/null @@ -1,438 +0,0 @@ -""" -invokeai.frontend.merge exports a single function called merge_diffusion_models(). - -It merges 2-3 models together and create a new InvokeAI-registered diffusion model. - -Copyright (c) 2023-24 Lincoln Stein and the InvokeAI Development Team -""" -import argparse -import curses -import re -import sys -from argparse import Namespace -from pathlib import Path -from typing import List, Optional, Tuple - -import npyscreen -from npyscreen import widget - -import invokeai.backend.util.logging as logger -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.model_install import ModelInstallServiceBase -from invokeai.app.services.model_records import ModelRecordServiceBase -from invokeai.backend.install.install_helper import initialize_installer -from invokeai.backend.model_manager import ( - BaseModelType, - ModelFormat, - ModelType, - ModelVariantType, -) -from invokeai.backend.model_manager.merge import ModelMerger -from invokeai.frontend.install.widgets import FloatTitleSlider, SingleSelectColumns, TextBox - -config = InvokeAIAppConfig.get_config() - -BASE_TYPES = [ - (BaseModelType.StableDiffusion1, "Models Built on SD-1.x"), - (BaseModelType.StableDiffusion2, "Models Built on SD-2.x"), - (BaseModelType.StableDiffusionXL, "Models Built on SDXL"), -] - - -def _parse_args() -> Namespace: - parser = argparse.ArgumentParser(description="InvokeAI model merging") - parser.add_argument( - "--root_dir", - type=Path, - default=config.root, - help="Path to the invokeai runtime directory", - ) - parser.add_argument( - "--front_end", - "--gui", - dest="front_end", - action="store_true", - default=False, - help="Activate the text-based graphical front end for collecting parameters. Aside from --root_dir, other parameters will be ignored.", - ) - parser.add_argument( - "--models", - dest="model_names", - type=str, - nargs="+", - help="Two to three model names to be merged", - ) - parser.add_argument( - "--base_model", - type=str, - choices=[x[0].value for x in BASE_TYPES], - help="The base model shared by the models to be merged", - ) - parser.add_argument( - "--merged_model_name", - "--destination", - dest="merged_model_name", - type=str, - help="Name of the output model. If not specified, will be the concatenation of the input model names.", - ) - parser.add_argument( - "--alpha", - type=float, - default=0.5, - help="The interpolation parameter, ranging from 0 to 1. It affects the ratio in which the checkpoints are merged. Higher values give more weight to the 2d and 3d models", - ) - parser.add_argument( - "--interpolation", - dest="interp", - type=str, - choices=["weighted_sum", "sigmoid", "inv_sigmoid", "add_difference"], - default="weighted_sum", - help='Interpolation method to use. If three models are present, only "add_difference" will work.', - ) - parser.add_argument( - "--force", - action="store_true", - help="Try to merge models even if they are incompatible with each other", - ) - parser.add_argument( - "--clobber", - "--overwrite", - dest="clobber", - action="store_true", - help="Overwrite the merged model if --merged_model_name already exists", - ) - return parser.parse_args() - - -# ------------------------- GUI HERE ------------------------- -class mergeModelsForm(npyscreen.FormMultiPageAction): - interpolations = ["weighted_sum", "sigmoid", "inv_sigmoid"] - - def __init__(self, parentApp, name): - self.parentApp = parentApp - self.ALLOW_RESIZE = True - self.FIX_MINIMUM_SIZE_WHEN_CREATED = False - super().__init__(parentApp, name) - - @property - def model_record_store(self) -> ModelRecordServiceBase: - installer: ModelInstallServiceBase = self.parentApp.installer - return installer.record_store - - def afterEditing(self) -> None: - self.parentApp.setNextForm(None) - - def create(self) -> None: - window_height, window_width = curses.initscr().getmaxyx() - self.current_base = 0 - self.models = self.get_models(BASE_TYPES[self.current_base][0]) - self.model_names = [x[1] for x in self.models] - max_width = max([len(x) for x in self.model_names]) - max_width += 6 - horizontal_layout = max_width * 3 < window_width - - self.add_widget_intelligent( - npyscreen.FixedText, - color="CONTROL", - value="Select two models to merge and optionally a third.", - editable=False, - ) - self.add_widget_intelligent( - npyscreen.FixedText, - color="CONTROL", - value="Use up and down arrows to move, to select an item, and to move from one field to the next.", - editable=False, - ) - self.nextrely += 1 - self.base_select = self.add_widget_intelligent( - SingleSelectColumns, - values=[x[1] for x in BASE_TYPES], - value=[self.current_base], - columns=4, - max_height=2, - relx=8, - scroll_exit=True, - ) - self.base_select.on_changed = self._populate_models - self.add_widget_intelligent( - npyscreen.FixedText, - value="MODEL 1", - color="GOOD", - editable=False, - rely=6 if horizontal_layout else None, - ) - self.model1 = self.add_widget_intelligent( - npyscreen.SelectOne, - values=self.model_names, - value=0, - max_height=len(self.model_names), - max_width=max_width, - scroll_exit=True, - rely=7, - ) - self.add_widget_intelligent( - npyscreen.FixedText, - value="MODEL 2", - color="GOOD", - editable=False, - relx=max_width + 3 if horizontal_layout else None, - rely=6 if horizontal_layout else None, - ) - self.model2 = self.add_widget_intelligent( - npyscreen.SelectOne, - name="(2)", - values=self.model_names, - value=1, - max_height=len(self.model_names), - max_width=max_width, - relx=max_width + 3 if horizontal_layout else None, - rely=7 if horizontal_layout else None, - scroll_exit=True, - ) - self.add_widget_intelligent( - npyscreen.FixedText, - value="MODEL 3", - color="GOOD", - editable=False, - relx=max_width * 2 + 3 if horizontal_layout else None, - rely=6 if horizontal_layout else None, - ) - models_plus_none = self.model_names.copy() - models_plus_none.insert(0, "None") - self.model3 = self.add_widget_intelligent( - npyscreen.SelectOne, - name="(3)", - values=models_plus_none, - value=0, - max_height=len(self.model_names) + 1, - max_width=max_width, - scroll_exit=True, - relx=max_width * 2 + 3 if horizontal_layout else None, - rely=7 if horizontal_layout else None, - ) - for m in [self.model1, self.model2, self.model3]: - m.when_value_edited = self.models_changed - self.merged_model_name = self.add_widget_intelligent( - TextBox, - name="Name for merged model:", - labelColor="CONTROL", - max_height=3, - value="", - scroll_exit=True, - ) - self.force = self.add_widget_intelligent( - npyscreen.Checkbox, - name="Force merge of models created by different diffusers library versions", - labelColor="CONTROL", - value=True, - scroll_exit=True, - ) - self.nextrely += 1 - self.merge_method = self.add_widget_intelligent( - npyscreen.TitleSelectOne, - name="Merge Method:", - values=self.interpolations, - value=0, - labelColor="CONTROL", - max_height=len(self.interpolations) + 1, - scroll_exit=True, - ) - self.alpha = self.add_widget_intelligent( - FloatTitleSlider, - name="Weight (alpha) to assign to second and third models:", - out_of=1.0, - step=0.01, - lowest=0, - value=0.5, - labelColor="CONTROL", - scroll_exit=True, - ) - self.model1.editing = True - - def models_changed(self) -> None: - models = self.model1.values - selected_model1 = self.model1.value[0] - selected_model2 = self.model2.value[0] - selected_model3 = self.model3.value[0] - merged_model_name = f"{models[selected_model1]}+{models[selected_model2]}" - self.merged_model_name.value = merged_model_name - - if selected_model3 > 0: - self.merge_method.values = ["add_difference ( A+(B-C) )"] - self.merged_model_name.value += f"+{models[selected_model3 -1]}" # In model3 there is one more element in the list (None). So we have to subtract one. - else: - self.merge_method.values = self.interpolations - self.merge_method.value = 0 - - def on_ok(self) -> None: - if self.validate_field_values() and self.check_for_overwrite(): - self.parentApp.setNextForm(None) - self.editing = False - self.parentApp.merge_arguments = self.marshall_arguments() - npyscreen.notify("Starting the merge...") - else: - self.editing = True - - def on_cancel(self) -> None: - sys.exit(0) - - def marshall_arguments(self) -> dict: - model_keys = [x[0] for x in self.models] - models = [ - model_keys[self.model1.value[0]], - model_keys[self.model2.value[0]], - ] - if self.model3.value[0] > 0: - models.append(model_keys[self.model3.value[0] - 1]) - interp = "add_difference" - else: - interp = self.interpolations[self.merge_method.value[0]] - - args = { - "model_keys": models, - "alpha": self.alpha.value, - "interp": interp, - "force": self.force.value, - "merged_model_name": self.merged_model_name.value, - } - return args - - def check_for_overwrite(self) -> bool: - model_out = self.merged_model_name.value - if model_out not in self.model_names: - return True - else: - result: bool = npyscreen.notify_yes_no( - f"The chosen merged model destination, {model_out}, is already in use. Overwrite?" - ) - return result - - def validate_field_values(self) -> bool: - bad_fields = [] - model_names = self.model_names - selected_models = {model_names[self.model1.value[0]], model_names[self.model2.value[0]]} - if self.model3.value[0] > 0: - selected_models.add(model_names[self.model3.value[0] - 1]) - if len(selected_models) < 2: - bad_fields.append(f"Please select two or three DIFFERENT models to compare. You selected {selected_models}") - if len(bad_fields) > 0: - message = "The following problems were detected and must be corrected:" - for problem in bad_fields: - message += f"\n* {problem}" - npyscreen.notify_confirm(message) - return False - else: - return True - - def get_models(self, base_model: Optional[BaseModelType] = None) -> List[Tuple[str, str]]: # key to name - models = [ - (x.key, x.name) - for x in self.model_record_store.search_by_attr(model_type=ModelType.Main, base_model=base_model) - if x.format == ModelFormat("diffusers") - and hasattr(x, "variant") - and x.variant == ModelVariantType("normal") - ] - return sorted(models, key=lambda x: x[1]) - - def _populate_models(self, value: List[int]) -> None: - base_model = BASE_TYPES[value[0]][0] - self.models = self.get_models(base_model) - self.model_names = [x[1] for x in self.models] - - models_plus_none = self.model_names.copy() - models_plus_none.insert(0, "None") - self.model1.values = self.model_names - self.model2.values = self.model_names - self.model3.values = models_plus_none - - self.display() - - -# npyscreen is untyped and causes mypy to get naggy -class Mergeapp(npyscreen.NPSAppManaged): # type: ignore - def __init__(self, installer: ModelInstallServiceBase): - """Initialize the npyscreen application.""" - super().__init__() - self.installer = installer - - def onStart(self) -> None: - npyscreen.setTheme(npyscreen.Themes.ElegantTheme) - self.main = self.addForm("MAIN", mergeModelsForm, name="Merge Models Settings") - - -def run_gui(args: Namespace) -> None: - installer = initialize_installer(config) - mergeapp = Mergeapp(installer) - mergeapp.run() - merge_args = mergeapp.merge_arguments - merger = ModelMerger(installer) - merger.merge_diffusion_models_and_save(**merge_args) - logger.info(f'Models merged into new model: "{merge_args.merged_model_name}".') - - -def run_cli(args: Namespace) -> None: - assert args.alpha >= 0 and args.alpha <= 1.0, "alpha must be between 0 and 1" - assert ( - args.model_names and len(args.model_names) >= 1 and len(args.model_names) <= 3 - ), "Please provide the --models argument to list 2 to 3 models to merge. Use --help for full usage." - - if not args.merged_model_name: - args.merged_model_name = "+".join(args.model_names) - logger.info(f'No --merged_model_name provided. Defaulting to "{args.merged_model_name}"') - - installer = initialize_installer(config) - store = installer.record_store - assert ( - len(store.search_by_attr(args.merged_model_name, args.base_model, ModelType.Main)) == 0 or args.clobber - ), f'A model named "{args.merged_model_name}" already exists. Use --clobber to overwrite.' - - merger = ModelMerger(installer) - model_keys = [] - for name in args.model_names: - if len(name) == 32 and re.match(r"^[0-9a-f]$", name): - model_keys.append(name) - else: - models = store.search_by_attr( - model_name=name, model_type=ModelType.Main, base_model=BaseModelType(args.base_model) - ) - assert len(models) > 0, f"{name}: Unknown model" - assert len(models) < 2, f"{name}: More than one model by this name. Please specify the model key instead." - model_keys.append(models[0].key) - - merger.merge_diffusion_models_and_save( - alpha=args.alpha, - model_keys=model_keys, - merged_model_name=args.merged_model_name, - interp=args.interp, - force=args.force, - ) - logger.info(f'Models merged into new model: "{args.merged_model_name}".') - - -def main() -> None: - args = _parse_args() - if args.root_dir: - config.parse_args(["--root", str(args.root_dir)]) - else: - config.parse_args([]) - - try: - if args.front_end: - run_gui(args) - else: - run_cli(args) - except widget.NotEnoughSpaceForWidget as e: - if str(e).startswith("Height of 1 allocated"): - logger.error("You need to have at least two diffusers models defined in models.yaml in order to merge") - else: - logger.error("Not enough room for the user interface. Try making this window larger.") - sys.exit(-1) - except Exception as e: - logger.error(str(e)) - sys.exit(-1) - except KeyboardInterrupt: - sys.exit(-1) - - -if __name__ == "__main__": - main() diff --git a/tests/app/services/model_install/test_model_install.py b/tests/app/services/model_install/test_model_install.py index 5694432ebd..55f7e86541 100644 --- a/tests/app/services/model_install/test_model_install.py +++ b/tests/app/services/model_install/test_model_install.py @@ -20,7 +20,7 @@ from invokeai.app.services.model_install import ( ) from invokeai.app.services.model_records import UnknownModelException from invokeai.backend.model_manager.config import BaseModelType, ModelFormat, ModelType -from tests.backend.model_manager_2.model_manager_2_fixtures import * # noqa F403 +from tests.backend.model_manager.model_manager_fixtures import * # noqa F403 OS = platform.uname().system diff --git a/tests/app/services/model_records/test_model_records_sql.py b/tests/app/services/model_records/test_model_records_sql.py index 852e1da979..57515ac81b 100644 --- a/tests/app/services/model_records/test_model_records_sql.py +++ b/tests/app/services/model_records/test_model_records_sql.py @@ -26,7 +26,7 @@ from invokeai.backend.model_manager.config import ( ) from invokeai.backend.model_manager.metadata import BaseMetadata from invokeai.backend.util.logging import InvokeAILogger -from tests.backend.model_manager_2.model_manager_2_fixtures import * # noqa F403 +from tests.backend.model_manager.model_manager_fixtures import * # noqa F403 from tests.fixtures.sqlite_database import create_mock_sqlite_database diff --git a/tests/backend/ip_adapter/test_ip_adapter.py b/tests/backend/ip_adapter/test_ip_adapter.py index 6a3ec510a2..9ed3c9bc50 100644 --- a/tests/backend/ip_adapter/test_ip_adapter.py +++ b/tests/backend/ip_adapter/test_ip_adapter.py @@ -2,7 +2,7 @@ import pytest import torch from invokeai.backend.ip_adapter.unet_patcher import UNetPatcher -from invokeai.backend.model_management.models.base import BaseModelType, ModelType, SubModelType +from invokeai.backend.model_manager import BaseModelType, ModelType, SubModelType from invokeai.backend.util.test_utils import install_and_load_model diff --git a/tests/backend/model_manager_2/data/invokeai_root/README b/tests/backend/model_manager/data/invokeai_root/README similarity index 100% rename from tests/backend/model_manager_2/data/invokeai_root/README rename to tests/backend/model_manager/data/invokeai_root/README diff --git a/tests/backend/model_manager_2/data/invokeai_root/configs/stable-diffusion/v1-inference.yaml b/tests/backend/model_manager/data/invokeai_root/configs/stable-diffusion/v1-inference.yaml similarity index 100% rename from tests/backend/model_manager_2/data/invokeai_root/configs/stable-diffusion/v1-inference.yaml rename to tests/backend/model_manager/data/invokeai_root/configs/stable-diffusion/v1-inference.yaml diff --git a/tests/backend/model_manager_2/data/invokeai_root/databases/README b/tests/backend/model_manager/data/invokeai_root/databases/README similarity index 100% rename from tests/backend/model_manager_2/data/invokeai_root/databases/README rename to tests/backend/model_manager/data/invokeai_root/databases/README diff --git a/tests/backend/model_manager_2/data/invokeai_root/models/README b/tests/backend/model_manager/data/invokeai_root/models/README similarity index 100% rename from tests/backend/model_manager_2/data/invokeai_root/models/README rename to tests/backend/model_manager/data/invokeai_root/models/README diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/model_index.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/model_index.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/model_index.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/model_index.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/scheduler/scheduler_config.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/scheduler/scheduler_config.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/scheduler/scheduler_config.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/scheduler/scheduler_config.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder/config.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder/config.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder/config.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder/config.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder/model.fp16.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder/model.fp16.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder/model.fp16.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder/model.fp16.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder/model.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder/model.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder/model.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder/model.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder_2/config.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder_2/config.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder_2/config.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder_2/config.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder_2/model.fp16.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder_2/model.fp16.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder_2/model.fp16.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder_2/model.fp16.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder_2/model.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder_2/model.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/text_encoder_2/model.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/text_encoder_2/model.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/merges.txt b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/merges.txt similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/merges.txt rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/merges.txt diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/special_tokens_map.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/special_tokens_map.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/special_tokens_map.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/special_tokens_map.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/tokenizer_config.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/tokenizer_config.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/tokenizer_config.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/tokenizer_config.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/vocab.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/vocab.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer/vocab.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer/vocab.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/merges.txt b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/merges.txt similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/merges.txt rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/merges.txt diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/special_tokens_map.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/special_tokens_map.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/special_tokens_map.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/special_tokens_map.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/tokenizer_config.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/tokenizer_config.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/tokenizer_config.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/tokenizer_config.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/vocab.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/vocab.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/tokenizer_2/vocab.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/tokenizer_2/vocab.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/unet/config.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/unet/config.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/unet/config.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/unet/config.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.fp16.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.fp16.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.fp16.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.fp16.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/unet/diffusion_pytorch_model.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/vae/config.json b/tests/backend/model_manager/data/test_files/test-diffusers-main/vae/config.json similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/vae/config.json rename to tests/backend/model_manager/data/test_files/test-diffusers-main/vae/config.json diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.fp16.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.fp16.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.fp16.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.fp16.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.safetensors b/tests/backend/model_manager/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.safetensors rename to tests/backend/model_manager/data/test_files/test-diffusers-main/vae/diffusion_pytorch_model.safetensors diff --git a/tests/backend/model_manager_2/data/test_files/test_embedding.safetensors b/tests/backend/model_manager/data/test_files/test_embedding.safetensors similarity index 100% rename from tests/backend/model_manager_2/data/test_files/test_embedding.safetensors rename to tests/backend/model_manager/data/test_files/test_embedding.safetensors diff --git a/tests/backend/model_manager_2/model_loading/test_model_load.py b/tests/backend/model_manager/model_loading/test_model_load.py similarity index 61% rename from tests/backend/model_manager_2/model_loading/test_model_load.py rename to tests/backend/model_manager/model_loading/test_model_load.py index a7a64e91ac..38d9b8afb8 100644 --- a/tests/backend/model_manager_2/model_loading/test_model_load.py +++ b/tests/backend/model_manager/model_loading/test_model_load.py @@ -5,17 +5,16 @@ Test model loading from pathlib import Path from invokeai.app.services.model_install import ModelInstallServiceBase -from invokeai.backend.embeddings.textual_inversion import TextualInversionModelRaw -from invokeai.backend.model_manager.load import AnyModelLoader -from tests.backend.model_manager_2.model_manager_2_fixtures import * # noqa F403 +from invokeai.app.services.model_load import ModelLoadServiceBase +from invokeai.backend.textual_inversion import TextualInversionModelRaw +from tests.backend.model_manager.model_manager_fixtures import * # noqa F403 - -def test_loading(mm2_installer: ModelInstallServiceBase, mm2_loader: AnyModelLoader, embedding_file: Path): +def test_loading(mm2_installer: ModelInstallServiceBase, mm2_loader: ModelLoadServiceBase, embedding_file: Path): store = mm2_installer.record_store matches = store.search_by_attr(model_name="test_embedding") assert len(matches) == 0 key = mm2_installer.register_path(embedding_file) - loaded_model = mm2_loader.load_model(store.get_model(key)) + loaded_model = mm2_loader.load_model_by_config(store.get_model(key)) assert loaded_model is not None assert loaded_model.config.key == key with loaded_model as model: diff --git a/tests/backend/model_manager_2/model_manager_2_fixtures.py b/tests/backend/model_manager/model_manager_fixtures.py similarity index 80% rename from tests/backend/model_manager_2/model_manager_2_fixtures.py rename to tests/backend/model_manager/model_manager_fixtures.py index ebdc9cb5cd..5f7f44c018 100644 --- a/tests/backend/model_manager_2/model_manager_2_fixtures.py +++ b/tests/backend/model_manager/model_manager_fixtures.py @@ -6,24 +6,27 @@ from pathlib import Path from typing import Any, Dict, List import pytest +from pytest import FixtureRequest from pydantic import BaseModel from requests.sessions import Session from requests_testadapter import TestAdapter, TestSession from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.download import DownloadQueueService +from invokeai.app.services.download import DownloadQueueServiceBase, DownloadQueueService from invokeai.app.services.events.events_base import EventServiceBase +from invokeai.app.services.model_manager import ModelManagerServiceBase, ModelManagerService +from invokeai.app.services.model_load import ModelLoadServiceBase, ModelLoadService from invokeai.app.services.model_install import ModelInstallService, ModelInstallServiceBase from invokeai.app.services.model_metadata import ModelMetadataStoreBase, ModelMetadataStoreSQL -from invokeai.app.services.model_records import ModelRecordServiceSQL +from invokeai.app.services.model_records import ModelRecordServiceBase, ModelRecordServiceSQL from invokeai.backend.model_manager.config import ( BaseModelType, ModelFormat, ModelType, ) -from invokeai.backend.model_manager.load import AnyModelLoader, ModelCache, ModelConvertCache +from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache from invokeai.backend.util.logging import InvokeAILogger -from tests.backend.model_manager_2.model_metadata.metadata_examples import ( +from tests.backend.model_manager.model_metadata.metadata_examples import ( RepoCivitaiModelMetadata1, RepoCivitaiVersionMetadata1, RepoHFMetadata1, @@ -86,22 +89,71 @@ def mm2_app_config(mm2_root_dir: Path) -> InvokeAIAppConfig: app_config = InvokeAIAppConfig( root=mm2_root_dir, models_dir=mm2_root_dir / "models", + log_level="info", ) return app_config @pytest.fixture -def mm2_loader(mm2_app_config: InvokeAIAppConfig, mm2_record_store: ModelRecordServiceSQL) -> AnyModelLoader: - logger = InvokeAILogger.get_logger(config=mm2_app_config) +def mm2_download_queue(mm2_session: Session, + request: FixtureRequest + ) -> DownloadQueueServiceBase: + download_queue = DownloadQueueService(requests_session=mm2_session) + download_queue.start() + + def stop_queue() -> None: + download_queue.stop() + + request.addfinalizer(stop_queue) + return download_queue + +@pytest.fixture +def mm2_metadata_store(mm2_record_store: ModelRecordServiceSQL) -> ModelMetadataStoreBase: + return mm2_record_store.metadata_store + +@pytest.fixture +def mm2_loader(mm2_app_config: InvokeAIAppConfig, mm2_record_store: ModelRecordServiceBase) -> ModelLoadServiceBase: ram_cache = ModelCache( - logger=logger, max_cache_size=mm2_app_config.ram_cache_size, max_vram_cache_size=mm2_app_config.vram_cache_size + logger=InvokeAILogger.get_logger(), + max_cache_size=mm2_app_config.ram_cache_size, + max_vram_cache_size=mm2_app_config.vram_cache_size ) convert_cache = ModelConvertCache(mm2_app_config.models_convert_cache_path) - return AnyModelLoader(app_config=mm2_app_config, logger=logger, ram_cache=ram_cache, convert_cache=convert_cache) + return ModelLoadService(app_config=mm2_app_config, + record_store=mm2_record_store, + ram_cache=ram_cache, + convert_cache=convert_cache, + ) + +@pytest.fixture +def mm2_installer(mm2_app_config: InvokeAIAppConfig, + mm2_download_queue: DownloadQueueServiceBase, + mm2_session: Session, + request: FixtureRequest, + ) -> ModelInstallServiceBase: + logger = InvokeAILogger.get_logger() + db = create_mock_sqlite_database(mm2_app_config, logger) + events = DummyEventService() + store = ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) + + installer = ModelInstallService( + app_config=mm2_app_config, + record_store=store, + download_queue=mm2_download_queue, + event_bus=events, + session=mm2_session, + ) + installer.start() + + def stop_installer() -> None: + installer.stop() + + request.addfinalizer(stop_installer) + return installer @pytest.fixture -def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceSQL: +def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceBase: logger = InvokeAILogger.get_logger(config=mm2_app_config) db = create_mock_sqlite_database(mm2_app_config, logger) store = ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) @@ -161,11 +213,15 @@ def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceSQL store.add_model("test_config_5", raw5) return store - @pytest.fixture -def mm2_metadata_store(mm2_record_store: ModelRecordServiceSQL) -> ModelMetadataStoreBase: - return mm2_record_store.metadata_store - +def mm2_model_manager(mm2_record_store: ModelRecordServiceBase, + mm2_installer: ModelInstallServiceBase, + mm2_loader: ModelLoadServiceBase) -> ModelManagerServiceBase: + return ModelManagerService( + store=mm2_record_store, + install=mm2_installer, + load=mm2_loader + ) @pytest.fixture def mm2_session(embedding_file: Path, diffusers_dir: Path) -> Session: @@ -252,22 +308,3 @@ def mm2_session(embedding_file: Path, diffusers_dir: Path) -> Session: return sess -@pytest.fixture -def mm2_installer(mm2_app_config: InvokeAIAppConfig, mm2_session: Session) -> ModelInstallServiceBase: - logger = InvokeAILogger.get_logger() - db = create_mock_sqlite_database(mm2_app_config, logger) - events = DummyEventService() - store = ModelRecordServiceSQL(db, ModelMetadataStoreSQL(db)) - - download_queue = DownloadQueueService(requests_session=mm2_session) - download_queue.start() - - installer = ModelInstallService( - app_config=mm2_app_config, - record_store=store, - download_queue=download_queue, - event_bus=events, - session=mm2_session, - ) - installer.start() - return installer diff --git a/tests/backend/model_manager_2/model_metadata/metadata_examples.py b/tests/backend/model_manager/model_metadata/metadata_examples.py similarity index 100% rename from tests/backend/model_manager_2/model_metadata/metadata_examples.py rename to tests/backend/model_manager/model_metadata/metadata_examples.py diff --git a/tests/backend/model_manager_2/model_metadata/test_model_metadata.py b/tests/backend/model_manager/model_metadata/test_model_metadata.py similarity index 99% rename from tests/backend/model_manager_2/model_metadata/test_model_metadata.py rename to tests/backend/model_manager/model_metadata/test_model_metadata.py index f61eab1b5d..09b18916d3 100644 --- a/tests/backend/model_manager_2/model_metadata/test_model_metadata.py +++ b/tests/backend/model_manager/model_metadata/test_model_metadata.py @@ -19,7 +19,7 @@ from invokeai.backend.model_manager.metadata import ( UnknownMetadataException, ) from invokeai.backend.model_manager.util import select_hf_files -from tests.backend.model_manager_2.model_manager_2_fixtures import * # noqa F403 +from tests.backend.model_manager.model_manager_fixtures import * # noqa F403 def test_metadata_store_put_get(mm2_metadata_store: ModelMetadataStoreBase) -> None: diff --git a/tests/backend/model_management/test_libc_util.py b/tests/backend/model_manager/test_libc_util.py similarity index 88% rename from tests/backend/model_management/test_libc_util.py rename to tests/backend/model_manager/test_libc_util.py index e13a2fd3a2..4309dc7c34 100644 --- a/tests/backend/model_management/test_libc_util.py +++ b/tests/backend/model_manager/test_libc_util.py @@ -1,6 +1,6 @@ import pytest -from invokeai.backend.model_management.libc_util import LibcUtil, Struct_mallinfo2 +from invokeai.backend.model_manager.util.libc_util import LibcUtil, Struct_mallinfo2 def test_libc_util_mallinfo2(): diff --git a/tests/backend/model_management/test_lora.py b/tests/backend/model_manager/test_lora.py similarity index 96% rename from tests/backend/model_management/test_lora.py rename to tests/backend/model_manager/test_lora.py index 14bcc87c89..e124bb68ef 100644 --- a/tests/backend/model_management/test_lora.py +++ b/tests/backend/model_manager/test_lora.py @@ -5,8 +5,8 @@ import pytest import torch -from invokeai.backend.model_management.lora import ModelPatcher -from invokeai.backend.model_management.models.lora import LoRALayer, LoRAModelRaw +from invokeai.backend.model_patcher import ModelPatcher +from invokeai.backend.lora import LoRALayer, LoRAModelRaw @pytest.mark.parametrize( diff --git a/tests/backend/model_management/test_memory_snapshot.py b/tests/backend/model_manager/test_memory_snapshot.py similarity index 87% rename from tests/backend/model_management/test_memory_snapshot.py rename to tests/backend/model_manager/test_memory_snapshot.py index 216cd62171..87ec8c34ee 100644 --- a/tests/backend/model_management/test_memory_snapshot.py +++ b/tests/backend/model_manager/test_memory_snapshot.py @@ -1,8 +1,7 @@ import pytest -from invokeai.backend.model_management.libc_util import Struct_mallinfo2 -from invokeai.backend.model_management.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff - +from invokeai.backend.model_manager.util.libc_util import Struct_mallinfo2 +from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff def test_memory_snapshot_capture(): """Smoke test of MemorySnapshot.capture().""" @@ -26,6 +25,7 @@ snapshots = [ def test_get_pretty_snapshot_diff(snapshot_1, snapshot_2): """Test that get_pretty_snapshot_diff() works with various combinations of missing MemorySnapshot fields.""" msg = get_pretty_snapshot_diff(snapshot_1, snapshot_2) + print(msg) expected_lines = 0 if snapshot_1 is not None and snapshot_2 is not None: diff --git a/tests/backend/model_management/test_model_load_optimization.py b/tests/backend/model_manager/test_model_load_optimization.py similarity index 96% rename from tests/backend/model_management/test_model_load_optimization.py rename to tests/backend/model_manager/test_model_load_optimization.py index a4fe1dd597..f627f3a298 100644 --- a/tests/backend/model_management/test_model_load_optimization.py +++ b/tests/backend/model_manager/test_model_load_optimization.py @@ -1,7 +1,7 @@ import pytest import torch -from invokeai.backend.model_management.model_load_optimizations import _no_op, skip_torch_weight_init +from invokeai.backend.model_manager.load.optimizations import _no_op, skip_torch_weight_init @pytest.mark.parametrize( diff --git a/tests/backend/model_manager_2/util/test_hf_model_select.py b/tests/backend/model_manager/util/test_hf_model_select.py similarity index 100% rename from tests/backend/model_manager_2/util/test_hf_model_select.py rename to tests/backend/model_manager/util/test_hf_model_select.py diff --git a/tests/conftest.py b/tests/conftest.py index 6e7d559be4..1c81600229 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,2 @@ # conftest.py is a special pytest file. Fixtures defined in this file will be accessible to all tests in this directory # without needing to explicitly import them. (https://docs.pytest.org/en/6.2.x/fixture.html) - - -# We import the model_installer and torch_device fixtures here so that they can be used by all tests. Flake8 does not -# play well with fixtures (F401 and F811), so this is cleaner than importing in all files that use these fixtures. -from invokeai.backend.util.test_utils import model_installer, torch_device # noqa: F401 From 5a3195f7578a1b4eaf053444f8d6d513e966048f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 17:27:42 +1100 Subject: [PATCH 146/411] final tidying before marking PR as ready for review - Replace AnyModelLoader with ModelLoaderRegistry - Fix type check errors in multiple files - Remove apparently unneeded `get_model_config_enum()` method from model manager - Remove last vestiges of old model manager - Updated tests and documentation resolve conflict with seamless.py --- docs/contributing/MODEL_MANAGER.md | 129 +- .../{model_manager_v2.py => model_manager.py} | 38 +- invokeai/app/api/routers/models.py | 426 ---- invokeai/app/api_app.py | 32 +- invokeai/app/invocations/compel.py | 4 +- invokeai/app/services/config/config_base.py | 2 +- .../invocation_stats/invocation_stats_base.py | 11 +- .../model_install/model_install_base.py | 2 +- .../services/model_load/model_load_base.py | 48 +- .../services/model_load/model_load_default.py | 100 +- .../app/services/model_manager/__init__.py | 2 +- .../model_manager/model_manager_base.py | 33 + .../model_manager/model_manager_default.py | 60 +- .../app/services/shared/invocation_context.py | 4 +- invokeai/backend/install/migrate_to_3.py | 591 ------ .../backend/install/model_install_backend.py | 637 ------ invokeai/backend/ip_adapter/ip_adapter.py | 2 +- invokeai/backend/lora.py | 2 + .../backend/model_management_OLD/README.md | 27 - .../backend/model_management_OLD/__init__.py | 20 - .../convert_ckpt_to_diffusers.py | 1739 ----------------- .../detect_baked_in_vae.py | 31 - invokeai/backend/model_management_OLD/lora.py | 582 ------ .../model_management_OLD/memory_snapshot.py | 99 - .../model_management_OLD/model_cache.py | 553 ------ .../model_load_optimizations.py | 30 - .../model_management_OLD/model_manager.py | 1121 ----------- .../model_management_OLD/model_merge.py | 140 -- .../model_management_OLD/model_probe.py | 664 ------- .../model_management_OLD/model_search.py | 112 -- .../model_management_OLD/models/__init__.py | 167 -- .../model_management_OLD/models/base.py | 681 ------- .../models/clip_vision.py | 82 - .../model_management_OLD/models/controlnet.py | 162 -- .../model_management_OLD/models/ip_adapter.py | 98 - .../model_management_OLD/models/lora.py | 696 ------- .../model_management_OLD/models/sdxl.py | 148 -- .../models/stable_diffusion.py | 337 ---- .../models/stable_diffusion_onnx.py | 150 -- .../models/t2i_adapter.py | 102 - .../models/textual_inversion.py | 87 - .../model_management_OLD/models/vae.py | 179 -- .../backend/model_management_OLD/seamless.py | 84 - invokeai/backend/model_management_OLD/util.py | 79 - invokeai/backend/model_manager/__init__.py | 40 +- .../backend/model_manager/load/__init__.py | 14 +- .../backend/model_manager/load/load_base.py | 130 +- .../model_manager/load/load_default.py | 54 +- .../model_manager/load/memory_snapshot.py | 2 +- .../load/model_loader_registry.py | 122 ++ .../load/model_loaders/controlnet.py | 6 +- .../load/model_loaders/generic_diffusers.py | 66 +- .../load/model_loaders/ip_adapter.py | 5 +- .../model_manager/load/model_loaders/lora.py | 8 +- .../model_manager/load/model_loaders/onnx.py | 13 +- .../load/model_loaders/stable_diffusion.py | 13 +- .../load/model_loaders/textual_inversion.py | 12 +- .../model_manager/load/model_loaders/vae.py | 8 +- .../model_manager/load/optimizations.py | 13 +- invokeai/backend/model_manager/merge.py | 4 +- .../model_manager/metadata/metadata_base.py | 9 +- invokeai/backend/model_manager/probe.py | 3 +- invokeai/backend/model_manager/search.py | 6 +- .../backend/model_manager/util/libc_util.py | 7 +- .../backend/model_manager/util/model_util.py | 20 +- invokeai/backend/onnx/onnx_runtime.py | 3 +- invokeai/backend/raw_model.py | 1 + invokeai/backend/stable_diffusion/seamless.py | 96 +- invokeai/backend/textual_inversion.py | 2 + invokeai/backend/util/test_utils.py | 4 +- .../model_loading/test_model_load.py | 21 +- .../model_manager/model_manager_fixtures.py | 54 +- tests/backend/model_manager/test_lora.py | 2 +- .../model_manager/test_memory_snapshot.py | 3 +- 74 files changed, 672 insertions(+), 10362 deletions(-) rename invokeai/app/api/routers/{model_manager_v2.py => model_manager.py} (97%) delete mode 100644 invokeai/app/api/routers/models.py delete mode 100644 invokeai/backend/install/migrate_to_3.py delete mode 100644 invokeai/backend/install/model_install_backend.py delete mode 100644 invokeai/backend/model_management_OLD/README.md delete mode 100644 invokeai/backend/model_management_OLD/__init__.py delete mode 100644 invokeai/backend/model_management_OLD/convert_ckpt_to_diffusers.py delete mode 100644 invokeai/backend/model_management_OLD/detect_baked_in_vae.py delete mode 100644 invokeai/backend/model_management_OLD/lora.py delete mode 100644 invokeai/backend/model_management_OLD/memory_snapshot.py delete mode 100644 invokeai/backend/model_management_OLD/model_cache.py delete mode 100644 invokeai/backend/model_management_OLD/model_load_optimizations.py delete mode 100644 invokeai/backend/model_management_OLD/model_manager.py delete mode 100644 invokeai/backend/model_management_OLD/model_merge.py delete mode 100644 invokeai/backend/model_management_OLD/model_probe.py delete mode 100644 invokeai/backend/model_management_OLD/model_search.py delete mode 100644 invokeai/backend/model_management_OLD/models/__init__.py delete mode 100644 invokeai/backend/model_management_OLD/models/base.py delete mode 100644 invokeai/backend/model_management_OLD/models/clip_vision.py delete mode 100644 invokeai/backend/model_management_OLD/models/controlnet.py delete mode 100644 invokeai/backend/model_management_OLD/models/ip_adapter.py delete mode 100644 invokeai/backend/model_management_OLD/models/lora.py delete mode 100644 invokeai/backend/model_management_OLD/models/sdxl.py delete mode 100644 invokeai/backend/model_management_OLD/models/stable_diffusion.py delete mode 100644 invokeai/backend/model_management_OLD/models/stable_diffusion_onnx.py delete mode 100644 invokeai/backend/model_management_OLD/models/t2i_adapter.py delete mode 100644 invokeai/backend/model_management_OLD/models/textual_inversion.py delete mode 100644 invokeai/backend/model_management_OLD/models/vae.py delete mode 100644 invokeai/backend/model_management_OLD/seamless.py delete mode 100644 invokeai/backend/model_management_OLD/util.py create mode 100644 invokeai/backend/model_manager/load/model_loader_registry.py diff --git a/docs/contributing/MODEL_MANAGER.md b/docs/contributing/MODEL_MANAGER.md index b19699de73..8351904b61 100644 --- a/docs/contributing/MODEL_MANAGER.md +++ b/docs/contributing/MODEL_MANAGER.md @@ -1531,23 +1531,29 @@ Here is a typical initialization pattern: ``` from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.model_records import ModelRecordServiceBase -from invokeai.app.services.model_load import ModelLoadService +from invokeai.app.services.model_load import ModelLoadService, ModelLoaderRegistry config = InvokeAIAppConfig.get_config() -store = ModelRecordServiceBase.open(config) -loader = ModelLoadService(config, store) +ram_cache = ModelCache( + max_cache_size=config.ram_cache_size, max_vram_cache_size=config.vram_cache_size, logger=logger +) +convert_cache = ModelConvertCache( + cache_path=config.models_convert_cache_path, max_size=config.convert_cache_size +) +loader = ModelLoadService( + app_config=config, + ram_cache=ram_cache, + convert_cache=convert_cache, + registry=ModelLoaderRegistry +) ``` -Note that we are relying on the contents of the application -configuration to choose the implementation of -`ModelRecordServiceBase`. +### load_model(model_config, [submodel_type], [context]) -> LoadedModel -### load_model_by_key(key, [submodel_type], [context]) -> LoadedModel - -The `load_model_by_key()` method receives the unique key that -identifies the model. It loads the model into memory, gets the model -ready for use, and returns a `LoadedModel` object. +The `load_model()` method takes an `AnyModelConfig` returned by +`ModelRecordService.get_model()` and returns the corresponding loaded +model. It loads the model into memory, gets the model ready for use, +and returns a `LoadedModel` object. The optional second argument, `subtype` is a `SubModelType` string enum, such as "vae". It is mandatory when used with a main model, and @@ -1593,25 +1599,6 @@ with model_info as vae: - `ModelNotFoundException` -- key in database but model not found at path - `NotImplementedException` -- the loader doesn't know how to load this type of model -### load_model_by_attr(model_name, base_model, model_type, [submodel], [context]) -> LoadedModel - -This is similar to `load_model_by_key`, but instead it accepts the -combination of the model's name, type and base, which it passes to the -model record config store for retrieval. If successful, this method -returns a `LoadedModel`. It can raise the following exceptions: - -``` -UnknownModelException -- model with these attributes not known -NotImplementedException -- the loader doesn't know how to load this type of model -ValueError -- more than one model matches this combination of base/type/name -``` - -### load_model_by_config(config, [submodel], [context]) -> LoadedModel - -This method takes an `AnyModelConfig` returned by -ModelRecordService.get_model() and returns the corresponding loaded -model. It may raise a `NotImplementedException`. - ### Emitting model loading events When the `context` argument is passed to `load_model_*()`, it will @@ -1656,7 +1643,7 @@ onnx models. To install a new loader, place it in `invokeai/backend/model_manager/load/model_loaders`. Inherit from -`ModelLoader` and use the `@AnyModelLoader.register()` decorator to +`ModelLoader` and use the `@ModelLoaderRegistry.register()` decorator to indicate what type of models the loader can handle. Here is a complete example from `generic_diffusers.py`, which is able @@ -1674,12 +1661,11 @@ from invokeai.backend.model_manager import ( ModelType, SubModelType, ) -from ..load_base import AnyModelLoader -from ..load_default import ModelLoader +from .. import ModelLoader, ModelLoaderRegistry -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) class GenericDiffusersLoader(ModelLoader): """Class to load simple diffusers models.""" @@ -1728,3 +1714,74 @@ model. It does whatever it needs to do to get the model into diffusers format, and returns the Path of the resulting model. (The path should ordinarily be the same as `output_path`.) +## The ModelManagerService object + +For convenience, the API provides a `ModelManagerService` object which +gives a single point of access to the major model manager +services. This object is created at initialization time and can be +found in the global `ApiDependencies.invoker.services.model_manager` +object, or in `context.services.model_manager` from within an +invocation. + +In the examples below, we have retrieved the manager using: +``` +mm = ApiDependencies.invoker.services.model_manager +``` + +The following properties and methods will be available: + +### mm.store + +This retrieves the `ModelRecordService` associated with the +manager. Example: + +``` +configs = mm.store.get_model_by_attr(name='stable-diffusion-v1-5') +``` + +### mm.install + +This retrieves the `ModelInstallService` associated with the manager. +Example: + +``` +job = mm.install.heuristic_import(`https://civitai.com/models/58390/detail-tweaker-lora-lora`) +``` + +### mm.load + +This retrieves the `ModelLoaderService` associated with the manager. Example: + +``` +configs = mm.store.get_model_by_attr(name='stable-diffusion-v1-5') +assert len(configs) > 0 + +loaded_model = mm.load.load_model(configs[0]) +``` + +The model manager also offers a few convenience shortcuts for loading +models: + +### mm.load_model_by_config(model_config, [submodel], [context]) -> LoadedModel + +Same as `mm.load.load_model()`. + +### mm.load_model_by_attr(model_name, base_model, model_type, [submodel], [context]) -> LoadedModel + +This accepts the combination of the model's name, type and base, which +it passes to the model record config store for retrieval. If a unique +model config is found, this method returns a `LoadedModel`. It can +raise the following exceptions: + +``` +UnknownModelException -- model with these attributes not known +NotImplementedException -- the loader doesn't know how to load this type of model +ValueError -- more than one model matches this combination of base/type/name +``` + +### mm.load_model_by_key(key, [submodel], [context]) -> LoadedModel + +This method takes a model key, looks it up using the +`ModelRecordServiceBase` object in `mm.store`, and passes the returned +model configuration to `load_model_by_config()`. It may raise a +`NotImplementedException`. diff --git a/invokeai/app/api/routers/model_manager_v2.py b/invokeai/app/api/routers/model_manager.py similarity index 97% rename from invokeai/app/api/routers/model_manager_v2.py rename to invokeai/app/api/routers/model_manager.py index 2471e0d8c9..6b7111dd2c 100644 --- a/invokeai/app/api/routers/model_manager_v2.py +++ b/invokeai/app/api/routers/model_manager.py @@ -35,7 +35,7 @@ from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata from ..dependencies import ApiDependencies -model_manager_v2_router = APIRouter(prefix="/v2/models", tags=["model_manager_v2"]) +model_manager_router = APIRouter(prefix="/v2/models", tags=["model_manager"]) class ModelsList(BaseModel): @@ -135,7 +135,7 @@ example_model_metadata = { ############################################################################## -@model_manager_v2_router.get( +@model_manager_router.get( "/", operation_id="list_model_records", ) @@ -164,7 +164,7 @@ async def list_model_records( return ModelsList(models=found_models) -@model_manager_v2_router.get( +@model_manager_router.get( "/i/{key}", operation_id="get_model_record", responses={ @@ -188,7 +188,7 @@ async def get_model_record( raise HTTPException(status_code=404, detail=str(e)) -@model_manager_v2_router.get("/summary", operation_id="list_model_summary") +@model_manager_router.get("/summary", operation_id="list_model_summary") async def list_model_summary( page: int = Query(default=0, description="The page to get"), per_page: int = Query(default=10, description="The number of models per page"), @@ -200,7 +200,7 @@ async def list_model_summary( return results -@model_manager_v2_router.get( +@model_manager_router.get( "/meta/i/{key}", operation_id="get_model_metadata", responses={ @@ -223,7 +223,7 @@ async def get_model_metadata( return result -@model_manager_v2_router.get( +@model_manager_router.get( "/tags", operation_id="list_tags", ) @@ -234,7 +234,7 @@ async def list_tags() -> Set[str]: return result -@model_manager_v2_router.get( +@model_manager_router.get( "/tags/search", operation_id="search_by_metadata_tags", ) @@ -247,7 +247,7 @@ async def search_by_metadata_tags( return ModelsList(models=results) -@model_manager_v2_router.patch( +@model_manager_router.patch( "/i/{key}", operation_id="update_model_record", responses={ @@ -281,7 +281,7 @@ async def update_model_record( return model_response -@model_manager_v2_router.delete( +@model_manager_router.delete( "/i/{key}", operation_id="del_model_record", responses={ @@ -311,7 +311,7 @@ async def del_model_record( raise HTTPException(status_code=404, detail=str(e)) -@model_manager_v2_router.post( +@model_manager_router.post( "/i/", operation_id="add_model_record", responses={ @@ -349,7 +349,7 @@ async def add_model_record( return result -@model_manager_v2_router.post( +@model_manager_router.post( "/heuristic_import", operation_id="heuristic_import_model", responses={ @@ -416,7 +416,7 @@ async def heuristic_import( return result -@model_manager_v2_router.post( +@model_manager_router.post( "/install", operation_id="import_model", responses={ @@ -516,7 +516,7 @@ async def import_model( return result -@model_manager_v2_router.get( +@model_manager_router.get( "/import", operation_id="list_model_install_jobs", ) @@ -544,7 +544,7 @@ async def list_model_install_jobs() -> List[ModelInstallJob]: return jobs -@model_manager_v2_router.get( +@model_manager_router.get( "/import/{id}", operation_id="get_model_install_job", responses={ @@ -564,7 +564,7 @@ async def get_model_install_job(id: int = Path(description="Model install id")) raise HTTPException(status_code=404, detail=str(e)) -@model_manager_v2_router.delete( +@model_manager_router.delete( "/import/{id}", operation_id="cancel_model_install_job", responses={ @@ -583,7 +583,7 @@ async def cancel_model_install_job(id: int = Path(description="Model install job installer.cancel_job(job) -@model_manager_v2_router.patch( +@model_manager_router.patch( "/import", operation_id="prune_model_install_jobs", responses={ @@ -597,7 +597,7 @@ async def prune_model_install_jobs() -> Response: return Response(status_code=204) -@model_manager_v2_router.patch( +@model_manager_router.patch( "/sync", operation_id="sync_models_to_config", responses={ @@ -616,7 +616,7 @@ async def sync_models_to_config() -> Response: return Response(status_code=204) -@model_manager_v2_router.put( +@model_manager_router.put( "/convert/{key}", operation_id="convert_model", responses={ @@ -694,7 +694,7 @@ async def convert_model( return new_config -@model_manager_v2_router.put( +@model_manager_router.put( "/merge", operation_id="merge", responses={ diff --git a/invokeai/app/api/routers/models.py b/invokeai/app/api/routers/models.py deleted file mode 100644 index 0aa7aa0ecb..0000000000 --- a/invokeai/app/api/routers/models.py +++ /dev/null @@ -1,426 +0,0 @@ -# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654), 2023 Kent Keirsey (https://github.com/hipsterusername), 2023 Lincoln D. Stein - -import pathlib -from typing import Annotated, List, Literal, Optional, Union - -from fastapi import Body, Path, Query, Response -from fastapi.routing import APIRouter -from pydantic import BaseModel, ConfigDict, Field, TypeAdapter -from starlette.exceptions import HTTPException - -from invokeai.backend.model_management import BaseModelType, MergeInterpolationMethod, ModelType -from invokeai.backend.model_management.models import ( - OPENAPI_MODEL_CONFIGS, - InvalidModelException, - ModelNotFoundException, - SchedulerPredictionType, -) - -from ..dependencies import ApiDependencies - -models_router = APIRouter(prefix="/v1/models", tags=["models"]) - -UpdateModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] -UpdateModelResponseValidator = TypeAdapter(UpdateModelResponse) - -ImportModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] -ImportModelResponseValidator = TypeAdapter(ImportModelResponse) - -ConvertModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] -ConvertModelResponseValidator = TypeAdapter(ConvertModelResponse) - -MergeModelResponse = Union[tuple(OPENAPI_MODEL_CONFIGS)] -ImportModelAttributes = Union[tuple(OPENAPI_MODEL_CONFIGS)] - - -class ModelsList(BaseModel): - models: list[Union[tuple(OPENAPI_MODEL_CONFIGS)]] - - model_config = ConfigDict(use_enum_values=True) - - -ModelsListValidator = TypeAdapter(ModelsList) - - -@models_router.get( - "/", - operation_id="list_models", - responses={200: {"model": ModelsList}}, -) -async def list_models( - base_models: Optional[List[BaseModelType]] = Query(default=None, description="Base models to include"), - model_type: Optional[ModelType] = Query(default=None, description="The type of model to get"), -) -> ModelsList: - """Gets a list of models""" - if base_models and len(base_models) > 0: - models_raw = [] - for base_model in base_models: - models_raw.extend(ApiDependencies.invoker.services.model_manager.list_models(base_model, model_type)) - else: - models_raw = ApiDependencies.invoker.services.model_manager.list_models(None, model_type) - models = ModelsListValidator.validate_python({"models": models_raw}) - return models - - -@models_router.patch( - "/{base_model}/{model_type}/{model_name}", - operation_id="update_model", - responses={ - 200: {"description": "The model was updated successfully"}, - 400: {"description": "Bad request"}, - 404: {"description": "The model could not be found"}, - 409: {"description": "There is already a model corresponding to the new name"}, - }, - status_code=200, - response_model=UpdateModelResponse, -) -async def update_model( - base_model: BaseModelType = Path(description="Base model"), - model_type: ModelType = Path(description="The type of model"), - model_name: str = Path(description="model name"), - info: Union[tuple(OPENAPI_MODEL_CONFIGS)] = Body(description="Model configuration"), -) -> UpdateModelResponse: - """Update model contents with a new config. If the model name or base fields are changed, then the model is renamed.""" - logger = ApiDependencies.invoker.services.logger - - try: - previous_info = ApiDependencies.invoker.services.model_manager.list_model( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ) - - # rename operation requested - if info.model_name != model_name or info.base_model != base_model: - ApiDependencies.invoker.services.model_manager.rename_model( - base_model=base_model, - model_type=model_type, - model_name=model_name, - new_name=info.model_name, - new_base=info.base_model, - ) - logger.info(f"Successfully renamed {base_model.value}/{model_name}=>{info.base_model}/{info.model_name}") - # update information to support an update of attributes - model_name = info.model_name - base_model = info.base_model - new_info = ApiDependencies.invoker.services.model_manager.list_model( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ) - if new_info.get("path") != previous_info.get( - "path" - ): # model manager moved model path during rename - don't overwrite it - info.path = new_info.get("path") - - # replace empty string values with None/null to avoid phenomenon of vae: '' - info_dict = info.model_dump() - info_dict = {x: info_dict[x] if info_dict[x] else None for x in info_dict.keys()} - - ApiDependencies.invoker.services.model_manager.update_model( - model_name=model_name, - base_model=base_model, - model_type=model_type, - model_attributes=info_dict, - ) - - model_raw = ApiDependencies.invoker.services.model_manager.list_model( - model_name=model_name, - base_model=base_model, - model_type=model_type, - ) - model_response = UpdateModelResponseValidator.validate_python(model_raw) - except ModelNotFoundException as e: - raise HTTPException(status_code=404, detail=str(e)) - except ValueError as e: - logger.error(str(e)) - raise HTTPException(status_code=409, detail=str(e)) - except Exception as e: - logger.error(str(e)) - raise HTTPException(status_code=400, detail=str(e)) - - return model_response - - -@models_router.post( - "/import", - operation_id="import_model", - responses={ - 201: {"description": "The model imported successfully"}, - 404: {"description": "The model could not be found"}, - 415: {"description": "Unrecognized file/folder format"}, - 424: {"description": "The model appeared to import successfully, but could not be found in the model manager"}, - 409: {"description": "There is already a model corresponding to this path or repo_id"}, - }, - status_code=201, - response_model=ImportModelResponse, -) -async def import_model( - location: str = Body(description="A model path, repo_id or URL to import"), - prediction_type: Optional[Literal["v_prediction", "epsilon", "sample"]] = Body( - description="Prediction type for SDv2 checkpoints and rare SDv1 checkpoints", - default=None, - ), -) -> ImportModelResponse: - """Add a model using its local path, repo_id, or remote URL. Model characteristics will be probed and configured automatically""" - - location = location.strip("\"' ") - items_to_import = {location} - prediction_types = {x.value: x for x in SchedulerPredictionType} - logger = ApiDependencies.invoker.services.logger - - try: - installed_models = ApiDependencies.invoker.services.model_manager.heuristic_import( - items_to_import=items_to_import, - prediction_type_helper=lambda x: prediction_types.get(prediction_type), - ) - info = installed_models.get(location) - - if not info: - logger.error("Import failed") - raise HTTPException(status_code=415) - - logger.info(f"Successfully imported {location}, got {info}") - model_raw = ApiDependencies.invoker.services.model_manager.list_model( - model_name=info.name, base_model=info.base_model, model_type=info.model_type - ) - return ImportModelResponseValidator.validate_python(model_raw) - - except ModelNotFoundException as e: - logger.error(str(e)) - raise HTTPException(status_code=404, detail=str(e)) - except InvalidModelException as e: - logger.error(str(e)) - raise HTTPException(status_code=415) - except ValueError as e: - logger.error(str(e)) - raise HTTPException(status_code=409, detail=str(e)) - - -@models_router.post( - "/add", - operation_id="add_model", - responses={ - 201: {"description": "The model added successfully"}, - 404: {"description": "The model could not be found"}, - 424: {"description": "The model appeared to add successfully, but could not be found in the model manager"}, - 409: {"description": "There is already a model corresponding to this path or repo_id"}, - }, - status_code=201, - response_model=ImportModelResponse, -) -async def add_model( - info: Union[tuple(OPENAPI_MODEL_CONFIGS)] = Body(description="Model configuration"), -) -> ImportModelResponse: - """Add a model using the configuration information appropriate for its type. Only local models can be added by path""" - - logger = ApiDependencies.invoker.services.logger - - try: - ApiDependencies.invoker.services.model_manager.add_model( - info.model_name, - info.base_model, - info.model_type, - model_attributes=info.model_dump(), - ) - logger.info(f"Successfully added {info.model_name}") - model_raw = ApiDependencies.invoker.services.model_manager.list_model( - model_name=info.model_name, - base_model=info.base_model, - model_type=info.model_type, - ) - return ImportModelResponseValidator.validate_python(model_raw) - except ModelNotFoundException as e: - logger.error(str(e)) - raise HTTPException(status_code=404, detail=str(e)) - except ValueError as e: - logger.error(str(e)) - raise HTTPException(status_code=409, detail=str(e)) - - -@models_router.delete( - "/{base_model}/{model_type}/{model_name}", - operation_id="del_model", - responses={ - 204: {"description": "Model deleted successfully"}, - 404: {"description": "Model not found"}, - }, - status_code=204, - response_model=None, -) -async def delete_model( - base_model: BaseModelType = Path(description="Base model"), - model_type: ModelType = Path(description="The type of model"), - model_name: str = Path(description="model name"), -) -> Response: - """Delete Model""" - logger = ApiDependencies.invoker.services.logger - - try: - ApiDependencies.invoker.services.model_manager.del_model( - model_name, base_model=base_model, model_type=model_type - ) - logger.info(f"Deleted model: {model_name}") - return Response(status_code=204) - except ModelNotFoundException as e: - logger.error(str(e)) - raise HTTPException(status_code=404, detail=str(e)) - - -@models_router.put( - "/convert/{base_model}/{model_type}/{model_name}", - operation_id="convert_model", - responses={ - 200: {"description": "Model converted successfully"}, - 400: {"description": "Bad request"}, - 404: {"description": "Model not found"}, - }, - status_code=200, - response_model=ConvertModelResponse, -) -async def convert_model( - base_model: BaseModelType = Path(description="Base model"), - model_type: ModelType = Path(description="The type of model"), - model_name: str = Path(description="model name"), - convert_dest_directory: Optional[str] = Query( - default=None, description="Save the converted model to the designated directory" - ), -) -> ConvertModelResponse: - """Convert a checkpoint model into a diffusers model, optionally saving to the indicated destination directory, or `models` if none.""" - logger = ApiDependencies.invoker.services.logger - try: - logger.info(f"Converting model: {model_name}") - dest = pathlib.Path(convert_dest_directory) if convert_dest_directory else None - ApiDependencies.invoker.services.model_manager.convert_model( - model_name, - base_model=base_model, - model_type=model_type, - convert_dest_directory=dest, - ) - model_raw = ApiDependencies.invoker.services.model_manager.list_model( - model_name, base_model=base_model, model_type=model_type - ) - response = ConvertModelResponseValidator.validate_python(model_raw) - except ModelNotFoundException as e: - raise HTTPException(status_code=404, detail=f"Model '{model_name}' not found: {str(e)}") - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - return response - - -@models_router.get( - "/search", - operation_id="search_for_models", - responses={ - 200: {"description": "Directory searched successfully"}, - 404: {"description": "Invalid directory path"}, - }, - status_code=200, - response_model=List[pathlib.Path], -) -async def search_for_models( - search_path: pathlib.Path = Query(description="Directory path to search for models"), -) -> List[pathlib.Path]: - if not search_path.is_dir(): - raise HTTPException( - status_code=404, - detail=f"The search path '{search_path}' does not exist or is not directory", - ) - return ApiDependencies.invoker.services.model_manager.search_for_models(search_path) - - -@models_router.get( - "/ckpt_confs", - operation_id="list_ckpt_configs", - responses={ - 200: {"description": "paths retrieved successfully"}, - }, - status_code=200, - response_model=List[pathlib.Path], -) -async def list_ckpt_configs() -> List[pathlib.Path]: - """Return a list of the legacy checkpoint configuration files stored in `ROOT/configs/stable-diffusion`, relative to ROOT.""" - return ApiDependencies.invoker.services.model_manager.list_checkpoint_configs() - - -@models_router.post( - "/sync", - operation_id="sync_to_config", - responses={ - 201: {"description": "synchronization successful"}, - }, - status_code=201, - response_model=bool, -) -async def sync_to_config() -> bool: - """Call after making changes to models.yaml, autoimport directories or models directory to synchronize - in-memory data structures with disk data structures.""" - ApiDependencies.invoker.services.model_manager.sync_to_config() - return True - - -# There's some weird pydantic-fastapi behaviour that requires this to be a separate class -# TODO: After a few updates, see if it works inside the route operation handler? -class MergeModelsBody(BaseModel): - model_names: List[str] = Field(description="model name", min_length=2, max_length=3) - merged_model_name: Optional[str] = Field(description="Name of destination model") - alpha: Optional[float] = Field(description="Alpha weighting strength to apply to 2d and 3d models", default=0.5) - interp: Optional[MergeInterpolationMethod] = Field(description="Interpolation method") - force: Optional[bool] = Field( - description="Force merging of models created with different versions of diffusers", - default=False, - ) - - merge_dest_directory: Optional[str] = Field( - description="Save the merged model to the designated directory (with 'merged_model_name' appended)", - default=None, - ) - - model_config = ConfigDict(protected_namespaces=()) - - -@models_router.put( - "/merge/{base_model}", - operation_id="merge_models", - responses={ - 200: {"description": "Model converted successfully"}, - 400: {"description": "Incompatible models"}, - 404: {"description": "One or more models not found"}, - }, - status_code=200, - response_model=MergeModelResponse, -) -async def merge_models( - body: Annotated[MergeModelsBody, Body(description="Model configuration", embed=True)], - base_model: BaseModelType = Path(description="Base model"), -) -> MergeModelResponse: - """Convert a checkpoint model into a diffusers model""" - logger = ApiDependencies.invoker.services.logger - try: - logger.info( - f"Merging models: {body.model_names} into {body.merge_dest_directory or ''}/{body.merged_model_name}" - ) - dest = pathlib.Path(body.merge_dest_directory) if body.merge_dest_directory else None - result = ApiDependencies.invoker.services.model_manager.merge_models( - model_names=body.model_names, - base_model=base_model, - merged_model_name=body.merged_model_name or "+".join(body.model_names), - alpha=body.alpha, - interp=body.interp, - force=body.force, - merge_dest_directory=dest, - ) - model_raw = ApiDependencies.invoker.services.model_manager.list_model( - result.name, - base_model=base_model, - model_type=ModelType.Main, - ) - response = ConvertModelResponseValidator.validate_python(model_raw) - except ModelNotFoundException: - raise HTTPException( - status_code=404, - detail=f"One or more of the models '{body.model_names}' not found", - ) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - return response diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 1831b54c13..149d47fb96 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -48,7 +48,7 @@ if True: # hack to make flake8 happy with imports coming after setting up the c boards, download_queue, images, - model_manager_v2, + model_manager, session_queue, sessions, utilities, @@ -113,7 +113,7 @@ async def shutdown_event() -> None: app.include_router(sessions.session_router, prefix="/api") app.include_router(utilities.utilities_router, prefix="/api") -app.include_router(model_manager_v2.model_manager_v2_router, prefix="/api") +app.include_router(model_manager.model_manager_router, prefix="/api") app.include_router(download_queue.download_queue_router, prefix="/api") app.include_router(images.images_router, prefix="/api") app.include_router(boards.boards_router, prefix="/api") @@ -175,21 +175,23 @@ def custom_openapi() -> dict[str, Any]: invoker_schema["class"] = "invocation" openapi_schema["components"]["schemas"][f"{output_type_title}"]["class"] = "output" - from invokeai.backend.model_management.models import get_model_config_enums + # This code no longer seems to be necessary? + # Leave it here just in case + # + # from invokeai.backend.model_manager import get_model_config_formats + # formats = get_model_config_formats() + # for model_config_name, enum_set in formats.items(): - for model_config_format_enum in set(get_model_config_enums()): - name = model_config_format_enum.__qualname__ + # if model_config_name in openapi_schema["components"]["schemas"]: + # # print(f"Config with name {name} already defined") + # continue - if name in openapi_schema["components"]["schemas"]: - # print(f"Config with name {name} already defined") - continue - - openapi_schema["components"]["schemas"][name] = { - "title": name, - "description": "An enumeration.", - "type": "string", - "enum": [v.value for v in model_config_format_enum], - } + # openapi_schema["components"]["schemas"][model_config_name] = { + # "title": model_config_name, + # "description": "An enumeration.", + # "type": "string", + # "enum": [v.value for v in enum_set], + # } app.openapi_schema = openapi_schema return app.openapi_schema diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 593121ba60..517da4375e 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -18,15 +18,15 @@ from invokeai.app.services.model_records import UnknownModelException from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.app.util.ti_utils import extract_ti_triggers_from_prompt from invokeai.backend.lora import LoRAModelRaw -from invokeai.backend.model_patcher import ModelPatcher -from invokeai.backend.textual_inversion import TextualInversionModelRaw from invokeai.backend.model_manager import ModelType +from invokeai.backend.model_patcher import ModelPatcher from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ( BasicConditioningInfo, ConditioningFieldData, ExtraConditioningInfo, SDXLConditioningInfo, ) +from invokeai.backend.textual_inversion import TextualInversionModelRaw from invokeai.backend.util.devices import torch_dtype from .baseinvocation import ( diff --git a/invokeai/app/services/config/config_base.py b/invokeai/app/services/config/config_base.py index 983df6b468..c73aa43809 100644 --- a/invokeai/app/services/config/config_base.py +++ b/invokeai/app/services/config/config_base.py @@ -68,7 +68,7 @@ class InvokeAISettings(BaseSettings): return OmegaConf.to_yaml(conf) @classmethod - def add_parser_arguments(cls, parser) -> None: + def add_parser_arguments(cls, parser: ArgumentParser) -> None: """Dynamically create arguments for a settings parser.""" if "type" in get_type_hints(cls): settings_stanza = get_args(get_type_hints(cls)["type"])[0] diff --git a/invokeai/app/services/invocation_stats/invocation_stats_base.py b/invokeai/app/services/invocation_stats/invocation_stats_base.py index 22624a6579..ec8a453323 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_base.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_base.py @@ -29,8 +29,8 @@ writes to the system log is stored in InvocationServices.performance_statistics. """ from abc import ABC, abstractmethod -from contextlib import AbstractContextManager from pathlib import Path +from typing import Iterator from invokeai.app.invocations.baseinvocation import BaseInvocation from invokeai.app.services.invocation_stats.invocation_stats_common import InvocationStatsSummary @@ -40,18 +40,17 @@ class InvocationStatsServiceBase(ABC): "Abstract base class for recording node memory/time performance statistics" @abstractmethod - def __init__(self): + def __init__(self) -> None: """ Initialize the InvocationStatsService and reset counters to zero """ - pass @abstractmethod def collect_stats( self, invocation: BaseInvocation, graph_execution_state_id: str, - ) -> AbstractContextManager: + ) -> Iterator[None]: """ Return a context object that will capture the statistics on the execution of invocaation. Use with: to place around the part of the code that executes the invocation. @@ -61,7 +60,7 @@ class InvocationStatsServiceBase(ABC): pass @abstractmethod - def reset_stats(self, graph_execution_state_id: str): + def reset_stats(self, graph_execution_state_id: str) -> None: """ Reset all statistics for the indicated graph. :param graph_execution_state_id: The id of the session whose stats to reset. @@ -70,7 +69,7 @@ class InvocationStatsServiceBase(ABC): pass @abstractmethod - def log_stats(self, graph_execution_state_id: str): + def log_stats(self, graph_execution_state_id: str) -> None: """ Write out the accumulated statistics to the log or somewhere else. :param graph_execution_state_id: The id of the session whose stats to log. diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py index 2f03db0af7..080219af75 100644 --- a/invokeai/app/services/model_install/model_install_base.py +++ b/invokeai/app/services/model_install/model_install_base.py @@ -14,7 +14,7 @@ from typing_extensions import Annotated from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.download import DownloadJob, DownloadQueueServiceBase -from invokeai.app.services.events import EventServiceBase +from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker from invokeai.app.services.model_records import ModelRecordServiceBase from invokeai.backend.model_manager import AnyModelConfig, ModelRepoVariant diff --git a/invokeai/app/services/model_load/model_load_base.py b/invokeai/app/services/model_load/model_load_base.py index f4dd905135..cc80333e93 100644 --- a/invokeai/app/services/model_load/model_load_base.py +++ b/invokeai/app/services/model_load/model_load_base.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from typing import Optional from invokeai.app.services.shared.invocation_context import InvocationContextData -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType from invokeai.backend.model_manager.load import LoadedModel from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase @@ -15,23 +15,7 @@ class ModelLoadServiceBase(ABC): """Wrapper around AnyModelLoader.""" @abstractmethod - def load_model_by_key( - self, - key: str, - submodel_type: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModel: - """ - Given a model's key, load it and return the LoadedModel object. - - :param key: Key of model config to be fetched. - :param submodel: For main (pipeline models), the submodel to fetch. - :param context_data: Invocation context data used for event reporting - """ - pass - - @abstractmethod - def load_model_by_config( + def load_model( self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None, @@ -44,34 +28,6 @@ class ModelLoadServiceBase(ABC): :param submodel: For main (pipeline models), the submodel to fetch. :param context_data: Invocation context data used for event reporting """ - pass - - @abstractmethod - def load_model_by_attr( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModel: - """ - Given a model's attributes, search the database for it, and if found, load and return the LoadedModel object. - - This is provided for API compatability with the get_model() method - in the original model manager. However, note that LoadedModel is - not the same as the original ModelInfo that ws returned. - - :param model_name: Name of to be fetched. - :param base_model: Base model - :param model_type: Type of the model - :param submodel: For main (pipeline models), the submodel to fetch - :param context_data: The invocation context data. - - Exceptions: UnknownModelException -- model with these attributes not known - NotImplementedException -- a model loader was not provided at initialization time - ValueError -- more than one model matches this combination - """ @property @abstractmethod diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index fa96a4672d..15c6283d8a 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -1,15 +1,18 @@ # Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Team """Implementation of model loader service.""" -from typing import Optional +from typing import Optional, Type from invokeai.app.services.config import InvokeAIAppConfig from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException from invokeai.app.services.invoker import Invoker -from invokeai.app.services.model_records import ModelRecordServiceBase, UnknownModelException from invokeai.app.services.shared.invocation_context import InvocationContextData -from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType -from invokeai.backend.model_manager.load import AnyModelLoader, LoadedModel, ModelCache, ModelConvertCache +from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType +from invokeai.backend.model_manager.load import ( + LoadedModel, + ModelLoaderRegistry, + ModelLoaderRegistryBase, +) from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase from invokeai.backend.util.logging import InvokeAILogger @@ -18,25 +21,23 @@ from .model_load_base import ModelLoadServiceBase class ModelLoadService(ModelLoadServiceBase): - """Wrapper around AnyModelLoader.""" + """Wrapper around ModelLoaderRegistry.""" def __init__( - self, - app_config: InvokeAIAppConfig, - record_store: ModelRecordServiceBase, - ram_cache: ModelCacheBase[AnyModel], - convert_cache: ModelConvertCacheBase, + self, + app_config: InvokeAIAppConfig, + ram_cache: ModelCacheBase[AnyModel], + convert_cache: ModelConvertCacheBase, + registry: Optional[Type[ModelLoaderRegistryBase]] = ModelLoaderRegistry, ): """Initialize the model load service.""" logger = InvokeAILogger.get_logger(self.__class__.__name__) logger.setLevel(app_config.log_level.upper()) - self._store = record_store - self._any_loader = AnyModelLoader( - app_config=app_config, - logger=logger, - ram_cache=ram_cache, - convert_cache=convert_cache, - ) + self._logger = logger + self._app_config = app_config + self._ram_cache = ram_cache + self._convert_cache = convert_cache + self._registry = registry def start(self, invoker: Invoker) -> None: self._invoker = invoker @@ -44,63 +45,14 @@ class ModelLoadService(ModelLoadServiceBase): @property def ram_cache(self) -> ModelCacheBase[AnyModel]: """Return the RAM cache used by this loader.""" - return self._any_loader.ram_cache + return self._ram_cache @property def convert_cache(self) -> ModelConvertCacheBase: """Return the checkpoint convert cache used by this loader.""" - return self._any_loader.convert_cache + return self._convert_cache - def load_model_by_key( - self, - key: str, - submodel_type: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModel: - """ - Given a model's key, load it and return the LoadedModel object. - - :param key: Key of model config to be fetched. - :param submodel: For main (pipeline models), the submodel to fetch. - :param context: Invocation context used for event reporting - """ - config = self._store.get_model(key) - return self.load_model_by_config(config, submodel_type, context_data) - - def load_model_by_attr( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - context_data: Optional[InvocationContextData] = None, - ) -> LoadedModel: - """ - Given a model's attributes, search the database for it, and if found, load and return the LoadedModel object. - - This is provided for API compatability with the get_model() method - in the original model manager. However, note that LoadedModel is - not the same as the original ModelInfo that ws returned. - - :param model_name: Name of to be fetched. - :param base_model: Base model - :param model_type: Type of the model - :param submodel: For main (pipeline models), the submodel to fetch - :param context: The invocation context. - - Exceptions: UnknownModelException -- model with this key not known - NotImplementedException -- a model loader was not provided at initialization time - ValueError -- more than one model matches this combination - """ - configs = self._store.search_by_attr(model_name, base_model, model_type) - if len(configs) == 0: - raise UnknownModelException(f"{base_model}/{model_type}/{model_name}: Unknown model") - elif len(configs) > 1: - raise ValueError(f"{base_model}/{model_type}/{model_name}: More than one model matches.") - else: - return self.load_model_by_key(configs[0].key, submodel) - - def load_model_by_config( + def load_model( self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None, @@ -118,7 +70,15 @@ class ModelLoadService(ModelLoadServiceBase): context_data=context_data, model_config=model_config, ) - loaded_model = self._any_loader.load_model(model_config, submodel_type) + + implementation, model_config, submodel_type = self._registry.get_implementation(model_config, submodel_type) # type: ignore + loaded_model: LoadedModel = implementation( + app_config=self._app_config, + logger=self._logger, + ram_cache=self._ram_cache, + convert_cache=self._convert_cache, + ).load_model(model_config, submodel_type) + if context_data: self._emit_load_event( context_data=context_data, diff --git a/invokeai/app/services/model_manager/__init__.py b/invokeai/app/services/model_manager/__init__.py index 66707493f7..5455577266 100644 --- a/invokeai/app/services/model_manager/__init__.py +++ b/invokeai/app/services/model_manager/__init__.py @@ -3,7 +3,7 @@ from invokeai.backend.model_manager import AnyModel, AnyModelConfig, BaseModelType, ModelType, SubModelType from invokeai.backend.model_manager.load import LoadedModel -from .model_manager_default import ModelManagerServiceBase, ModelManagerService +from .model_manager_default import ModelManagerService, ModelManagerServiceBase __all__ = [ "ModelManagerServiceBase", diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index 1116c82ff1..c25aa6fb47 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -1,10 +1,14 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team from abc import ABC, abstractmethod +from typing import Optional from typing_extensions import Self from invokeai.app.services.invoker import Invoker +from invokeai.app.services.shared.invocation_context import InvocationContextData +from invokeai.backend.model_manager.config import AnyModelConfig, BaseModelType, ModelType, SubModelType +from invokeai.backend.model_manager.load.load_base import LoadedModel from ..config import InvokeAIAppConfig from ..download import DownloadQueueServiceBase @@ -65,3 +69,32 @@ class ModelManagerServiceBase(ABC): @abstractmethod def stop(self, invoker: Invoker) -> None: pass + + @abstractmethod + def load_model_by_config( + self, + model_config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + context_data: Optional[InvocationContextData] = None, + ) -> LoadedModel: + pass + + @abstractmethod + def load_model_by_key( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + context_data: Optional[InvocationContextData] = None, + ) -> LoadedModel: + pass + + @abstractmethod + def load_model_by_attr( + self, + model_name: str, + base_model: BaseModelType, + model_type: ModelType, + submodel: Optional[SubModelType] = None, + context_data: Optional[InvocationContextData] = None, + ) -> LoadedModel: + pass diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index b96341be69..d029f9e033 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -1,10 +1,14 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team """Implementation of ModelManagerServiceBase.""" +from typing import Optional + from typing_extensions import Self from invokeai.app.services.invoker import Invoker -from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache +from invokeai.app.services.shared.invocation_context import InvocationContextData +from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, LoadedModel, ModelType, SubModelType +from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache, ModelLoaderRegistry from invokeai.backend.util.logging import InvokeAILogger from ..config import InvokeAIAppConfig @@ -12,7 +16,7 @@ from ..download import DownloadQueueServiceBase from ..events.events_base import EventServiceBase from ..model_install import ModelInstallService, ModelInstallServiceBase from ..model_load import ModelLoadService, ModelLoadServiceBase -from ..model_records import ModelRecordServiceBase +from ..model_records import ModelRecordServiceBase, UnknownModelException from .model_manager_base import ModelManagerServiceBase @@ -58,6 +62,56 @@ class ModelManagerService(ModelManagerServiceBase): if hasattr(service, "stop"): service.stop(invoker) + def load_model_by_config( + self, + model_config: AnyModelConfig, + submodel_type: Optional[SubModelType] = None, + context_data: Optional[InvocationContextData] = None, + ) -> LoadedModel: + return self.load.load_model(model_config, submodel_type, context_data) + + def load_model_by_key( + self, + key: str, + submodel_type: Optional[SubModelType] = None, + context_data: Optional[InvocationContextData] = None, + ) -> LoadedModel: + config = self.store.get_model(key) + return self.load.load_model(config, submodel_type, context_data) + + def load_model_by_attr( + self, + model_name: str, + base_model: BaseModelType, + model_type: ModelType, + submodel: Optional[SubModelType] = None, + context_data: Optional[InvocationContextData] = None, + ) -> LoadedModel: + """ + Given a model's attributes, search the database for it, and if found, load and return the LoadedModel object. + + This is provided for API compatability with the get_model() method + in the original model manager. However, note that LoadedModel is + not the same as the original ModelInfo that ws returned. + + :param model_name: Name of to be fetched. + :param base_model: Base model + :param model_type: Type of the model + :param submodel: For main (pipeline models), the submodel to fetch + :param context: The invocation context. + + Exceptions: UnknownModelException -- model with this key not known + NotImplementedException -- a model loader was not provided at initialization time + ValueError -- more than one model matches this combination + """ + configs = self.store.search_by_attr(model_name, base_model, model_type) + if len(configs) == 0: + raise UnknownModelException(f"{base_model}/{model_type}/{model_name}: Unknown model") + elif len(configs) > 1: + raise ValueError(f"{base_model}/{model_type}/{model_name}: More than one model matches.") + else: + return self.load.load_model(configs[0], submodel, context_data) + @classmethod def build_model_manager( cls, @@ -82,9 +136,9 @@ class ModelManagerService(ModelManagerServiceBase): ) loader = ModelLoadService( app_config=app_config, - record_store=model_record_service, ram_cache=ram_cache, convert_cache=convert_cache, + registry=ModelLoaderRegistry, ) installer = ModelInstallService( app_config=app_config, diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 089d09f825..1395427a97 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -281,7 +281,7 @@ class ModelsInterface(InvocationContextInterface): # The model manager emits events as it loads the model. It needs the context data to build # the event payloads. - return self._services.model_manager.load.load_model_by_key( + return self._services.model_manager.load_model_by_key( key=key, submodel_type=submodel_type, context_data=self._context_data ) @@ -296,7 +296,7 @@ class ModelsInterface(InvocationContextInterface): :param model_type: Type of the model :param submodel: For main (pipeline models), the submodel to fetch """ - return self._services.model_manager.load.load_model_by_attr( + return self._services.model_manager.load_model_by_attr( model_name=model_name, base_model=base_model, model_type=model_type, diff --git a/invokeai/backend/install/migrate_to_3.py b/invokeai/backend/install/migrate_to_3.py deleted file mode 100644 index e15eb23f5b..0000000000 --- a/invokeai/backend/install/migrate_to_3.py +++ /dev/null @@ -1,591 +0,0 @@ -""" -Migrate the models directory and models.yaml file from an existing -InvokeAI 2.3 installation to 3.0.0. -""" - -import argparse -import os -import shutil -import warnings -from dataclasses import dataclass -from pathlib import Path -from typing import Union - -import diffusers -import transformers -import yaml -from diffusers import AutoencoderKL, StableDiffusionPipeline -from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker -from omegaconf import DictConfig, OmegaConf -from transformers import AutoFeatureExtractor, BertTokenizerFast, CLIPTextModel, CLIPTokenizer - -import invokeai.backend.util.logging as logger -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_management import ModelManager -from invokeai.backend.model_management.model_probe import BaseModelType, ModelProbe, ModelProbeInfo, ModelType - -warnings.filterwarnings("ignore") -transformers.logging.set_verbosity_error() -diffusers.logging.set_verbosity_error() - - -# holder for paths that we will migrate -@dataclass -class ModelPaths: - models: Path - embeddings: Path - loras: Path - controlnets: Path - - -class MigrateTo3(object): - def __init__( - self, - from_root: Path, - to_models: Path, - model_manager: ModelManager, - src_paths: ModelPaths, - ): - self.root_directory = from_root - self.dest_models = to_models - self.mgr = model_manager - self.src_paths = src_paths - - @classmethod - def initialize_yaml(cls, yaml_file: Path): - with open(yaml_file, "w") as file: - file.write(yaml.dump({"__metadata__": {"version": "3.0.0"}})) - - def create_directory_structure(self): - """ - Create the basic directory structure for the models folder. - """ - for model_base in [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]: - for model_type in [ - ModelType.Main, - ModelType.Vae, - ModelType.Lora, - ModelType.ControlNet, - ModelType.TextualInversion, - ]: - path = self.dest_models / model_base.value / model_type.value - path.mkdir(parents=True, exist_ok=True) - path = self.dest_models / "core" - path.mkdir(parents=True, exist_ok=True) - - @staticmethod - def copy_file(src: Path, dest: Path): - """ - copy a single file with logging - """ - if dest.exists(): - logger.info(f"Skipping existing {str(dest)}") - return - logger.info(f"Copying {str(src)} to {str(dest)}") - try: - shutil.copy(src, dest) - except Exception as e: - logger.error(f"COPY FAILED: {str(e)}") - - @staticmethod - def copy_dir(src: Path, dest: Path): - """ - Recursively copy a directory with logging - """ - if dest.exists(): - logger.info(f"Skipping existing {str(dest)}") - return - - logger.info(f"Copying {str(src)} to {str(dest)}") - try: - shutil.copytree(src, dest) - except Exception as e: - logger.error(f"COPY FAILED: {str(e)}") - - def migrate_models(self, src_dir: Path): - """ - Recursively walk through src directory, probe anything - that looks like a model, and copy the model into the - appropriate location within the destination models directory. - """ - directories_scanned = set() - for root, dirs, files in os.walk(src_dir, followlinks=True): - for d in dirs: - try: - model = Path(root, d) - info = ModelProbe().heuristic_probe(model) - if not info: - continue - dest = self._model_probe_to_path(info) / model.name - self.copy_dir(model, dest) - directories_scanned.add(model) - except Exception as e: - logger.error(str(e)) - except KeyboardInterrupt: - raise - for f in files: - # don't copy raw learned_embeds.bin or pytorch_lora_weights.bin - # let them be copied as part of a tree copy operation - try: - if f in {"learned_embeds.bin", "pytorch_lora_weights.bin"}: - continue - model = Path(root, f) - if model.parent in directories_scanned: - continue - info = ModelProbe().heuristic_probe(model) - if not info: - continue - dest = self._model_probe_to_path(info) / f - self.copy_file(model, dest) - except Exception as e: - logger.error(str(e)) - except KeyboardInterrupt: - raise - - def migrate_support_models(self): - """ - Copy the clipseg, upscaler, and restoration models to their new - locations. - """ - dest_directory = self.dest_models - if (self.root_directory / "models/clipseg").exists(): - self.copy_dir(self.root_directory / "models/clipseg", dest_directory / "core/misc/clipseg") - if (self.root_directory / "models/realesrgan").exists(): - self.copy_dir(self.root_directory / "models/realesrgan", dest_directory / "core/upscaling/realesrgan") - for d in ["codeformer", "gfpgan"]: - path = self.root_directory / "models" / d - if path.exists(): - self.copy_dir(path, dest_directory / f"core/face_restoration/{d}") - - def migrate_tuning_models(self): - """ - Migrate the embeddings, loras and controlnets directories to their new homes. - """ - for src in [self.src_paths.embeddings, self.src_paths.loras, self.src_paths.controlnets]: - if not src: - continue - if src.is_dir(): - logger.info(f"Scanning {src}") - self.migrate_models(src) - else: - logger.info(f"{src} directory not found; skipping") - continue - - def migrate_conversion_models(self): - """ - Migrate all the models that are needed by the ckpt_to_diffusers conversion - script. - """ - - dest_directory = self.dest_models - kwargs = { - "cache_dir": self.root_directory / "models/hub", - # local_files_only = True - } - try: - logger.info("Migrating core tokenizers and text encoders") - target_dir = dest_directory / "core" / "convert" - - self._migrate_pretrained( - BertTokenizerFast, repo_id="bert-base-uncased", dest=target_dir / "bert-base-uncased", **kwargs - ) - - # sd-1 - repo_id = "openai/clip-vit-large-patch14" - self._migrate_pretrained( - CLIPTokenizer, repo_id=repo_id, dest=target_dir / "clip-vit-large-patch14", **kwargs - ) - self._migrate_pretrained( - CLIPTextModel, repo_id=repo_id, dest=target_dir / "clip-vit-large-patch14", force=True, **kwargs - ) - - # sd-2 - repo_id = "stabilityai/stable-diffusion-2" - self._migrate_pretrained( - CLIPTokenizer, - repo_id=repo_id, - dest=target_dir / "stable-diffusion-2-clip" / "tokenizer", - **{"subfolder": "tokenizer", **kwargs}, - ) - self._migrate_pretrained( - CLIPTextModel, - repo_id=repo_id, - dest=target_dir / "stable-diffusion-2-clip" / "text_encoder", - **{"subfolder": "text_encoder", **kwargs}, - ) - - # VAE - logger.info("Migrating stable diffusion VAE") - self._migrate_pretrained( - AutoencoderKL, repo_id="stabilityai/sd-vae-ft-mse", dest=target_dir / "sd-vae-ft-mse", **kwargs - ) - - # safety checking - logger.info("Migrating safety checker") - repo_id = "CompVis/stable-diffusion-safety-checker" - self._migrate_pretrained( - AutoFeatureExtractor, repo_id=repo_id, dest=target_dir / "stable-diffusion-safety-checker", **kwargs - ) - self._migrate_pretrained( - StableDiffusionSafetyChecker, - repo_id=repo_id, - dest=target_dir / "stable-diffusion-safety-checker", - **kwargs, - ) - except KeyboardInterrupt: - raise - except Exception as e: - logger.error(str(e)) - - def _model_probe_to_path(self, info: ModelProbeInfo) -> Path: - return Path(self.dest_models, info.base_type.value, info.model_type.value) - - def _migrate_pretrained(self, model_class, repo_id: str, dest: Path, force: bool = False, **kwargs): - if dest.exists() and not force: - logger.info(f"Skipping existing {dest}") - return - model = model_class.from_pretrained(repo_id, **kwargs) - self._save_pretrained(model, dest, overwrite=force) - - def _save_pretrained(self, model, dest: Path, overwrite: bool = False): - model_name = dest.name - if overwrite: - model.save_pretrained(dest, safe_serialization=True) - else: - download_path = dest.with_name(f"{model_name}.downloading") - model.save_pretrained(download_path, safe_serialization=True) - download_path.replace(dest) - - def _download_vae(self, repo_id: str, subfolder: str = None) -> Path: - vae = AutoencoderKL.from_pretrained(repo_id, cache_dir=self.root_directory / "models/hub", subfolder=subfolder) - info = ModelProbe().heuristic_probe(vae) - _, model_name = repo_id.split("/") - dest = self._model_probe_to_path(info) / self.unique_name(model_name, info) - vae.save_pretrained(dest, safe_serialization=True) - return dest - - def _vae_path(self, vae: Union[str, dict]) -> Path: - """ - Convert 2.3 VAE stanza to a straight path. - """ - vae_path = None - - # First get a path - if isinstance(vae, str): - vae_path = vae - - elif isinstance(vae, DictConfig): - if p := vae.get("path"): - vae_path = p - elif repo_id := vae.get("repo_id"): - if repo_id == "stabilityai/sd-vae-ft-mse": # this guy is already downloaded - vae_path = "models/core/convert/sd-vae-ft-mse" - return vae_path - else: - vae_path = self._download_vae(repo_id, vae.get("subfolder")) - - assert vae_path is not None, "Couldn't find VAE for this model" - - # if the VAE is in the old models directory, then we must move it into the new - # one. VAEs outside of this directory can stay where they are. - vae_path = Path(vae_path) - if vae_path.is_relative_to(self.src_paths.models): - info = ModelProbe().heuristic_probe(vae_path) - dest = self._model_probe_to_path(info) / vae_path.name - if not dest.exists(): - if vae_path.is_dir(): - self.copy_dir(vae_path, dest) - else: - self.copy_file(vae_path, dest) - vae_path = dest - - if vae_path.is_relative_to(self.dest_models): - rel_path = vae_path.relative_to(self.dest_models) - return Path("models", rel_path) - else: - return vae_path - - def migrate_repo_id(self, repo_id: str, model_name: str = None, **extra_config): - """ - Migrate a locally-cached diffusers pipeline identified with a repo_id - """ - dest_dir = self.dest_models - - cache = self.root_directory / "models/hub" - kwargs = { - "cache_dir": cache, - "safety_checker": None, - # local_files_only = True, - } - - owner, repo_name = repo_id.split("/") - model_name = model_name or repo_name - model = cache / "--".join(["models", owner, repo_name]) - - if len(list(model.glob("snapshots/**/model_index.json"))) == 0: - return - revisions = [x.name for x in model.glob("refs/*")] - - # if an fp16 is available we use that - revision = "fp16" if len(revisions) > 1 and "fp16" in revisions else revisions[0] - pipeline = StableDiffusionPipeline.from_pretrained(repo_id, revision=revision, **kwargs) - - info = ModelProbe().heuristic_probe(pipeline) - if not info: - return - - if self.mgr.model_exists(model_name, info.base_type, info.model_type): - logger.warning(f"A model named {model_name} already exists at the destination. Skipping migration.") - return - - dest = self._model_probe_to_path(info) / model_name - self._save_pretrained(pipeline, dest) - - rel_path = Path("models", dest.relative_to(dest_dir)) - self._add_model(model_name, info, rel_path, **extra_config) - - def migrate_path(self, location: Path, model_name: str = None, **extra_config): - """ - Migrate a model referred to using 'weights' or 'path' - """ - - # handle relative paths - dest_dir = self.dest_models - location = self.root_directory / location - model_name = model_name or location.stem - - info = ModelProbe().heuristic_probe(location) - if not info: - return - - if self.mgr.model_exists(model_name, info.base_type, info.model_type): - logger.warning(f"A model named {model_name} already exists at the destination. Skipping migration.") - return - - # uh oh, weights is in the old models directory - move it into the new one - if Path(location).is_relative_to(self.src_paths.models): - dest = Path(dest_dir, info.base_type.value, info.model_type.value, location.name) - if location.is_dir(): - self.copy_dir(location, dest) - else: - self.copy_file(location, dest) - location = Path("models", info.base_type.value, info.model_type.value, location.name) - - self._add_model(model_name, info, location, **extra_config) - - def _add_model(self, model_name: str, info: ModelProbeInfo, location: Path, **extra_config): - if info.model_type != ModelType.Main: - return - - self.mgr.add_model( - model_name=model_name, - base_model=info.base_type, - model_type=info.model_type, - clobber=True, - model_attributes={ - "path": str(location), - "description": f"A {info.base_type.value} {info.model_type.value} model", - "model_format": info.format, - "variant": info.variant_type.value, - **extra_config, - }, - ) - - def migrate_defined_models(self): - """ - Migrate models defined in models.yaml - """ - # find any models referred to in old models.yaml - conf = OmegaConf.load(self.root_directory / "configs/models.yaml") - - for model_name, stanza in conf.items(): - try: - passthru_args = {} - - if vae := stanza.get("vae"): - try: - passthru_args["vae"] = str(self._vae_path(vae)) - except Exception as e: - logger.warning(f'Could not find a VAE matching "{vae}" for model "{model_name}"') - logger.warning(str(e)) - - if config := stanza.get("config"): - passthru_args["config"] = config - - if description := stanza.get("description"): - passthru_args["description"] = description - - if repo_id := stanza.get("repo_id"): - logger.info(f"Migrating diffusers model {model_name}") - self.migrate_repo_id(repo_id, model_name, **passthru_args) - - elif location := stanza.get("weights"): - logger.info(f"Migrating checkpoint model {model_name}") - self.migrate_path(Path(location), model_name, **passthru_args) - - elif location := stanza.get("path"): - logger.info(f"Migrating diffusers model {model_name}") - self.migrate_path(Path(location), model_name, **passthru_args) - - except KeyboardInterrupt: - raise - except Exception as e: - logger.error(str(e)) - - def migrate(self): - self.create_directory_structure() - # the configure script is doing this - self.migrate_support_models() - self.migrate_conversion_models() - self.migrate_tuning_models() - self.migrate_defined_models() - - -def _parse_legacy_initfile(root: Path, initfile: Path) -> ModelPaths: - """ - Returns tuple of (embedding_path, lora_path, controlnet_path) - """ - parser = argparse.ArgumentParser(fromfile_prefix_chars="@") - parser.add_argument( - "--embedding_directory", - "--embedding_path", - type=Path, - dest="embedding_path", - default=Path("embeddings"), - ) - parser.add_argument( - "--lora_directory", - dest="lora_path", - type=Path, - default=Path("loras"), - ) - opt, _ = parser.parse_known_args([f"@{str(initfile)}"]) - return ModelPaths( - models=root / "models", - embeddings=root / str(opt.embedding_path).strip('"'), - loras=root / str(opt.lora_path).strip('"'), - controlnets=root / "controlnets", - ) - - -def _parse_legacy_yamlfile(root: Path, initfile: Path) -> ModelPaths: - """ - Returns tuple of (embedding_path, lora_path, controlnet_path) - """ - # Don't use the config object because it is unforgiving of version updates - # Just use omegaconf directly - opt = OmegaConf.load(initfile) - paths = opt.InvokeAI.Paths - models = paths.get("models_dir", "models") - embeddings = paths.get("embedding_dir", "embeddings") - loras = paths.get("lora_dir", "loras") - controlnets = paths.get("controlnet_dir", "controlnets") - return ModelPaths( - models=root / models if models else None, - embeddings=root / embeddings if embeddings else None, - loras=root / loras if loras else None, - controlnets=root / controlnets if controlnets else None, - ) - - -def get_legacy_embeddings(root: Path) -> ModelPaths: - path = root / "invokeai.init" - if path.exists(): - return _parse_legacy_initfile(root, path) - path = root / "invokeai.yaml" - if path.exists(): - return _parse_legacy_yamlfile(root, path) - - -def do_migrate(src_directory: Path, dest_directory: Path): - """ - Migrate models from src to dest InvokeAI root directories - """ - config_file = dest_directory / "configs" / "models.yaml.3" - dest_models = dest_directory / "models.3" - - version_3 = (dest_directory / "models" / "core").exists() - - # Here we create the destination models.yaml file. - # If we are writing into a version 3 directory and the - # file already exists, then we write into a copy of it to - # avoid deleting its previous customizations. Otherwise we - # create a new empty one. - if version_3: # write into the dest directory - try: - shutil.copy(dest_directory / "configs" / "models.yaml", config_file) - except Exception: - MigrateTo3.initialize_yaml(config_file) - mgr = ModelManager(config_file) # important to initialize BEFORE moving the models directory - (dest_directory / "models").replace(dest_models) - else: - MigrateTo3.initialize_yaml(config_file) - mgr = ModelManager(config_file) - - paths = get_legacy_embeddings(src_directory) - migrator = MigrateTo3(from_root=src_directory, to_models=dest_models, model_manager=mgr, src_paths=paths) - migrator.migrate() - print("Migration successful.") - - if not version_3: - (dest_directory / "models").replace(src_directory / "models.orig") - print(f"Original models directory moved to {dest_directory}/models.orig") - - (dest_directory / "configs" / "models.yaml").replace(src_directory / "configs" / "models.yaml.orig") - print(f"Original models.yaml file moved to {dest_directory}/configs/models.yaml.orig") - - config_file.replace(config_file.with_suffix("")) - dest_models.replace(dest_models.with_suffix("")) - - -def main(): - parser = argparse.ArgumentParser( - prog="invokeai-migrate3", - description=""" -This will copy and convert the models directory and the configs/models.yaml from the InvokeAI 2.3 format -'--from-directory' root to the InvokeAI 3.0 '--to-directory' root. These may be abbreviated '--from' and '--to'.a - -The old models directory and config file will be renamed 'models.orig' and 'models.yaml.orig' respectively. -It is safe to provide the same directory for both arguments, but it is better to use the invokeai_configure -script, which will perform a full upgrade in place.""", - ) - parser.add_argument( - "--from-directory", - dest="src_root", - type=Path, - required=True, - help='Source InvokeAI 2.3 root directory (containing "invokeai.init" or "invokeai.yaml")', - ) - parser.add_argument( - "--to-directory", - dest="dest_root", - type=Path, - required=True, - help='Destination InvokeAI 3.0 directory (containing "invokeai.yaml")', - ) - args = parser.parse_args() - src_root = args.src_root - assert src_root.is_dir(), f"{src_root} is not a valid directory" - assert (src_root / "models").is_dir(), f"{src_root} does not contain a 'models' subdirectory" - assert (src_root / "models" / "hub").exists(), f"{src_root} does not contain a version 2.3 models directory" - assert (src_root / "invokeai.init").exists() or ( - src_root / "invokeai.yaml" - ).exists(), f"{src_root} does not contain an InvokeAI init file." - - dest_root = args.dest_root - assert dest_root.is_dir(), f"{dest_root} is not a valid directory" - config = InvokeAIAppConfig.get_config() - config.parse_args(["--root", str(dest_root)]) - - # TODO: revisit - don't rely on invokeai.yaml to exist yet! - dest_is_setup = (dest_root / "models/core").exists() and (dest_root / "databases").exists() - if not dest_is_setup: - from invokeai.backend.install.invokeai_configure import initialize_rootdir - - initialize_rootdir(dest_root, True) - - do_migrate(src_root, dest_root) - - -if __name__ == "__main__": - main() diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py deleted file mode 100644 index fdbe714f62..0000000000 --- a/invokeai/backend/install/model_install_backend.py +++ /dev/null @@ -1,637 +0,0 @@ -""" -Utility (backend) functions used by model_install.py -""" -import os -import re -import shutil -import warnings -from dataclasses import dataclass, field -from pathlib import Path -from tempfile import TemporaryDirectory -from typing import Callable, Dict, List, Optional, Set, Union - -import requests -import torch -from diffusers import DiffusionPipeline -from diffusers import logging as dlogging -from huggingface_hub import HfApi, HfFolder, hf_hub_url -from omegaconf import OmegaConf -from tqdm import tqdm - -import invokeai.configs as configs -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_management import AddModelResult, BaseModelType, ModelManager, ModelType, ModelVariantType -from invokeai.backend.model_management.model_probe import ModelProbe, ModelProbeInfo, SchedulerPredictionType -from invokeai.backend.util import download_with_resume -from invokeai.backend.util.devices import choose_torch_device, torch_dtype - -from ..util.logging import InvokeAILogger - -warnings.filterwarnings("ignore") - -# --------------------------globals----------------------- -config = InvokeAIAppConfig.get_config() -logger = InvokeAILogger.get_logger(name="InvokeAI") - -# the initial "configs" dir is now bundled in the `invokeai.configs` package -Dataset_path = Path(configs.__path__[0]) / "INITIAL_MODELS.yaml" - -Config_preamble = """ -# This file describes the alternative machine learning models -# available to InvokeAI script. -# -# To add a new model, follow the examples below. Each -# model requires a model config file, a weights file, -# and the width and height of the images it -# was trained on. -""" - -LEGACY_CONFIGS = { - BaseModelType.StableDiffusion1: { - ModelVariantType.Normal: { - SchedulerPredictionType.Epsilon: "v1-inference.yaml", - SchedulerPredictionType.VPrediction: "v1-inference-v.yaml", - }, - ModelVariantType.Inpaint: { - SchedulerPredictionType.Epsilon: "v1-inpainting-inference.yaml", - SchedulerPredictionType.VPrediction: "v1-inpainting-inference-v.yaml", - }, - }, - BaseModelType.StableDiffusion2: { - ModelVariantType.Normal: { - SchedulerPredictionType.Epsilon: "v2-inference.yaml", - SchedulerPredictionType.VPrediction: "v2-inference-v.yaml", - }, - ModelVariantType.Inpaint: { - SchedulerPredictionType.Epsilon: "v2-inpainting-inference.yaml", - SchedulerPredictionType.VPrediction: "v2-inpainting-inference-v.yaml", - }, - }, - BaseModelType.StableDiffusionXL: { - ModelVariantType.Normal: "sd_xl_base.yaml", - }, - BaseModelType.StableDiffusionXLRefiner: { - ModelVariantType.Normal: "sd_xl_refiner.yaml", - }, -} - - -@dataclass -class InstallSelections: - install_models: List[str] = field(default_factory=list) - remove_models: List[str] = field(default_factory=list) - - -@dataclass -class ModelLoadInfo: - name: str - model_type: ModelType - base_type: BaseModelType - path: Optional[Path] = None - repo_id: Optional[str] = None - subfolder: Optional[str] = None - description: str = "" - installed: bool = False - recommended: bool = False - default: bool = False - requires: Optional[List[str]] = field(default_factory=list) - - -class ModelInstall(object): - def __init__( - self, - config: InvokeAIAppConfig, - prediction_type_helper: Optional[Callable[[Path], SchedulerPredictionType]] = None, - model_manager: Optional[ModelManager] = None, - access_token: Optional[str] = None, - civitai_api_key: Optional[str] = None, - ): - self.config = config - self.mgr = model_manager or ModelManager(config.model_conf_path) - self.datasets = OmegaConf.load(Dataset_path) - self.prediction_helper = prediction_type_helper - self.access_token = access_token or HfFolder.get_token() - self.civitai_api_key = civitai_api_key or config.civitai_api_key - self.reverse_paths = self._reverse_paths(self.datasets) - - def all_models(self) -> Dict[str, ModelLoadInfo]: - """ - Return dict of model_key=>ModelLoadInfo objects. - This method consolidates and simplifies the entries in both - models.yaml and INITIAL_MODELS.yaml so that they can - be treated uniformly. It also sorts the models alphabetically - by their name, to improve the display somewhat. - """ - model_dict = {} - - # first populate with the entries in INITIAL_MODELS.yaml - for key, value in self.datasets.items(): - name, base, model_type = ModelManager.parse_key(key) - value["name"] = name - value["base_type"] = base - value["model_type"] = model_type - model_info = ModelLoadInfo(**value) - if model_info.subfolder and model_info.repo_id: - model_info.repo_id += f":{model_info.subfolder}" - model_dict[key] = model_info - - # supplement with entries in models.yaml - installed_models = list(self.mgr.list_models()) - - for md in installed_models: - base = md["base_model"] - model_type = md["model_type"] - name = md["model_name"] - key = ModelManager.create_key(name, base, model_type) - if key in model_dict: - model_dict[key].installed = True - else: - model_dict[key] = ModelLoadInfo( - name=name, - base_type=base, - model_type=model_type, - path=value.get("path"), - installed=True, - ) - return {x: model_dict[x] for x in sorted(model_dict.keys(), key=lambda y: model_dict[y].name.lower())} - - def _is_autoloaded(self, model_info: dict) -> bool: - path = model_info.get("path") - if not path: - return False - for autodir in ["autoimport_dir", "lora_dir", "embedding_dir", "controlnet_dir"]: - if autodir_path := getattr(self.config, autodir): - autodir_path = self.config.root_path / autodir_path - if Path(path).is_relative_to(autodir_path): - return True - return False - - def list_models(self, model_type): - installed = self.mgr.list_models(model_type=model_type) - print() - print(f"Installed models of type `{model_type}`:") - print(f"{'Model Key':50} Model Path") - for i in installed: - print(f"{'/'.join([i['base_model'],i['model_type'],i['model_name']]):50} {i['path']}") - print() - - # logic here a little reversed to maintain backward compatibility - def starter_models(self, all_models: bool = False) -> Set[str]: - models = set() - for key, _value in self.datasets.items(): - name, base, model_type = ModelManager.parse_key(key) - if all_models or model_type in [ModelType.Main, ModelType.Vae]: - models.add(key) - return models - - def recommended_models(self) -> Set[str]: - starters = self.starter_models(all_models=True) - return {x for x in starters if self.datasets[x].get("recommended", False)} - - def default_model(self) -> str: - starters = self.starter_models() - defaults = [x for x in starters if self.datasets[x].get("default", False)] - return defaults[0] - - def install(self, selections: InstallSelections): - verbosity = dlogging.get_verbosity() # quench NSFW nags - dlogging.set_verbosity_error() - - job = 1 - jobs = len(selections.remove_models) + len(selections.install_models) - - # remove requested models - for key in selections.remove_models: - name, base, mtype = self.mgr.parse_key(key) - logger.info(f"Deleting {mtype} model {name} [{job}/{jobs}]") - try: - self.mgr.del_model(name, base, mtype) - except FileNotFoundError as e: - logger.warning(e) - job += 1 - - # add requested models - self._remove_installed(selections.install_models) - self._add_required_models(selections.install_models) - for path in selections.install_models: - logger.info(f"Installing {path} [{job}/{jobs}]") - try: - self.heuristic_import(path) - except (ValueError, KeyError) as e: - logger.error(str(e)) - job += 1 - - dlogging.set_verbosity(verbosity) - self.mgr.commit() - - def heuristic_import( - self, - model_path_id_or_url: Union[str, Path], - models_installed: Set[Path] = None, - ) -> Dict[str, AddModelResult]: - """ - :param model_path_id_or_url: A Path to a local model to import, or a string representing its repo_id or URL - :param models_installed: Set of installed models, used for recursive invocation - Returns a set of dict objects corresponding to newly-created stanzas in models.yaml. - """ - - if not models_installed: - models_installed = {} - - model_path_id_or_url = str(model_path_id_or_url).strip("\"' ") - - # A little hack to allow nested routines to retrieve info on the requested ID - self.current_id = model_path_id_or_url - path = Path(model_path_id_or_url) - - # fix relative paths - if path.exists() and not path.is_absolute(): - path = path.absolute() # make relative to current WD - - # checkpoint file, or similar - if path.is_file(): - models_installed.update({str(path): self._install_path(path)}) - - # folders style or similar - elif path.is_dir() and any( - (path / x).exists() - for x in { - "config.json", - "model_index.json", - "learned_embeds.bin", - "pytorch_lora_weights.bin", - "pytorch_lora_weights.safetensors", - } - ): - models_installed.update({str(model_path_id_or_url): self._install_path(path)}) - - # recursive scan - elif path.is_dir(): - for child in path.iterdir(): - self.heuristic_import(child, models_installed=models_installed) - - # huggingface repo - elif len(str(model_path_id_or_url).split("/")) == 2: - models_installed.update({str(model_path_id_or_url): self._install_repo(str(model_path_id_or_url))}) - - # a URL - elif str(model_path_id_or_url).startswith(("http:", "https:", "ftp:")): - models_installed.update({str(model_path_id_or_url): self._install_url(model_path_id_or_url)}) - - else: - raise KeyError(f"{str(model_path_id_or_url)} is not recognized as a local path, repo ID or URL. Skipping") - - return models_installed - - def _remove_installed(self, model_list: List[str]): - all_models = self.all_models() - models_to_remove = [] - - for path in model_list: - key = self.reverse_paths.get(path) - if key and all_models[key].installed: - models_to_remove.append(path) - - for path in models_to_remove: - logger.warning(f"{path} already installed. Skipping") - model_list.remove(path) - - def _add_required_models(self, model_list: List[str]): - additional_models = [] - all_models = self.all_models() - for path in model_list: - if not (key := self.reverse_paths.get(path)): - continue - for requirement in all_models[key].requires: - requirement_key = self.reverse_paths.get(requirement) - if not all_models[requirement_key].installed: - additional_models.append(requirement) - model_list.extend(additional_models) - - # install a model from a local path. The optional info parameter is there to prevent - # the model from being probed twice in the event that it has already been probed. - def _install_path(self, path: Path, info: ModelProbeInfo = None) -> AddModelResult: - info = info or ModelProbe().heuristic_probe(path, self.prediction_helper) - if not info: - logger.warning(f"Unable to parse format of {path}") - return None - model_name = path.stem if path.is_file() else path.name - if self.mgr.model_exists(model_name, info.base_type, info.model_type): - raise ValueError(f'A model named "{model_name}" is already installed.') - attributes = self._make_attributes(path, info) - return self.mgr.add_model( - model_name=model_name, - base_model=info.base_type, - model_type=info.model_type, - model_attributes=attributes, - ) - - def _install_url(self, url: str) -> AddModelResult: - with TemporaryDirectory(dir=self.config.models_path) as staging: - CIVITAI_RE = r".*civitai.com.*" - civit_url = re.match(CIVITAI_RE, url, re.IGNORECASE) - location = download_with_resume( - url, Path(staging), access_token=self.civitai_api_key if civit_url else None - ) - if not location: - logger.error(f"Unable to download {url}. Skipping.") - info = ModelProbe().heuristic_probe(location, self.prediction_helper) - dest = self.config.models_path / info.base_type.value / info.model_type.value / location.name - dest.parent.mkdir(parents=True, exist_ok=True) - models_path = shutil.move(location, dest) - - # staged version will be garbage-collected at this time - return self._install_path(Path(models_path), info) - - def _install_repo(self, repo_id: str) -> AddModelResult: - # hack to recover models stored in subfolders -- - # Required to get the "v2" model of monster-labs/control_v1p_sd15_qrcode_monster - subfolder = None - if match := re.match(r"^([^/]+/[^/]+):(\w+)$", repo_id): - repo_id = match.group(1) - subfolder = match.group(2) - - hinfo = HfApi().model_info(repo_id) - - # we try to figure out how to download this most economically - # list all the files in the repo - files = [x.rfilename for x in hinfo.siblings] - if subfolder: - files = [x for x in files if x.startswith(f"{subfolder}/")] - prefix = f"{subfolder}/" if subfolder else "" - - location = None - - with TemporaryDirectory(dir=self.config.models_path) as staging: - staging = Path(staging) - if f"{prefix}model_index.json" in files: - location = self._download_hf_pipeline(repo_id, staging, subfolder=subfolder) # pipeline - elif f"{prefix}unet/model.onnx" in files: - location = self._download_hf_model(repo_id, files, staging) - else: - for suffix in ["safetensors", "bin"]: - if f"{prefix}pytorch_lora_weights.{suffix}" in files: - location = self._download_hf_model( - repo_id, [f"pytorch_lora_weights.{suffix}"], staging, subfolder=subfolder - ) # LoRA - break - elif ( - self.config.precision == "float16" and f"{prefix}diffusion_pytorch_model.fp16.{suffix}" in files - ): # vae, controlnet or some other standalone - files = ["config.json", f"diffusion_pytorch_model.fp16.{suffix}"] - location = self._download_hf_model(repo_id, files, staging, subfolder=subfolder) - break - elif f"{prefix}diffusion_pytorch_model.{suffix}" in files: - files = ["config.json", f"diffusion_pytorch_model.{suffix}"] - location = self._download_hf_model(repo_id, files, staging, subfolder=subfolder) - break - elif f"{prefix}learned_embeds.{suffix}" in files: - location = self._download_hf_model( - repo_id, [f"learned_embeds.{suffix}"], staging, subfolder=subfolder - ) - break - elif ( - f"{prefix}image_encoder.txt" in files and f"{prefix}ip_adapter.{suffix}" in files - ): # IP-Adapter - files = ["image_encoder.txt", f"ip_adapter.{suffix}"] - location = self._download_hf_model(repo_id, files, staging, subfolder=subfolder) - break - elif f"{prefix}model.{suffix}" in files and f"{prefix}config.json" in files: - # This elif-condition is pretty fragile, but it is intended to handle CLIP Vision models hosted - # by InvokeAI for use with IP-Adapters. - files = ["config.json", f"model.{suffix}"] - location = self._download_hf_model(repo_id, files, staging, subfolder=subfolder) - break - if not location: - logger.warning(f"Could not determine type of repo {repo_id}. Skipping install.") - return {} - - info = ModelProbe().heuristic_probe(location, self.prediction_helper) - if not info: - logger.warning(f"Could not probe {location}. Skipping install.") - return {} - dest = ( - self.config.models_path - / info.base_type.value - / info.model_type.value - / self._get_model_name(repo_id, location) - ) - if dest.exists(): - shutil.rmtree(dest) - shutil.copytree(location, dest) - return self._install_path(dest, info) - - def _get_model_name(self, path_name: str, location: Path) -> str: - """ - Calculate a name for the model - primitive implementation. - """ - if key := self.reverse_paths.get(path_name): - (name, base, mtype) = ModelManager.parse_key(key) - return name - elif location.is_dir(): - return location.name - else: - return location.stem - - def _make_attributes(self, path: Path, info: ModelProbeInfo) -> dict: - model_name = path.name if path.is_dir() else path.stem - description = f"{info.base_type.value} {info.model_type.value} model {model_name}" - if key := self.reverse_paths.get(self.current_id): - if key in self.datasets: - description = self.datasets[key].get("description") or description - - rel_path = self.relative_to_root(path, self.config.models_path) - - attributes = { - "path": str(rel_path), - "description": str(description), - "model_format": info.format, - } - legacy_conf = None - if info.model_type == ModelType.Main or info.model_type == ModelType.ONNX: - attributes.update( - { - "variant": info.variant_type, - } - ) - if info.format == "checkpoint": - try: - possible_conf = path.with_suffix(".yaml") - if possible_conf.exists(): - legacy_conf = str(self.relative_to_root(possible_conf)) - elif info.base_type in [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]: - legacy_conf = Path( - self.config.legacy_conf_dir, - LEGACY_CONFIGS[info.base_type][info.variant_type][info.prediction_type], - ) - else: - legacy_conf = Path( - self.config.legacy_conf_dir, LEGACY_CONFIGS[info.base_type][info.variant_type] - ) - except KeyError: - legacy_conf = Path(self.config.legacy_conf_dir, "v1-inference.yaml") # best guess - - if info.model_type == ModelType.ControlNet and info.format == "checkpoint": - possible_conf = path.with_suffix(".yaml") - if possible_conf.exists(): - legacy_conf = str(self.relative_to_root(possible_conf)) - else: - legacy_conf = Path( - self.config.root_path, - "configs/controlnet", - ("cldm_v15.yaml" if info.base_type == BaseModelType("sd-1") else "cldm_v21.yaml"), - ) - - if legacy_conf: - attributes.update({"config": str(legacy_conf)}) - return attributes - - def relative_to_root(self, path: Path, root: Optional[Path] = None) -> Path: - root = root or self.config.root_path - if path.is_relative_to(root): - return path.relative_to(root) - else: - return path - - def _download_hf_pipeline(self, repo_id: str, staging: Path, subfolder: str = None) -> Path: - """ - Retrieve a StableDiffusion model from cache or remote and then - does a save_pretrained() to the indicated staging area. - """ - _, name = repo_id.split("/") - precision = torch_dtype(choose_torch_device()) - variants = ["fp16", None] if precision == torch.float16 else [None, "fp16"] - - model = None - for variant in variants: - try: - model = DiffusionPipeline.from_pretrained( - repo_id, - variant=variant, - torch_dtype=precision, - safety_checker=None, - subfolder=subfolder, - ) - except Exception as e: # most errors are due to fp16 not being present. Fix this to catch other errors - if "fp16" not in str(e): - print(e) - - if model: - break - - if not model: - logger.error(f"Diffusers model {repo_id} could not be downloaded. Skipping.") - return None - model.save_pretrained(staging / name, safe_serialization=True) - return staging / name - - def _download_hf_model(self, repo_id: str, files: List[str], staging: Path, subfolder: None) -> Path: - _, name = repo_id.split("/") - location = staging / name - paths = [] - for filename in files: - filePath = Path(filename) - p = hf_download_with_resume( - repo_id, - model_dir=location / filePath.parent, - model_name=filePath.name, - access_token=self.access_token, - subfolder=filePath.parent / subfolder if subfolder else filePath.parent, - ) - if p: - paths.append(p) - else: - logger.warning(f"Could not download {filename} from {repo_id}.") - - return location if len(paths) > 0 else None - - @classmethod - def _reverse_paths(cls, datasets) -> dict: - """ - Reverse mapping from repo_id/path to destination name. - """ - return {v.get("path") or v.get("repo_id"): k for k, v in datasets.items()} - - -# ------------------------------------- -def yes_or_no(prompt: str, default_yes=True): - default = "y" if default_yes else "n" - response = input(f"{prompt} [{default}] ") or default - if default_yes: - return response[0] not in ("n", "N") - else: - return response[0] in ("y", "Y") - - -# --------------------------------------------- -def hf_download_from_pretrained(model_class: object, model_name: str, destination: Path, **kwargs): - logger = InvokeAILogger.get_logger("InvokeAI") - logger.addFilter(lambda x: "fp16 is not a valid" not in x.getMessage()) - - model = model_class.from_pretrained( - model_name, - resume_download=True, - **kwargs, - ) - model.save_pretrained(destination, safe_serialization=True) - return destination - - -# --------------------------------------------- -def hf_download_with_resume( - repo_id: str, - model_dir: str, - model_name: str, - model_dest: Path = None, - access_token: str = None, - subfolder: str = None, -) -> Path: - model_dest = model_dest or Path(os.path.join(model_dir, model_name)) - os.makedirs(model_dir, exist_ok=True) - - url = hf_hub_url(repo_id, model_name, subfolder=subfolder) - - header = {"Authorization": f"Bearer {access_token}"} if access_token else {} - open_mode = "wb" - exist_size = 0 - - if os.path.exists(model_dest): - exist_size = os.path.getsize(model_dest) - header["Range"] = f"bytes={exist_size}-" - open_mode = "ab" - - resp = requests.get(url, headers=header, stream=True) - total = int(resp.headers.get("content-length", 0)) - - if resp.status_code == 416: # "range not satisfiable", which means nothing to return - logger.info(f"{model_name}: complete file found. Skipping.") - return model_dest - elif resp.status_code == 404: - logger.warning("File not found") - return None - elif resp.status_code != 200: - logger.warning(f"{model_name}: {resp.reason}") - elif exist_size > 0: - logger.info(f"{model_name}: partial file found. Resuming...") - else: - logger.info(f"{model_name}: Downloading...") - - try: - with ( - open(model_dest, open_mode) as file, - tqdm( - desc=model_name, - initial=exist_size, - total=total + exist_size, - unit="iB", - unit_scale=True, - unit_divisor=1000, - ) as bar, - ): - for data in resp.iter_content(chunk_size=1024): - size = file.write(data) - bar.update(size) - except Exception as e: - logger.error(f"An error occurred while downloading {model_name}: {str(e)}") - return None - return model_dest diff --git a/invokeai/backend/ip_adapter/ip_adapter.py b/invokeai/backend/ip_adapter/ip_adapter.py index 3ba6fc5a23..e51966c779 100644 --- a/invokeai/backend/ip_adapter/ip_adapter.py +++ b/invokeai/backend/ip_adapter/ip_adapter.py @@ -9,8 +9,8 @@ from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection from invokeai.backend.ip_adapter.ip_attention_weights import IPAttentionWeights -from .resampler import Resampler from ..raw_model import RawModel +from .resampler import Resampler class ImageProjModel(torch.nn.Module): diff --git a/invokeai/backend/lora.py b/invokeai/backend/lora.py index fb0c23067f..0b7128034a 100644 --- a/invokeai/backend/lora.py +++ b/invokeai/backend/lora.py @@ -10,6 +10,7 @@ from safetensors.torch import load_file from typing_extensions import Self from invokeai.backend.model_manager import BaseModelType + from .raw_model import RawModel @@ -366,6 +367,7 @@ class IA3Layer(LoRALayerBase): AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer] + class LoRAModelRaw(RawModel): # (torch.nn.Module): _name: str layers: Dict[str, AnyLoRALayer] diff --git a/invokeai/backend/model_management_OLD/README.md b/invokeai/backend/model_management_OLD/README.md deleted file mode 100644 index 0d94f39642..0000000000 --- a/invokeai/backend/model_management_OLD/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Model Cache - -## `glibc` Memory Allocator Fragmentation - -Python (and PyTorch) relies on the memory allocator from the C Standard Library (`libc`). On linux, with the GNU C Standard Library implementation (`glibc`), our memory access patterns have been observed to cause severe memory fragmentation. This fragmentation results in large amounts of memory that has been freed but can't be released back to the OS. Loading models from disk and moving them between CPU/CUDA seem to be the operations that contribute most to the fragmentation. This memory fragmentation issue can result in OOM crashes during frequent model switching, even if `max_cache_size` is set to a reasonable value (e.g. a OOM crash with `max_cache_size=16` on a system with 32GB of RAM). - -This problem may also exist on other OSes, and other `libc` implementations. But, at the time of writing, it has only been investigated on linux with `glibc`. - -To better understand how the `glibc` memory allocator works, see these references: -- Basics: https://www.gnu.org/software/libc/manual/html_node/The-GNU-Allocator.html -- Details: https://sourceware.org/glibc/wiki/MallocInternals - -Note the differences between memory allocated as chunks in an arena vs. memory allocated with `mmap`. Under `glibc`'s default configuration, most model tensors get allocated as chunks in an arena making them vulnerable to the problem of fragmentation. - -We can work around this memory fragmentation issue by setting the following env var: - -```bash -# Force blocks >1MB to be allocated with `mmap` so that they are released to the system immediately when they are freed. -MALLOC_MMAP_THRESHOLD_=1048576 -``` - -See the following references for more information about the `malloc` tunable parameters: -- https://www.gnu.org/software/libc/manual/html_node/Malloc-Tunable-Parameters.html -- https://www.gnu.org/software/libc/manual/html_node/Memory-Allocation-Tunables.html -- https://man7.org/linux/man-pages/man3/mallopt.3.html - -The model cache emits debug logs that provide visibility into the state of the `libc` memory allocator. See the `LibcUtil` class for more info on how these `libc` malloc stats are collected. diff --git a/invokeai/backend/model_management_OLD/__init__.py b/invokeai/backend/model_management_OLD/__init__.py deleted file mode 100644 index d523a7a0c8..0000000000 --- a/invokeai/backend/model_management_OLD/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# ruff: noqa: I001, F401 -""" -Initialization file for invokeai.backend.model_management -""" -# This import must be first -from .model_manager import AddModelResult, LoadedModelInfo, ModelManager, SchedulerPredictionType -from .lora import ModelPatcher, ONNXModelPatcher -from .model_cache import ModelCache - -from .models import ( - BaseModelType, - DuplicateModelException, - ModelNotFoundException, - ModelType, - ModelVariantType, - SubModelType, -) - -# This import must be last -from .model_merge import MergeInterpolationMethod, ModelMerger diff --git a/invokeai/backend/model_management_OLD/convert_ckpt_to_diffusers.py b/invokeai/backend/model_management_OLD/convert_ckpt_to_diffusers.py deleted file mode 100644 index 6878218f67..0000000000 --- a/invokeai/backend/model_management_OLD/convert_ckpt_to_diffusers.py +++ /dev/null @@ -1,1739 +0,0 @@ -# coding=utf-8 -# Copyright 2023 The HuggingFace Inc. team. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Adapted for use in InvokeAI by Lincoln Stein, July 2023 -# -""" Conversion script for the Stable Diffusion checkpoints.""" - -import re -from contextlib import nullcontext -from io import BytesIO -from pathlib import Path -from typing import Optional, Union - -import requests -import torch -from diffusers.models import AutoencoderKL, ControlNetModel, PriorTransformer, UNet2DConditionModel -from diffusers.pipelines.latent_diffusion.pipeline_latent_diffusion import LDMBertConfig, LDMBertModel -from diffusers.pipelines.paint_by_example import PaintByExampleImageEncoder -from diffusers.pipelines.pipeline_utils import DiffusionPipeline -from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker -from diffusers.pipelines.stable_diffusion.stable_unclip_image_normalizer import StableUnCLIPImageNormalizer -from diffusers.schedulers import ( - DDIMScheduler, - DDPMScheduler, - DPMSolverMultistepScheduler, - EulerAncestralDiscreteScheduler, - EulerDiscreteScheduler, - HeunDiscreteScheduler, - LMSDiscreteScheduler, - PNDMScheduler, - UnCLIPScheduler, -) -from diffusers.utils import is_accelerate_available -from picklescan.scanner import scan_file_path -from transformers import ( - AutoFeatureExtractor, - BertTokenizerFast, - CLIPImageProcessor, - CLIPTextConfig, - CLIPTextModel, - CLIPTextModelWithProjection, - CLIPTokenizer, - CLIPVisionConfig, - CLIPVisionModelWithProjection, -) - -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.util.logging import InvokeAILogger - -from .models import BaseModelType, ModelVariantType - -try: - from omegaconf import OmegaConf - from omegaconf.dictconfig import DictConfig -except ImportError: - raise ImportError( - "OmegaConf is required to convert the LDM checkpoints. Please install it with `pip install OmegaConf`." - ) - -if is_accelerate_available(): - from accelerate import init_empty_weights - from accelerate.utils import set_module_tensor_to_device - -logger = InvokeAILogger.get_logger(__name__) -CONVERT_MODEL_ROOT = InvokeAIAppConfig.get_config().models_path / "core/convert" - - -def shave_segments(path, n_shave_prefix_segments=1): - """ - Removes segments. Positive values shave the first segments, negative shave the last segments. - """ - if n_shave_prefix_segments >= 0: - return ".".join(path.split(".")[n_shave_prefix_segments:]) - else: - return ".".join(path.split(".")[:n_shave_prefix_segments]) - - -def renew_resnet_paths(old_list, n_shave_prefix_segments=0): - """ - Updates paths inside resnets to the new naming scheme (local renaming) - """ - mapping = [] - for old_item in old_list: - new_item = old_item.replace("in_layers.0", "norm1") - new_item = new_item.replace("in_layers.2", "conv1") - - new_item = new_item.replace("out_layers.0", "norm2") - new_item = new_item.replace("out_layers.3", "conv2") - - new_item = new_item.replace("emb_layers.1", "time_emb_proj") - new_item = new_item.replace("skip_connection", "conv_shortcut") - - new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) - - mapping.append({"old": old_item, "new": new_item}) - - return mapping - - -def renew_vae_resnet_paths(old_list, n_shave_prefix_segments=0): - """ - Updates paths inside resnets to the new naming scheme (local renaming) - """ - mapping = [] - for old_item in old_list: - new_item = old_item - - new_item = new_item.replace("nin_shortcut", "conv_shortcut") - new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) - - mapping.append({"old": old_item, "new": new_item}) - - return mapping - - -def renew_attention_paths(old_list, n_shave_prefix_segments=0): - """ - Updates paths inside attentions to the new naming scheme (local renaming) - """ - mapping = [] - for old_item in old_list: - new_item = old_item - - # new_item = new_item.replace('norm.weight', 'group_norm.weight') - # new_item = new_item.replace('norm.bias', 'group_norm.bias') - - # new_item = new_item.replace('proj_out.weight', 'proj_attn.weight') - # new_item = new_item.replace('proj_out.bias', 'proj_attn.bias') - - # new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) - - mapping.append({"old": old_item, "new": new_item}) - - return mapping - - -def renew_vae_attention_paths(old_list, n_shave_prefix_segments=0): - """ - Updates paths inside attentions to the new naming scheme (local renaming) - """ - mapping = [] - for old_item in old_list: - new_item = old_item - - new_item = new_item.replace("norm.weight", "group_norm.weight") - new_item = new_item.replace("norm.bias", "group_norm.bias") - - new_item = new_item.replace("q.weight", "to_q.weight") - new_item = new_item.replace("q.bias", "to_q.bias") - - new_item = new_item.replace("k.weight", "to_k.weight") - new_item = new_item.replace("k.bias", "to_k.bias") - - new_item = new_item.replace("v.weight", "to_v.weight") - new_item = new_item.replace("v.bias", "to_v.bias") - - new_item = new_item.replace("proj_out.weight", "to_out.0.weight") - new_item = new_item.replace("proj_out.bias", "to_out.0.bias") - - new_item = shave_segments(new_item, n_shave_prefix_segments=n_shave_prefix_segments) - - mapping.append({"old": old_item, "new": new_item}) - - return mapping - - -def assign_to_checkpoint( - paths, checkpoint, old_checkpoint, attention_paths_to_split=None, additional_replacements=None, config=None -): - """ - This does the final conversion step: take locally converted weights and apply a global renaming to them. It splits - attention layers, and takes into account additional replacements that may arise. - - Assigns the weights to the new checkpoint. - """ - assert isinstance(paths, list), "Paths should be a list of dicts containing 'old' and 'new' keys." - - # Splits the attention layers into three variables. - if attention_paths_to_split is not None: - for path, path_map in attention_paths_to_split.items(): - old_tensor = old_checkpoint[path] - channels = old_tensor.shape[0] // 3 - - target_shape = (-1, channels) if len(old_tensor.shape) == 3 else (-1) - - num_heads = old_tensor.shape[0] // config["num_head_channels"] // 3 - - old_tensor = old_tensor.reshape((num_heads, 3 * channels // num_heads) + old_tensor.shape[1:]) - query, key, value = old_tensor.split(channels // num_heads, dim=1) - - checkpoint[path_map["query"]] = query.reshape(target_shape) - checkpoint[path_map["key"]] = key.reshape(target_shape) - checkpoint[path_map["value"]] = value.reshape(target_shape) - - for path in paths: - new_path = path["new"] - - # These have already been assigned - if attention_paths_to_split is not None and new_path in attention_paths_to_split: - continue - - # Global renaming happens here - new_path = new_path.replace("middle_block.0", "mid_block.resnets.0") - new_path = new_path.replace("middle_block.1", "mid_block.attentions.0") - new_path = new_path.replace("middle_block.2", "mid_block.resnets.1") - - if additional_replacements is not None: - for replacement in additional_replacements: - new_path = new_path.replace(replacement["old"], replacement["new"]) - - # proj_attn.weight has to be converted from conv 1D to linear - is_attn_weight = "proj_attn.weight" in new_path or ("attentions" in new_path and "to_" in new_path) - shape = old_checkpoint[path["old"]].shape - if is_attn_weight and len(shape) == 3: - checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0] - elif is_attn_weight and len(shape) == 4: - checkpoint[new_path] = old_checkpoint[path["old"]][:, :, 0, 0] - else: - checkpoint[new_path] = old_checkpoint[path["old"]] - - -def conv_attn_to_linear(checkpoint): - keys = list(checkpoint.keys()) - attn_keys = ["query.weight", "key.weight", "value.weight"] - for key in keys: - if ".".join(key.split(".")[-2:]) in attn_keys: - if checkpoint[key].ndim > 2: - checkpoint[key] = checkpoint[key][:, :, 0, 0] - elif "proj_attn.weight" in key: - if checkpoint[key].ndim > 2: - checkpoint[key] = checkpoint[key][:, :, 0] - - -def create_unet_diffusers_config(original_config, image_size: int, controlnet=False): - """ - Creates a config for the diffusers based on the config of the LDM model. - """ - if controlnet: - unet_params = original_config.model.params.control_stage_config.params - else: - if "unet_config" in original_config.model.params and original_config.model.params.unet_config is not None: - unet_params = original_config.model.params.unet_config.params - else: - unet_params = original_config.model.params.network_config.params - - vae_params = original_config.model.params.first_stage_config.params.ddconfig - - block_out_channels = [unet_params.model_channels * mult for mult in unet_params.channel_mult] - - down_block_types = [] - resolution = 1 - for i in range(len(block_out_channels)): - block_type = "CrossAttnDownBlock2D" if resolution in unet_params.attention_resolutions else "DownBlock2D" - down_block_types.append(block_type) - if i != len(block_out_channels) - 1: - resolution *= 2 - - up_block_types = [] - for _i in range(len(block_out_channels)): - block_type = "CrossAttnUpBlock2D" if resolution in unet_params.attention_resolutions else "UpBlock2D" - up_block_types.append(block_type) - resolution //= 2 - - if unet_params.transformer_depth is not None: - transformer_layers_per_block = ( - unet_params.transformer_depth - if isinstance(unet_params.transformer_depth, int) - else list(unet_params.transformer_depth) - ) - else: - transformer_layers_per_block = 1 - - vae_scale_factor = 2 ** (len(vae_params.ch_mult) - 1) - - head_dim = unet_params.num_heads if "num_heads" in unet_params else None - use_linear_projection = ( - unet_params.use_linear_in_transformer if "use_linear_in_transformer" in unet_params else False - ) - if use_linear_projection: - # stable diffusion 2-base-512 and 2-768 - if head_dim is None: - head_dim_mult = unet_params.model_channels // unet_params.num_head_channels - head_dim = [head_dim_mult * c for c in list(unet_params.channel_mult)] - - class_embed_type = None - addition_embed_type = None - addition_time_embed_dim = None - projection_class_embeddings_input_dim = None - context_dim = None - - if unet_params.context_dim is not None: - context_dim = ( - unet_params.context_dim if isinstance(unet_params.context_dim, int) else unet_params.context_dim[0] - ) - - if "num_classes" in unet_params: - if unet_params.num_classes == "sequential": - if context_dim in [2048, 1280]: - # SDXL - addition_embed_type = "text_time" - addition_time_embed_dim = 256 - else: - class_embed_type = "projection" - assert "adm_in_channels" in unet_params - projection_class_embeddings_input_dim = unet_params.adm_in_channels - else: - raise NotImplementedError(f"Unknown conditional unet num_classes config: {unet_params.num_classes}") - - config = { - "sample_size": image_size // vae_scale_factor, - "in_channels": unet_params.in_channels, - "down_block_types": tuple(down_block_types), - "block_out_channels": tuple(block_out_channels), - "layers_per_block": unet_params.num_res_blocks, - "cross_attention_dim": context_dim, - "attention_head_dim": head_dim, - "use_linear_projection": use_linear_projection, - "class_embed_type": class_embed_type, - "addition_embed_type": addition_embed_type, - "addition_time_embed_dim": addition_time_embed_dim, - "projection_class_embeddings_input_dim": projection_class_embeddings_input_dim, - "transformer_layers_per_block": transformer_layers_per_block, - } - - if controlnet: - config["conditioning_channels"] = unet_params.hint_channels - else: - config["out_channels"] = unet_params.out_channels - config["up_block_types"] = tuple(up_block_types) - - return config - - -def create_vae_diffusers_config(original_config, image_size: int): - """ - Creates a config for the diffusers based on the config of the LDM model. - """ - vae_params = original_config.model.params.first_stage_config.params.ddconfig - _ = original_config.model.params.first_stage_config.params.embed_dim - - block_out_channels = [vae_params.ch * mult for mult in vae_params.ch_mult] - down_block_types = ["DownEncoderBlock2D"] * len(block_out_channels) - up_block_types = ["UpDecoderBlock2D"] * len(block_out_channels) - - config = { - "sample_size": image_size, - "in_channels": vae_params.in_channels, - "out_channels": vae_params.out_ch, - "down_block_types": tuple(down_block_types), - "up_block_types": tuple(up_block_types), - "block_out_channels": tuple(block_out_channels), - "latent_channels": vae_params.z_channels, - "layers_per_block": vae_params.num_res_blocks, - } - return config - - -def create_diffusers_schedular(original_config): - schedular = DDIMScheduler( - num_train_timesteps=original_config.model.params.timesteps, - beta_start=original_config.model.params.linear_start, - beta_end=original_config.model.params.linear_end, - beta_schedule="scaled_linear", - ) - return schedular - - -def create_ldm_bert_config(original_config): - bert_params = original_config.model.parms.cond_stage_config.params - config = LDMBertConfig( - d_model=bert_params.n_embed, - encoder_layers=bert_params.n_layer, - encoder_ffn_dim=bert_params.n_embed * 4, - ) - return config - - -def convert_ldm_unet_checkpoint( - checkpoint, config, path=None, extract_ema=False, controlnet=False, skip_extract_state_dict=False -): - """ - Takes a state dict and a config, and returns a converted checkpoint. - """ - - if skip_extract_state_dict: - unet_state_dict = checkpoint - else: - # extract state_dict for UNet - unet_state_dict = {} - keys = list(checkpoint.keys()) - - if controlnet: - unet_key = "control_model." - else: - unet_key = "model.diffusion_model." - - # at least a 100 parameters have to start with `model_ema` in order for the checkpoint to be EMA - if sum(k.startswith("model_ema") for k in keys) > 100 and extract_ema: - logger.warning(f"Checkpoint {path} has both EMA and non-EMA weights.") - logger.warning( - "In this conversion only the EMA weights are extracted. If you want to instead extract the non-EMA" - " weights (useful to continue fine-tuning), please make sure to remove the `--extract_ema` flag." - ) - for key in keys: - if key.startswith("model.diffusion_model"): - flat_ema_key = "model_ema." + "".join(key.split(".")[1:]) - unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(flat_ema_key) - else: - if sum(k.startswith("model_ema") for k in keys) > 100: - logger.warning( - "In this conversion only the non-EMA weights are extracted. If you want to instead extract the EMA" - " weights (usually better for inference), please make sure to add the `--extract_ema` flag." - ) - - for key in keys: - if key.startswith(unet_key): - unet_state_dict[key.replace(unet_key, "")] = checkpoint.pop(key) - - new_checkpoint = {} - - new_checkpoint["time_embedding.linear_1.weight"] = unet_state_dict["time_embed.0.weight"] - new_checkpoint["time_embedding.linear_1.bias"] = unet_state_dict["time_embed.0.bias"] - new_checkpoint["time_embedding.linear_2.weight"] = unet_state_dict["time_embed.2.weight"] - new_checkpoint["time_embedding.linear_2.bias"] = unet_state_dict["time_embed.2.bias"] - - if config["class_embed_type"] is None: - # No parameters to port - ... - elif config["class_embed_type"] == "timestep" or config["class_embed_type"] == "projection": - new_checkpoint["class_embedding.linear_1.weight"] = unet_state_dict["label_emb.0.0.weight"] - new_checkpoint["class_embedding.linear_1.bias"] = unet_state_dict["label_emb.0.0.bias"] - new_checkpoint["class_embedding.linear_2.weight"] = unet_state_dict["label_emb.0.2.weight"] - new_checkpoint["class_embedding.linear_2.bias"] = unet_state_dict["label_emb.0.2.bias"] - else: - raise NotImplementedError(f"Not implemented `class_embed_type`: {config['class_embed_type']}") - - if config["addition_embed_type"] == "text_time": - new_checkpoint["add_embedding.linear_1.weight"] = unet_state_dict["label_emb.0.0.weight"] - new_checkpoint["add_embedding.linear_1.bias"] = unet_state_dict["label_emb.0.0.bias"] - new_checkpoint["add_embedding.linear_2.weight"] = unet_state_dict["label_emb.0.2.weight"] - new_checkpoint["add_embedding.linear_2.bias"] = unet_state_dict["label_emb.0.2.bias"] - - new_checkpoint["conv_in.weight"] = unet_state_dict["input_blocks.0.0.weight"] - new_checkpoint["conv_in.bias"] = unet_state_dict["input_blocks.0.0.bias"] - - if not controlnet: - new_checkpoint["conv_norm_out.weight"] = unet_state_dict["out.0.weight"] - new_checkpoint["conv_norm_out.bias"] = unet_state_dict["out.0.bias"] - new_checkpoint["conv_out.weight"] = unet_state_dict["out.2.weight"] - new_checkpoint["conv_out.bias"] = unet_state_dict["out.2.bias"] - - # Retrieves the keys for the input blocks only - num_input_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "input_blocks" in layer}) - input_blocks = { - layer_id: [key for key in unet_state_dict if f"input_blocks.{layer_id}" in key] - for layer_id in range(num_input_blocks) - } - - # Retrieves the keys for the middle blocks only - num_middle_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "middle_block" in layer}) - middle_blocks = { - layer_id: [key for key in unet_state_dict if f"middle_block.{layer_id}" in key] - for layer_id in range(num_middle_blocks) - } - - # Retrieves the keys for the output blocks only - num_output_blocks = len({".".join(layer.split(".")[:2]) for layer in unet_state_dict if "output_blocks" in layer}) - output_blocks = { - layer_id: [key for key in unet_state_dict if f"output_blocks.{layer_id}" in key] - for layer_id in range(num_output_blocks) - } - - for i in range(1, num_input_blocks): - block_id = (i - 1) // (config["layers_per_block"] + 1) - layer_in_block_id = (i - 1) % (config["layers_per_block"] + 1) - - resnets = [ - key for key in input_blocks[i] if f"input_blocks.{i}.0" in key and f"input_blocks.{i}.0.op" not in key - ] - attentions = [key for key in input_blocks[i] if f"input_blocks.{i}.1" in key] - - if f"input_blocks.{i}.0.op.weight" in unet_state_dict: - new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.weight"] = unet_state_dict.pop( - f"input_blocks.{i}.0.op.weight" - ) - new_checkpoint[f"down_blocks.{block_id}.downsamplers.0.conv.bias"] = unet_state_dict.pop( - f"input_blocks.{i}.0.op.bias" - ) - - paths = renew_resnet_paths(resnets) - meta_path = {"old": f"input_blocks.{i}.0", "new": f"down_blocks.{block_id}.resnets.{layer_in_block_id}"} - assign_to_checkpoint(paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config) - - if len(attentions): - paths = renew_attention_paths(attentions) - meta_path = {"old": f"input_blocks.{i}.1", "new": f"down_blocks.{block_id}.attentions.{layer_in_block_id}"} - assign_to_checkpoint( - paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config - ) - - resnet_0 = middle_blocks[0] - attentions = middle_blocks[1] - resnet_1 = middle_blocks[2] - - resnet_0_paths = renew_resnet_paths(resnet_0) - assign_to_checkpoint(resnet_0_paths, new_checkpoint, unet_state_dict, config=config) - - resnet_1_paths = renew_resnet_paths(resnet_1) - assign_to_checkpoint(resnet_1_paths, new_checkpoint, unet_state_dict, config=config) - - attentions_paths = renew_attention_paths(attentions) - meta_path = {"old": "middle_block.1", "new": "mid_block.attentions.0"} - assign_to_checkpoint( - attentions_paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config - ) - - for i in range(num_output_blocks): - block_id = i // (config["layers_per_block"] + 1) - layer_in_block_id = i % (config["layers_per_block"] + 1) - output_block_layers = [shave_segments(name, 2) for name in output_blocks[i]] - output_block_list = {} - - for layer in output_block_layers: - layer_id, layer_name = layer.split(".")[0], shave_segments(layer, 1) - if layer_id in output_block_list: - output_block_list[layer_id].append(layer_name) - else: - output_block_list[layer_id] = [layer_name] - - if len(output_block_list) > 1: - resnets = [key for key in output_blocks[i] if f"output_blocks.{i}.0" in key] - attentions = [key for key in output_blocks[i] if f"output_blocks.{i}.1" in key] - - resnet_0_paths = renew_resnet_paths(resnets) - paths = renew_resnet_paths(resnets) - - meta_path = {"old": f"output_blocks.{i}.0", "new": f"up_blocks.{block_id}.resnets.{layer_in_block_id}"} - assign_to_checkpoint( - paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config - ) - - output_block_list = {k: sorted(v) for k, v in output_block_list.items()} - if ["conv.bias", "conv.weight"] in output_block_list.values(): - index = list(output_block_list.values()).index(["conv.bias", "conv.weight"]) - new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.weight"] = unet_state_dict[ - f"output_blocks.{i}.{index}.conv.weight" - ] - new_checkpoint[f"up_blocks.{block_id}.upsamplers.0.conv.bias"] = unet_state_dict[ - f"output_blocks.{i}.{index}.conv.bias" - ] - - # Clear attentions as they have been attributed above. - if len(attentions) == 2: - attentions = [] - - if len(attentions): - paths = renew_attention_paths(attentions) - meta_path = { - "old": f"output_blocks.{i}.1", - "new": f"up_blocks.{block_id}.attentions.{layer_in_block_id}", - } - assign_to_checkpoint( - paths, new_checkpoint, unet_state_dict, additional_replacements=[meta_path], config=config - ) - else: - resnet_0_paths = renew_resnet_paths(output_block_layers, n_shave_prefix_segments=1) - for path in resnet_0_paths: - old_path = ".".join(["output_blocks", str(i), path["old"]]) - new_path = ".".join(["up_blocks", str(block_id), "resnets", str(layer_in_block_id), path["new"]]) - - new_checkpoint[new_path] = unet_state_dict[old_path] - - if controlnet: - # conditioning embedding - - orig_index = 0 - - new_checkpoint["controlnet_cond_embedding.conv_in.weight"] = unet_state_dict.pop( - f"input_hint_block.{orig_index}.weight" - ) - new_checkpoint["controlnet_cond_embedding.conv_in.bias"] = unet_state_dict.pop( - f"input_hint_block.{orig_index}.bias" - ) - - orig_index += 2 - - diffusers_index = 0 - - while diffusers_index < 6: - new_checkpoint[f"controlnet_cond_embedding.blocks.{diffusers_index}.weight"] = unet_state_dict.pop( - f"input_hint_block.{orig_index}.weight" - ) - new_checkpoint[f"controlnet_cond_embedding.blocks.{diffusers_index}.bias"] = unet_state_dict.pop( - f"input_hint_block.{orig_index}.bias" - ) - diffusers_index += 1 - orig_index += 2 - - new_checkpoint["controlnet_cond_embedding.conv_out.weight"] = unet_state_dict.pop( - f"input_hint_block.{orig_index}.weight" - ) - new_checkpoint["controlnet_cond_embedding.conv_out.bias"] = unet_state_dict.pop( - f"input_hint_block.{orig_index}.bias" - ) - - # down blocks - for i in range(num_input_blocks): - new_checkpoint[f"controlnet_down_blocks.{i}.weight"] = unet_state_dict.pop(f"zero_convs.{i}.0.weight") - new_checkpoint[f"controlnet_down_blocks.{i}.bias"] = unet_state_dict.pop(f"zero_convs.{i}.0.bias") - - # mid block - new_checkpoint["controlnet_mid_block.weight"] = unet_state_dict.pop("middle_block_out.0.weight") - new_checkpoint["controlnet_mid_block.bias"] = unet_state_dict.pop("middle_block_out.0.bias") - - return new_checkpoint - - -def convert_ldm_vae_checkpoint(checkpoint, config): - # extract state dict for VAE - vae_state_dict = {} - keys = list(checkpoint.keys()) - vae_key = "first_stage_model." if any(k.startswith("first_stage_model.") for k in keys) else "" - for key in keys: - if key.startswith(vae_key): - vae_state_dict[key.replace(vae_key, "")] = checkpoint.get(key) - - new_checkpoint = {} - - new_checkpoint["encoder.conv_in.weight"] = vae_state_dict["encoder.conv_in.weight"] - new_checkpoint["encoder.conv_in.bias"] = vae_state_dict["encoder.conv_in.bias"] - new_checkpoint["encoder.conv_out.weight"] = vae_state_dict["encoder.conv_out.weight"] - new_checkpoint["encoder.conv_out.bias"] = vae_state_dict["encoder.conv_out.bias"] - new_checkpoint["encoder.conv_norm_out.weight"] = vae_state_dict["encoder.norm_out.weight"] - new_checkpoint["encoder.conv_norm_out.bias"] = vae_state_dict["encoder.norm_out.bias"] - - new_checkpoint["decoder.conv_in.weight"] = vae_state_dict["decoder.conv_in.weight"] - new_checkpoint["decoder.conv_in.bias"] = vae_state_dict["decoder.conv_in.bias"] - new_checkpoint["decoder.conv_out.weight"] = vae_state_dict["decoder.conv_out.weight"] - new_checkpoint["decoder.conv_out.bias"] = vae_state_dict["decoder.conv_out.bias"] - new_checkpoint["decoder.conv_norm_out.weight"] = vae_state_dict["decoder.norm_out.weight"] - new_checkpoint["decoder.conv_norm_out.bias"] = vae_state_dict["decoder.norm_out.bias"] - - new_checkpoint["quant_conv.weight"] = vae_state_dict["quant_conv.weight"] - new_checkpoint["quant_conv.bias"] = vae_state_dict["quant_conv.bias"] - new_checkpoint["post_quant_conv.weight"] = vae_state_dict["post_quant_conv.weight"] - new_checkpoint["post_quant_conv.bias"] = vae_state_dict["post_quant_conv.bias"] - - # Retrieves the keys for the encoder down blocks only - num_down_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "encoder.down" in layer}) - down_blocks = { - layer_id: [key for key in vae_state_dict if f"down.{layer_id}" in key] for layer_id in range(num_down_blocks) - } - - # Retrieves the keys for the decoder up blocks only - num_up_blocks = len({".".join(layer.split(".")[:3]) for layer in vae_state_dict if "decoder.up" in layer}) - up_blocks = { - layer_id: [key for key in vae_state_dict if f"up.{layer_id}" in key] for layer_id in range(num_up_blocks) - } - - for i in range(num_down_blocks): - resnets = [key for key in down_blocks[i] if f"down.{i}" in key and f"down.{i}.downsample" not in key] - - if f"encoder.down.{i}.downsample.conv.weight" in vae_state_dict: - new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.weight"] = vae_state_dict.pop( - f"encoder.down.{i}.downsample.conv.weight" - ) - new_checkpoint[f"encoder.down_blocks.{i}.downsamplers.0.conv.bias"] = vae_state_dict.pop( - f"encoder.down.{i}.downsample.conv.bias" - ) - - paths = renew_vae_resnet_paths(resnets) - meta_path = {"old": f"down.{i}.block", "new": f"down_blocks.{i}.resnets"} - assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) - - mid_resnets = [key for key in vae_state_dict if "encoder.mid.block" in key] - num_mid_res_blocks = 2 - for i in range(1, num_mid_res_blocks + 1): - resnets = [key for key in mid_resnets if f"encoder.mid.block_{i}" in key] - - paths = renew_vae_resnet_paths(resnets) - meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} - assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) - - mid_attentions = [key for key in vae_state_dict if "encoder.mid.attn" in key] - paths = renew_vae_attention_paths(mid_attentions) - meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} - assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) - conv_attn_to_linear(new_checkpoint) - - for i in range(num_up_blocks): - block_id = num_up_blocks - 1 - i - resnets = [ - key for key in up_blocks[block_id] if f"up.{block_id}" in key and f"up.{block_id}.upsample" not in key - ] - - if f"decoder.up.{block_id}.upsample.conv.weight" in vae_state_dict: - new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.weight"] = vae_state_dict[ - f"decoder.up.{block_id}.upsample.conv.weight" - ] - new_checkpoint[f"decoder.up_blocks.{i}.upsamplers.0.conv.bias"] = vae_state_dict[ - f"decoder.up.{block_id}.upsample.conv.bias" - ] - - paths = renew_vae_resnet_paths(resnets) - meta_path = {"old": f"up.{block_id}.block", "new": f"up_blocks.{i}.resnets"} - assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) - - mid_resnets = [key for key in vae_state_dict if "decoder.mid.block" in key] - num_mid_res_blocks = 2 - for i in range(1, num_mid_res_blocks + 1): - resnets = [key for key in mid_resnets if f"decoder.mid.block_{i}" in key] - - paths = renew_vae_resnet_paths(resnets) - meta_path = {"old": f"mid.block_{i}", "new": f"mid_block.resnets.{i - 1}"} - assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) - - mid_attentions = [key for key in vae_state_dict if "decoder.mid.attn" in key] - paths = renew_vae_attention_paths(mid_attentions) - meta_path = {"old": "mid.attn_1", "new": "mid_block.attentions.0"} - assign_to_checkpoint(paths, new_checkpoint, vae_state_dict, additional_replacements=[meta_path], config=config) - conv_attn_to_linear(new_checkpoint) - return new_checkpoint - - -def convert_ldm_bert_checkpoint(checkpoint, config): - def _copy_attn_layer(hf_attn_layer, pt_attn_layer): - hf_attn_layer.q_proj.weight.data = pt_attn_layer.to_q.weight - hf_attn_layer.k_proj.weight.data = pt_attn_layer.to_k.weight - hf_attn_layer.v_proj.weight.data = pt_attn_layer.to_v.weight - - hf_attn_layer.out_proj.weight = pt_attn_layer.to_out.weight - hf_attn_layer.out_proj.bias = pt_attn_layer.to_out.bias - - def _copy_linear(hf_linear, pt_linear): - hf_linear.weight = pt_linear.weight - hf_linear.bias = pt_linear.bias - - def _copy_layer(hf_layer, pt_layer): - # copy layer norms - _copy_linear(hf_layer.self_attn_layer_norm, pt_layer[0][0]) - _copy_linear(hf_layer.final_layer_norm, pt_layer[1][0]) - - # copy attn - _copy_attn_layer(hf_layer.self_attn, pt_layer[0][1]) - - # copy MLP - pt_mlp = pt_layer[1][1] - _copy_linear(hf_layer.fc1, pt_mlp.net[0][0]) - _copy_linear(hf_layer.fc2, pt_mlp.net[2]) - - def _copy_layers(hf_layers, pt_layers): - for i, hf_layer in enumerate(hf_layers): - if i != 0: - i += i - pt_layer = pt_layers[i : i + 2] - _copy_layer(hf_layer, pt_layer) - - hf_model = LDMBertModel(config).eval() - - # copy embeds - hf_model.model.embed_tokens.weight = checkpoint.transformer.token_emb.weight - hf_model.model.embed_positions.weight.data = checkpoint.transformer.pos_emb.emb.weight - - # copy layer norm - _copy_linear(hf_model.model.layer_norm, checkpoint.transformer.norm) - - # copy hidden layers - _copy_layers(hf_model.model.layers, checkpoint.transformer.attn_layers.layers) - - _copy_linear(hf_model.to_logits, checkpoint.transformer.to_logits) - - return hf_model - - -def convert_ldm_clip_checkpoint(checkpoint, local_files_only=False, text_encoder=None): - if text_encoder is None: - config = CLIPTextConfig.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") - - ctx = init_empty_weights if is_accelerate_available() else nullcontext - with ctx(): - text_model = CLIPTextModel(config) - - keys = list(checkpoint.keys()) - - text_model_dict = {} - - remove_prefixes = ["cond_stage_model.transformer", "conditioner.embedders.0.transformer"] - - for key in keys: - for prefix in remove_prefixes: - if key.startswith(prefix): - text_model_dict[key[len(prefix + ".") :]] = checkpoint[key] - - if is_accelerate_available(): - for param_name, param in text_model_dict.items(): - set_module_tensor_to_device(text_model, param_name, "cpu", value=param) - else: - text_model.load_state_dict(text_model_dict) - - return text_model - - -textenc_conversion_lst = [ - ("positional_embedding", "text_model.embeddings.position_embedding.weight"), - ("token_embedding.weight", "text_model.embeddings.token_embedding.weight"), - ("ln_final.weight", "text_model.final_layer_norm.weight"), - ("ln_final.bias", "text_model.final_layer_norm.bias"), - ("text_projection", "text_projection.weight"), -] -textenc_conversion_map = {x[0]: x[1] for x in textenc_conversion_lst} - -textenc_transformer_conversion_lst = [ - # (stable-diffusion, HF Diffusers) - ("resblocks.", "text_model.encoder.layers."), - ("ln_1", "layer_norm1"), - ("ln_2", "layer_norm2"), - (".c_fc.", ".fc1."), - (".c_proj.", ".fc2."), - (".attn", ".self_attn"), - ("ln_final.", "transformer.text_model.final_layer_norm."), - ("token_embedding.weight", "transformer.text_model.embeddings.token_embedding.weight"), - ("positional_embedding", "transformer.text_model.embeddings.position_embedding.weight"), -] -protected = {re.escape(x[0]): x[1] for x in textenc_transformer_conversion_lst} -textenc_pattern = re.compile("|".join(protected.keys())) - - -def convert_paint_by_example_checkpoint(checkpoint): - config = CLIPVisionConfig.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") - model = PaintByExampleImageEncoder(config) - - keys = list(checkpoint.keys()) - - text_model_dict = {} - - for key in keys: - if key.startswith("cond_stage_model.transformer"): - text_model_dict[key[len("cond_stage_model.transformer.") :]] = checkpoint[key] - - # load clip vision - model.model.load_state_dict(text_model_dict) - - # load mapper - keys_mapper = { - k[len("cond_stage_model.mapper.res") :]: v - for k, v in checkpoint.items() - if k.startswith("cond_stage_model.mapper") - } - - MAPPING = { - "attn.c_qkv": ["attn1.to_q", "attn1.to_k", "attn1.to_v"], - "attn.c_proj": ["attn1.to_out.0"], - "ln_1": ["norm1"], - "ln_2": ["norm3"], - "mlp.c_fc": ["ff.net.0.proj"], - "mlp.c_proj": ["ff.net.2"], - } - - mapped_weights = {} - for key, value in keys_mapper.items(): - prefix = key[: len("blocks.i")] - suffix = key.split(prefix)[-1].split(".")[-1] - name = key.split(prefix)[-1].split(suffix)[0][1:-1] - mapped_names = MAPPING[name] - - num_splits = len(mapped_names) - for i, mapped_name in enumerate(mapped_names): - new_name = ".".join([prefix, mapped_name, suffix]) - shape = value.shape[0] // num_splits - mapped_weights[new_name] = value[i * shape : (i + 1) * shape] - - model.mapper.load_state_dict(mapped_weights) - - # load final layer norm - model.final_layer_norm.load_state_dict( - { - "bias": checkpoint["cond_stage_model.final_ln.bias"], - "weight": checkpoint["cond_stage_model.final_ln.weight"], - } - ) - - # load final proj - model.proj_out.load_state_dict( - { - "bias": checkpoint["proj_out.bias"], - "weight": checkpoint["proj_out.weight"], - } - ) - - # load uncond vector - model.uncond_vector.data = torch.nn.Parameter(checkpoint["learnable_vector"]) - return model - - -def convert_open_clip_checkpoint( - checkpoint, config_name, prefix="cond_stage_model.model.", has_projection=False, **config_kwargs -): - # text_model = CLIPTextModel.from_pretrained("stabilityai/stable-diffusion-2", subfolder="text_encoder") - # text_model = CLIPTextModelWithProjection.from_pretrained( - # "laion/CLIP-ViT-bigG-14-laion2B-39B-b160k", projection_dim=1280 - # ) - config = CLIPTextConfig.from_pretrained(config_name, **config_kwargs) - - ctx = init_empty_weights if is_accelerate_available() else nullcontext - with ctx(): - text_model = CLIPTextModelWithProjection(config) if has_projection else CLIPTextModel(config) - - keys = list(checkpoint.keys()) - - keys_to_ignore = [] - if config_name == "stabilityai/stable-diffusion-2" and config.num_hidden_layers == 23: - # make sure to remove all keys > 22 - keys_to_ignore += [k for k in keys if k.startswith("cond_stage_model.model.transformer.resblocks.23")] - keys_to_ignore += ["cond_stage_model.model.text_projection"] - - text_model_dict = {} - - if prefix + "text_projection" in checkpoint: - d_model = int(checkpoint[prefix + "text_projection"].shape[0]) - else: - d_model = 1024 - - text_model_dict["text_model.embeddings.position_ids"] = text_model.text_model.embeddings.get_buffer("position_ids") - - for key in keys: - if key in keys_to_ignore: - continue - if key[len(prefix) :] in textenc_conversion_map: - if key.endswith("text_projection"): - value = checkpoint[key].T.contiguous() - else: - value = checkpoint[key] - - text_model_dict[textenc_conversion_map[key[len(prefix) :]]] = value - - if key.startswith(prefix + "transformer."): - new_key = key[len(prefix + "transformer.") :] - if new_key.endswith(".in_proj_weight"): - new_key = new_key[: -len(".in_proj_weight")] - new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) - text_model_dict[new_key + ".q_proj.weight"] = checkpoint[key][:d_model, :] - text_model_dict[new_key + ".k_proj.weight"] = checkpoint[key][d_model : d_model * 2, :] - text_model_dict[new_key + ".v_proj.weight"] = checkpoint[key][d_model * 2 :, :] - elif new_key.endswith(".in_proj_bias"): - new_key = new_key[: -len(".in_proj_bias")] - new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) - text_model_dict[new_key + ".q_proj.bias"] = checkpoint[key][:d_model] - text_model_dict[new_key + ".k_proj.bias"] = checkpoint[key][d_model : d_model * 2] - text_model_dict[new_key + ".v_proj.bias"] = checkpoint[key][d_model * 2 :] - else: - new_key = textenc_pattern.sub(lambda m: protected[re.escape(m.group(0))], new_key) - - text_model_dict[new_key] = checkpoint[key] - - if is_accelerate_available(): - for param_name, param in text_model_dict.items(): - set_module_tensor_to_device(text_model, param_name, "cpu", value=param) - else: - text_model.load_state_dict(text_model_dict) - - return text_model - - -def stable_unclip_image_encoder(original_config): - """ - Returns the image processor and clip image encoder for the img2img unclip pipeline. - - We currently know of two types of stable unclip models which separately use the clip and the openclip image - encoders. - """ - - image_embedder_config = original_config.model.params.embedder_config - - sd_clip_image_embedder_class = image_embedder_config.target - sd_clip_image_embedder_class = sd_clip_image_embedder_class.split(".")[-1] - - if sd_clip_image_embedder_class == "ClipImageEmbedder": - clip_model_name = image_embedder_config.params.model - - if clip_model_name == "ViT-L/14": - feature_extractor = CLIPImageProcessor() - image_encoder = CLIPVisionModelWithProjection.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") - else: - raise NotImplementedError(f"Unknown CLIP checkpoint name in stable diffusion checkpoint {clip_model_name}") - - elif sd_clip_image_embedder_class == "FrozenOpenCLIPImageEmbedder": - feature_extractor = CLIPImageProcessor() - # InvokeAI doesn't use CLIPVisionModelWithProjection so it isn't in the core - if this code is hit a download will occur - image_encoder = CLIPVisionModelWithProjection.from_pretrained( - CONVERT_MODEL_ROOT / "CLIP-ViT-H-14-laion2B-s32B-b79K" - ) - else: - raise NotImplementedError( - f"Unknown CLIP image embedder class in stable diffusion checkpoint {sd_clip_image_embedder_class}" - ) - - return feature_extractor, image_encoder - - -def stable_unclip_image_noising_components( - original_config, clip_stats_path: Optional[str] = None, device: Optional[str] = None -): - """ - Returns the noising components for the img2img and txt2img unclip pipelines. - - Converts the stability noise augmentor into - 1. a `StableUnCLIPImageNormalizer` for holding the CLIP stats - 2. a `DDPMScheduler` for holding the noise schedule - - If the noise augmentor config specifies a clip stats path, the `clip_stats_path` must be provided. - """ - noise_aug_config = original_config.model.params.noise_aug_config - noise_aug_class = noise_aug_config.target - noise_aug_class = noise_aug_class.split(".")[-1] - - if noise_aug_class == "CLIPEmbeddingNoiseAugmentation": - noise_aug_config = noise_aug_config.params - embedding_dim = noise_aug_config.timestep_dim - max_noise_level = noise_aug_config.noise_schedule_config.timesteps - beta_schedule = noise_aug_config.noise_schedule_config.beta_schedule - - image_normalizer = StableUnCLIPImageNormalizer(embedding_dim=embedding_dim) - image_noising_scheduler = DDPMScheduler(num_train_timesteps=max_noise_level, beta_schedule=beta_schedule) - - if "clip_stats_path" in noise_aug_config: - if clip_stats_path is None: - raise ValueError("This stable unclip config requires a `clip_stats_path`") - - clip_mean, clip_std = torch.load(clip_stats_path, map_location=device) - clip_mean = clip_mean[None, :] - clip_std = clip_std[None, :] - - clip_stats_state_dict = { - "mean": clip_mean, - "std": clip_std, - } - - image_normalizer.load_state_dict(clip_stats_state_dict) - else: - raise NotImplementedError(f"Unknown noise augmentor class: {noise_aug_class}") - - return image_normalizer, image_noising_scheduler - - -def convert_controlnet_checkpoint( - checkpoint, - original_config, - checkpoint_path, - image_size, - upcast_attention, - extract_ema, - use_linear_projection=None, - cross_attention_dim=None, - precision: Optional[torch.dtype] = None, -): - ctrlnet_config = create_unet_diffusers_config(original_config, image_size=image_size, controlnet=True) - ctrlnet_config["upcast_attention"] = upcast_attention - - ctrlnet_config.pop("sample_size") - original_config = ctrlnet_config.copy() - - ctrlnet_config.pop("addition_embed_type") - ctrlnet_config.pop("addition_time_embed_dim") - ctrlnet_config.pop("transformer_layers_per_block") - - if use_linear_projection is not None: - ctrlnet_config["use_linear_projection"] = use_linear_projection - - if cross_attention_dim is not None: - ctrlnet_config["cross_attention_dim"] = cross_attention_dim - - controlnet = ControlNetModel(**ctrlnet_config) - - # Some controlnet ckpt files are distributed independently from the rest of the - # model components i.e. https://huggingface.co/thibaud/controlnet-sd21/ - if "time_embed.0.weight" in checkpoint: - skip_extract_state_dict = True - else: - skip_extract_state_dict = False - - converted_ctrl_checkpoint = convert_ldm_unet_checkpoint( - checkpoint, - original_config, - path=checkpoint_path, - extract_ema=extract_ema, - controlnet=True, - skip_extract_state_dict=skip_extract_state_dict, - ) - - controlnet.load_state_dict(converted_ctrl_checkpoint) - - return controlnet.to(precision) - - -def download_from_original_stable_diffusion_ckpt( - checkpoint_path: str, - model_version: BaseModelType, - model_variant: ModelVariantType, - original_config_file: str = None, - image_size: Optional[int] = None, - prediction_type: str = None, - model_type: str = None, - extract_ema: bool = False, - precision: Optional[torch.dtype] = None, - scheduler_type: str = "pndm", - num_in_channels: Optional[int] = None, - upcast_attention: Optional[bool] = None, - device: str = None, - from_safetensors: bool = False, - stable_unclip: Optional[str] = None, - stable_unclip_prior: Optional[str] = None, - clip_stats_path: Optional[str] = None, - controlnet: Optional[bool] = None, - load_safety_checker: bool = True, - pipeline_class: DiffusionPipeline = None, - local_files_only=False, - vae_path=None, - text_encoder=None, - tokenizer=None, - scan_needed: bool = True, -) -> DiffusionPipeline: - """ - Load a Stable Diffusion pipeline object from a CompVis-style `.ckpt`/`.safetensors` file and (ideally) a `.yaml` - config file. - - Although many of the arguments can be automatically inferred, some of these rely on brittle checks against the - global step count, which will likely fail for models that have undergone further fine-tuning. Therefore, it is - recommended that you override the default values and/or supply an `original_config_file` wherever possible. - - Args: - checkpoint_path (`str`): Path to `.ckpt` file. - original_config_file (`str`): - Path to `.yaml` config file corresponding to the original architecture. If `None`, will be automatically - inferred by looking for a key that only exists in SD2.0 models. - image_size (`int`, *optional*, defaults to 512): - The image size that the model was trained on. Use 512 for Stable Diffusion v1.X and Stable Diffusion v2 - Base. Use 768 for Stable Diffusion v2. - prediction_type (`str`, *optional*): - The prediction type that the model was trained on. Use `'epsilon'` for Stable Diffusion v1.X and Stable - Diffusion v2 Base. Use `'v_prediction'` for Stable Diffusion v2. - num_in_channels (`int`, *optional*, defaults to None): - The number of input channels. If `None`, it will be automatically inferred. - scheduler_type (`str`, *optional*, defaults to 'pndm'): - Type of scheduler to use. Should be one of `["pndm", "lms", "heun", "euler", "euler-ancestral", "dpm", - "ddim"]`. - model_type (`str`, *optional*, defaults to `None`): - The pipeline type. `None` to automatically infer, or one of `["FrozenOpenCLIPEmbedder", - "FrozenCLIPEmbedder", "PaintByExample"]`. - is_img2img (`bool`, *optional*, defaults to `False`): - Whether the model should be loaded as an img2img pipeline. - extract_ema (`bool`, *optional*, defaults to `False`): Only relevant for - checkpoints that have both EMA and non-EMA weights. Whether to extract the EMA weights or not. Defaults to - `False`. Pass `True` to extract the EMA weights. EMA weights usually yield higher quality images for - inference. Non-EMA weights are usually better to continue fine-tuning. - upcast_attention (`bool`, *optional*, defaults to `None`): - Whether the attention computation should always be upcasted. This is necessary when running stable - diffusion 2.1. - device (`str`, *optional*, defaults to `None`): - The device to use. Pass `None` to determine automatically. - from_safetensors (`str`, *optional*, defaults to `False`): - If `checkpoint_path` is in `safetensors` format, load checkpoint with safetensors instead of PyTorch. - load_safety_checker (`bool`, *optional*, defaults to `True`): - Whether to load the safety checker or not. Defaults to `True`. - pipeline_class (`str`, *optional*, defaults to `None`): - The pipeline class to use. Pass `None` to determine automatically. - local_files_only (`bool`, *optional*, defaults to `False`): - Whether or not to only look at local files (i.e., do not try to download the model). - text_encoder (`CLIPTextModel`, *optional*, defaults to `None`): - An instance of [CLIP](https://huggingface.co/docs/transformers/model_doc/clip#transformers.CLIPTextModel) - to use, specifically the [clip-vit-large-patch14](https://huggingface.co/openai/clip-vit-large-patch14) - variant. If this parameter is `None`, the function will load a new instance of [CLIP] by itself, if needed. - tokenizer (`CLIPTokenizer`, *optional*, defaults to `None`): - An instance of - [CLIPTokenizer](https://huggingface.co/docs/transformers/v4.21.0/en/model_doc/clip#transformers.CLIPTokenizer) - to use. If this parameter is `None`, the function will load a new instance of [CLIPTokenizer] by itself, if - needed. - precision (`torch.dtype`, *optional*, defauts to `None`): - If not provided the precision will be set to the precision of the original file. - return: A StableDiffusionPipeline object representing the passed-in `.ckpt`/`.safetensors` file. - """ - - # import pipelines here to avoid circular import error when using from_single_file method - from diffusers import ( - LDMTextToImagePipeline, - PaintByExamplePipeline, - StableDiffusionControlNetPipeline, - StableDiffusionInpaintPipeline, - StableDiffusionPipeline, - StableDiffusionXLImg2ImgPipeline, - StableDiffusionXLPipeline, - StableUnCLIPImg2ImgPipeline, - StableUnCLIPPipeline, - ) - - if pipeline_class is None: - pipeline_class = StableDiffusionPipeline if not controlnet else StableDiffusionControlNetPipeline - - if prediction_type == "v-prediction": - prediction_type = "v_prediction" - - if from_safetensors: - from safetensors.torch import load_file as safe_load - - checkpoint = safe_load(checkpoint_path, device="cpu") - else: - if scan_needed: - # scan model - scan_result = scan_file_path(checkpoint_path) - if scan_result.infected_files != 0: - raise Exception("The model {checkpoint_path} is potentially infected by malware. Aborting import.") - if device is None: - device = "cuda" if torch.cuda.is_available() else "cpu" - checkpoint = torch.load(checkpoint_path, map_location=device) - else: - checkpoint = torch.load(checkpoint_path, map_location=device) - - # Sometimes models don't have the global_step item - if "global_step" in checkpoint: - global_step = checkpoint["global_step"] - else: - logger.debug("global_step key not found in model") - global_step = None - - # NOTE: this while loop isn't great but this controlnet checkpoint has one additional - # "state_dict" key https://huggingface.co/thibaud/controlnet-canny-sd21 - while "state_dict" in checkpoint: - checkpoint = checkpoint["state_dict"] - - logger.debug(f"model_type = {model_type}; original_config_file = {original_config_file}") - - precision_probing_key = "model.diffusion_model.input_blocks.0.0.bias" - logger.debug(f"original checkpoint precision == {checkpoint[precision_probing_key].dtype}") - precision = precision or checkpoint[precision_probing_key].dtype - - if original_config_file is None: - key_name_v2_1 = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" - key_name_sd_xl_base = "conditioner.embedders.1.model.transformer.resblocks.9.mlp.c_proj.bias" - key_name_sd_xl_refiner = "conditioner.embedders.0.model.transformer.resblocks.9.mlp.c_proj.bias" - - # model_type = "v1" - config_url = ( - "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/configs/stable-diffusion/v1-inference.yaml" - ) - - if key_name_v2_1 in checkpoint and checkpoint[key_name_v2_1].shape[-1] == 1024: - # model_type = "v2" - config_url = "https://raw.githubusercontent.com/Stability-AI/stablediffusion/main/configs/stable-diffusion/v2-inference-v.yaml" - - if global_step == 110000: - # v2.1 needs to upcast attention - upcast_attention = True - elif key_name_sd_xl_base in checkpoint: - # only base xl has two text embedders - config_url = "https://raw.githubusercontent.com/Stability-AI/generative-models/main/configs/inference/sd_xl_base.yaml" - elif key_name_sd_xl_refiner in checkpoint: - # only refiner xl has embedder and one text embedders - config_url = "https://raw.githubusercontent.com/Stability-AI/generative-models/main/configs/inference/sd_xl_refiner.yaml" - - original_config_file = BytesIO(requests.get(config_url).content) - - original_config = OmegaConf.load(original_config_file) - if original_config["model"]["params"].get("use_ema") is not None: - extract_ema = original_config["model"]["params"]["use_ema"] - - if ( - model_version in [BaseModelType.StableDiffusion2, BaseModelType.StableDiffusion1] - and original_config["model"]["params"].get("parameterization") == "v" - ): - prediction_type = "v_prediction" - upcast_attention = True - image_size = 768 if model_version == BaseModelType.StableDiffusion2 else 512 - else: - prediction_type = "epsilon" - upcast_attention = False - image_size = 512 - - # Convert the text model. - if ( - model_type is None - and "cond_stage_config" in original_config.model.params - and original_config.model.params.cond_stage_config is not None - ): - model_type = original_config.model.params.cond_stage_config.target.split(".")[-1] - logger.debug(f"no `model_type` given, `model_type` inferred as: {model_type}") - elif model_type is None and original_config.model.params.network_config is not None: - if original_config.model.params.network_config.params.context_dim == 2048: - model_type = "SDXL" - else: - model_type = "SDXL-Refiner" - if image_size is None: - image_size = 1024 - - if num_in_channels is None and pipeline_class == StableDiffusionInpaintPipeline: - num_in_channels = 9 - elif num_in_channels is None: - num_in_channels = 4 - - if "unet_config" in original_config.model.params: - original_config["model"]["params"]["unet_config"]["params"]["in_channels"] = num_in_channels - - if ( - "parameterization" in original_config["model"]["params"] - and original_config["model"]["params"]["parameterization"] == "v" - ): - if prediction_type is None: - # NOTE: For stable diffusion 2 base it is recommended to pass `prediction_type=="epsilon"` - # as it relies on a brittle global step parameter here - prediction_type = "epsilon" if global_step == 875000 else "v_prediction" - if image_size is None: - # NOTE: For stable diffusion 2 base one has to pass `image_size==512` - # as it relies on a brittle global step parameter here - image_size = 512 if global_step == 875000 else 768 - else: - if prediction_type is None: - prediction_type = "epsilon" - if image_size is None: - image_size = 512 - - if controlnet is None and "control_stage_config" in original_config.model.params: - controlnet = convert_controlnet_checkpoint( - checkpoint, original_config, checkpoint_path, image_size, upcast_attention, extract_ema - ) - - num_train_timesteps = getattr(original_config.model.params, "timesteps", None) or 1000 - - if model_type in ["SDXL", "SDXL-Refiner"]: - scheduler_dict = { - "beta_schedule": "scaled_linear", - "beta_start": 0.00085, - "beta_end": 0.012, - "interpolation_type": "linear", - "num_train_timesteps": num_train_timesteps, - "prediction_type": "epsilon", - "sample_max_value": 1.0, - "set_alpha_to_one": False, - "skip_prk_steps": True, - "steps_offset": 1, - "timestep_spacing": "leading", - } - scheduler = EulerDiscreteScheduler.from_config(scheduler_dict) - scheduler_type = "euler" - else: - beta_start = getattr(original_config.model.params, "linear_start", None) or 0.02 - beta_end = getattr(original_config.model.params, "linear_end", None) or 0.085 - scheduler = DDIMScheduler( - beta_end=beta_end, - beta_schedule="scaled_linear", - beta_start=beta_start, - num_train_timesteps=num_train_timesteps, - steps_offset=1, - clip_sample=False, - set_alpha_to_one=False, - prediction_type=prediction_type, - ) - # make sure scheduler works correctly with DDIM - scheduler.register_to_config(clip_sample=False) - - if scheduler_type == "pndm": - config = dict(scheduler.config) - config["skip_prk_steps"] = True - scheduler = PNDMScheduler.from_config(config) - elif scheduler_type == "lms": - scheduler = LMSDiscreteScheduler.from_config(scheduler.config) - elif scheduler_type == "heun": - scheduler = HeunDiscreteScheduler.from_config(scheduler.config) - elif scheduler_type == "euler": - scheduler = EulerDiscreteScheduler.from_config(scheduler.config) - elif scheduler_type == "euler-ancestral": - scheduler = EulerAncestralDiscreteScheduler.from_config(scheduler.config) - elif scheduler_type == "dpm": - scheduler = DPMSolverMultistepScheduler.from_config(scheduler.config) - elif scheduler_type == "ddim": - scheduler = scheduler - else: - raise ValueError(f"Scheduler of type {scheduler_type} doesn't exist!") - - # Convert the UNet2DConditionModel model. - unet_config = create_unet_diffusers_config(original_config, image_size=image_size) - unet_config["upcast_attention"] = upcast_attention - converted_unet_checkpoint = convert_ldm_unet_checkpoint( - checkpoint, unet_config, path=checkpoint_path, extract_ema=extract_ema - ) - - ctx = init_empty_weights if is_accelerate_available() else nullcontext - with ctx(): - unet = UNet2DConditionModel(**unet_config) - - if is_accelerate_available(): - for param_name, param in converted_unet_checkpoint.items(): - set_module_tensor_to_device(unet, param_name, "cpu", value=param) - else: - unet.load_state_dict(converted_unet_checkpoint) - - # Convert the VAE model. - if vae_path is None: - vae_config = create_vae_diffusers_config(original_config, image_size=image_size) - converted_vae_checkpoint = convert_ldm_vae_checkpoint(checkpoint, vae_config) - - if ( - "model" in original_config - and "params" in original_config.model - and "scale_factor" in original_config.model.params - ): - vae_scaling_factor = original_config.model.params.scale_factor - else: - vae_scaling_factor = 0.18215 # default SD scaling factor - - vae_config["scaling_factor"] = vae_scaling_factor - - ctx = init_empty_weights if is_accelerate_available() else nullcontext - with ctx(): - vae = AutoencoderKL(**vae_config) - - if is_accelerate_available(): - for param_name, param in converted_vae_checkpoint.items(): - set_module_tensor_to_device(vae, param_name, "cpu", value=param) - else: - vae.load_state_dict(converted_vae_checkpoint) - else: - vae = AutoencoderKL.from_pretrained(vae_path) - - if model_type == "FrozenOpenCLIPEmbedder": - config_name = "stabilityai/stable-diffusion-2" - config_kwargs = {"subfolder": "text_encoder"} - - text_model = convert_open_clip_checkpoint(checkpoint, config_name, **config_kwargs) - tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "stable-diffusion-2-clip", subfolder="tokenizer") - - if stable_unclip is None: - if controlnet: - pipe = pipeline_class( - vae=vae.to(precision), - text_encoder=text_model.to(precision), - tokenizer=tokenizer, - unet=unet.to(precision), - scheduler=scheduler, - controlnet=controlnet, - safety_checker=None, - feature_extractor=None, - requires_safety_checker=False, - ) - else: - pipe = pipeline_class( - vae=vae.to(precision), - text_encoder=text_model.to(precision), - tokenizer=tokenizer, - unet=unet.to(precision), - scheduler=scheduler, - safety_checker=None, - feature_extractor=None, - requires_safety_checker=False, - ) - else: - image_normalizer, image_noising_scheduler = stable_unclip_image_noising_components( - original_config, clip_stats_path=clip_stats_path, device=device - ) - - if stable_unclip == "img2img": - feature_extractor, image_encoder = stable_unclip_image_encoder(original_config) - - pipe = StableUnCLIPImg2ImgPipeline( - # image encoding components - feature_extractor=feature_extractor, - image_encoder=image_encoder, - # image noising components - image_normalizer=image_normalizer, - image_noising_scheduler=image_noising_scheduler, - # regular denoising components - tokenizer=tokenizer, - text_encoder=text_model.to(precision), - unet=unet.to(precision), - scheduler=scheduler, - # vae - vae=vae, - ) - elif stable_unclip == "txt2img": - if stable_unclip_prior is None or stable_unclip_prior == "karlo": - karlo_model = "kakaobrain/karlo-v1-alpha" - prior = PriorTransformer.from_pretrained(karlo_model, subfolder="prior") - - prior_tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") - prior_text_model = CLIPTextModelWithProjection.from_pretrained( - CONVERT_MODEL_ROOT / "clip-vit-large-patch14" - ) - - prior_scheduler = UnCLIPScheduler.from_pretrained(karlo_model, subfolder="prior_scheduler") - prior_scheduler = DDPMScheduler.from_config(prior_scheduler.config) - else: - raise NotImplementedError(f"unknown prior for stable unclip model: {stable_unclip_prior}") - - pipe = StableUnCLIPPipeline( - # prior components - prior_tokenizer=prior_tokenizer, - prior_text_encoder=prior_text_model, - prior=prior, - prior_scheduler=prior_scheduler, - # image noising components - image_normalizer=image_normalizer, - image_noising_scheduler=image_noising_scheduler, - # regular denoising components - tokenizer=tokenizer, - text_encoder=text_model, - unet=unet, - scheduler=scheduler, - # vae - vae=vae, - ) - else: - raise NotImplementedError(f"unknown `stable_unclip` type: {stable_unclip}") - elif model_type == "PaintByExample": - vision_model = convert_paint_by_example_checkpoint(checkpoint) - tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") - feature_extractor = AutoFeatureExtractor.from_pretrained(CONVERT_MODEL_ROOT / "stable-diffusion-safety-checker") - pipe = PaintByExamplePipeline( - vae=vae, - image_encoder=vision_model, - unet=unet, - scheduler=scheduler, - safety_checker=None, - feature_extractor=feature_extractor, - ) - elif model_type == "FrozenCLIPEmbedder": - text_model = convert_ldm_clip_checkpoint( - checkpoint, local_files_only=local_files_only, text_encoder=text_encoder - ) - tokenizer = ( - CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") - if tokenizer is None - else tokenizer - ) - - if load_safety_checker: - safety_checker = StableDiffusionSafetyChecker.from_pretrained( - CONVERT_MODEL_ROOT / "stable-diffusion-safety-checker" - ) - feature_extractor = AutoFeatureExtractor.from_pretrained( - CONVERT_MODEL_ROOT / "stable-diffusion-safety-checker" - ) - else: - safety_checker = None - feature_extractor = None - - if controlnet: - pipe = pipeline_class( - vae=vae.to(precision), - text_encoder=text_model.to(precision), - tokenizer=tokenizer, - unet=unet.to(precision), - controlnet=controlnet, - scheduler=scheduler, - safety_checker=safety_checker, - feature_extractor=feature_extractor, - ) - else: - pipe = pipeline_class( - vae=vae.to(precision), - text_encoder=text_model.to(precision), - tokenizer=tokenizer, - unet=unet.to(precision), - scheduler=scheduler, - safety_checker=safety_checker, - feature_extractor=feature_extractor, - ) - elif model_type in ["SDXL", "SDXL-Refiner"]: - if model_type == "SDXL": - tokenizer = CLIPTokenizer.from_pretrained(CONVERT_MODEL_ROOT / "clip-vit-large-patch14") - text_encoder = convert_ldm_clip_checkpoint(checkpoint, local_files_only=local_files_only) - - tokenizer_name = CONVERT_MODEL_ROOT / "CLIP-ViT-bigG-14-laion2B-39B-b160k" - tokenizer_2 = CLIPTokenizer.from_pretrained(tokenizer_name, pad_token="!") - - config_name = tokenizer_name - config_kwargs = {"projection_dim": 1280} - text_encoder_2 = convert_open_clip_checkpoint( - checkpoint, config_name, prefix="conditioner.embedders.1.model.", has_projection=True, **config_kwargs - ) - - pipe = StableDiffusionXLPipeline( - vae=vae.to(precision), - text_encoder=text_encoder.to(precision), - tokenizer=tokenizer, - text_encoder_2=text_encoder_2.to(precision), - tokenizer_2=tokenizer_2, - unet=unet.to(precision), - scheduler=scheduler, - force_zeros_for_empty_prompt=True, - ) - else: - tokenizer = None - text_encoder = None - tokenizer_name = CONVERT_MODEL_ROOT / "CLIP-ViT-bigG-14-laion2B-39B-b160k" - tokenizer_2 = CLIPTokenizer.from_pretrained(tokenizer_name, pad_token="!") - - config_name = tokenizer_name - config_kwargs = {"projection_dim": 1280} - text_encoder_2 = convert_open_clip_checkpoint( - checkpoint, config_name, prefix="conditioner.embedders.0.model.", has_projection=True, **config_kwargs - ) - - pipe = StableDiffusionXLImg2ImgPipeline( - vae=vae.to(precision), - text_encoder=text_encoder, - tokenizer=tokenizer, - text_encoder_2=text_encoder_2, - tokenizer_2=tokenizer_2, - unet=unet.to(precision), - scheduler=scheduler, - requires_aesthetics_score=True, - force_zeros_for_empty_prompt=False, - ) - else: - text_config = create_ldm_bert_config(original_config) - text_model = convert_ldm_bert_checkpoint(checkpoint, text_config) - tokenizer = BertTokenizerFast.from_pretrained(CONVERT_MODEL_ROOT / "bert-base-uncased") - pipe = LDMTextToImagePipeline(vqvae=vae, bert=text_model, tokenizer=tokenizer, unet=unet, scheduler=scheduler) - - return pipe - - -def download_controlnet_from_original_ckpt( - checkpoint_path: str, - original_config_file: str, - image_size: int = 512, - extract_ema: bool = False, - precision: Optional[torch.dtype] = None, - num_in_channels: Optional[int] = None, - upcast_attention: Optional[bool] = None, - device: str = None, - from_safetensors: bool = False, - use_linear_projection: Optional[bool] = None, - cross_attention_dim: Optional[bool] = None, - scan_needed: bool = False, -) -> DiffusionPipeline: - if from_safetensors: - from safetensors import safe_open - - checkpoint = {} - with safe_open(checkpoint_path, framework="pt", device="cpu") as f: - for key in f.keys(): - checkpoint[key] = f.get_tensor(key) - else: - if scan_needed: - # scan model - scan_result = scan_file_path(checkpoint_path) - if scan_result.infected_files != 0: - raise Exception("The model {checkpoint_path} is potentially infected by malware. Aborting import.") - if device is None: - device = "cuda" if torch.cuda.is_available() else "cpu" - checkpoint = torch.load(checkpoint_path, map_location=device) - else: - checkpoint = torch.load(checkpoint_path, map_location=device) - - # NOTE: this while loop isn't great but this controlnet checkpoint has one additional - # "state_dict" key https://huggingface.co/thibaud/controlnet-canny-sd21 - while "state_dict" in checkpoint: - checkpoint = checkpoint["state_dict"] - - # use original precision - precision_probing_key = "input_blocks.0.0.bias" - ckpt_precision = checkpoint[precision_probing_key].dtype - logger.debug(f"original controlnet precision = {ckpt_precision}") - precision = precision or ckpt_precision - - original_config = OmegaConf.load(original_config_file) - - if num_in_channels is not None: - original_config["model"]["params"]["unet_config"]["params"]["in_channels"] = num_in_channels - - if "control_stage_config" not in original_config.model.params: - raise ValueError("`control_stage_config` not present in original config") - - controlnet = convert_controlnet_checkpoint( - checkpoint, - original_config, - checkpoint_path, - image_size, - upcast_attention, - extract_ema, - use_linear_projection=use_linear_projection, - cross_attention_dim=cross_attention_dim, - ) - - return controlnet.to(precision) - - -def convert_ldm_vae_to_diffusers(checkpoint, vae_config: DictConfig, image_size: int) -> AutoencoderKL: - vae_config = create_vae_diffusers_config(vae_config, image_size=image_size) - - converted_vae_checkpoint = convert_ldm_vae_checkpoint(checkpoint, vae_config) - - vae = AutoencoderKL(**vae_config) - vae.load_state_dict(converted_vae_checkpoint) - return vae - - -def convert_ckpt_to_diffusers( - checkpoint_path: Union[str, Path], - dump_path: Union[str, Path], - use_safetensors: bool = True, - **kwargs, -): - """ - Takes all the arguments of download_from_original_stable_diffusion_ckpt(), - and in addition a path-like object indicating the location of the desired diffusers - model to be written. - """ - pipe = download_from_original_stable_diffusion_ckpt(checkpoint_path, **kwargs) - - pipe.save_pretrained( - dump_path, - safe_serialization=use_safetensors, - ) - - -def convert_controlnet_to_diffusers( - checkpoint_path: Union[str, Path], - dump_path: Union[str, Path], - **kwargs, -): - """ - Takes all the arguments of download_controlnet_from_original_ckpt(), - and in addition a path-like object indicating the location of the desired diffusers - model to be written. - """ - pipe = download_controlnet_from_original_ckpt(checkpoint_path, **kwargs) - - pipe.save_pretrained(dump_path, safe_serialization=True) diff --git a/invokeai/backend/model_management_OLD/detect_baked_in_vae.py b/invokeai/backend/model_management_OLD/detect_baked_in_vae.py deleted file mode 100644 index 9118438548..0000000000 --- a/invokeai/backend/model_management_OLD/detect_baked_in_vae.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2024 Lincoln Stein and the InvokeAI Development Team -""" -This module exports the function has_baked_in_sdxl_vae(). -It returns True if an SDXL checkpoint model has the original SDXL 1.0 VAE, -which doesn't work properly in fp16 mode. -""" - -import hashlib -from pathlib import Path - -from safetensors.torch import load_file - -SDXL_1_0_VAE_HASH = "bc40b16c3a0fa4625abdfc01c04ffc21bf3cefa6af6c7768ec61eb1f1ac0da51" - - -def has_baked_in_sdxl_vae(checkpoint_path: Path) -> bool: - """Return true if the checkpoint contains a custom (non SDXL-1.0) VAE.""" - hash = _vae_hash(checkpoint_path) - return hash != SDXL_1_0_VAE_HASH - - -def _vae_hash(checkpoint_path: Path) -> str: - checkpoint = load_file(checkpoint_path, device="cpu") - vae_keys = [x for x in checkpoint.keys() if x.startswith("first_stage_model.")] - hash = hashlib.new("sha256") - for key in vae_keys: - value = checkpoint[key] - hash.update(bytes(key, "UTF-8")) - hash.update(bytes(str(value), "UTF-8")) - - return hash.hexdigest() diff --git a/invokeai/backend/model_management_OLD/lora.py b/invokeai/backend/model_management_OLD/lora.py deleted file mode 100644 index aed5eb60d5..0000000000 --- a/invokeai/backend/model_management_OLD/lora.py +++ /dev/null @@ -1,582 +0,0 @@ -from __future__ import annotations - -import pickle -from contextlib import contextmanager -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union - -import numpy as np -import torch -from compel.embeddings_provider import BaseTextualInversionManager -from diffusers.models import UNet2DConditionModel -from safetensors.torch import load_file -from transformers import CLIPTextModel, CLIPTokenizer - -from invokeai.app.shared.models import FreeUConfig -from invokeai.backend.model_management.model_load_optimizations import skip_torch_weight_init - -from .models.lora import LoRAModel - -""" -loras = [ - (lora_model1, 0.7), - (lora_model2, 0.4), -] -with LoRAHelper.apply_lora_unet(unet, loras): - # unet with applied loras -# unmodified unet - -""" - - -# TODO: rename smth like ModelPatcher and add TI method? -class ModelPatcher: - @staticmethod - def _resolve_lora_key(model: torch.nn.Module, lora_key: str, prefix: str) -> Tuple[str, torch.nn.Module]: - assert "." not in lora_key - - if not lora_key.startswith(prefix): - raise Exception(f"lora_key with invalid prefix: {lora_key}, {prefix}") - - module = model - module_key = "" - key_parts = lora_key[len(prefix) :].split("_") - - submodule_name = key_parts.pop(0) - - while len(key_parts) > 0: - try: - module = module.get_submodule(submodule_name) - module_key += "." + submodule_name - submodule_name = key_parts.pop(0) - except Exception: - submodule_name += "_" + key_parts.pop(0) - - module = module.get_submodule(submodule_name) - module_key = (module_key + "." + submodule_name).lstrip(".") - - return (module_key, module) - - @classmethod - @contextmanager - def apply_lora_unet( - cls, - unet: UNet2DConditionModel, - loras: List[Tuple[LoRAModel, float]], - ): - with cls.apply_lora(unet, loras, "lora_unet_"): - yield - - @classmethod - @contextmanager - def apply_lora_text_encoder( - cls, - text_encoder: CLIPTextModel, - loras: List[Tuple[LoRAModel, float]], - ): - with cls.apply_lora(text_encoder, loras, "lora_te_"): - yield - - @classmethod - @contextmanager - def apply_sdxl_lora_text_encoder( - cls, - text_encoder: CLIPTextModel, - loras: List[Tuple[LoRAModel, float]], - ): - with cls.apply_lora(text_encoder, loras, "lora_te1_"): - yield - - @classmethod - @contextmanager - def apply_sdxl_lora_text_encoder2( - cls, - text_encoder: CLIPTextModel, - loras: List[Tuple[LoRAModel, float]], - ): - with cls.apply_lora(text_encoder, loras, "lora_te2_"): - yield - - @classmethod - @contextmanager - def apply_lora( - cls, - model: torch.nn.Module, - loras: List[Tuple[LoRAModel, float]], # THIS IS INCORRECT. IT IS ACTUALLY A LoRAModelRaw - prefix: str, - ): - original_weights = {} - try: - with torch.no_grad(): - for lora, lora_weight in loras: - # assert lora.device.type == "cpu" - for layer_key, layer in lora.layers.items(): - if not layer_key.startswith(prefix): - continue - - # TODO(ryand): A non-negligible amount of time is currently spent resolving LoRA keys. This - # should be improved in the following ways: - # 1. The key mapping could be more-efficiently pre-computed. This would save time every time a - # LoRA model is applied. - # 2. From an API perspective, there's no reason that the `ModelPatcher` should be aware of the - # intricacies of Stable Diffusion key resolution. It should just expect the input LoRA - # weights to have valid keys. - module_key, module = cls._resolve_lora_key(model, layer_key, prefix) - - # All of the LoRA weight calculations will be done on the same device as the module weight. - # (Performance will be best if this is a CUDA device.) - device = module.weight.device - dtype = module.weight.dtype - - if module_key not in original_weights: - original_weights[module_key] = module.weight.detach().to(device="cpu", copy=True) - - layer_scale = layer.alpha / layer.rank if (layer.alpha and layer.rank) else 1.0 - - # We intentionally move to the target device first, then cast. Experimentally, this was found to - # be significantly faster for 16-bit CPU tensors being moved to a CUDA device than doing the - # same thing in a single call to '.to(...)'. - layer.to(device=device) - layer.to(dtype=torch.float32) - # TODO(ryand): Using torch.autocast(...) over explicit casting may offer a speed benefit on CUDA - # devices here. Experimentally, it was found to be very slow on CPU. More investigation needed. - layer_weight = layer.get_weight(module.weight) * (lora_weight * layer_scale) - layer.to(device="cpu") - - if module.weight.shape != layer_weight.shape: - # TODO: debug on lycoris - layer_weight = layer_weight.reshape(module.weight.shape) - - module.weight += layer_weight.to(dtype=dtype) - - yield # wait for context manager exit - - finally: - with torch.no_grad(): - for module_key, weight in original_weights.items(): - model.get_submodule(module_key).weight.copy_(weight) - - @classmethod - @contextmanager - def apply_ti( - cls, - tokenizer: CLIPTokenizer, - text_encoder: CLIPTextModel, - ti_list: List[Tuple[str, Any]], - ) -> Tuple[CLIPTokenizer, TextualInversionManager]: - init_tokens_count = None - new_tokens_added = None - - # TODO: This is required since Transformers 4.32 see - # https://github.com/huggingface/transformers/pull/25088 - # More information by NVIDIA: - # https://docs.nvidia.com/deeplearning/performance/dl-performance-matrix-multiplication/index.html#requirements-tc - # This value might need to be changed in the future and take the GPUs model into account as there seem - # to be ideal values for different GPUS. This value is temporary! - # For references to the current discussion please see https://github.com/invoke-ai/InvokeAI/pull/4817 - pad_to_multiple_of = 8 - - try: - # HACK: The CLIPTokenizer API does not include a way to remove tokens after calling add_tokens(...). As a - # workaround, we create a full copy of `tokenizer` so that its original behavior can be restored after - # exiting this `apply_ti(...)` context manager. - # - # In a previous implementation, the deep copy was obtained with `ti_tokenizer = copy.deepcopy(tokenizer)`, - # but a pickle roundtrip was found to be much faster (1 sec vs. 0.05 secs). - ti_tokenizer = pickle.loads(pickle.dumps(tokenizer)) - ti_manager = TextualInversionManager(ti_tokenizer) - init_tokens_count = text_encoder.resize_token_embeddings(None, pad_to_multiple_of).num_embeddings - - def _get_trigger(ti_name, index): - trigger = ti_name - if index > 0: - trigger += f"-!pad-{i}" - return f"<{trigger}>" - - def _get_ti_embedding(model_embeddings, ti): - print(f"DEBUG: model_embeddings={type(model_embeddings)}, ti={type(ti)}") - print(f"DEBUG: is it an nn.Module? {isinstance(model_embeddings, torch.nn.Module)}") - # for SDXL models, select the embedding that matches the text encoder's dimensions - if ti.embedding_2 is not None: - return ( - ti.embedding_2 - if ti.embedding_2.shape[1] == model_embeddings.weight.data[0].shape[0] - else ti.embedding - ) - else: - print(f"DEBUG: ti.embedding={type(ti.embedding)}") - return ti.embedding - - # modify tokenizer - new_tokens_added = 0 - for ti_name, ti in ti_list: - ti_embedding = _get_ti_embedding(text_encoder.get_input_embeddings(), ti) - - for i in range(ti_embedding.shape[0]): - new_tokens_added += ti_tokenizer.add_tokens(_get_trigger(ti_name, i)) - - # Modify text_encoder. - # resize_token_embeddings(...) constructs a new torch.nn.Embedding internally. Initializing the weights of - # this embedding is slow and unnecessary, so we wrap this step in skip_torch_weight_init() to save some - # time. - with skip_torch_weight_init(): - text_encoder.resize_token_embeddings(init_tokens_count + new_tokens_added, pad_to_multiple_of) - model_embeddings = text_encoder.get_input_embeddings() - - for ti_name, ti in ti_list: - ti_embedding = _get_ti_embedding(text_encoder.get_input_embeddings(), ti) - - ti_tokens = [] - for i in range(ti_embedding.shape[0]): - embedding = ti_embedding[i] - trigger = _get_trigger(ti_name, i) - - token_id = ti_tokenizer.convert_tokens_to_ids(trigger) - if token_id == ti_tokenizer.unk_token_id: - raise RuntimeError(f"Unable to find token id for token '{trigger}'") - - if model_embeddings.weight.data[token_id].shape != embedding.shape: - raise ValueError( - f"Cannot load embedding for {trigger}. It was trained on a model with token dimension" - f" {embedding.shape[0]}, but the current model has token dimension" - f" {model_embeddings.weight.data[token_id].shape[0]}." - ) - - model_embeddings.weight.data[token_id] = embedding.to( - device=text_encoder.device, dtype=text_encoder.dtype - ) - ti_tokens.append(token_id) - - if len(ti_tokens) > 1: - ti_manager.pad_tokens[ti_tokens[0]] = ti_tokens[1:] - - yield ti_tokenizer, ti_manager - - finally: - if init_tokens_count and new_tokens_added: - text_encoder.resize_token_embeddings(init_tokens_count, pad_to_multiple_of) - - @classmethod - @contextmanager - def apply_clip_skip( - cls, - text_encoder: CLIPTextModel, - clip_skip: int, - ): - skipped_layers = [] - try: - for _i in range(clip_skip): - skipped_layers.append(text_encoder.text_model.encoder.layers.pop(-1)) - - yield - - finally: - while len(skipped_layers) > 0: - text_encoder.text_model.encoder.layers.append(skipped_layers.pop()) - - @classmethod - @contextmanager - def apply_freeu( - cls, - unet: UNet2DConditionModel, - freeu_config: Optional[FreeUConfig] = None, - ): - did_apply_freeu = False - try: - if freeu_config is not None: - unet.enable_freeu(b1=freeu_config.b1, b2=freeu_config.b2, s1=freeu_config.s1, s2=freeu_config.s2) - did_apply_freeu = True - - yield - - finally: - if did_apply_freeu: - unet.disable_freeu() - - -class TextualInversionModel: - embedding: torch.Tensor # [n, 768]|[n, 1280] - embedding_2: Optional[torch.Tensor] = None # [n, 768]|[n, 1280] - for SDXL models - - @classmethod - def from_checkpoint( - cls, - file_path: Union[str, Path], - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - if not isinstance(file_path, Path): - file_path = Path(file_path) - - result = cls() # TODO: - - if file_path.suffix == ".safetensors": - state_dict = load_file(file_path.absolute().as_posix(), device="cpu") - else: - state_dict = torch.load(file_path, map_location="cpu") - - # both v1 and v2 format embeddings - # difference mostly in metadata - if "string_to_param" in state_dict: - if len(state_dict["string_to_param"]) > 1: - print( - f'Warn: Embedding "{file_path.name}" contains multiple tokens, which is not supported. The first', - " token will be used.", - ) - - result.embedding = next(iter(state_dict["string_to_param"].values())) - - # v3 (easynegative) - elif "emb_params" in state_dict: - result.embedding = state_dict["emb_params"] - - # v5(sdxl safetensors file) - elif "clip_g" in state_dict and "clip_l" in state_dict: - result.embedding = state_dict["clip_g"] - result.embedding_2 = state_dict["clip_l"] - - # v4(diffusers bin files) - else: - result.embedding = next(iter(state_dict.values())) - - if len(result.embedding.shape) == 1: - result.embedding = result.embedding.unsqueeze(0) - - if not isinstance(result.embedding, torch.Tensor): - raise ValueError(f"Invalid embeddings file: {file_path.name}") - - return result - - -class TextualInversionManager(BaseTextualInversionManager): - pad_tokens: Dict[int, List[int]] - tokenizer: CLIPTokenizer - - def __init__(self, tokenizer: CLIPTokenizer): - self.pad_tokens = {} - self.tokenizer = tokenizer - - def expand_textual_inversion_token_ids_if_necessary(self, token_ids: list[int]) -> list[int]: - if len(self.pad_tokens) == 0: - return token_ids - - if token_ids[0] == self.tokenizer.bos_token_id: - raise ValueError("token_ids must not start with bos_token_id") - if token_ids[-1] == self.tokenizer.eos_token_id: - raise ValueError("token_ids must not end with eos_token_id") - - new_token_ids = [] - for token_id in token_ids: - new_token_ids.append(token_id) - if token_id in self.pad_tokens: - new_token_ids.extend(self.pad_tokens[token_id]) - - # Do not exceed the max model input size - # The -2 here is compensating for compensate compel.embeddings_provider.get_token_ids(), - # which first removes and then adds back the start and end tokens. - max_length = list(self.tokenizer.max_model_input_sizes.values())[0] - 2 - if len(new_token_ids) > max_length: - new_token_ids = new_token_ids[0:max_length] - - return new_token_ids - - -class ONNXModelPatcher: - from diffusers import OnnxRuntimeModel - - from .models.base import IAIOnnxRuntimeModel - - @classmethod - @contextmanager - def apply_lora_unet( - cls, - unet: OnnxRuntimeModel, - loras: List[Tuple[LoRAModel, float]], - ): - with cls.apply_lora(unet, loras, "lora_unet_"): - yield - - @classmethod - @contextmanager - def apply_lora_text_encoder( - cls, - text_encoder: OnnxRuntimeModel, - loras: List[Tuple[LoRAModel, float]], - ): - with cls.apply_lora(text_encoder, loras, "lora_te_"): - yield - - # based on - # https://github.com/ssube/onnx-web/blob/ca2e436f0623e18b4cfe8a0363fcfcf10508acf7/api/onnx_web/convert/diffusion/lora.py#L323 - @classmethod - @contextmanager - def apply_lora( - cls, - model: IAIOnnxRuntimeModel, - loras: List[Tuple[LoRAModel, float]], - prefix: str, - ): - from .models.base import IAIOnnxRuntimeModel - - if not isinstance(model, IAIOnnxRuntimeModel): - raise Exception("Only IAIOnnxRuntimeModel models supported") - - orig_weights = {} - - try: - blended_loras = {} - - for lora, lora_weight in loras: - for layer_key, layer in lora.layers.items(): - if not layer_key.startswith(prefix): - continue - - layer.to(dtype=torch.float32) - layer_key = layer_key.replace(prefix, "") - # TODO: rewrite to pass original tensor weight(required by ia3) - layer_weight = layer.get_weight(None).detach().cpu().numpy() * lora_weight - if layer_key is blended_loras: - blended_loras[layer_key] += layer_weight - else: - blended_loras[layer_key] = layer_weight - - node_names = {} - for node in model.nodes.values(): - node_names[node.name.replace("/", "_").replace(".", "_").lstrip("_")] = node.name - - for layer_key, lora_weight in blended_loras.items(): - conv_key = layer_key + "_Conv" - gemm_key = layer_key + "_Gemm" - matmul_key = layer_key + "_MatMul" - - if conv_key in node_names or gemm_key in node_names: - if conv_key in node_names: - conv_node = model.nodes[node_names[conv_key]] - else: - conv_node = model.nodes[node_names[gemm_key]] - - weight_name = [n for n in conv_node.input if ".weight" in n][0] - orig_weight = model.tensors[weight_name] - - if orig_weight.shape[-2:] == (1, 1): - if lora_weight.shape[-2:] == (1, 1): - new_weight = orig_weight.squeeze((3, 2)) + lora_weight.squeeze((3, 2)) - else: - new_weight = orig_weight.squeeze((3, 2)) + lora_weight - - new_weight = np.expand_dims(new_weight, (2, 3)) - else: - if orig_weight.shape != lora_weight.shape: - new_weight = orig_weight + lora_weight.reshape(orig_weight.shape) - else: - new_weight = orig_weight + lora_weight - - orig_weights[weight_name] = orig_weight - model.tensors[weight_name] = new_weight.astype(orig_weight.dtype) - - elif matmul_key in node_names: - weight_node = model.nodes[node_names[matmul_key]] - matmul_name = [n for n in weight_node.input if "MatMul" in n][0] - - orig_weight = model.tensors[matmul_name] - new_weight = orig_weight + lora_weight.transpose() - - orig_weights[matmul_name] = orig_weight - model.tensors[matmul_name] = new_weight.astype(orig_weight.dtype) - - else: - # warn? err? - pass - - yield - - finally: - # restore original weights - for name, orig_weight in orig_weights.items(): - model.tensors[name] = orig_weight - - @classmethod - @contextmanager - def apply_ti( - cls, - tokenizer: CLIPTokenizer, - text_encoder: IAIOnnxRuntimeModel, - ti_list: List[Tuple[str, Any]], - ) -> Tuple[CLIPTokenizer, TextualInversionManager]: - from .models.base import IAIOnnxRuntimeModel - - if not isinstance(text_encoder, IAIOnnxRuntimeModel): - raise Exception("Only IAIOnnxRuntimeModel models supported") - - orig_embeddings = None - - try: - # HACK: The CLIPTokenizer API does not include a way to remove tokens after calling add_tokens(...). As a - # workaround, we create a full copy of `tokenizer` so that its original behavior can be restored after - # exiting this `apply_ti(...)` context manager. - # - # In a previous implementation, the deep copy was obtained with `ti_tokenizer = copy.deepcopy(tokenizer)`, - # but a pickle roundtrip was found to be much faster (1 sec vs. 0.05 secs). - ti_tokenizer = pickle.loads(pickle.dumps(tokenizer)) - ti_manager = TextualInversionManager(ti_tokenizer) - - def _get_trigger(ti_name, index): - trigger = ti_name - if index > 0: - trigger += f"-!pad-{i}" - return f"<{trigger}>" - - # modify text_encoder - orig_embeddings = text_encoder.tensors["text_model.embeddings.token_embedding.weight"] - - # modify tokenizer - new_tokens_added = 0 - for ti_name, ti in ti_list: - if ti.embedding_2 is not None: - ti_embedding = ( - ti.embedding_2 if ti.embedding_2.shape[1] == orig_embeddings.shape[0] else ti.embedding - ) - else: - ti_embedding = ti.embedding - - for i in range(ti_embedding.shape[0]): - new_tokens_added += ti_tokenizer.add_tokens(_get_trigger(ti_name, i)) - - embeddings = np.concatenate( - (np.copy(orig_embeddings), np.zeros((new_tokens_added, orig_embeddings.shape[1]))), - axis=0, - ) - - for ti_name, _ in ti_list: - ti_tokens = [] - for i in range(ti_embedding.shape[0]): - embedding = ti_embedding[i].detach().numpy() - trigger = _get_trigger(ti_name, i) - - token_id = ti_tokenizer.convert_tokens_to_ids(trigger) - if token_id == ti_tokenizer.unk_token_id: - raise RuntimeError(f"Unable to find token id for token '{trigger}'") - - if embeddings[token_id].shape != embedding.shape: - raise ValueError( - f"Cannot load embedding for {trigger}. It was trained on a model with token dimension" - f" {embedding.shape[0]}, but the current model has token dimension" - f" {embeddings[token_id].shape[0]}." - ) - - embeddings[token_id] = embedding - ti_tokens.append(token_id) - - if len(ti_tokens) > 1: - ti_manager.pad_tokens[ti_tokens[0]] = ti_tokens[1:] - - text_encoder.tensors["text_model.embeddings.token_embedding.weight"] = embeddings.astype( - orig_embeddings.dtype - ) - - yield ti_tokenizer, ti_manager - - finally: - # restore - if orig_embeddings is not None: - text_encoder.tensors["text_model.embeddings.token_embedding.weight"] = orig_embeddings diff --git a/invokeai/backend/model_management_OLD/memory_snapshot.py b/invokeai/backend/model_management_OLD/memory_snapshot.py deleted file mode 100644 index fe54af191c..0000000000 --- a/invokeai/backend/model_management_OLD/memory_snapshot.py +++ /dev/null @@ -1,99 +0,0 @@ -import gc -from typing import Optional - -import psutil -import torch - -from invokeai.backend.model_management.libc_util import LibcUtil, Struct_mallinfo2 - -GB = 2**30 # 1 GB - - -class MemorySnapshot: - """A snapshot of RAM and VRAM usage. All values are in bytes.""" - - def __init__(self, process_ram: int, vram: Optional[int], malloc_info: Optional[Struct_mallinfo2]): - """Initialize a MemorySnapshot. - - Most of the time, `MemorySnapshot` will be constructed with `MemorySnapshot.capture()`. - - Args: - process_ram (int): CPU RAM used by the current process. - vram (Optional[int]): VRAM used by torch. - malloc_info (Optional[Struct_mallinfo2]): Malloc info obtained from LibcUtil. - """ - self.process_ram = process_ram - self.vram = vram - self.malloc_info = malloc_info - - @classmethod - def capture(cls, run_garbage_collector: bool = True): - """Capture and return a MemorySnapshot. - - Note: This function has significant overhead, particularly if `run_garbage_collector == True`. - - Args: - run_garbage_collector (bool, optional): If true, gc.collect() will be run before checking the process RAM - usage. Defaults to True. - - Returns: - MemorySnapshot - """ - if run_garbage_collector: - gc.collect() - - # According to the psutil docs (https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_info), rss is - # supported on all platforms. - process_ram = psutil.Process().memory_info().rss - - if torch.cuda.is_available(): - vram = torch.cuda.memory_allocated() - else: - # TODO: We could add support for mps.current_allocated_memory() as well. Leaving out for now until we have - # time to test it properly. - vram = None - - try: - malloc_info = LibcUtil().mallinfo2() - except (OSError, AttributeError): - # OSError: This is expected in environments that do not have the 'libc.so.6' shared library. - # AttributeError: This is expected in environments that have `libc.so.6` but do not have the `mallinfo2` (e.g. glibc < 2.33) - # TODO: Does `mallinfo` work? - malloc_info = None - - return cls(process_ram, vram, malloc_info) - - -def get_pretty_snapshot_diff(snapshot_1: Optional[MemorySnapshot], snapshot_2: Optional[MemorySnapshot]) -> str: - """Get a pretty string describing the difference between two `MemorySnapshot`s.""" - - def get_msg_line(prefix: str, val1: int, val2: int): - diff = val2 - val1 - return f"{prefix: <30} ({(diff/GB):+5.3f}): {(val1/GB):5.3f}GB -> {(val2/GB):5.3f}GB\n" - - msg = "" - - if snapshot_1 is None or snapshot_2 is None: - return msg - - msg += get_msg_line("Process RAM", snapshot_1.process_ram, snapshot_2.process_ram) - - if snapshot_1.malloc_info is not None and snapshot_2.malloc_info is not None: - msg += get_msg_line("libc mmap allocated", snapshot_1.malloc_info.hblkhd, snapshot_2.malloc_info.hblkhd) - - msg += get_msg_line("libc arena used", snapshot_1.malloc_info.uordblks, snapshot_2.malloc_info.uordblks) - - msg += get_msg_line("libc arena free", snapshot_1.malloc_info.fordblks, snapshot_2.malloc_info.fordblks) - - libc_total_allocated_1 = snapshot_1.malloc_info.arena + snapshot_1.malloc_info.hblkhd - libc_total_allocated_2 = snapshot_2.malloc_info.arena + snapshot_2.malloc_info.hblkhd - msg += get_msg_line("libc total allocated", libc_total_allocated_1, libc_total_allocated_2) - - libc_total_used_1 = snapshot_1.malloc_info.uordblks + snapshot_1.malloc_info.hblkhd - libc_total_used_2 = snapshot_2.malloc_info.uordblks + snapshot_2.malloc_info.hblkhd - msg += get_msg_line("libc total used", libc_total_used_1, libc_total_used_2) - - if snapshot_1.vram is not None and snapshot_2.vram is not None: - msg += get_msg_line("VRAM", snapshot_1.vram, snapshot_2.vram) - - return msg diff --git a/invokeai/backend/model_management_OLD/model_cache.py b/invokeai/backend/model_management_OLD/model_cache.py deleted file mode 100644 index 2a7f4b5a95..0000000000 --- a/invokeai/backend/model_management_OLD/model_cache.py +++ /dev/null @@ -1,553 +0,0 @@ -""" -Manage a RAM cache of diffusion/transformer models for fast switching. -They are moved between GPU VRAM and CPU RAM as necessary. If the cache -grows larger than a preset maximum, then the least recently used -model will be cleared and (re)loaded from disk when next needed. - -The cache returns context manager generators designed to load the -model into the GPU within the context, and unload outside the -context. Use like this: - - cache = ModelCache(max_cache_size=7.5) - with cache.get_model('runwayml/stable-diffusion-1-5') as SD1, - cache.get_model('stabilityai/stable-diffusion-2') as SD2: - do_something_in_GPU(SD1,SD2) - - -""" - -import gc -import hashlib -import math -import os -import sys -import time -from contextlib import suppress -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any, Dict, Optional, Type, Union, types - -import torch - -import invokeai.backend.util.logging as logger -from invokeai.backend.model_management.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff -from invokeai.backend.model_management.model_load_optimizations import skip_torch_weight_init - -from ..util.devices import choose_torch_device -from .models import BaseModelType, ModelBase, ModelType, SubModelType - -if choose_torch_device() == torch.device("mps"): - from torch import mps - -# Maximum size of the cache, in gigs -# Default is roughly enough to hold three fp16 diffusers models in RAM simultaneously -DEFAULT_MAX_CACHE_SIZE = 6.0 - -# amount of GPU memory to hold in reserve for use by generations (GB) -DEFAULT_MAX_VRAM_CACHE_SIZE = 2.75 - -# actual size of a gig -GIG = 1073741824 -# Size of a MB in bytes. -MB = 2**20 - - -@dataclass -class CacheStats(object): - hits: int = 0 # cache hits - misses: int = 0 # cache misses - high_watermark: int = 0 # amount of cache used - in_cache: int = 0 # number of models in cache - cleared: int = 0 # number of models cleared to make space - cache_size: int = 0 # total size of cache - # {submodel_key => size} - loaded_model_sizes: Dict[str, int] = field(default_factory=dict) - - -class ModelLocker(object): - "Forward declaration" - - pass - - -class ModelCache(object): - "Forward declaration" - - pass - - -class _CacheRecord: - size: int - model: Any - cache: ModelCache - _locks: int - - def __init__(self, cache, model: Any, size: int): - self.size = size - self.model = model - self.cache = cache - self._locks = 0 - - def lock(self): - self._locks += 1 - - def unlock(self): - self._locks -= 1 - assert self._locks >= 0 - - @property - def locked(self): - return self._locks > 0 - - @property - def loaded(self): - if self.model is not None and hasattr(self.model, "device"): - return self.model.device != self.cache.storage_device - else: - return False - - -class ModelCache(object): - def __init__( - self, - max_cache_size: float = DEFAULT_MAX_CACHE_SIZE, - max_vram_cache_size: float = DEFAULT_MAX_VRAM_CACHE_SIZE, - execution_device: torch.device = torch.device("cuda"), - storage_device: torch.device = torch.device("cpu"), - precision: torch.dtype = torch.float16, - sequential_offload: bool = False, - lazy_offloading: bool = True, - sha_chunksize: int = 16777216, - logger: types.ModuleType = logger, - log_memory_usage: bool = False, - ): - """ - :param max_cache_size: Maximum size of the RAM cache [6.0 GB] - :param execution_device: Torch device to load active model into [torch.device('cuda')] - :param storage_device: Torch device to save inactive model in [torch.device('cpu')] - :param precision: Precision for loaded models [torch.float16] - :param lazy_offloading: Keep model in VRAM until another model needs to be loaded - :param sequential_offload: Conserve VRAM by loading and unloading each stage of the pipeline sequentially - :param sha_chunksize: Chunksize to use when calculating sha256 model hash - :param log_memory_usage: If True, a memory snapshot will be captured before and after every model cache - operation, and the result will be logged (at debug level). There is a time cost to capturing the memory - snapshots, so it is recommended to disable this feature unless you are actively inspecting the model cache's - behaviour. - """ - self.model_infos: Dict[str, ModelBase] = {} - # allow lazy offloading only when vram cache enabled - self.lazy_offloading = lazy_offloading and max_vram_cache_size > 0 - self.precision: torch.dtype = precision - self.max_cache_size: float = max_cache_size - self.max_vram_cache_size: float = max_vram_cache_size - self.execution_device: torch.device = execution_device - self.storage_device: torch.device = storage_device - self.sha_chunksize = sha_chunksize - self.logger = logger - self._log_memory_usage = log_memory_usage - - # used for stats collection - self.stats = None - - self._cached_models = {} - self._cache_stack = [] - - def _capture_memory_snapshot(self) -> Optional[MemorySnapshot]: - if self._log_memory_usage: - return MemorySnapshot.capture() - return None - - def get_key( - self, - model_path: str, - base_model: BaseModelType, - model_type: ModelType, - submodel_type: Optional[SubModelType] = None, - ): - key = f"{model_path}:{base_model}:{model_type}" - if submodel_type: - key += f":{submodel_type}" - return key - - def _get_model_info( - self, - model_path: str, - model_class: Type[ModelBase], - base_model: BaseModelType, - model_type: ModelType, - ): - model_info_key = self.get_key( - model_path=model_path, - base_model=base_model, - model_type=model_type, - submodel_type=None, - ) - - if model_info_key not in self.model_infos: - self.model_infos[model_info_key] = model_class( - model_path, - base_model, - model_type, - ) - - return self.model_infos[model_info_key] - - # TODO: args - def get_model( - self, - model_path: Union[str, Path], - model_class: Type[ModelBase], - base_model: BaseModelType, - model_type: ModelType, - submodel: Optional[SubModelType] = None, - gpu_load: bool = True, - ) -> Any: - if not isinstance(model_path, Path): - model_path = Path(model_path) - - if not os.path.exists(model_path): - raise Exception(f"Model not found: {model_path}") - - model_info = self._get_model_info( - model_path=model_path, - model_class=model_class, - base_model=base_model, - model_type=model_type, - ) - key = self.get_key( - model_path=model_path, - base_model=base_model, - model_type=model_type, - submodel_type=submodel, - ) - # TODO: lock for no copies on simultaneous calls? - cache_entry = self._cached_models.get(key, None) - if cache_entry is None: - self.logger.info( - f"Loading model {model_path}, type" - f" {base_model.value}:{model_type.value}{':'+submodel.value if submodel else ''}" - ) - if self.stats: - self.stats.misses += 1 - - self_reported_model_size_before_load = model_info.get_size(submodel) - # Remove old models from the cache to make room for the new model. - self._make_cache_room(self_reported_model_size_before_load) - - # Load the model from disk and capture a memory snapshot before/after. - start_load_time = time.time() - snapshot_before = self._capture_memory_snapshot() - with skip_torch_weight_init(): - model = model_info.get_model(child_type=submodel, torch_dtype=self.precision) - snapshot_after = self._capture_memory_snapshot() - end_load_time = time.time() - - self_reported_model_size_after_load = model_info.get_size(submodel) - - self.logger.debug( - f"Moved model '{key}' from disk to cpu in {(end_load_time-start_load_time):.2f}s.\n" - f"Self-reported size before/after load: {(self_reported_model_size_before_load/GIG):.3f}GB /" - f" {(self_reported_model_size_after_load/GIG):.3f}GB.\n" - f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" - ) - - if abs(self_reported_model_size_after_load - self_reported_model_size_before_load) > 10 * MB: - self.logger.debug( - f"Model '{key}' mis-reported its size before load. Self-reported size before/after load:" - f" {(self_reported_model_size_before_load/GIG):.2f}GB /" - f" {(self_reported_model_size_after_load/GIG):.2f}GB." - ) - - cache_entry = _CacheRecord(self, model, self_reported_model_size_after_load) - self._cached_models[key] = cache_entry - else: - if self.stats: - self.stats.hits += 1 - - if self.stats: - self.stats.cache_size = self.max_cache_size * GIG - self.stats.high_watermark = max(self.stats.high_watermark, self._cache_size()) - self.stats.in_cache = len(self._cached_models) - self.stats.loaded_model_sizes[key] = max( - self.stats.loaded_model_sizes.get(key, 0), model_info.get_size(submodel) - ) - - with suppress(Exception): - self._cache_stack.remove(key) - self._cache_stack.append(key) - - return self.ModelLocker(self, key, cache_entry.model, gpu_load, cache_entry.size) - - def _move_model_to_device(self, key: str, target_device: torch.device): - cache_entry = self._cached_models[key] - - source_device = cache_entry.model.device - # Note: We compare device types only so that 'cuda' == 'cuda:0'. This would need to be revised to support - # multi-GPU. - if torch.device(source_device).type == torch.device(target_device).type: - return - - start_model_to_time = time.time() - snapshot_before = self._capture_memory_snapshot() - cache_entry.model.to(target_device) - snapshot_after = self._capture_memory_snapshot() - end_model_to_time = time.time() - self.logger.debug( - f"Moved model '{key}' from {source_device} to" - f" {target_device} in {(end_model_to_time-start_model_to_time):.2f}s.\n" - f"Estimated model size: {(cache_entry.size/GIG):.3f} GB.\n" - f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" - ) - - if ( - snapshot_before is not None - and snapshot_after is not None - and snapshot_before.vram is not None - and snapshot_after.vram is not None - ): - vram_change = abs(snapshot_before.vram - snapshot_after.vram) - - # If the estimated model size does not match the change in VRAM, log a warning. - if not math.isclose( - vram_change, - cache_entry.size, - rel_tol=0.1, - abs_tol=10 * MB, - ): - self.logger.debug( - f"Moving model '{key}' from {source_device} to" - f" {target_device} caused an unexpected change in VRAM usage. The model's" - " estimated size may be incorrect. Estimated model size:" - f" {(cache_entry.size/GIG):.3f} GB.\n" - f"{get_pretty_snapshot_diff(snapshot_before, snapshot_after)}" - ) - - class ModelLocker(object): - def __init__(self, cache, key, model, gpu_load, size_needed): - """ - :param cache: The model_cache object - :param key: The key of the model to lock in GPU - :param model: The model to lock - :param gpu_load: True if load into gpu - :param size_needed: Size of the model to load - """ - self.gpu_load = gpu_load - self.cache = cache - self.key = key - self.model = model - self.size_needed = size_needed - self.cache_entry = self.cache._cached_models[self.key] - - def __enter__(self) -> Any: - if not hasattr(self.model, "to"): - return self.model - - # NOTE that the model has to have the to() method in order for this - # code to move it into GPU! - if self.gpu_load: - self.cache_entry.lock() - - try: - if self.cache.lazy_offloading: - self.cache._offload_unlocked_models(self.size_needed) - - self.cache._move_model_to_device(self.key, self.cache.execution_device) - - self.cache.logger.debug(f"Locking {self.key} in {self.cache.execution_device}") - self.cache._print_cuda_stats() - - except Exception: - self.cache_entry.unlock() - raise - - # TODO: not fully understand - # in the event that the caller wants the model in RAM, we - # move it into CPU if it is in GPU and not locked - elif self.cache_entry.loaded and not self.cache_entry.locked: - self.cache._move_model_to_device(self.key, self.cache.storage_device) - - return self.model - - def __exit__(self, type, value, traceback): - if not hasattr(self.model, "to"): - return - - self.cache_entry.unlock() - if not self.cache.lazy_offloading: - self.cache._offload_unlocked_models() - self.cache._print_cuda_stats() - - # TODO: should it be called untrack_model? - def uncache_model(self, cache_id: str): - with suppress(ValueError): - self._cache_stack.remove(cache_id) - self._cached_models.pop(cache_id, None) - - def model_hash( - self, - model_path: Union[str, Path], - ) -> str: - """ - Given the HF repo id or path to a model on disk, returns a unique - hash. Works for legacy checkpoint files, HF models on disk, and HF repo IDs - - :param model_path: Path to model file/directory on disk. - """ - return self._local_model_hash(model_path) - - def cache_size(self) -> float: - """Return the current size of the cache, in GB.""" - return self._cache_size() / GIG - - def _has_cuda(self) -> bool: - return self.execution_device.type == "cuda" - - def _print_cuda_stats(self): - vram = "%4.2fG" % (torch.cuda.memory_allocated() / GIG) - ram = "%4.2fG" % self.cache_size() - - cached_models = 0 - loaded_models = 0 - locked_models = 0 - for model_info in self._cached_models.values(): - cached_models += 1 - if model_info.loaded: - loaded_models += 1 - if model_info.locked: - locked_models += 1 - - self.logger.debug( - f"Current VRAM/RAM usage: {vram}/{ram}; cached_models/loaded_models/locked_models/ =" - f" {cached_models}/{loaded_models}/{locked_models}" - ) - - def _cache_size(self) -> int: - return sum([m.size for m in self._cached_models.values()]) - - def _make_cache_room(self, model_size): - # calculate how much memory this model will require - # multiplier = 2 if self.precision==torch.float32 else 1 - bytes_needed = model_size - maximum_size = self.max_cache_size * GIG # stored in GB, convert to bytes - current_size = self._cache_size() - - if current_size + bytes_needed > maximum_size: - self.logger.debug( - f"Max cache size exceeded: {(current_size/GIG):.2f}/{self.max_cache_size:.2f} GB, need an additional" - f" {(bytes_needed/GIG):.2f} GB" - ) - - self.logger.debug(f"Before unloading: cached_models={len(self._cached_models)}") - - pos = 0 - models_cleared = 0 - while current_size + bytes_needed > maximum_size and pos < len(self._cache_stack): - model_key = self._cache_stack[pos] - cache_entry = self._cached_models[model_key] - - refs = sys.getrefcount(cache_entry.model) - - # HACK: This is a workaround for a memory-management issue that we haven't tracked down yet. We are directly - # going against the advice in the Python docs by using `gc.get_referrers(...)` in this way: - # https://docs.python.org/3/library/gc.html#gc.get_referrers - - # manualy clear local variable references of just finished function calls - # for some reason python don't want to collect it even by gc.collect() immidiately - if refs > 2: - while True: - cleared = False - for referrer in gc.get_referrers(cache_entry.model): - if type(referrer).__name__ == "frame": - # RuntimeError: cannot clear an executing frame - with suppress(RuntimeError): - referrer.clear() - cleared = True - # break - - # repeat if referrers changes(due to frame clear), else exit loop - if cleared: - gc.collect() - else: - break - - device = cache_entry.model.device if hasattr(cache_entry.model, "device") else None - self.logger.debug( - f"Model: {model_key}, locks: {cache_entry._locks}, device: {device}, loaded: {cache_entry.loaded}," - f" refs: {refs}" - ) - - # Expected refs: - # 1 from cache_entry - # 1 from getrefcount function - # 1 from onnx runtime object - if not cache_entry.locked and refs <= (3 if "onnx" in model_key else 2): - self.logger.debug( - f"Unloading model {model_key} to free {(model_size/GIG):.2f} GB (-{(cache_entry.size/GIG):.2f} GB)" - ) - current_size -= cache_entry.size - models_cleared += 1 - if self.stats: - self.stats.cleared += 1 - del self._cache_stack[pos] - del self._cached_models[model_key] - del cache_entry - - else: - pos += 1 - - if models_cleared > 0: - # There would likely be some 'garbage' to be collected regardless of whether a model was cleared or not, but - # there is a significant time cost to calling `gc.collect()`, so we want to use it sparingly. (The time cost - # is high even if no garbage gets collected.) - # - # Calling gc.collect(...) when a model is cleared seems like a good middle-ground: - # - If models had to be cleared, it's a signal that we are close to our memory limit. - # - If models were cleared, there's a good chance that there's a significant amount of garbage to be - # collected. - # - # Keep in mind that gc is only responsible for handling reference cycles. Most objects should be cleaned up - # immediately when their reference count hits 0. - gc.collect() - - torch.cuda.empty_cache() - if choose_torch_device() == torch.device("mps"): - mps.empty_cache() - - self.logger.debug(f"After unloading: cached_models={len(self._cached_models)}") - - def _offload_unlocked_models(self, size_needed: int = 0): - reserved = self.max_vram_cache_size * GIG - vram_in_use = torch.cuda.memory_allocated() - self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM used for models; max allowed={(reserved/GIG):.2f}GB") - for model_key, cache_entry in sorted(self._cached_models.items(), key=lambda x: x[1].size): - if vram_in_use <= reserved: - break - if not cache_entry.locked and cache_entry.loaded: - self._move_model_to_device(model_key, self.storage_device) - - vram_in_use = torch.cuda.memory_allocated() - self.logger.debug(f"{(vram_in_use/GIG):.2f}GB VRAM used for models; max allowed={(reserved/GIG):.2f}GB") - - torch.cuda.empty_cache() - if choose_torch_device() == torch.device("mps"): - mps.empty_cache() - - def _local_model_hash(self, model_path: Union[str, Path]) -> str: - sha = hashlib.sha256() - path = Path(model_path) - - hashpath = path / "checksum.sha256" - if hashpath.exists() and path.stat().st_mtime <= hashpath.stat().st_mtime: - with open(hashpath) as f: - hash = f.read() - return hash - - self.logger.debug(f"computing hash of model {path.name}") - for file in list(path.rglob("*.ckpt")) + list(path.rglob("*.safetensors")) + list(path.rglob("*.pth")): - with open(file, "rb") as f: - while chunk := f.read(self.sha_chunksize): - sha.update(chunk) - hash = sha.hexdigest() - with open(hashpath, "w") as f: - f.write(hash) - return hash diff --git a/invokeai/backend/model_management_OLD/model_load_optimizations.py b/invokeai/backend/model_management_OLD/model_load_optimizations.py deleted file mode 100644 index a46d262175..0000000000 --- a/invokeai/backend/model_management_OLD/model_load_optimizations.py +++ /dev/null @@ -1,30 +0,0 @@ -from contextlib import contextmanager - -import torch - - -def _no_op(*args, **kwargs): - pass - - -@contextmanager -def skip_torch_weight_init(): - """A context manager that monkey-patches several of the common torch layers (torch.nn.Linear, torch.nn.Conv1d, etc.) - to skip weight initialization. - - By default, `torch.nn.Linear` and `torch.nn.ConvNd` layers initialize their weights (according to a particular - distribution) when __init__ is called. This weight initialization step can take a significant amount of time, and is - completely unnecessary if the intent is to load checkpoint weights from disk for the layer. This context manager - monkey-patches common torch layers to skip the weight initialization step. - """ - torch_modules = [torch.nn.Linear, torch.nn.modules.conv._ConvNd, torch.nn.Embedding] - saved_functions = [m.reset_parameters for m in torch_modules] - - try: - for torch_module in torch_modules: - torch_module.reset_parameters = _no_op - - yield None - finally: - for torch_module, saved_function in zip(torch_modules, saved_functions, strict=True): - torch_module.reset_parameters = saved_function diff --git a/invokeai/backend/model_management_OLD/model_manager.py b/invokeai/backend/model_management_OLD/model_manager.py deleted file mode 100644 index 84d93f15fa..0000000000 --- a/invokeai/backend/model_management_OLD/model_manager.py +++ /dev/null @@ -1,1121 +0,0 @@ -"""This module manages the InvokeAI `models.yaml` file, mapping -symbolic diffusers model names to the paths and repo_ids used by the -underlying `from_pretrained()` call. - -SYNOPSIS: - - mgr = ModelManager('/home/phi/invokeai/configs/models.yaml') - sd1_5 = mgr.get_model('stable-diffusion-v1-5', - model_type=ModelType.Main, - base_model=BaseModelType.StableDiffusion1, - submodel_type=SubModelType.Unet) - with sd1_5 as unet: - run_some_inference(unet) - -FETCHING MODELS: - -Models are described using four attributes: - - 1) model_name -- the symbolic name for the model - - 2) ModelType -- an enum describing the type of the model. Currently - defined types are: - ModelType.Main -- a full model capable of generating images - ModelType.Vae -- a VAE model - ModelType.Lora -- a LoRA or LyCORIS fine-tune - ModelType.TextualInversion -- a textual inversion embedding - ModelType.ControlNet -- a ControlNet model - ModelType.IPAdapter -- an IPAdapter model - - 3) BaseModelType -- an enum indicating the stable diffusion base model, one of: - BaseModelType.StableDiffusion1 - BaseModelType.StableDiffusion2 - - 4) SubModelType (optional) -- an enum that refers to one of the submodels contained - within the main model. Values are: - - SubModelType.UNet - SubModelType.TextEncoder - SubModelType.Tokenizer - SubModelType.Scheduler - SubModelType.SafetyChecker - -To fetch a model, use `manager.get_model()`. This takes the symbolic -name of the model, the ModelType, the BaseModelType and the -SubModelType. The latter is required for ModelType.Main. - -get_model() will return a ModelInfo object that can then be used in -context to retrieve the model and move it into GPU VRAM (on GPU -systems). - -A typical example is: - - sd1_5 = mgr.get_model('stable-diffusion-v1-5', - model_type=ModelType.Main, - base_model=BaseModelType.StableDiffusion1, - submodel_type=SubModelType.UNet) - with sd1_5 as unet: - run_some_inference(unet) - -The ModelInfo object provides a number of useful fields describing the -model, including: - - name -- symbolic name of the model - base_model -- base model (BaseModelType) - type -- model type (ModelType) - location -- path to the model file - precision -- torch precision of the model - hash -- unique sha256 checksum for this model - -SUBMODELS: - -When fetching a main model, you must specify the submodel. Retrieval -of full pipelines is not supported. - - vae_info = mgr.get_model('stable-diffusion-1.5', - model_type = ModelType.Main, - base_model = BaseModelType.StableDiffusion1, - submodel_type = SubModelType.Vae - ) - with vae_info as vae: - do_something(vae) - -This rule does not apply to controlnets, embeddings, loras and standalone -VAEs, which do not have submodels. - -LISTING MODELS - -The model_names() method will return a list of Tuples describing each -model it knows about: - - >> mgr.model_names() - [ - ('stable-diffusion-1.5', , ), - ('stable-diffusion-2.1', , ), - ('inpaint', , ) - ('Ink scenery', , ) - ... - ] - -The tuple is in the correct order to pass to get_model(): - - for m in mgr.model_names(): - info = get_model(*m) - -In contrast, the list_models() method returns a list of dicts, each -providing information about a model defined in models.yaml. For example: - - >>> models = mgr.list_models() - >>> json.dumps(models[0]) - {"path": "/home/lstein/invokeai-main/models/sd-1/controlnet/canny", - "model_format": "diffusers", - "name": "canny", - "base_model": "sd-1", - "type": "controlnet" - } - -You can filter by model type and base model as shown here: - - - controlnets = mgr.list_models(model_type=ModelType.ControlNet, - base_model=BaseModelType.StableDiffusion1) - for c in controlnets: - name = c['name'] - format = c['model_format'] - path = c['path'] - type = c['type'] - # etc - -ADDING AND REMOVING MODELS - -At startup time, the `models` directory will be scanned for -checkpoints, diffusers pipelines, controlnets, LoRAs and TI -embeddings. New entries will be added to the model manager and defunct -ones removed. Anything that is a main model (ModelType.Main) will be -added to models.yaml. For scanning to succeed, files need to be in -their proper places. For example, a controlnet folder built on the -stable diffusion 2 base, will need to be placed in -`models/sd-2/controlnet`. - -Layout of the `models` directory: - - models - ├── sd-1 - │ ├── controlnet - │ ├── lora - │ ├── main - │ └── embedding - ├── sd-2 - │ ├── controlnet - │ ├── lora - │ ├── main - │ └── embedding - └── core - ├── face_reconstruction - │ ├── codeformer - │ └── gfpgan - ├── sd-conversion - │ ├── clip-vit-large-patch14 - tokenizer, text_encoder subdirs - │ ├── stable-diffusion-2 - tokenizer, text_encoder subdirs - │ └── stable-diffusion-safety-checker - └── upscaling - └─── esrgan - - - -class ConfigMeta(BaseModel):Loras, textual_inversion and controlnet models are not listed -explicitly in models.yaml, but are added to the in-memory data -structure at initialization time by scanning the models directory. The -in-memory data structure can be resynchronized by calling -`manager.scan_models_directory()`. - -Files and folders placed inside the `autoimport` paths (paths -defined in `invokeai.yaml`) will also be scanned for new models at -initialization time and added to `models.yaml`. Files will not be -moved from this location but preserved in-place. These directories -are: - - configuration default description - ------------- ------- ----------- - autoimport_dir autoimport/main main models - lora_dir autoimport/lora LoRA/LyCORIS models - embedding_dir autoimport/embedding TI embeddings - controlnet_dir autoimport/controlnet ControlNet models - -In actuality, models located in any of these directories are scanned -to determine their type, so it isn't strictly necessary to organize -the different types in this way. This entry in `invokeai.yaml` will -recursively scan all subdirectories within `autoimport`, scan models -files it finds, and import them if recognized. - - Paths: - autoimport_dir: autoimport - -A model can be manually added using `add_model()` using the model's -name, base model, type and a dict of model attributes. See -`invokeai/backend/model_management/models` for the attributes required -by each model type. - -A model can be deleted using `del_model()`, providing the same -identifying information as `get_model()` - -The `heuristic_import()` method will take a set of strings -corresponding to local paths, remote URLs, and repo_ids, probe the -object to determine what type of model it is (if any), and import new -models into the manager. If passed a directory, it will recursively -scan it for models to import. The return value is a set of the models -successfully added. - -MODELS.YAML - -The general format of a models.yaml section is: - - type-of-model/name-of-model: - path: /path/to/local/file/or/directory - description: a description - format: diffusers|checkpoint - variant: normal|inpaint|depth - -The type of model is given in the stanza key, and is one of -{main, vae, lora, controlnet, textual} - -The format indicates whether the model is organized as a diffusers -folder with model subdirectories, or is contained in a single -checkpoint or safetensors file. - -The path points to a file or directory on disk. If a relative path, -the root is the InvokeAI ROOTDIR. - -""" -from __future__ import annotations - -import hashlib -import os -import textwrap -import types -from dataclasses import dataclass -from pathlib import Path -from shutil import move, rmtree -from typing import Callable, Dict, List, Literal, Optional, Set, Tuple, Union, cast - -import torch -import yaml -from omegaconf import OmegaConf -from omegaconf.dictconfig import DictConfig -from pydantic import BaseModel, ConfigDict, Field - -import invokeai.backend.util.logging as logger -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.util import CUDA_DEVICE, Chdir - -from .model_cache import ModelCache, ModelLocker -from .model_search import ModelSearch -from .models import ( - MODEL_CLASSES, - BaseModelType, - DuplicateModelException, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelError, - ModelNotFoundException, - ModelType, - SchedulerPredictionType, - SubModelType, -) - -# We are only starting to number the config file with release 3. -# The config file version doesn't have to start at release version, but it will help -# reduce confusion. -CONFIG_FILE_VERSION = "3.0.0" - - -@dataclass -class LoadedModelInfo: - context: ModelLocker - name: str - base_model: BaseModelType - type: ModelType - hash: str - location: Union[Path, str] - precision: torch.dtype - _cache: Optional[ModelCache] = None - - def __enter__(self): - return self.context.__enter__() - - def __exit__(self, *args, **kwargs): - self.context.__exit__(*args, **kwargs) - - -class AddModelResult(BaseModel): - name: str = Field(description="The name of the model after installation") - model_type: ModelType = Field(description="The type of model") - base_model: BaseModelType = Field(description="The base model") - config: ModelConfigBase = Field(description="The configuration of the model") - - model_config = ConfigDict(protected_namespaces=()) - - -MAX_CACHE_SIZE = 6.0 # GB - - -class ConfigMeta(BaseModel): - version: str - - -class ModelManager(object): - """ - High-level interface to model management. - """ - - logger: types.ModuleType = logger - - def __init__( - self, - config: Union[Path, DictConfig, str], - device_type: torch.device = CUDA_DEVICE, - precision: torch.dtype = torch.float16, - max_cache_size=MAX_CACHE_SIZE, - sequential_offload=False, - logger: types.ModuleType = logger, - ): - """ - Initialize with the path to the models.yaml config file. - Optional parameters are the torch device type, precision, max_models, - and sequential_offload boolean. Note that the default device - type and precision are set up for a CUDA system running at half precision. - """ - self.config_path = None - if isinstance(config, (str, Path)): - self.config_path = Path(config) - if not self.config_path.exists(): - logger.warning(f"The file {self.config_path} was not found. Initializing a new file") - self.initialize_model_config(self.config_path) - config = OmegaConf.load(self.config_path) - - elif not isinstance(config, DictConfig): - raise ValueError("config argument must be an OmegaConf object, a Path or a string") - - self.config_meta = ConfigMeta(**config.pop("__metadata__")) - # TODO: metadata not found - # TODO: version check - - self.app_config = InvokeAIAppConfig.get_config() - self.logger = logger - self.cache = ModelCache( - max_cache_size=max_cache_size, - max_vram_cache_size=self.app_config.vram_cache_size, - lazy_offloading=self.app_config.lazy_offload, - execution_device=device_type, - precision=precision, - sequential_offload=sequential_offload, - logger=logger, - log_memory_usage=self.app_config.log_memory_usage, - ) - - self._read_models(config) - - def _read_models(self, config: Optional[DictConfig] = None): - if not config: - if self.config_path: - config = OmegaConf.load(self.config_path) - else: - return - - self.models = {} - for model_key, model_config in config.items(): - if model_key.startswith("_"): - continue - model_name, base_model, model_type = self.parse_key(model_key) - model_class = self._get_implementation(base_model, model_type) - # alias for config file - model_config["model_format"] = model_config.pop("format") - self.models[model_key] = model_class.create_config(**model_config) - - # check config version number and update on disk/RAM if necessary - self.cache_keys = {} - - # add controlnet, lora and textual_inversion models from disk - self.scan_models_directory() - - def sync_to_config(self): - """ - Call this when `models.yaml` has been changed externally. - This will reinitialize internal data structures - """ - # Reread models directory; note that this will reinitialize the cache, - # causing otherwise unreferenced models to be removed from memory - self._read_models() - - def model_exists(self, model_name: str, base_model: BaseModelType, model_type: ModelType, *, rescan=False) -> bool: - """ - Given a model name, returns True if it is a valid identifier. - - :param model_name: symbolic name of the model in models.yaml - :param model_type: ModelType enum indicating the type of model to return - :param base_model: BaseModelType enum indicating the base model used by this model - :param rescan: if True, scan_models_directory - """ - model_key = self.create_key(model_name, base_model, model_type) - exists = model_key in self.models - - # if model not found try to find it (maybe file just pasted) - if rescan and not exists: - self.scan_models_directory(base_model=base_model, model_type=model_type) - exists = self.model_exists(model_name, base_model, model_type, rescan=False) - - return exists - - @classmethod - def create_key( - cls, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ) -> str: - # In 3.11, the behavior of (str,enum) when interpolated into a - # string has changed. The next two lines are defensive. - base_model = BaseModelType(base_model) - model_type = ModelType(model_type) - return f"{base_model.value}/{model_type.value}/{model_name}" - - @classmethod - def parse_key(cls, model_key: str) -> Tuple[str, BaseModelType, ModelType]: - base_model_str, model_type_str, model_name = model_key.split("/", 2) - try: - model_type = ModelType(model_type_str) - except Exception: - raise Exception(f"Unknown model type: {model_type_str}") - - try: - base_model = BaseModelType(base_model_str) - except Exception: - raise Exception(f"Unknown base model: {base_model_str}") - - return (model_name, base_model, model_type) - - def _get_model_cache_path(self, model_path): - return self.resolve_model_path(Path(".cache") / hashlib.md5(str(model_path).encode()).hexdigest()) - - @classmethod - def initialize_model_config(cls, config_path: Path): - """Create empty config file""" - with open(config_path, "w") as yaml_file: - yaml_file.write(yaml.dump({"__metadata__": {"version": "3.0.0"}})) - - def get_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel_type: Optional[SubModelType] = None, - ) -> LoadedModelInfo: - """Given a model named identified in models.yaml, return - an ModelInfo object describing it. - :param model_name: symbolic name of the model in models.yaml - :param model_type: ModelType enum indicating the type of model to return - :param base_model: BaseModelType enum indicating the base model used by this model - :param submodel_type: an ModelType enum indicating the portion of - the model to retrieve (e.g. ModelType.Vae) - """ - model_key = self.create_key(model_name, base_model, model_type) - - if not self.model_exists(model_name, base_model, model_type, rescan=True): - raise ModelNotFoundException(f"Model not found - {model_key}") - - model_config = self._get_model_config(base_model, model_name, model_type) - - model_path, is_submodel_override = self._get_model_path(model_config, submodel_type) - - if is_submodel_override: - model_type = submodel_type - submodel_type = None - - model_class = self._get_implementation(base_model, model_type) - - if not model_path.exists(): - if model_class.save_to_config: - self.models[model_key].error = ModelError.NotFound - raise Exception(f'Files for model "{model_key}" not found at {model_path}') - - else: - self.models.pop(model_key, None) - raise ModelNotFoundException(f'Files for model "{model_key}" not found at {model_path}') - - # TODO: path - # TODO: is it accurate to use path as id - dst_convert_path = self._get_model_cache_path(model_path) - - model_path = model_class.convert_if_required( - base_model=base_model, - model_path=str(model_path), # TODO: refactor str/Path types logic - output_path=dst_convert_path, - config=model_config, - ) - - model_context = self.cache.get_model( - model_path=model_path, - model_class=model_class, - base_model=base_model, - model_type=model_type, - submodel_type=submodel_type, - ) - - if model_key not in self.cache_keys: - self.cache_keys[model_key] = set() - self.cache_keys[model_key].add(model_context.key) - - model_hash = "" # TODO: - - return LoadedModelInfo( - context=model_context, - name=model_name, - base_model=base_model, - type=submodel_type or model_type, - hash=model_hash, - location=model_path, # TODO: - precision=self.cache.precision, - _cache=self.cache, - ) - - def _get_model_path( - self, model_config: ModelConfigBase, submodel_type: Optional[SubModelType] = None - ) -> (Path, bool): - """Extract a model's filesystem path from its config. - - :return: The fully qualified Path of the module (or submodule). - """ - model_path = model_config.path - is_submodel_override = False - - # Does the config explicitly override the submodel? - if submodel_type is not None and hasattr(model_config, submodel_type): - submodel_path = getattr(model_config, submodel_type) - if submodel_path is not None and len(submodel_path) > 0: - model_path = getattr(model_config, submodel_type) - is_submodel_override = True - - model_path = self.resolve_model_path(model_path) - return model_path, is_submodel_override - - def _get_model_config(self, base_model: BaseModelType, model_name: str, model_type: ModelType) -> ModelConfigBase: - """Get a model's config object.""" - model_key = self.create_key(model_name, base_model, model_type) - try: - model_config = self.models[model_key] - except KeyError: - raise ModelNotFoundException(f"Model not found - {model_key}") - return model_config - - def _get_implementation(self, base_model: BaseModelType, model_type: ModelType) -> type[ModelBase]: - """Get the concrete implementation class for a specific model type.""" - model_class = MODEL_CLASSES[base_model][model_type] - return model_class - - def _instantiate( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - submodel_type: Optional[SubModelType] = None, - ) -> ModelBase: - """Make a new instance of this model, without loading it.""" - model_config = self._get_model_config(base_model, model_name, model_type) - model_path, is_submodel_override = self._get_model_path(model_config, submodel_type) - # FIXME: do non-overriden submodels get the right class? - constructor = self._get_implementation(base_model, model_type) - instance = constructor(model_path, base_model, model_type) - return instance - - def model_info( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ) -> Union[dict, None]: - """ - Given a model name returns the OmegaConf (dict-like) object describing it. - """ - model_key = self.create_key(model_name, base_model, model_type) - if model_key in self.models: - return self.models[model_key].model_dump(exclude_defaults=True) - else: - return None # TODO: None or empty dict on not found - - def model_names(self) -> List[Tuple[str, BaseModelType, ModelType]]: - """ - Return a list of (str, BaseModelType, ModelType) corresponding to all models - known to the configuration. - """ - return [(self.parse_key(x)) for x in self.models.keys()] - - def list_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ) -> Union[dict, None]: - """ - Returns a dict describing one installed model, using - the combined format of the list_models() method. - """ - models = self.list_models(base_model, model_type, model_name) - if len(models) >= 1: - return models[0] - else: - return None - - def list_models( - self, - base_model: Optional[BaseModelType] = None, - model_type: Optional[ModelType] = None, - model_name: Optional[str] = None, - ) -> list[dict]: - """ - Return a list of models. - """ - - model_keys = ( - [self.create_key(model_name, base_model, model_type)] - if model_name and base_model and model_type - else sorted(self.models, key=str.casefold) - ) - models = [] - for model_key in model_keys: - model_config = self.models.get(model_key) - if not model_config: - self.logger.error(f"Unknown model {model_name}") - raise ModelNotFoundException(f"Unknown model {model_name}") - - cur_model_name, cur_base_model, cur_model_type = self.parse_key(model_key) - if base_model is not None and cur_base_model != base_model: - continue - if model_type is not None and cur_model_type != model_type: - continue - - model_dict = dict( - **model_config.model_dump(exclude_defaults=True), - # OpenAPIModelInfoBase - model_name=cur_model_name, - base_model=cur_base_model, - model_type=cur_model_type, - ) - - # expose paths as absolute to help web UI - if path := model_dict.get("path"): - model_dict["path"] = str(self.resolve_model_path(path)) - models.append(model_dict) - - return models - - def print_models(self) -> None: - """ - Print a table of models and their descriptions. This needs to be redone - """ - # TODO: redo - for model_dict in self.list_models(): - for _model_name, model_info in model_dict.items(): - line = f'{model_info["name"]:25s} {model_info["type"]:10s} {model_info["description"]}' - print(line) - - # Tested - LS - def del_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - ): - """ - Delete the named model. - """ - model_key = self.create_key(model_name, base_model, model_type) - model_cfg = self.models.pop(model_key, None) - - if model_cfg is None: - raise ModelNotFoundException(f"Unknown model {model_key}") - - # note: it not garantie to release memory(model can has other references) - cache_ids = self.cache_keys.pop(model_key, []) - for cache_id in cache_ids: - self.cache.uncache_model(cache_id) - - # if model inside invoke models folder - delete files - model_path = self.resolve_model_path(model_cfg.path) - cache_path = self._get_model_cache_path(model_path) - if cache_path.exists(): - rmtree(str(cache_path)) - - if model_path.is_relative_to(self.app_config.models_path): - if model_path.is_dir(): - rmtree(str(model_path)) - else: - model_path.unlink() - self.commit() - - # LS: tested - def add_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - model_attributes: dict, - clobber: bool = False, - ) -> AddModelResult: - """ - Update the named model with a dictionary of attributes. Will fail with an - assertion error if the name already exists. Pass clobber=True to overwrite. - On a successful update, the config will be changed in memory and the - method will return True. Will fail with an assertion error if provided - attributes are incorrect or the model name is missing. - - The returned dict has the same format as the dict returned by - model_info(). - """ - # relativize paths as they go in - this makes it easier to move the models directory around - if path := model_attributes.get("path"): - model_attributes["path"] = str(self.relative_model_path(Path(path))) - - model_class = self._get_implementation(base_model, model_type) - model_config = model_class.create_config(**model_attributes) - model_key = self.create_key(model_name, base_model, model_type) - - if model_key in self.models and not clobber: - raise Exception(f'Attempt to overwrite existing model definition "{model_key}"') - - old_model = self.models.pop(model_key, None) - if old_model is not None: - # TODO: if path changed and old_model.path inside models folder should we delete this too? - - # remove conversion cache as config changed - old_model_path = self.resolve_model_path(old_model.path) - old_model_cache = self._get_model_cache_path(old_model_path) - if old_model_cache.exists(): - if old_model_cache.is_dir(): - rmtree(str(old_model_cache)) - else: - old_model_cache.unlink() - - # remove in-memory cache - # note: it not guaranteed to release memory(model can has other references) - cache_ids = self.cache_keys.pop(model_key, []) - for cache_id in cache_ids: - self.cache.uncache_model(cache_id) - - self.models[model_key] = model_config - self.commit() - - return AddModelResult( - name=model_name, - model_type=model_type, - base_model=base_model, - config=model_config, - ) - - def rename_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: ModelType, - new_name: Optional[str] = None, - new_base: Optional[BaseModelType] = None, - ) -> None: - """ - Rename or rebase a model. - """ - if new_name is None and new_base is None: - self.logger.error("rename_model() called with neither a new_name nor a new_base. {model_name} unchanged.") - return - - model_key = self.create_key(model_name, base_model, model_type) - model_cfg = self.models.get(model_key, None) - if not model_cfg: - raise ModelNotFoundException(f"Unknown model: {model_key}") - - old_path = self.resolve_model_path(model_cfg.path) - new_name = new_name or model_name - new_base = new_base or base_model - new_key = self.create_key(new_name, new_base, model_type) - if new_key in self.models: - raise ValueError(f'Attempt to overwrite existing model definition "{new_key}"') - - # if this is a model file/directory that we manage ourselves, we need to move it - if old_path.is_relative_to(self.app_config.models_path): - # keep the suffix! - if old_path.is_file(): - new_name = Path(new_name).with_suffix(old_path.suffix).as_posix() - new_path = self.resolve_model_path( - Path( - BaseModelType(new_base).value, - ModelType(model_type).value, - new_name, - ) - ) - move(old_path, new_path) - model_cfg.path = str(new_path.relative_to(self.app_config.models_path)) - - # clean up caches - old_model_cache = self._get_model_cache_path(old_path) - if old_model_cache.exists(): - if old_model_cache.is_dir(): - rmtree(str(old_model_cache)) - else: - old_model_cache.unlink() - - cache_ids = self.cache_keys.pop(model_key, []) - for cache_id in cache_ids: - self.cache.uncache_model(cache_id) - - self.models.pop(model_key, None) # delete - self.models[new_key] = model_cfg - self.commit() - - def convert_model( - self, - model_name: str, - base_model: BaseModelType, - model_type: Literal[ModelType.Main, ModelType.Vae], - dest_directory: Optional[Path] = None, - ) -> AddModelResult: - """ - Convert a checkpoint file into a diffusers folder, deleting the cached - version and deleting the original checkpoint file if it is in the models - directory. - :param model_name: Name of the model to convert - :param base_model: Base model type - :param model_type: Type of model ['vae' or 'main'] - - This will raise a ValueError unless the model is a checkpoint. - """ - info = self.model_info(model_name, base_model, model_type) - - if info is None: - raise FileNotFoundError(f"model not found: {model_name}") - - if info["model_format"] != "checkpoint": - raise ValueError(f"not a checkpoint format model: {model_name}") - - # We are taking advantage of a side effect of get_model() that converts check points - # into cached diffusers directories stored at `location`. It doesn't matter - # what submodeltype we request here, so we get the smallest. - submodel = {"submodel_type": SubModelType.Scheduler} if model_type == ModelType.Main else {} - model = self.get_model( - model_name, - base_model, - model_type, - **submodel, - ) - checkpoint_path = self.resolve_model_path(info["path"]) - old_diffusers_path = self.resolve_model_path(model.location) - new_diffusers_path = ( - dest_directory or self.app_config.models_path / base_model.value / model_type.value - ) / model_name - if new_diffusers_path.exists(): - raise ValueError(f"A diffusers model already exists at {new_diffusers_path}") - - try: - move(old_diffusers_path, new_diffusers_path) - info["model_format"] = "diffusers" - info["path"] = ( - str(new_diffusers_path) - if dest_directory - else str(new_diffusers_path.relative_to(self.app_config.models_path)) - ) - info.pop("config") - - result = self.add_model(model_name, base_model, model_type, model_attributes=info, clobber=True) - except Exception: - # something went wrong, so don't leave dangling diffusers model in directory or it will cause a duplicate model error! - rmtree(new_diffusers_path) - raise - - if checkpoint_path.exists() and checkpoint_path.is_relative_to(self.app_config.models_path): - checkpoint_path.unlink() - - return result - - def resolve_model_path(self, path: Union[Path, str]) -> Path: - """return relative paths based on configured models_path""" - return self.app_config.models_path / path - - def relative_model_path(self, model_path: Path) -> Path: - if model_path.is_relative_to(self.app_config.models_path): - model_path = model_path.relative_to(self.app_config.models_path) - return model_path - - def search_models(self, search_folder): - self.logger.info(f"Finding Models In: {search_folder}") - models_folder_ckpt = Path(search_folder).glob("**/*.ckpt") - models_folder_safetensors = Path(search_folder).glob("**/*.safetensors") - - ckpt_files = [x for x in models_folder_ckpt if x.is_file()] - safetensor_files = [x for x in models_folder_safetensors if x.is_file()] - - files = ckpt_files + safetensor_files - - found_models = [] - for file in files: - location = str(file.resolve()).replace("\\", "/") - if "model.safetensors" not in location and "diffusion_pytorch_model.safetensors" not in location: - found_models.append({"name": file.stem, "location": location}) - - return search_folder, found_models - - def commit(self, conf_file: Optional[Path] = None) -> None: - """ - Write current configuration out to the indicated file. - """ - data_to_save = {} - data_to_save["__metadata__"] = self.config_meta.model_dump() - - for model_key, model_config in self.models.items(): - model_name, base_model, model_type = self.parse_key(model_key) - model_class = self._get_implementation(base_model, model_type) - if model_class.save_to_config: - # TODO: or exclude_unset better fits here? - data_to_save[model_key] = cast(BaseModel, model_config).model_dump( - exclude_defaults=True, exclude={"error"}, mode="json" - ) - # alias for config file - data_to_save[model_key]["format"] = data_to_save[model_key].pop("model_format") - - yaml_str = OmegaConf.to_yaml(data_to_save) - config_file_path = conf_file or self.config_path - assert config_file_path is not None, "no config file path to write to" - config_file_path = self.app_config.root_path / config_file_path - tmpfile = os.path.join(os.path.dirname(config_file_path), "new_config.tmp") - try: - with open(tmpfile, "w", encoding="utf-8") as outfile: - outfile.write(self.preamble()) - outfile.write(yaml_str) - os.replace(tmpfile, config_file_path) - except OSError as err: - self.logger.warning(f"Could not modify the config file at {config_file_path}") - self.logger.warning(err) - - def preamble(self) -> str: - """ - Returns the preamble for the config file. - """ - return textwrap.dedent( - """ - # This file describes the alternative machine learning models - # available to InvokeAI script. - # - # To add a new model, follow the examples below. Each - # model requires a model config file, a weights file, - # and the width and height of the images it - # was trained on. - """ - ) - - def scan_models_directory( - self, - base_model: Optional[BaseModelType] = None, - model_type: Optional[ModelType] = None, - ): - loaded_files = set() - new_models_found = False - - self.logger.info(f"Scanning {self.app_config.models_path} for new models") - with Chdir(self.app_config.models_path): - for model_key, model_config in list(self.models.items()): - model_name, cur_base_model, cur_model_type = self.parse_key(model_key) - - # Patch for relative path bug in older models.yaml - paths should not - # be starting with a hard-coded 'models'. This will also fix up - # models.yaml when committed. - if model_config.path.startswith("models"): - model_config.path = str(Path(*Path(model_config.path).parts[1:])) - - model_path = self.resolve_model_path(model_config.path).absolute() - if not model_path.exists(): - model_class = self._get_implementation(cur_base_model, cur_model_type) - if model_class.save_to_config: - model_config.error = ModelError.NotFound - self.models.pop(model_key, None) - else: - self.models.pop(model_key, None) - else: - loaded_files.add(model_path) - - for cur_base_model in BaseModelType: - if base_model is not None and cur_base_model != base_model: - continue - - for cur_model_type in ModelType: - if model_type is not None and cur_model_type != model_type: - continue - model_class = self._get_implementation(cur_base_model, cur_model_type) - models_dir = self.resolve_model_path(Path(cur_base_model.value, cur_model_type.value)) - - if not models_dir.exists(): - continue # TODO: or create all folders? - - for model_path in models_dir.iterdir(): - if model_path not in loaded_files: # TODO: check - if model_path.name.startswith("."): - continue - model_name = model_path.name if model_path.is_dir() else model_path.stem - model_key = self.create_key(model_name, cur_base_model, cur_model_type) - - try: - if model_key in self.models: - raise DuplicateModelException(f"Model with key {model_key} added twice") - - model_path = self.relative_model_path(model_path) - model_config: ModelConfigBase = model_class.probe_config( - str(model_path), model_base=cur_base_model - ) - self.models[model_key] = model_config - new_models_found = True - except DuplicateModelException as e: - self.logger.warning(e) - except InvalidModelException as e: - self.logger.warning(f"Not a valid model: {model_path}. {e}") - except NotImplementedError as e: - self.logger.warning(e) - except Exception as e: - self.logger.warning(f"Error loading model {model_path}. {e}") - - imported_models = self.scan_autoimport_directory() - if (new_models_found or imported_models) and self.config_path: - self.commit() - - def scan_autoimport_directory(self) -> Dict[str, AddModelResult]: - """ - Scan the autoimport directory (if defined) and import new models, delete defunct models. - """ - # avoid circular import - from invokeai.backend.install.model_install_backend import ModelInstall - from invokeai.frontend.install.model_install import ask_user_for_prediction_type - - class ScanAndImport(ModelSearch): - def __init__(self, directories, logger, ignore: Set[Path], installer: ModelInstall): - super().__init__(directories, logger) - self.installer = installer - self.ignore = ignore - - def on_search_started(self): - self.new_models_found = {} - - def on_model_found(self, model: Path): - if model not in self.ignore: - self.new_models_found.update(self.installer.heuristic_import(model)) - - def on_search_completed(self): - self.logger.info( - f"Scanned {self._items_scanned} files and directories, imported {len(self.new_models_found)} models" - ) - - def models_found(self): - return self.new_models_found - - config = self.app_config - - # LS: hacky - # Patch in the SD VAE from core so that it is available for use by the UI - try: - self.heuristic_import({str(self.resolve_model_path("core/convert/sd-vae-ft-mse"))}) - except Exception: - pass - - installer = ModelInstall( - config=self.app_config, - model_manager=self, - prediction_type_helper=ask_user_for_prediction_type, - ) - known_paths = {self.resolve_model_path(x["path"]) for x in self.list_models()} - directories = { - config.root_path / x - for x in [ - config.autoimport_dir, - config.lora_dir, - config.embedding_dir, - config.controlnet_dir, - ] - if x - } - scanner = ScanAndImport(directories, self.logger, ignore=known_paths, installer=installer) - scanner.search() - - return scanner.models_found() - - def heuristic_import( - self, - items_to_import: Set[str], - prediction_type_helper: Optional[Callable[[Path], SchedulerPredictionType]] = None, - ) -> Dict[str, AddModelResult]: - """Import a list of paths, repo_ids or URLs. Returns the set of - successfully imported items. - :param items_to_import: Set of strings corresponding to models to be imported. - :param prediction_type_helper: A callback that receives the Path of a Stable Diffusion 2 checkpoint model and returns a SchedulerPredictionType. - - The prediction type helper is necessary to distinguish between - models based on Stable Diffusion 2 Base (requiring - SchedulerPredictionType.Epsilson) and Stable Diffusion 768 - (requiring SchedulerPredictionType.VPrediction). It is - generally impossible to do this programmatically, so the - prediction_type_helper usually asks the user to choose. - - The result is a set of successfully installed models. Each element - of the set is a dict corresponding to the newly-created OmegaConf stanza for - that model. - - May return the following exceptions: - - ModelNotFoundException - one or more of the items to import is not a valid path, repo_id or URL - - ValueError - a corresponding model already exists - """ - # avoid circular import here - from invokeai.backend.install.model_install_backend import ModelInstall - - successfully_installed = {} - - installer = ModelInstall( - config=self.app_config, prediction_type_helper=prediction_type_helper, model_manager=self - ) - for thing in items_to_import: - installed = installer.heuristic_import(thing) - successfully_installed.update(installed) - self.commit() - return successfully_installed diff --git a/invokeai/backend/model_management_OLD/model_merge.py b/invokeai/backend/model_management_OLD/model_merge.py deleted file mode 100644 index a9f0a23618..0000000000 --- a/invokeai/backend/model_management_OLD/model_merge.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -invokeai.backend.model_management.model_merge exports: -merge_diffusion_models() -- combine multiple models by location and return a pipeline object -merge_diffusion_models_and_commit() -- combine multiple models by ModelManager ID and write to models.yaml - -Copyright (c) 2023 Lincoln Stein and the InvokeAI Development Team -""" - -import warnings -from enum import Enum -from pathlib import Path -from typing import List, Optional, Union - -from diffusers import DiffusionPipeline -from diffusers import logging as dlogging - -import invokeai.backend.util.logging as logger - -from ...backend.model_management import AddModelResult, BaseModelType, ModelManager, ModelType, ModelVariantType - - -class MergeInterpolationMethod(str, Enum): - WeightedSum = "weighted_sum" - Sigmoid = "sigmoid" - InvSigmoid = "inv_sigmoid" - AddDifference = "add_difference" - - -class ModelMerger(object): - def __init__(self, manager: ModelManager): - self.manager = manager - - def merge_diffusion_models( - self, - model_paths: List[Path], - alpha: float = 0.5, - interp: Optional[MergeInterpolationMethod] = None, - force: bool = False, - **kwargs, - ) -> DiffusionPipeline: - """ - :param model_paths: up to three models, designated by their local paths or HuggingFace repo_ids - :param alpha: The interpolation parameter. Ranges from 0 to 1. It affects the ratio in which the checkpoints are merged. A 0.8 alpha - would mean that the first model checkpoints would affect the final result far less than an alpha of 0.2 - :param interp: The interpolation method to use for the merging. Supports "sigmoid", "inv_sigmoid", "add_difference" and None. - Passing None uses the default interpolation which is weighted sum interpolation. For merging three checkpoints, only "add_difference" is supported. - :param force: Whether to ignore mismatch in model_config.json for the current models. Defaults to False. - - **kwargs - the default DiffusionPipeline.get_config_dict kwargs: - cache_dir, resume_download, force_download, proxies, local_files_only, use_auth_token, revision, torch_dtype, device_map - """ - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - verbosity = dlogging.get_verbosity() - dlogging.set_verbosity_error() - - pipe = DiffusionPipeline.from_pretrained( - model_paths[0], - custom_pipeline="checkpoint_merger", - ) - merged_pipe = pipe.merge( - pretrained_model_name_or_path_list=model_paths, - alpha=alpha, - interp=interp.value if interp else None, # diffusers API treats None as "weighted sum" - force=force, - **kwargs, - ) - dlogging.set_verbosity(verbosity) - return merged_pipe - - def merge_diffusion_models_and_save( - self, - model_names: List[str], - base_model: Union[BaseModelType, str], - merged_model_name: str, - alpha: float = 0.5, - interp: Optional[MergeInterpolationMethod] = None, - force: bool = False, - merge_dest_directory: Optional[Path] = None, - **kwargs, - ) -> AddModelResult: - """ - :param models: up to three models, designated by their InvokeAI models.yaml model name - :param base_model: base model (must be the same for all merged models!) - :param merged_model_name: name for new model - :param alpha: The interpolation parameter. Ranges from 0 to 1. It affects the ratio in which the checkpoints are merged. A 0.8 alpha - would mean that the first model checkpoints would affect the final result far less than an alpha of 0.2 - :param interp: The interpolation method to use for the merging. Supports "weighted_average", "sigmoid", "inv_sigmoid", "add_difference" and None. - Passing None uses the default interpolation which is weighted sum interpolation. For merging three checkpoints, only "add_difference" is supported. Add_difference is A+(B-C). - :param force: Whether to ignore mismatch in model_config.json for the current models. Defaults to False. - :param merge_dest_directory: Save the merged model to the designated directory (with 'merged_model_name' appended) - **kwargs - the default DiffusionPipeline.get_config_dict kwargs: - cache_dir, resume_download, force_download, proxies, local_files_only, use_auth_token, revision, torch_dtype, device_map - """ - model_paths = [] - config = self.manager.app_config - base_model = BaseModelType(base_model) - vae = None - - for mod in model_names: - info = self.manager.list_model(mod, base_model=base_model, model_type=ModelType.Main) - assert info, f"model {mod}, base_model {base_model}, is unknown" - assert ( - info["model_format"] == "diffusers" - ), f"{mod} is not a diffusers model. It must be optimized before merging" - assert info["variant"] == "normal", f"{mod} is a {info['variant']} model, which cannot currently be merged" - assert ( - len(model_names) <= 2 or interp == MergeInterpolationMethod.AddDifference - ), "When merging three models, only the 'add_difference' merge method is supported" - # pick up the first model's vae - if mod == model_names[0]: - vae = info.get("vae") - model_paths.extend([(config.root_path / info["path"]).as_posix()]) - - merge_method = None if interp == "weighted_sum" else MergeInterpolationMethod(interp) - logger.debug(f"interp = {interp}, merge_method={merge_method}") - merged_pipe = self.merge_diffusion_models(model_paths, alpha, merge_method, force, **kwargs) - dump_path = ( - Path(merge_dest_directory) - if merge_dest_directory - else config.models_path / base_model.value / ModelType.Main.value - ) - dump_path.mkdir(parents=True, exist_ok=True) - dump_path = (dump_path / merged_model_name).as_posix() - - merged_pipe.save_pretrained(dump_path, safe_serialization=True) - attributes = { - "path": dump_path, - "description": f"Merge of models {', '.join(model_names)}", - "model_format": "diffusers", - "variant": ModelVariantType.Normal.value, - "vae": vae, - } - return self.manager.add_model( - merged_model_name, - base_model=base_model, - model_type=ModelType.Main, - model_attributes=attributes, - clobber=True, - ) diff --git a/invokeai/backend/model_management_OLD/model_probe.py b/invokeai/backend/model_management_OLD/model_probe.py deleted file mode 100644 index 74b1b72d31..0000000000 --- a/invokeai/backend/model_management_OLD/model_probe.py +++ /dev/null @@ -1,664 +0,0 @@ -import json -import re -from dataclasses import dataclass -from pathlib import Path -from typing import Callable, Dict, Literal, Optional, Union - -import safetensors.torch -import torch -from diffusers import ConfigMixin, ModelMixin -from picklescan.scanner import scan_file_path - -from invokeai.backend.model_management.models.ip_adapter import IPAdapterModelFormat - -from .models import ( - BaseModelType, - InvalidModelException, - ModelType, - ModelVariantType, - SchedulerPredictionType, - SilenceWarnings, -) -from .models.base import read_checkpoint_meta -from .util import lora_token_vector_length - - -@dataclass -class ModelProbeInfo(object): - model_type: ModelType - base_type: BaseModelType - variant_type: ModelVariantType - prediction_type: SchedulerPredictionType - upcast_attention: bool - format: Literal["diffusers", "checkpoint", "lycoris", "olive", "onnx"] - image_size: int - name: Optional[str] = None - description: Optional[str] = None - - -class ProbeBase(object): - """forward declaration""" - - pass - - -class ModelProbe(object): - PROBES = { - "diffusers": {}, - "checkpoint": {}, - "onnx": {}, - } - - CLASS2TYPE = { - "StableDiffusionPipeline": ModelType.Main, - "StableDiffusionInpaintPipeline": ModelType.Main, - "StableDiffusionXLPipeline": ModelType.Main, - "StableDiffusionXLImg2ImgPipeline": ModelType.Main, - "StableDiffusionXLInpaintPipeline": ModelType.Main, - "LatentConsistencyModelPipeline": ModelType.Main, - "AutoencoderKL": ModelType.Vae, - "AutoencoderTiny": ModelType.Vae, - "ControlNetModel": ModelType.ControlNet, - "CLIPVisionModelWithProjection": ModelType.CLIPVision, - "T2IAdapter": ModelType.T2IAdapter, - } - - @classmethod - def register_probe( - cls, format: Literal["diffusers", "checkpoint", "onnx"], model_type: ModelType, probe_class: ProbeBase - ): - cls.PROBES[format][model_type] = probe_class - - @classmethod - def heuristic_probe( - cls, - model: Union[Dict, ModelMixin, Path], - prediction_type_helper: Callable[[Path], SchedulerPredictionType] = None, - ) -> ModelProbeInfo: - if isinstance(model, Path): - return cls.probe(model_path=model, prediction_type_helper=prediction_type_helper) - elif isinstance(model, (dict, ModelMixin, ConfigMixin)): - return cls.probe(model_path=None, model=model, prediction_type_helper=prediction_type_helper) - else: - raise InvalidModelException("model parameter {model} is neither a Path, nor a model") - - @classmethod - def probe( - cls, - model_path: Path, - model: Optional[Union[Dict, ModelMixin]] = None, - prediction_type_helper: Optional[Callable[[Path], SchedulerPredictionType]] = None, - ) -> ModelProbeInfo: - """ - Probe the model at model_path and return sufficient information about it - to place it somewhere in the models directory hierarchy. If the model is - already loaded into memory, you may provide it as model in order to avoid - opening it a second time. The prediction_type_helper callable is a function that receives - the path to the model and returns the SchedulerPredictionType. - """ - if model_path: - format_type = "diffusers" if model_path.is_dir() else "checkpoint" - else: - format_type = "diffusers" if isinstance(model, (ConfigMixin, ModelMixin)) else "checkpoint" - model_info = None - try: - model_type = ( - cls.get_model_type_from_folder(model_path, model) - if format_type == "diffusers" - else cls.get_model_type_from_checkpoint(model_path, model) - ) - format_type = "onnx" if model_type == ModelType.ONNX else format_type - probe_class = cls.PROBES[format_type].get(model_type) - if not probe_class: - return None - probe = probe_class(model_path, model, prediction_type_helper) - base_type = probe.get_base_type() - variant_type = probe.get_variant_type() - prediction_type = probe.get_scheduler_prediction_type() - name = cls.get_model_name(model_path) - description = f"{base_type.value} {model_type.value} model {name}" - format = probe.get_format() - model_info = ModelProbeInfo( - model_type=model_type, - base_type=base_type, - variant_type=variant_type, - prediction_type=prediction_type, - name=name, - description=description, - upcast_attention=( - base_type == BaseModelType.StableDiffusion2 - and prediction_type == SchedulerPredictionType.VPrediction - ), - format=format, - image_size=( - 1024 - if (base_type in {BaseModelType.StableDiffusionXL, BaseModelType.StableDiffusionXLRefiner}) - else ( - 768 - if ( - base_type == BaseModelType.StableDiffusion2 - and prediction_type == SchedulerPredictionType.VPrediction - ) - else 512 - ) - ), - ) - except Exception: - raise - - return model_info - - @classmethod - def get_model_name(cls, model_path: Path) -> str: - if model_path.suffix in {".safetensors", ".bin", ".pt", ".ckpt"}: - return model_path.stem - else: - return model_path.name - - @classmethod - def get_model_type_from_checkpoint(cls, model_path: Path, checkpoint: dict) -> ModelType: - if model_path.suffix not in (".bin", ".pt", ".ckpt", ".safetensors", ".pth"): - return None - - if model_path.name == "learned_embeds.bin": - return ModelType.TextualInversion - - ckpt = checkpoint if checkpoint else read_checkpoint_meta(model_path, scan=True) - ckpt = ckpt.get("state_dict", ckpt) - - for key in ckpt.keys(): - if any(key.startswith(v) for v in {"cond_stage_model.", "first_stage_model.", "model.diffusion_model."}): - return ModelType.Main - elif any(key.startswith(v) for v in {"encoder.conv_in", "decoder.conv_in"}): - return ModelType.Vae - elif any(key.startswith(v) for v in {"lora_te_", "lora_unet_"}): - return ModelType.Lora - elif any(key.endswith(v) for v in {"to_k_lora.up.weight", "to_q_lora.down.weight"}): - return ModelType.Lora - elif any(key.startswith(v) for v in {"control_model", "input_blocks"}): - return ModelType.ControlNet - elif key in {"emb_params", "string_to_param"}: - return ModelType.TextualInversion - - else: - # diffusers-ti - if len(ckpt) < 10 and all(isinstance(v, torch.Tensor) for v in ckpt.values()): - return ModelType.TextualInversion - - raise InvalidModelException(f"Unable to determine model type for {model_path}") - - @classmethod - def get_model_type_from_folder(cls, folder_path: Path, model: ModelMixin) -> ModelType: - """ - Get the model type of a hugging-face style folder. - """ - class_name = None - error_hint = None - if model: - class_name = model.__class__.__name__ - else: - for suffix in ["bin", "safetensors"]: - if (folder_path / f"learned_embeds.{suffix}").exists(): - return ModelType.TextualInversion - if (folder_path / f"pytorch_lora_weights.{suffix}").exists(): - return ModelType.Lora - if (folder_path / "unet/model.onnx").exists(): - return ModelType.ONNX - if (folder_path / "image_encoder.txt").exists(): - return ModelType.IPAdapter - - i = folder_path / "model_index.json" - c = folder_path / "config.json" - config_path = i if i.exists() else c if c.exists() else None - - if config_path: - with open(config_path, "r") as file: - conf = json.load(file) - if "_class_name" in conf: - class_name = conf["_class_name"] - elif "architectures" in conf: - class_name = conf["architectures"][0] - else: - class_name = None - else: - error_hint = f"No model_index.json or config.json found in {folder_path}." - - if class_name and (type := cls.CLASS2TYPE.get(class_name)): - return type - else: - error_hint = f"class {class_name} is not one of the supported classes [{', '.join(cls.CLASS2TYPE.keys())}]" - - # give up - raise InvalidModelException( - f"Unable to determine model type for {folder_path}" + (f"; {error_hint}" if error_hint else "") - ) - - @classmethod - def _scan_and_load_checkpoint(cls, model_path: Path) -> dict: - with SilenceWarnings(): - if model_path.suffix.endswith((".ckpt", ".pt", ".bin")): - cls._scan_model(model_path, model_path) - return torch.load(model_path, map_location="cpu") - else: - return safetensors.torch.load_file(model_path) - - @classmethod - def _scan_model(cls, model_name, checkpoint): - """ - Apply picklescanner to the indicated checkpoint and issue a warning - and option to exit if an infected file is identified. - """ - # scan model - scan_result = scan_file_path(checkpoint) - if scan_result.infected_files != 0: - raise Exception("The model {model_name} is potentially infected by malware. Aborting import.") - - -# ##################################################3 -# Checkpoint probing -# ##################################################3 -class ProbeBase(object): - def get_base_type(self) -> BaseModelType: - pass - - def get_variant_type(self) -> ModelVariantType: - pass - - def get_scheduler_prediction_type(self) -> SchedulerPredictionType: - pass - - def get_format(self) -> str: - pass - - -class CheckpointProbeBase(ProbeBase): - def __init__( - self, checkpoint_path: Path, checkpoint: dict, helper: Callable[[Path], SchedulerPredictionType] = None - ) -> BaseModelType: - self.checkpoint = checkpoint or ModelProbe._scan_and_load_checkpoint(checkpoint_path) - self.checkpoint_path = checkpoint_path - self.helper = helper - - def get_base_type(self) -> BaseModelType: - pass - - def get_format(self) -> str: - return "checkpoint" - - def get_variant_type(self) -> ModelVariantType: - model_type = ModelProbe.get_model_type_from_checkpoint(self.checkpoint_path, self.checkpoint) - if model_type != ModelType.Main: - return ModelVariantType.Normal - state_dict = self.checkpoint.get("state_dict") or self.checkpoint - in_channels = state_dict["model.diffusion_model.input_blocks.0.0.weight"].shape[1] - if in_channels == 9: - return ModelVariantType.Inpaint - elif in_channels == 5: - return ModelVariantType.Depth - elif in_channels == 4: - return ModelVariantType.Normal - else: - raise InvalidModelException( - f"Cannot determine variant type (in_channels={in_channels}) at {self.checkpoint_path}" - ) - - -class PipelineCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - state_dict = self.checkpoint.get("state_dict") or checkpoint - key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" - if key_name in state_dict and state_dict[key_name].shape[-1] == 768: - return BaseModelType.StableDiffusion1 - if key_name in state_dict and state_dict[key_name].shape[-1] == 1024: - return BaseModelType.StableDiffusion2 - key_name = "model.diffusion_model.input_blocks.4.1.transformer_blocks.0.attn2.to_k.weight" - if key_name in state_dict and state_dict[key_name].shape[-1] == 2048: - return BaseModelType.StableDiffusionXL - elif key_name in state_dict and state_dict[key_name].shape[-1] == 1280: - return BaseModelType.StableDiffusionXLRefiner - else: - raise InvalidModelException("Cannot determine base type") - - def get_scheduler_prediction_type(self) -> Optional[SchedulerPredictionType]: - """Return model prediction type.""" - # if there is a .yaml associated with this checkpoint, then we do not need - # to probe for the prediction type as it will be ignored. - if self.checkpoint_path and self.checkpoint_path.with_suffix(".yaml").exists(): - return None - - type = self.get_base_type() - if type == BaseModelType.StableDiffusion2: - checkpoint = self.checkpoint - state_dict = self.checkpoint.get("state_dict") or checkpoint - key_name = "model.diffusion_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight" - if key_name in state_dict and state_dict[key_name].shape[-1] == 1024: - if "global_step" in checkpoint: - if checkpoint["global_step"] == 220000: - return SchedulerPredictionType.Epsilon - elif checkpoint["global_step"] == 110000: - return SchedulerPredictionType.VPrediction - if self.helper and self.checkpoint_path: - if helper_guess := self.helper(self.checkpoint_path): - return helper_guess - return SchedulerPredictionType.VPrediction # a guess for sd2 ckpts - - elif type == BaseModelType.StableDiffusion1: - if self.helper and self.checkpoint_path: - if helper_guess := self.helper(self.checkpoint_path): - return helper_guess - return SchedulerPredictionType.Epsilon # a reasonable guess for sd1 ckpts - else: - return None - - -class VaeCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - # I can't find any standalone 2.X VAEs to test with! - return BaseModelType.StableDiffusion1 - - -class LoRACheckpointProbe(CheckpointProbeBase): - def get_format(self) -> str: - return "lycoris" - - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - token_vector_length = lora_token_vector_length(checkpoint) - - if token_vector_length == 768: - return BaseModelType.StableDiffusion1 - elif token_vector_length == 1024: - return BaseModelType.StableDiffusion2 - elif token_vector_length == 1280: - return BaseModelType.StableDiffusionXL # recognizes format at https://civitai.com/models/224641 - elif token_vector_length == 2048: - return BaseModelType.StableDiffusionXL - else: - raise InvalidModelException(f"Unknown LoRA type: {self.checkpoint_path}") - - -class TextualInversionCheckpointProbe(CheckpointProbeBase): - def get_format(self) -> str: - return None - - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - if "string_to_token" in checkpoint: - token_dim = list(checkpoint["string_to_param"].values())[0].shape[-1] - elif "emb_params" in checkpoint: - token_dim = checkpoint["emb_params"].shape[-1] - elif "clip_g" in checkpoint: - token_dim = checkpoint["clip_g"].shape[-1] - else: - token_dim = list(checkpoint.values())[0].shape[-1] - if token_dim == 768: - return BaseModelType.StableDiffusion1 - elif token_dim == 1024: - return BaseModelType.StableDiffusion2 - elif token_dim == 1280: - return BaseModelType.StableDiffusionXL - else: - return None - - -class ControlNetCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - checkpoint = self.checkpoint - for key_name in ( - "control_model.input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight", - "input_blocks.2.1.transformer_blocks.0.attn2.to_k.weight", - ): - if key_name not in checkpoint: - continue - if checkpoint[key_name].shape[-1] == 768: - return BaseModelType.StableDiffusion1 - elif checkpoint[key_name].shape[-1] == 1024: - return BaseModelType.StableDiffusion2 - elif self.checkpoint_path and self.helper: - return self.helper(self.checkpoint_path) - raise InvalidModelException("Unable to determine base type for {self.checkpoint_path}") - - -class IPAdapterCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - raise NotImplementedError() - - -class CLIPVisionCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - raise NotImplementedError() - - -class T2IAdapterCheckpointProbe(CheckpointProbeBase): - def get_base_type(self) -> BaseModelType: - raise NotImplementedError() - - -######################################################## -# classes for probing folders -####################################################### -class FolderProbeBase(ProbeBase): - def __init__(self, folder_path: Path, model: ModelMixin = None, helper: Callable = None): # not used - self.model = model - self.folder_path = folder_path - - def get_variant_type(self) -> ModelVariantType: - return ModelVariantType.Normal - - def get_format(self) -> str: - return "diffusers" - - -class PipelineFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - if self.model: - unet_conf = self.model.unet.config - else: - with open(self.folder_path / "unet" / "config.json", "r") as file: - unet_conf = json.load(file) - if unet_conf["cross_attention_dim"] == 768: - return BaseModelType.StableDiffusion1 - elif unet_conf["cross_attention_dim"] == 1024: - return BaseModelType.StableDiffusion2 - elif unet_conf["cross_attention_dim"] == 1280: - return BaseModelType.StableDiffusionXLRefiner - elif unet_conf["cross_attention_dim"] == 2048: - return BaseModelType.StableDiffusionXL - else: - raise InvalidModelException(f"Unknown base model for {self.folder_path}") - - def get_scheduler_prediction_type(self) -> SchedulerPredictionType: - if self.model: - scheduler_conf = self.model.scheduler.config - else: - with open(self.folder_path / "scheduler" / "scheduler_config.json", "r") as file: - scheduler_conf = json.load(file) - if scheduler_conf["prediction_type"] == "v_prediction": - return SchedulerPredictionType.VPrediction - elif scheduler_conf["prediction_type"] == "epsilon": - return SchedulerPredictionType.Epsilon - else: - return None - - def get_variant_type(self) -> ModelVariantType: - # This only works for pipelines! Any kind of - # exception results in our returning the - # "normal" variant type - try: - if self.model: - conf = self.model.unet.config - else: - config_file = self.folder_path / "unet" / "config.json" - with open(config_file, "r") as file: - conf = json.load(file) - - in_channels = conf["in_channels"] - if in_channels == 9: - return ModelVariantType.Inpaint - elif in_channels == 5: - return ModelVariantType.Depth - elif in_channels == 4: - return ModelVariantType.Normal - except Exception: - pass - return ModelVariantType.Normal - - -class VaeFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - if self._config_looks_like_sdxl(): - return BaseModelType.StableDiffusionXL - elif self._name_looks_like_sdxl(): - # but SD and SDXL VAE are the same shape (3-channel RGB to 4-channel float scaled down - # by a factor of 8), we can't necessarily tell them apart by config hyperparameters. - return BaseModelType.StableDiffusionXL - else: - return BaseModelType.StableDiffusion1 - - def _config_looks_like_sdxl(self) -> bool: - # config values that distinguish Stability's SD 1.x VAE from their SDXL VAE. - config_file = self.folder_path / "config.json" - if not config_file.exists(): - raise InvalidModelException(f"Cannot determine base type for {self.folder_path}") - with open(config_file, "r") as file: - config = json.load(file) - return config.get("scaling_factor", 0) == 0.13025 and config.get("sample_size") in [512, 1024] - - def _name_looks_like_sdxl(self) -> bool: - return bool(re.search(r"xl\b", self._guess_name(), re.IGNORECASE)) - - def _guess_name(self) -> str: - name = self.folder_path.name - if name == "vae": - name = self.folder_path.parent.name - return name - - -class TextualInversionFolderProbe(FolderProbeBase): - def get_format(self) -> str: - return None - - def get_base_type(self) -> BaseModelType: - path = self.folder_path / "learned_embeds.bin" - if not path.exists(): - return None - checkpoint = ModelProbe._scan_and_load_checkpoint(path) - return TextualInversionCheckpointProbe(None, checkpoint=checkpoint).get_base_type() - - -class ONNXFolderProbe(FolderProbeBase): - def get_format(self) -> str: - return "onnx" - - def get_base_type(self) -> BaseModelType: - return BaseModelType.StableDiffusion1 - - def get_variant_type(self) -> ModelVariantType: - return ModelVariantType.Normal - - -class ControlNetFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - config_file = self.folder_path / "config.json" - if not config_file.exists(): - raise InvalidModelException(f"Cannot determine base type for {self.folder_path}") - with open(config_file, "r") as file: - config = json.load(file) - # no obvious way to distinguish between sd2-base and sd2-768 - dimension = config["cross_attention_dim"] - base_model = ( - BaseModelType.StableDiffusion1 - if dimension == 768 - else ( - BaseModelType.StableDiffusion2 - if dimension == 1024 - else BaseModelType.StableDiffusionXL - if dimension == 2048 - else None - ) - ) - if not base_model: - raise InvalidModelException(f"Unable to determine model base for {self.folder_path}") - return base_model - - -class LoRAFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - model_file = None - for suffix in ["safetensors", "bin"]: - base_file = self.folder_path / f"pytorch_lora_weights.{suffix}" - if base_file.exists(): - model_file = base_file - break - if not model_file: - raise InvalidModelException("Unknown LoRA format encountered") - return LoRACheckpointProbe(model_file, None).get_base_type() - - -class IPAdapterFolderProbe(FolderProbeBase): - def get_format(self) -> str: - return IPAdapterModelFormat.InvokeAI.value - - def get_base_type(self) -> BaseModelType: - model_file = self.folder_path / "ip_adapter.bin" - if not model_file.exists(): - raise InvalidModelException("Unknown IP-Adapter model format.") - - state_dict = torch.load(model_file, map_location="cpu") - cross_attention_dim = state_dict["ip_adapter"]["1.to_k_ip.weight"].shape[-1] - if cross_attention_dim == 768: - return BaseModelType.StableDiffusion1 - elif cross_attention_dim == 1024: - return BaseModelType.StableDiffusion2 - elif cross_attention_dim == 2048: - return BaseModelType.StableDiffusionXL - else: - raise InvalidModelException(f"IP-Adapter had unexpected cross-attention dimension: {cross_attention_dim}.") - - -class CLIPVisionFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - return BaseModelType.Any - - -class T2IAdapterFolderProbe(FolderProbeBase): - def get_base_type(self) -> BaseModelType: - config_file = self.folder_path / "config.json" - if not config_file.exists(): - raise InvalidModelException(f"Cannot determine base type for {self.folder_path}") - with open(config_file, "r") as file: - config = json.load(file) - - adapter_type = config.get("adapter_type", None) - if adapter_type == "full_adapter_xl": - return BaseModelType.StableDiffusionXL - elif adapter_type == "full_adapter" or "light_adapter": - # I haven't seen any T2I adapter models for SD2, so assume that this is an SD1 adapter. - return BaseModelType.StableDiffusion1 - else: - raise InvalidModelException( - f"Unable to determine base model for '{self.folder_path}' (adapter_type = {adapter_type})." - ) - - -############## register probe classes ###### -ModelProbe.register_probe("diffusers", ModelType.Main, PipelineFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.Vae, VaeFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.Lora, LoRAFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.TextualInversion, TextualInversionFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.ControlNet, ControlNetFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.IPAdapter, IPAdapterFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.CLIPVision, CLIPVisionFolderProbe) -ModelProbe.register_probe("diffusers", ModelType.T2IAdapter, T2IAdapterFolderProbe) - -ModelProbe.register_probe("checkpoint", ModelType.Main, PipelineCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.Vae, VaeCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.Lora, LoRACheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.TextualInversion, TextualInversionCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.ControlNet, ControlNetCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.IPAdapter, IPAdapterCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.CLIPVision, CLIPVisionCheckpointProbe) -ModelProbe.register_probe("checkpoint", ModelType.T2IAdapter, T2IAdapterCheckpointProbe) - -ModelProbe.register_probe("onnx", ModelType.ONNX, ONNXFolderProbe) diff --git a/invokeai/backend/model_management_OLD/model_search.py b/invokeai/backend/model_management_OLD/model_search.py deleted file mode 100644 index e125c3ced7..0000000000 --- a/invokeai/backend/model_management_OLD/model_search.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2023, Lincoln D. Stein and the InvokeAI Team -""" -Abstract base class for recursive directory search for models. -""" - -import os -from abc import ABC, abstractmethod -from pathlib import Path -from typing import List, Set, types - -import invokeai.backend.util.logging as logger - - -class ModelSearch(ABC): - def __init__(self, directories: List[Path], logger: types.ModuleType = logger): - """ - Initialize a recursive model directory search. - :param directories: List of directory Paths to recurse through - :param logger: Logger to use - """ - self.directories = directories - self.logger = logger - self._items_scanned = 0 - self._models_found = 0 - self._scanned_dirs = set() - self._scanned_paths = set() - self._pruned_paths = set() - - @abstractmethod - def on_search_started(self): - """ - Called before the scan starts. - """ - pass - - @abstractmethod - def on_model_found(self, model: Path): - """ - Process a found model. Raise an exception if something goes wrong. - :param model: Model to process - could be a directory or checkpoint. - """ - pass - - @abstractmethod - def on_search_completed(self): - """ - Perform some activity when the scan is completed. May use instance - variables, items_scanned and models_found - """ - pass - - def search(self): - self.on_search_started() - for dir in self.directories: - self.walk_directory(dir) - self.on_search_completed() - - def walk_directory(self, path: Path): - for root, dirs, files in os.walk(path, followlinks=True): - if str(Path(root).name).startswith("."): - self._pruned_paths.add(root) - if any(Path(root).is_relative_to(x) for x in self._pruned_paths): - continue - - self._items_scanned += len(dirs) + len(files) - for d in dirs: - path = Path(root) / d - if path in self._scanned_paths or path.parent in self._scanned_dirs: - self._scanned_dirs.add(path) - continue - if any( - (path / x).exists() - for x in { - "config.json", - "model_index.json", - "learned_embeds.bin", - "pytorch_lora_weights.bin", - "image_encoder.txt", - } - ): - try: - self.on_model_found(path) - self._models_found += 1 - self._scanned_dirs.add(path) - except Exception as e: - self.logger.warning(f"Failed to process '{path}': {e}") - - for f in files: - path = Path(root) / f - if path.parent in self._scanned_dirs: - continue - if path.suffix in {".ckpt", ".bin", ".pth", ".safetensors", ".pt"}: - try: - self.on_model_found(path) - self._models_found += 1 - except Exception as e: - self.logger.warning(f"Failed to process '{path}': {e}") - - -class FindModels(ModelSearch): - def on_search_started(self): - self.models_found: Set[Path] = set() - - def on_model_found(self, model: Path): - self.models_found.add(model) - - def on_search_completed(self): - pass - - def list_models(self) -> List[Path]: - self.search() - return list(self.models_found) diff --git a/invokeai/backend/model_management_OLD/models/__init__.py b/invokeai/backend/model_management_OLD/models/__init__.py deleted file mode 100644 index 5f9b13b96f..0000000000 --- a/invokeai/backend/model_management_OLD/models/__init__.py +++ /dev/null @@ -1,167 +0,0 @@ -import inspect -from enum import Enum -from typing import Literal, get_origin - -from pydantic import BaseModel, ConfigDict, create_model - -from .base import ( # noqa: F401 - BaseModelType, - DuplicateModelException, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelError, - ModelNotFoundException, - ModelType, - ModelVariantType, - SchedulerPredictionType, - SilenceWarnings, - SubModelType, -) -from .clip_vision import CLIPVisionModel -from .controlnet import ControlNetModel # TODO: -from .ip_adapter import IPAdapterModel -from .lora import LoRAModel -from .sdxl import StableDiffusionXLModel -from .stable_diffusion import StableDiffusion1Model, StableDiffusion2Model -from .stable_diffusion_onnx import ONNXStableDiffusion1Model, ONNXStableDiffusion2Model -from .t2i_adapter import T2IAdapterModel -from .textual_inversion import TextualInversionModel -from .vae import VaeModel - -MODEL_CLASSES = { - BaseModelType.StableDiffusion1: { - ModelType.ONNX: ONNXStableDiffusion1Model, - ModelType.Main: StableDiffusion1Model, - ModelType.Vae: VaeModel, - ModelType.Lora: LoRAModel, - ModelType.ControlNet: ControlNetModel, - ModelType.TextualInversion: TextualInversionModel, - ModelType.IPAdapter: IPAdapterModel, - ModelType.CLIPVision: CLIPVisionModel, - ModelType.T2IAdapter: T2IAdapterModel, - }, - BaseModelType.StableDiffusion2: { - ModelType.ONNX: ONNXStableDiffusion2Model, - ModelType.Main: StableDiffusion2Model, - ModelType.Vae: VaeModel, - ModelType.Lora: LoRAModel, - ModelType.ControlNet: ControlNetModel, - ModelType.TextualInversion: TextualInversionModel, - ModelType.IPAdapter: IPAdapterModel, - ModelType.CLIPVision: CLIPVisionModel, - ModelType.T2IAdapter: T2IAdapterModel, - }, - BaseModelType.StableDiffusionXL: { - ModelType.Main: StableDiffusionXLModel, - ModelType.Vae: VaeModel, - # will not work until support written - ModelType.Lora: LoRAModel, - ModelType.ControlNet: ControlNetModel, - ModelType.TextualInversion: TextualInversionModel, - ModelType.ONNX: ONNXStableDiffusion2Model, - ModelType.IPAdapter: IPAdapterModel, - ModelType.CLIPVision: CLIPVisionModel, - ModelType.T2IAdapter: T2IAdapterModel, - }, - BaseModelType.StableDiffusionXLRefiner: { - ModelType.Main: StableDiffusionXLModel, - ModelType.Vae: VaeModel, - # will not work until support written - ModelType.Lora: LoRAModel, - ModelType.ControlNet: ControlNetModel, - ModelType.TextualInversion: TextualInversionModel, - ModelType.ONNX: ONNXStableDiffusion2Model, - ModelType.IPAdapter: IPAdapterModel, - ModelType.CLIPVision: CLIPVisionModel, - ModelType.T2IAdapter: T2IAdapterModel, - }, - BaseModelType.Any: { - ModelType.CLIPVision: CLIPVisionModel, - # The following model types are not expected to be used with BaseModelType.Any. - ModelType.ONNX: ONNXStableDiffusion2Model, - ModelType.Main: StableDiffusion2Model, - ModelType.Vae: VaeModel, - ModelType.Lora: LoRAModel, - ModelType.ControlNet: ControlNetModel, - ModelType.TextualInversion: TextualInversionModel, - ModelType.IPAdapter: IPAdapterModel, - ModelType.T2IAdapter: T2IAdapterModel, - }, - # BaseModelType.Kandinsky2_1: { - # ModelType.Main: Kandinsky2_1Model, - # ModelType.MoVQ: MoVQModel, - # ModelType.Lora: LoRAModel, - # ModelType.ControlNet: ControlNetModel, - # ModelType.TextualInversion: TextualInversionModel, - # }, -} - -MODEL_CONFIGS = [] -OPENAPI_MODEL_CONFIGS = [] - - -class OpenAPIModelInfoBase(BaseModel): - model_name: str - base_model: BaseModelType - model_type: ModelType - - model_config = ConfigDict(protected_namespaces=()) - - -for _base_model, models in MODEL_CLASSES.items(): - for model_type, model_class in models.items(): - model_configs = set(model_class._get_configs().values()) - model_configs.discard(None) - MODEL_CONFIGS.extend(model_configs) - - # LS: sort to get the checkpoint configs first, which makes - # for a better template in the Swagger docs - for cfg in sorted(model_configs, key=lambda x: str(x)): - model_name, cfg_name = cfg.__qualname__.split(".")[-2:] - openapi_cfg_name = model_name + cfg_name - if openapi_cfg_name in vars(): - continue - - api_wrapper = create_model( - openapi_cfg_name, - __base__=(cfg, OpenAPIModelInfoBase), - model_type=(Literal[model_type], model_type), # type: ignore - ) - vars()[openapi_cfg_name] = api_wrapper - OPENAPI_MODEL_CONFIGS.append(api_wrapper) - - -def get_model_config_enums(): - enums = [] - - for model_config in MODEL_CONFIGS: - if hasattr(inspect, "get_annotations"): - fields = inspect.get_annotations(model_config) - else: - fields = model_config.__annotations__ - try: - field = fields["model_format"] - except Exception: - raise Exception("format field not found") - - # model_format: None - # model_format: SomeModelFormat - # model_format: Literal[SomeModelFormat.Diffusers] - # model_format: Literal[SomeModelFormat.Diffusers, SomeModelFormat.Checkpoint] - - if isinstance(field, type) and issubclass(field, str) and issubclass(field, Enum): - enums.append(field) - - elif get_origin(field) is Literal and all( - isinstance(arg, str) and isinstance(arg, Enum) for arg in field.__args__ - ): - enums.append(type(field.__args__[0])) - - elif field is None: - pass - - else: - raise Exception(f"Unsupported format definition in {model_configs.__qualname__}") - - return enums diff --git a/invokeai/backend/model_management_OLD/models/base.py b/invokeai/backend/model_management_OLD/models/base.py deleted file mode 100644 index 7807cb9a54..0000000000 --- a/invokeai/backend/model_management_OLD/models/base.py +++ /dev/null @@ -1,681 +0,0 @@ -import inspect -import json -import os -import sys -import typing -import warnings -from abc import ABCMeta, abstractmethod -from contextlib import suppress -from enum import Enum -from pathlib import Path -from typing import Any, Callable, Dict, Generic, List, Literal, Optional, Type, TypeVar, Union - -import numpy as np -import onnx -import safetensors.torch -import torch -from diffusers import ConfigMixin, DiffusionPipeline -from diffusers import logging as diffusers_logging -from onnx import numpy_helper -from onnxruntime import InferenceSession, SessionOptions, get_available_providers -from picklescan.scanner import scan_file_path -from pydantic import BaseModel, ConfigDict, Field -from transformers import logging as transformers_logging - - -class DuplicateModelException(Exception): - pass - - -class InvalidModelException(Exception): - pass - - -class ModelNotFoundException(Exception): - pass - - -class BaseModelType(str, Enum): - Any = "any" # For models that are not associated with any particular base model. - StableDiffusion1 = "sd-1" - StableDiffusion2 = "sd-2" - StableDiffusionXL = "sdxl" - StableDiffusionXLRefiner = "sdxl-refiner" - # Kandinsky2_1 = "kandinsky-2.1" - - -class ModelType(str, Enum): - ONNX = "onnx" - Main = "main" - Vae = "vae" - Lora = "lora" - ControlNet = "controlnet" # used by model_probe - TextualInversion = "embedding" - IPAdapter = "ip_adapter" - CLIPVision = "clip_vision" - T2IAdapter = "t2i_adapter" - - -class SubModelType(str, Enum): - UNet = "unet" - TextEncoder = "text_encoder" - TextEncoder2 = "text_encoder_2" - Tokenizer = "tokenizer" - Tokenizer2 = "tokenizer_2" - Vae = "vae" - VaeDecoder = "vae_decoder" - VaeEncoder = "vae_encoder" - Scheduler = "scheduler" - SafetyChecker = "safety_checker" - # MoVQ = "movq" - - -class ModelVariantType(str, Enum): - Normal = "normal" - Inpaint = "inpaint" - Depth = "depth" - - -class SchedulerPredictionType(str, Enum): - Epsilon = "epsilon" - VPrediction = "v_prediction" - Sample = "sample" - - -class ModelError(str, Enum): - NotFound = "not_found" - - -def model_config_json_schema_extra(schema: dict[str, Any]) -> None: - if "required" not in schema: - schema["required"] = [] - schema["required"].append("model_type") - - -class ModelConfigBase(BaseModel): - path: str # or Path - description: Optional[str] = Field(None) - model_format: Optional[str] = Field(None) - error: Optional[ModelError] = Field(None) - - model_config = ConfigDict( - use_enum_values=True, protected_namespaces=(), json_schema_extra=model_config_json_schema_extra - ) - - -class EmptyConfigLoader(ConfigMixin): - @classmethod - def load_config(cls, *args, **kwargs): - cls.config_name = kwargs.pop("config_name") - return super().load_config(*args, **kwargs) - - -T_co = TypeVar("T_co", covariant=True) - - -class classproperty(Generic[T_co]): - def __init__(self, fget: Callable[[Any], T_co]) -> None: - self.fget = fget - - def __get__(self, instance: Optional[Any], owner: Type[Any]) -> T_co: - return self.fget(owner) - - def __set__(self, instance: Optional[Any], value: Any) -> None: - raise AttributeError("cannot set attribute") - - -class ModelBase(metaclass=ABCMeta): - # model_path: str - # base_model: BaseModelType - # model_type: ModelType - - def __init__( - self, - model_path: str, - base_model: BaseModelType, - model_type: ModelType, - ): - self.model_path = model_path - self.base_model = base_model - self.model_type = model_type - - def _hf_definition_to_type(self, subtypes: List[str]) -> Type: - if len(subtypes) < 2: - raise Exception("Invalid subfolder definition!") - if all(t is None for t in subtypes): - return None - elif any(t is None for t in subtypes): - raise Exception(f"Unsupported definition: {subtypes}") - - if subtypes[0] in ["diffusers", "transformers"]: - res_type = sys.modules[subtypes[0]] - subtypes = subtypes[1:] - - else: - res_type = sys.modules["diffusers"] - res_type = res_type.pipelines - - for subtype in subtypes: - res_type = getattr(res_type, subtype) - return res_type - - @classmethod - def _get_configs(cls): - with suppress(Exception): - return cls.__configs - - configs = {} - for name in dir(cls): - if name.startswith("__"): - continue - - value = getattr(cls, name) - if not isinstance(value, type) or not issubclass(value, ModelConfigBase): - continue - - if hasattr(inspect, "get_annotations"): - fields = inspect.get_annotations(value) - else: - fields = value.__annotations__ - try: - field = fields["model_format"] - except Exception: - raise Exception(f"Invalid config definition - format field not found({cls.__qualname__})") - - if isinstance(field, type) and issubclass(field, str) and issubclass(field, Enum): - for model_format in field: - configs[model_format.value] = value - - elif typing.get_origin(field) is Literal and all( - isinstance(arg, str) and isinstance(arg, Enum) for arg in field.__args__ - ): - for model_format in field.__args__: - configs[model_format.value] = value - - elif field is None: - configs[None] = value - - else: - raise Exception(f"Unsupported format definition in {cls.__qualname__}") - - cls.__configs = configs - return cls.__configs - - @classmethod - def create_config(cls, **kwargs) -> ModelConfigBase: - if "model_format" not in kwargs: - raise Exception("Field 'model_format' not found in model config") - - configs = cls._get_configs() - return configs[kwargs["model_format"]](**kwargs) - - @classmethod - def probe_config(cls, path: str, **kwargs) -> ModelConfigBase: - return cls.create_config( - path=path, - model_format=cls.detect_format(path), - ) - - @classmethod - @abstractmethod - def detect_format(cls, path: str) -> str: - raise NotImplementedError() - - @classproperty - @abstractmethod - def save_to_config(cls) -> bool: - raise NotImplementedError() - - @abstractmethod - def get_size(self, child_type: Optional[SubModelType] = None) -> int: - raise NotImplementedError() - - @abstractmethod - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ) -> Any: - raise NotImplementedError() - - -class DiffusersModel(ModelBase): - # child_types: Dict[str, Type] - # child_sizes: Dict[str, int] - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - super().__init__(model_path, base_model, model_type) - - self.child_types: Dict[str, Type] = {} - self.child_sizes: Dict[str, int] = {} - - try: - config_data = DiffusionPipeline.load_config(self.model_path) - # config_data = json.loads(os.path.join(self.model_path, "model_index.json")) - except Exception: - raise Exception("Invalid diffusers model! (model_index.json not found or invalid)") - - config_data.pop("_ignore_files", None) - - # retrieve all folder_names that contain relevant files - child_components = [k for k, v in config_data.items() if isinstance(v, list)] - - for child_name in child_components: - child_type = self._hf_definition_to_type(config_data[child_name]) - self.child_types[child_name] = child_type - self.child_sizes[child_name] = calc_model_size_by_fs(self.model_path, subfolder=child_name) - - def get_size(self, child_type: Optional[SubModelType] = None): - if child_type is None: - return sum(self.child_sizes.values()) - else: - return self.child_sizes[child_type] - - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ): - # return pipeline in different function to pass more arguments - if child_type is None: - raise Exception("Child model type can't be null on diffusers model") - if child_type not in self.child_types: - return None # TODO: or raise - - if torch_dtype == torch.float16: - variants = ["fp16", None] - else: - variants = [None, "fp16"] - - # TODO: better error handling(differentiate not found from others) - for variant in variants: - try: - # TODO: set cache_dir to /dev/null to be sure that cache not used? - model = self.child_types[child_type].from_pretrained( - self.model_path, - subfolder=child_type.value, - torch_dtype=torch_dtype, - variant=variant, - local_files_only=True, - ) - break - except Exception as e: - if not str(e).startswith("Error no file"): - print("====ERR LOAD====") - print(f"{variant}: {e}") - pass - else: - raise Exception(f"Failed to load {self.base_model}:{self.model_type}:{child_type} model") - - # calc more accurate size - self.child_sizes[child_type] = calc_model_size_by_data(model) - return model - - # def convert_if_required(model_path: str, cache_path: str, config: Optional[dict]) -> str: - - -def calc_model_size_by_fs(model_path: str, subfolder: Optional[str] = None, variant: Optional[str] = None): - if subfolder is not None: - model_path = os.path.join(model_path, subfolder) - - # this can happen when, for example, the safety checker - # is not downloaded. - if not os.path.exists(model_path): - return 0 - - all_files = os.listdir(model_path) - all_files = [f for f in all_files if os.path.isfile(os.path.join(model_path, f))] - - fp16_files = {f for f in all_files if ".fp16." in f or ".fp16-" in f} - bit8_files = {f for f in all_files if ".8bit." in f or ".8bit-" in f} - other_files = set(all_files) - fp16_files - bit8_files - - if variant is None: - files = other_files - elif variant == "fp16": - files = fp16_files - elif variant == "8bit": - files = bit8_files - else: - raise NotImplementedError(f"Unknown variant: {variant}") - - # try read from index if exists - index_postfix = ".index.json" - if variant is not None: - index_postfix = f".index.{variant}.json" - - for file in files: - if not file.endswith(index_postfix): - continue - try: - with open(os.path.join(model_path, file), "r") as f: - index_data = json.loads(f.read()) - return int(index_data["metadata"]["total_size"]) - except Exception: - pass - - # calculate files size if there is no index file - formats = [ - (".safetensors",), # safetensors - (".bin",), # torch - (".onnx", ".pb"), # onnx - (".msgpack",), # flax - (".ckpt",), # tf - (".h5",), # tf2 - ] - - for file_format in formats: - model_files = [f for f in files if f.endswith(file_format)] - if len(model_files) == 0: - continue - - model_size = 0 - for model_file in model_files: - file_stats = os.stat(os.path.join(model_path, model_file)) - model_size += file_stats.st_size - return model_size - - # raise NotImplementedError(f"Unknown model structure! Files: {all_files}") - return 0 # scheduler/feature_extractor/tokenizer - models without loading to gpu - - -def calc_model_size_by_data(model) -> int: - if isinstance(model, DiffusionPipeline): - return _calc_pipeline_by_data(model) - elif isinstance(model, torch.nn.Module): - return _calc_model_by_data(model) - elif isinstance(model, IAIOnnxRuntimeModel): - return _calc_onnx_model_by_data(model) - else: - return 0 - - -def _calc_pipeline_by_data(pipeline) -> int: - res = 0 - for submodel_key in pipeline.components.keys(): - submodel = getattr(pipeline, submodel_key) - if submodel is not None and isinstance(submodel, torch.nn.Module): - res += _calc_model_by_data(submodel) - return res - - -def _calc_model_by_data(model) -> int: - mem_params = sum([param.nelement() * param.element_size() for param in model.parameters()]) - mem_bufs = sum([buf.nelement() * buf.element_size() for buf in model.buffers()]) - mem = mem_params + mem_bufs # in bytes - return mem - - -def _calc_onnx_model_by_data(model) -> int: - tensor_size = model.tensors.size() * 2 # The session doubles this - mem = tensor_size # in bytes - return mem - - -def _fast_safetensors_reader(path: str): - checkpoint = {} - device = torch.device("meta") - with open(path, "rb") as f: - definition_len = int.from_bytes(f.read(8), "little") - definition_json = f.read(definition_len) - definition = json.loads(definition_json) - - if "__metadata__" in definition and definition["__metadata__"].get("format", "pt") not in { - "pt", - "torch", - "pytorch", - }: - raise Exception("Supported only pytorch safetensors files") - definition.pop("__metadata__", None) - - for key, info in definition.items(): - dtype = { - "I8": torch.int8, - "I16": torch.int16, - "I32": torch.int32, - "I64": torch.int64, - "F16": torch.float16, - "F32": torch.float32, - "F64": torch.float64, - }[info["dtype"]] - - checkpoint[key] = torch.empty(info["shape"], dtype=dtype, device=device) - - return checkpoint - - -def read_checkpoint_meta(path: Union[str, Path], scan: bool = False): - if str(path).endswith(".safetensors"): - try: - checkpoint = _fast_safetensors_reader(path) - except Exception: - # TODO: create issue for support "meta"? - checkpoint = safetensors.torch.load_file(path, device="cpu") - else: - if scan: - scan_result = scan_file_path(path) - if scan_result.infected_files != 0: - raise Exception(f'The model file "{path}" is potentially infected by malware. Aborting import.') - checkpoint = torch.load(path, map_location=torch.device("meta")) - return checkpoint - - -class SilenceWarnings(object): - def __init__(self): - self.transformers_verbosity = transformers_logging.get_verbosity() - self.diffusers_verbosity = diffusers_logging.get_verbosity() - - def __enter__(self): - transformers_logging.set_verbosity_error() - diffusers_logging.set_verbosity_error() - warnings.simplefilter("ignore") - - def __exit__(self, type, value, traceback): - transformers_logging.set_verbosity(self.transformers_verbosity) - diffusers_logging.set_verbosity(self.diffusers_verbosity) - warnings.simplefilter("default") - - -ONNX_WEIGHTS_NAME = "model.onnx" - - -class IAIOnnxRuntimeModel: - class _tensor_access: - def __init__(self, model): - self.model = model - self.indexes = {} - for idx, obj in enumerate(self.model.proto.graph.initializer): - self.indexes[obj.name] = idx - - def __getitem__(self, key: str): - value = self.model.proto.graph.initializer[self.indexes[key]] - return numpy_helper.to_array(value) - - def __setitem__(self, key: str, value: np.ndarray): - new_node = numpy_helper.from_array(value) - # set_external_data(new_node, location="in-memory-location") - new_node.name = key - # new_node.ClearField("raw_data") - del self.model.proto.graph.initializer[self.indexes[key]] - self.model.proto.graph.initializer.insert(self.indexes[key], new_node) - # self.model.data[key] = OrtValue.ortvalue_from_numpy(value) - - # __delitem__ - - def __contains__(self, key: str): - return self.indexes[key] in self.model.proto.graph.initializer - - def items(self): - raise NotImplementedError("tensor.items") - # return [(obj.name, obj) for obj in self.raw_proto] - - def keys(self): - return self.indexes.keys() - - def values(self): - raise NotImplementedError("tensor.values") - # return [obj for obj in self.raw_proto] - - def size(self): - bytesSum = 0 - for node in self.model.proto.graph.initializer: - bytesSum += sys.getsizeof(node.raw_data) - return bytesSum - - class _access_helper: - def __init__(self, raw_proto): - self.indexes = {} - self.raw_proto = raw_proto - for idx, obj in enumerate(raw_proto): - self.indexes[obj.name] = idx - - def __getitem__(self, key: str): - return self.raw_proto[self.indexes[key]] - - def __setitem__(self, key: str, value): - index = self.indexes[key] - del self.raw_proto[index] - self.raw_proto.insert(index, value) - - # __delitem__ - - def __contains__(self, key: str): - return key in self.indexes - - def items(self): - return [(obj.name, obj) for obj in self.raw_proto] - - def keys(self): - return self.indexes.keys() - - def values(self): - return list(self.raw_proto) - - def __init__(self, model_path: str, provider: Optional[str]): - self.path = model_path - self.session = None - self.provider = provider - """ - self.data_path = self.path + "_data" - if not os.path.exists(self.data_path): - print(f"Moving model tensors to separate file: {self.data_path}") - tmp_proto = onnx.load(model_path, load_external_data=True) - onnx.save_model(tmp_proto, self.path, save_as_external_data=True, all_tensors_to_one_file=True, location=os.path.basename(self.data_path), size_threshold=1024, convert_attribute=False) - del tmp_proto - gc.collect() - - self.proto = onnx.load(model_path, load_external_data=False) - """ - - self.proto = onnx.load(model_path, load_external_data=True) - # self.data = dict() - # for tensor in self.proto.graph.initializer: - # name = tensor.name - - # if tensor.HasField("raw_data"): - # npt = numpy_helper.to_array(tensor) - # orv = OrtValue.ortvalue_from_numpy(npt) - # # self.data[name] = orv - # # set_external_data(tensor, location="in-memory-location") - # tensor.name = name - # # tensor.ClearField("raw_data") - - self.nodes = self._access_helper(self.proto.graph.node) - # self.initializers = self._access_helper(self.proto.graph.initializer) - # print(self.proto.graph.input) - # print(self.proto.graph.initializer) - - self.tensors = self._tensor_access(self) - - # TODO: integrate with model manager/cache - def create_session(self, height=None, width=None): - if self.session is None or self.session_width != width or self.session_height != height: - # onnx.save(self.proto, "tmp.onnx") - # onnx.save_model(self.proto, "tmp.onnx", save_as_external_data=True, all_tensors_to_one_file=True, location="tmp.onnx_data", size_threshold=1024, convert_attribute=False) - # TODO: something to be able to get weight when they already moved outside of model proto - # (trimmed_model, external_data) = buffer_external_data_tensors(self.proto) - sess = SessionOptions() - # self._external_data.update(**external_data) - # sess.add_external_initializers(list(self.data.keys()), list(self.data.values())) - # sess.enable_profiling = True - - # sess.intra_op_num_threads = 1 - # sess.inter_op_num_threads = 1 - # sess.execution_mode = ExecutionMode.ORT_SEQUENTIAL - # sess.graph_optimization_level = GraphOptimizationLevel.ORT_ENABLE_ALL - # sess.enable_cpu_mem_arena = True - # sess.enable_mem_pattern = True - # sess.add_session_config_entry("session.intra_op.use_xnnpack_threadpool", "1") ########### It's the key code - self.session_height = height - self.session_width = width - if height and width: - sess.add_free_dimension_override_by_name("unet_sample_batch", 2) - sess.add_free_dimension_override_by_name("unet_sample_channels", 4) - sess.add_free_dimension_override_by_name("unet_hidden_batch", 2) - sess.add_free_dimension_override_by_name("unet_hidden_sequence", 77) - sess.add_free_dimension_override_by_name("unet_sample_height", self.session_height) - sess.add_free_dimension_override_by_name("unet_sample_width", self.session_width) - sess.add_free_dimension_override_by_name("unet_time_batch", 1) - providers = [] - if self.provider: - providers.append(self.provider) - else: - providers = get_available_providers() - if "TensorrtExecutionProvider" in providers: - providers.remove("TensorrtExecutionProvider") - try: - self.session = InferenceSession(self.proto.SerializeToString(), providers=providers, sess_options=sess) - except Exception as e: - raise e - # self.session = InferenceSession("tmp.onnx", providers=[self.provider], sess_options=self.sess_options) - # self.io_binding = self.session.io_binding() - - def release_session(self): - self.session = None - import gc - - gc.collect() - return - - def __call__(self, **kwargs): - if self.session is None: - raise Exception("You should call create_session before running model") - - inputs = {k: np.array(v) for k, v in kwargs.items()} - # output_names = self.session.get_outputs() - # for k in inputs: - # self.io_binding.bind_cpu_input(k, inputs[k]) - # for name in output_names: - # self.io_binding.bind_output(name.name) - # self.session.run_with_iobinding(self.io_binding, None) - # return self.io_binding.copy_outputs_to_cpu() - return self.session.run(None, inputs) - - # compatability with diffusers load code - @classmethod - def from_pretrained( - cls, - model_id: Union[str, Path], - subfolder: Union[str, Path] = None, - file_name: Optional[str] = None, - provider: Optional[str] = None, - sess_options: Optional["SessionOptions"] = None, - **kwargs, - ): - file_name = file_name or ONNX_WEIGHTS_NAME - - if os.path.isdir(model_id): - model_path = model_id - if subfolder is not None: - model_path = os.path.join(model_path, subfolder) - model_path = os.path.join(model_path, file_name) - - else: - model_path = model_id - - # load model from local directory - if not os.path.isfile(model_path): - raise Exception(f"Model not found: {model_path}") - - # TODO: session options - return cls(model_path, provider=provider) diff --git a/invokeai/backend/model_management_OLD/models/clip_vision.py b/invokeai/backend/model_management_OLD/models/clip_vision.py deleted file mode 100644 index 2276c6beed..0000000000 --- a/invokeai/backend/model_management_OLD/models/clip_vision.py +++ /dev/null @@ -1,82 +0,0 @@ -import os -from enum import Enum -from typing import Literal, Optional - -import torch -from transformers import CLIPVisionModelWithProjection - -from invokeai.backend.model_management.models.base import ( - BaseModelType, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelType, - SubModelType, - calc_model_size_by_data, - calc_model_size_by_fs, - classproperty, -) - - -class CLIPVisionModelFormat(str, Enum): - Diffusers = "diffusers" - - -class CLIPVisionModel(ModelBase): - class DiffusersConfig(ModelConfigBase): - model_format: Literal[CLIPVisionModelFormat.Diffusers] - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert model_type == ModelType.CLIPVision - super().__init__(model_path, base_model, model_type) - - self.model_size = calc_model_size_by_fs(self.model_path) - - @classmethod - def detect_format(cls, path: str) -> str: - if not os.path.exists(path): - raise ModuleNotFoundError(f"No CLIP Vision model at path '{path}'.") - - if os.path.isdir(path) and os.path.exists(os.path.join(path, "config.json")): - return CLIPVisionModelFormat.Diffusers - - raise InvalidModelException(f"Unexpected CLIP Vision model format: {path}") - - @classproperty - def save_to_config(cls) -> bool: - return True - - def get_size(self, child_type: Optional[SubModelType] = None) -> int: - if child_type is not None: - raise ValueError("There are no child models in a CLIP Vision model.") - - return self.model_size - - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ) -> CLIPVisionModelWithProjection: - if child_type is not None: - raise ValueError("There are no child models in a CLIP Vision model.") - - model = CLIPVisionModelWithProjection.from_pretrained(self.model_path, torch_dtype=torch_dtype) - - # Calculate a more accurate model size. - self.model_size = calc_model_size_by_data(model) - - return model - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - format = cls.detect_format(model_path) - if format == CLIPVisionModelFormat.Diffusers: - return model_path - else: - raise ValueError(f"Unsupported format: '{format}'.") diff --git a/invokeai/backend/model_management_OLD/models/controlnet.py b/invokeai/backend/model_management_OLD/models/controlnet.py deleted file mode 100644 index 3b534cb9d1..0000000000 --- a/invokeai/backend/model_management_OLD/models/controlnet.py +++ /dev/null @@ -1,162 +0,0 @@ -import os -from enum import Enum -from pathlib import Path -from typing import Literal, Optional - -import torch - -import invokeai.backend.util.logging as logger -from invokeai.app.services.config import InvokeAIAppConfig - -from .base import ( - BaseModelType, - EmptyConfigLoader, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelNotFoundException, - ModelType, - SubModelType, - calc_model_size_by_data, - calc_model_size_by_fs, - classproperty, -) - - -class ControlNetModelFormat(str, Enum): - Checkpoint = "checkpoint" - Diffusers = "diffusers" - - -class ControlNetModel(ModelBase): - # model_class: Type - # model_size: int - - class DiffusersConfig(ModelConfigBase): - model_format: Literal[ControlNetModelFormat.Diffusers] - - class CheckpointConfig(ModelConfigBase): - model_format: Literal[ControlNetModelFormat.Checkpoint] - config: str - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert model_type == ModelType.ControlNet - super().__init__(model_path, base_model, model_type) - - try: - config = EmptyConfigLoader.load_config(self.model_path, config_name="config.json") - # config = json.loads(os.path.join(self.model_path, "config.json")) - except Exception: - raise Exception("Invalid controlnet model! (config.json not found or invalid)") - - model_class_name = config.get("_class_name", None) - if model_class_name not in {"ControlNetModel"}: - raise Exception(f"Invalid ControlNet model! Unknown _class_name: {model_class_name}") - - try: - self.model_class = self._hf_definition_to_type(["diffusers", model_class_name]) - self.model_size = calc_model_size_by_fs(self.model_path) - except Exception: - raise Exception("Invalid ControlNet model!") - - def get_size(self, child_type: Optional[SubModelType] = None): - if child_type is not None: - raise Exception("There is no child models in controlnet model") - return self.model_size - - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ): - if child_type is not None: - raise Exception("There are no child models in controlnet model") - - model = None - for variant in ["fp16", None]: - try: - model = self.model_class.from_pretrained( - self.model_path, - torch_dtype=torch_dtype, - variant=variant, - ) - break - except Exception: - pass - if not model: - raise ModelNotFoundException() - - # calc more accurate size - self.model_size = calc_model_size_by_data(model) - return model - - @classproperty - def save_to_config(cls) -> bool: - return False - - @classmethod - def detect_format(cls, path: str): - if not os.path.exists(path): - raise ModelNotFoundException() - - if os.path.isdir(path): - if os.path.exists(os.path.join(path, "config.json")): - return ControlNetModelFormat.Diffusers - - if os.path.isfile(path): - if any(path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt", "pth"]): - return ControlNetModelFormat.Checkpoint - - raise InvalidModelException(f"Not a valid model: {path}") - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - if cls.detect_format(model_path) == ControlNetModelFormat.Checkpoint: - return _convert_controlnet_ckpt_and_cache( - model_path=model_path, - model_config=config.config, - output_path=output_path, - base_model=base_model, - ) - else: - return model_path - - -def _convert_controlnet_ckpt_and_cache( - model_path: str, - output_path: str, - base_model: BaseModelType, - model_config: str, -) -> str: - """ - Convert the controlnet from checkpoint format to diffusers format, - cache it to disk, and return Path to converted - file. If already on disk then just returns Path. - """ - app_config = InvokeAIAppConfig.get_config() - weights = app_config.root_path / model_path - output_path = Path(output_path) - - logger.info(f"Converting {weights} to diffusers format") - # return cached version if it exists - if output_path.exists(): - return output_path - - # to avoid circular import errors - from ..convert_ckpt_to_diffusers import convert_controlnet_to_diffusers - - convert_controlnet_to_diffusers( - weights, - output_path, - original_config_file=app_config.root_path / model_config, - image_size=512, - scan_needed=True, - from_safetensors=weights.suffix == ".safetensors", - ) - return output_path diff --git a/invokeai/backend/model_management_OLD/models/ip_adapter.py b/invokeai/backend/model_management_OLD/models/ip_adapter.py deleted file mode 100644 index c60edd0abe..0000000000 --- a/invokeai/backend/model_management_OLD/models/ip_adapter.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import typing -from enum import Enum -from typing import Literal, Optional - -import torch - -from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus, build_ip_adapter -from invokeai.backend.model_management.models.base import ( - BaseModelType, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelType, - SubModelType, - calc_model_size_by_fs, - classproperty, -) - - -class IPAdapterModelFormat(str, Enum): - # The custom IP-Adapter model format defined by InvokeAI. - InvokeAI = "invokeai" - - -class IPAdapterModel(ModelBase): - class InvokeAIConfig(ModelConfigBase): - model_format: Literal[IPAdapterModelFormat.InvokeAI] - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert model_type == ModelType.IPAdapter - super().__init__(model_path, base_model, model_type) - - self.model_size = calc_model_size_by_fs(self.model_path) - - @classmethod - def detect_format(cls, path: str) -> str: - if not os.path.exists(path): - raise ModuleNotFoundError(f"No IP-Adapter model at path '{path}'.") - - if os.path.isdir(path): - model_file = os.path.join(path, "ip_adapter.bin") - image_encoder_config_file = os.path.join(path, "image_encoder.txt") - if os.path.exists(model_file) and os.path.exists(image_encoder_config_file): - return IPAdapterModelFormat.InvokeAI - - raise InvalidModelException(f"Unexpected IP-Adapter model format: {path}") - - @classproperty - def save_to_config(cls) -> bool: - return True - - def get_size(self, child_type: Optional[SubModelType] = None) -> int: - if child_type is not None: - raise ValueError("There are no child models in an IP-Adapter model.") - - return self.model_size - - def get_model( - self, - torch_dtype: torch.dtype, - child_type: Optional[SubModelType] = None, - ) -> typing.Union[IPAdapter, IPAdapterPlus]: - if child_type is not None: - raise ValueError("There are no child models in an IP-Adapter model.") - - model = build_ip_adapter( - ip_adapter_ckpt_path=os.path.join(self.model_path, "ip_adapter.bin"), - device=torch.device("cpu"), - dtype=torch_dtype, - ) - - self.model_size = model.calc_size() - return model - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - format = cls.detect_format(model_path) - if format == IPAdapterModelFormat.InvokeAI: - return model_path - else: - raise ValueError(f"Unsupported format: '{format}'.") - - -def get_ip_adapter_image_encoder_model_id(model_path: str): - """Read the ID of the image encoder associated with the IP-Adapter at `model_path`.""" - image_encoder_config_file = os.path.join(model_path, "image_encoder.txt") - - with open(image_encoder_config_file, "r") as f: - image_encoder_model = f.readline().strip() - - return image_encoder_model diff --git a/invokeai/backend/model_management_OLD/models/lora.py b/invokeai/backend/model_management_OLD/models/lora.py deleted file mode 100644 index b110d75d22..0000000000 --- a/invokeai/backend/model_management_OLD/models/lora.py +++ /dev/null @@ -1,696 +0,0 @@ -import bisect -import os -from enum import Enum -from pathlib import Path -from typing import Dict, Optional, Union - -import torch -from safetensors.torch import load_file - -from .base import ( - BaseModelType, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelNotFoundException, - ModelType, - SubModelType, - classproperty, -) - - -class LoRAModelFormat(str, Enum): - LyCORIS = "lycoris" - Diffusers = "diffusers" - - -class LoRAModel(ModelBase): - # model_size: int - - class Config(ModelConfigBase): - model_format: LoRAModelFormat # TODO: - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert model_type == ModelType.Lora - super().__init__(model_path, base_model, model_type) - - self.model_size = os.path.getsize(self.model_path) - - def get_size(self, child_type: Optional[SubModelType] = None): - if child_type is not None: - raise Exception("There is no child models in lora") - return self.model_size - - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ): - if child_type is not None: - raise Exception("There is no child models in lora") - - model = LoRAModelRaw.from_checkpoint( - file_path=self.model_path, - dtype=torch_dtype, - base_model=self.base_model, - ) - - self.model_size = model.calc_size() - return model - - @classproperty - def save_to_config(cls) -> bool: - return True - - @classmethod - def detect_format(cls, path: str): - if not os.path.exists(path): - raise ModelNotFoundException() - - if os.path.isdir(path): - for ext in ["safetensors", "bin"]: - if os.path.exists(os.path.join(path, f"pytorch_lora_weights.{ext}")): - return LoRAModelFormat.Diffusers - - if os.path.isfile(path): - if any(path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt"]): - return LoRAModelFormat.LyCORIS - - raise InvalidModelException(f"Not a valid model: {path}") - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - if cls.detect_format(model_path) == LoRAModelFormat.Diffusers: - for ext in ["safetensors", "bin"]: # return path to the safetensors file inside the folder - path = Path(model_path, f"pytorch_lora_weights.{ext}") - if path.exists(): - return path - else: - return model_path - - -class LoRALayerBase: - # rank: Optional[int] - # alpha: Optional[float] - # bias: Optional[torch.Tensor] - # layer_key: str - - # @property - # def scale(self): - # return self.alpha / self.rank if (self.alpha and self.rank) else 1.0 - - def __init__( - self, - layer_key: str, - values: dict, - ): - if "alpha" in values: - self.alpha = values["alpha"].item() - else: - self.alpha = None - - if "bias_indices" in values and "bias_values" in values and "bias_size" in values: - self.bias = torch.sparse_coo_tensor( - values["bias_indices"], - values["bias_values"], - tuple(values["bias_size"]), - ) - - else: - self.bias = None - - self.rank = None # set in layer implementation - self.layer_key = layer_key - - def get_weight(self, orig_weight: torch.Tensor): - raise NotImplementedError() - - def calc_size(self) -> int: - model_size = 0 - for val in [self.bias]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - if self.bias is not None: - self.bias = self.bias.to(device=device, dtype=dtype) - - -# TODO: find and debug lora/locon with bias -class LoRALayer(LoRALayerBase): - # up: torch.Tensor - # mid: Optional[torch.Tensor] - # down: torch.Tensor - - def __init__( - self, - layer_key: str, - values: dict, - ): - super().__init__(layer_key, values) - - self.up = values["lora_up.weight"] - self.down = values["lora_down.weight"] - if "lora_mid.weight" in values: - self.mid = values["lora_mid.weight"] - else: - self.mid = None - - self.rank = self.down.shape[0] - - def get_weight(self, orig_weight: torch.Tensor): - if self.mid is not None: - up = self.up.reshape(self.up.shape[0], self.up.shape[1]) - down = self.down.reshape(self.down.shape[0], self.down.shape[1]) - weight = torch.einsum("m n w h, i m, n j -> i j w h", self.mid, up, down) - else: - weight = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1) - - return weight - - def calc_size(self) -> int: - model_size = super().calc_size() - for val in [self.up, self.mid, self.down]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - super().to(device=device, dtype=dtype) - - self.up = self.up.to(device=device, dtype=dtype) - self.down = self.down.to(device=device, dtype=dtype) - - if self.mid is not None: - self.mid = self.mid.to(device=device, dtype=dtype) - - -class LoHALayer(LoRALayerBase): - # w1_a: torch.Tensor - # w1_b: torch.Tensor - # w2_a: torch.Tensor - # w2_b: torch.Tensor - # t1: Optional[torch.Tensor] = None - # t2: Optional[torch.Tensor] = None - - def __init__( - self, - layer_key: str, - values: dict, - ): - super().__init__(layer_key, values) - - self.w1_a = values["hada_w1_a"] - self.w1_b = values["hada_w1_b"] - self.w2_a = values["hada_w2_a"] - self.w2_b = values["hada_w2_b"] - - if "hada_t1" in values: - self.t1 = values["hada_t1"] - else: - self.t1 = None - - if "hada_t2" in values: - self.t2 = values["hada_t2"] - else: - self.t2 = None - - self.rank = self.w1_b.shape[0] - - def get_weight(self, orig_weight: torch.Tensor): - if self.t1 is None: - weight = (self.w1_a @ self.w1_b) * (self.w2_a @ self.w2_b) - - else: - rebuild1 = torch.einsum("i j k l, j r, i p -> p r k l", self.t1, self.w1_b, self.w1_a) - rebuild2 = torch.einsum("i j k l, j r, i p -> p r k l", self.t2, self.w2_b, self.w2_a) - weight = rebuild1 * rebuild2 - - return weight - - def calc_size(self) -> int: - model_size = super().calc_size() - for val in [self.w1_a, self.w1_b, self.w2_a, self.w2_b, self.t1, self.t2]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - super().to(device=device, dtype=dtype) - - self.w1_a = self.w1_a.to(device=device, dtype=dtype) - self.w1_b = self.w1_b.to(device=device, dtype=dtype) - if self.t1 is not None: - self.t1 = self.t1.to(device=device, dtype=dtype) - - self.w2_a = self.w2_a.to(device=device, dtype=dtype) - self.w2_b = self.w2_b.to(device=device, dtype=dtype) - if self.t2 is not None: - self.t2 = self.t2.to(device=device, dtype=dtype) - - -class LoKRLayer(LoRALayerBase): - # w1: Optional[torch.Tensor] = None - # w1_a: Optional[torch.Tensor] = None - # w1_b: Optional[torch.Tensor] = None - # w2: Optional[torch.Tensor] = None - # w2_a: Optional[torch.Tensor] = None - # w2_b: Optional[torch.Tensor] = None - # t2: Optional[torch.Tensor] = None - - def __init__( - self, - layer_key: str, - values: dict, - ): - super().__init__(layer_key, values) - - if "lokr_w1" in values: - self.w1 = values["lokr_w1"] - self.w1_a = None - self.w1_b = None - else: - self.w1 = None - self.w1_a = values["lokr_w1_a"] - self.w1_b = values["lokr_w1_b"] - - if "lokr_w2" in values: - self.w2 = values["lokr_w2"] - self.w2_a = None - self.w2_b = None - else: - self.w2 = None - self.w2_a = values["lokr_w2_a"] - self.w2_b = values["lokr_w2_b"] - - if "lokr_t2" in values: - self.t2 = values["lokr_t2"] - else: - self.t2 = None - - if "lokr_w1_b" in values: - self.rank = values["lokr_w1_b"].shape[0] - elif "lokr_w2_b" in values: - self.rank = values["lokr_w2_b"].shape[0] - else: - self.rank = None # unscaled - - def get_weight(self, orig_weight: torch.Tensor): - w1 = self.w1 - if w1 is None: - w1 = self.w1_a @ self.w1_b - - w2 = self.w2 - if w2 is None: - if self.t2 is None: - w2 = self.w2_a @ self.w2_b - else: - w2 = torch.einsum("i j k l, i p, j r -> p r k l", self.t2, self.w2_a, self.w2_b) - - if len(w2.shape) == 4: - w1 = w1.unsqueeze(2).unsqueeze(2) - w2 = w2.contiguous() - weight = torch.kron(w1, w2) - - return weight - - def calc_size(self) -> int: - model_size = super().calc_size() - for val in [self.w1, self.w1_a, self.w1_b, self.w2, self.w2_a, self.w2_b, self.t2]: - if val is not None: - model_size += val.nelement() * val.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - super().to(device=device, dtype=dtype) - - if self.w1 is not None: - self.w1 = self.w1.to(device=device, dtype=dtype) - else: - self.w1_a = self.w1_a.to(device=device, dtype=dtype) - self.w1_b = self.w1_b.to(device=device, dtype=dtype) - - if self.w2 is not None: - self.w2 = self.w2.to(device=device, dtype=dtype) - else: - self.w2_a = self.w2_a.to(device=device, dtype=dtype) - self.w2_b = self.w2_b.to(device=device, dtype=dtype) - - if self.t2 is not None: - self.t2 = self.t2.to(device=device, dtype=dtype) - - -class FullLayer(LoRALayerBase): - # weight: torch.Tensor - - def __init__( - self, - layer_key: str, - values: dict, - ): - super().__init__(layer_key, values) - - self.weight = values["diff"] - - if len(values.keys()) > 1: - _keys = list(values.keys()) - _keys.remove("diff") - raise NotImplementedError(f"Unexpected keys in lora diff layer: {_keys}") - - self.rank = None # unscaled - - def get_weight(self, orig_weight: torch.Tensor): - return self.weight - - def calc_size(self) -> int: - model_size = super().calc_size() - model_size += self.weight.nelement() * self.weight.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - super().to(device=device, dtype=dtype) - - self.weight = self.weight.to(device=device, dtype=dtype) - - -class IA3Layer(LoRALayerBase): - # weight: torch.Tensor - # on_input: torch.Tensor - - def __init__( - self, - layer_key: str, - values: dict, - ): - super().__init__(layer_key, values) - - self.weight = values["weight"] - self.on_input = values["on_input"] - - self.rank = None # unscaled - - def get_weight(self, orig_weight: torch.Tensor): - weight = self.weight - if not self.on_input: - weight = weight.reshape(-1, 1) - return orig_weight * weight - - def calc_size(self) -> int: - model_size = super().calc_size() - model_size += self.weight.nelement() * self.weight.element_size() - model_size += self.on_input.nelement() * self.on_input.element_size() - return model_size - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - super().to(device=device, dtype=dtype) - - self.weight = self.weight.to(device=device, dtype=dtype) - self.on_input = self.on_input.to(device=device, dtype=dtype) - - -# TODO: rename all methods used in model logic with Info postfix and remove here Raw postfix -class LoRAModelRaw: # (torch.nn.Module): - _name: str - layers: Dict[str, LoRALayer] - - def __init__( - self, - name: str, - layers: Dict[str, LoRALayer], - ): - self._name = name - self.layers = layers - - @property - def name(self): - return self._name - - def to( - self, - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - ): - # TODO: try revert if exception? - for _key, layer in self.layers.items(): - layer.to(device=device, dtype=dtype) - - def calc_size(self) -> int: - model_size = 0 - for _, layer in self.layers.items(): - model_size += layer.calc_size() - return model_size - - @classmethod - def _convert_sdxl_keys_to_diffusers_format(cls, state_dict): - """Convert the keys of an SDXL LoRA state_dict to diffusers format. - - The input state_dict can be in either Stability AI format or diffusers format. If the state_dict is already in - diffusers format, then this function will have no effect. - - This function is adapted from: - https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L385-L409 - - Args: - state_dict (Dict[str, Tensor]): The SDXL LoRA state_dict. - - Raises: - ValueError: If state_dict contains an unrecognized key, or not all keys could be converted. - - Returns: - Dict[str, Tensor]: The diffusers-format state_dict. - """ - converted_count = 0 # The number of Stability AI keys converted to diffusers format. - not_converted_count = 0 # The number of keys that were not converted. - - # Get a sorted list of Stability AI UNet keys so that we can efficiently search for keys with matching prefixes. - # For example, we want to efficiently find `input_blocks_4_1` in the list when searching for - # `input_blocks_4_1_proj_in`. - stability_unet_keys = list(SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP) - stability_unet_keys.sort() - - new_state_dict = {} - for full_key, value in state_dict.items(): - if full_key.startswith("lora_unet_"): - search_key = full_key.replace("lora_unet_", "") - # Use bisect to find the key in stability_unet_keys that *may* match the search_key's prefix. - position = bisect.bisect_right(stability_unet_keys, search_key) - map_key = stability_unet_keys[position - 1] - # Now, check if the map_key *actually* matches the search_key. - if search_key.startswith(map_key): - new_key = full_key.replace(map_key, SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP[map_key]) - new_state_dict[new_key] = value - converted_count += 1 - else: - new_state_dict[full_key] = value - not_converted_count += 1 - elif full_key.startswith("lora_te1_") or full_key.startswith("lora_te2_"): - # The CLIP text encoders have the same keys in both Stability AI and diffusers formats. - new_state_dict[full_key] = value - continue - else: - raise ValueError(f"Unrecognized SDXL LoRA key prefix: '{full_key}'.") - - if converted_count > 0 and not_converted_count > 0: - raise ValueError( - f"The SDXL LoRA could only be partially converted to diffusers format. converted={converted_count}," - f" not_converted={not_converted_count}" - ) - - return new_state_dict - - @classmethod - def from_checkpoint( - cls, - file_path: Union[str, Path], - device: Optional[torch.device] = None, - dtype: Optional[torch.dtype] = None, - base_model: Optional[BaseModelType] = None, - ): - device = device or torch.device("cpu") - dtype = dtype or torch.float32 - - if isinstance(file_path, str): - file_path = Path(file_path) - - model = cls( - name=file_path.stem, # TODO: - layers={}, - ) - - if file_path.suffix == ".safetensors": - state_dict = load_file(file_path.absolute().as_posix(), device="cpu") - else: - state_dict = torch.load(file_path, map_location="cpu") - - state_dict = cls._group_state(state_dict) - - if base_model == BaseModelType.StableDiffusionXL: - state_dict = cls._convert_sdxl_keys_to_diffusers_format(state_dict) - - for layer_key, values in state_dict.items(): - # lora and locon - if "lora_down.weight" in values: - layer = LoRALayer(layer_key, values) - - # loha - elif "hada_w1_b" in values: - layer = LoHALayer(layer_key, values) - - # lokr - elif "lokr_w1_b" in values or "lokr_w1" in values: - layer = LoKRLayer(layer_key, values) - - # diff - elif "diff" in values: - layer = FullLayer(layer_key, values) - - # ia3 - elif "weight" in values and "on_input" in values: - layer = IA3Layer(layer_key, values) - - else: - print(f">> Encountered unknown lora layer module in {model.name}: {layer_key} - {list(values.keys())}") - raise Exception("Unknown lora format!") - - # lower memory consumption by removing already parsed layer values - state_dict[layer_key].clear() - - layer.to(device=device, dtype=dtype) - model.layers[layer_key] = layer - - return model - - @staticmethod - def _group_state(state_dict: dict): - state_dict_groupped = {} - - for key, value in state_dict.items(): - stem, leaf = key.split(".", 1) - if stem not in state_dict_groupped: - state_dict_groupped[stem] = {} - state_dict_groupped[stem][leaf] = value - - return state_dict_groupped - - -# code from -# https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L15C1-L97C32 -def make_sdxl_unet_conversion_map(): - """Create a dict mapping state_dict keys from Stability AI SDXL format to diffusers SDXL format.""" - unet_conversion_map_layer = [] - - for i in range(3): # num_blocks is 3 in sdxl - # loop over downblocks/upblocks - for j in range(2): - # loop over resnets/attentions for downblocks - hf_down_res_prefix = f"down_blocks.{i}.resnets.{j}." - sd_down_res_prefix = f"input_blocks.{3*i + j + 1}.0." - unet_conversion_map_layer.append((sd_down_res_prefix, hf_down_res_prefix)) - - if i < 3: - # no attention layers in down_blocks.3 - hf_down_atn_prefix = f"down_blocks.{i}.attentions.{j}." - sd_down_atn_prefix = f"input_blocks.{3*i + j + 1}.1." - unet_conversion_map_layer.append((sd_down_atn_prefix, hf_down_atn_prefix)) - - for j in range(3): - # loop over resnets/attentions for upblocks - hf_up_res_prefix = f"up_blocks.{i}.resnets.{j}." - sd_up_res_prefix = f"output_blocks.{3*i + j}.0." - unet_conversion_map_layer.append((sd_up_res_prefix, hf_up_res_prefix)) - - # if i > 0: commentout for sdxl - # no attention layers in up_blocks.0 - hf_up_atn_prefix = f"up_blocks.{i}.attentions.{j}." - sd_up_atn_prefix = f"output_blocks.{3*i + j}.1." - unet_conversion_map_layer.append((sd_up_atn_prefix, hf_up_atn_prefix)) - - if i < 3: - # no downsample in down_blocks.3 - hf_downsample_prefix = f"down_blocks.{i}.downsamplers.0.conv." - sd_downsample_prefix = f"input_blocks.{3*(i+1)}.0.op." - unet_conversion_map_layer.append((sd_downsample_prefix, hf_downsample_prefix)) - - # no upsample in up_blocks.3 - hf_upsample_prefix = f"up_blocks.{i}.upsamplers.0." - sd_upsample_prefix = f"output_blocks.{3*i + 2}.{2}." # change for sdxl - unet_conversion_map_layer.append((sd_upsample_prefix, hf_upsample_prefix)) - - hf_mid_atn_prefix = "mid_block.attentions.0." - sd_mid_atn_prefix = "middle_block.1." - unet_conversion_map_layer.append((sd_mid_atn_prefix, hf_mid_atn_prefix)) - - for j in range(2): - hf_mid_res_prefix = f"mid_block.resnets.{j}." - sd_mid_res_prefix = f"middle_block.{2*j}." - unet_conversion_map_layer.append((sd_mid_res_prefix, hf_mid_res_prefix)) - - unet_conversion_map_resnet = [ - # (stable-diffusion, HF Diffusers) - ("in_layers.0.", "norm1."), - ("in_layers.2.", "conv1."), - ("out_layers.0.", "norm2."), - ("out_layers.3.", "conv2."), - ("emb_layers.1.", "time_emb_proj."), - ("skip_connection.", "conv_shortcut."), - ] - - unet_conversion_map = [] - for sd, hf in unet_conversion_map_layer: - if "resnets" in hf: - for sd_res, hf_res in unet_conversion_map_resnet: - unet_conversion_map.append((sd + sd_res, hf + hf_res)) - else: - unet_conversion_map.append((sd, hf)) - - for j in range(2): - hf_time_embed_prefix = f"time_embedding.linear_{j+1}." - sd_time_embed_prefix = f"time_embed.{j*2}." - unet_conversion_map.append((sd_time_embed_prefix, hf_time_embed_prefix)) - - for j in range(2): - hf_label_embed_prefix = f"add_embedding.linear_{j+1}." - sd_label_embed_prefix = f"label_emb.0.{j*2}." - unet_conversion_map.append((sd_label_embed_prefix, hf_label_embed_prefix)) - - unet_conversion_map.append(("input_blocks.0.0.", "conv_in.")) - unet_conversion_map.append(("out.0.", "conv_norm_out.")) - unet_conversion_map.append(("out.2.", "conv_out.")) - - return unet_conversion_map - - -SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP = { - sd.rstrip(".").replace(".", "_"): hf.rstrip(".").replace(".", "_") for sd, hf in make_sdxl_unet_conversion_map() -} diff --git a/invokeai/backend/model_management_OLD/models/sdxl.py b/invokeai/backend/model_management_OLD/models/sdxl.py deleted file mode 100644 index 01e9420fed..0000000000 --- a/invokeai/backend/model_management_OLD/models/sdxl.py +++ /dev/null @@ -1,148 +0,0 @@ -import json -import os -from enum import Enum -from pathlib import Path -from typing import Literal, Optional - -from omegaconf import OmegaConf -from pydantic import Field - -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.model_management.detect_baked_in_vae import has_baked_in_sdxl_vae -from invokeai.backend.util.logging import InvokeAILogger - -from .base import ( - BaseModelType, - DiffusersModel, - InvalidModelException, - ModelConfigBase, - ModelType, - ModelVariantType, - classproperty, - read_checkpoint_meta, -) - - -class StableDiffusionXLModelFormat(str, Enum): - Checkpoint = "checkpoint" - Diffusers = "diffusers" - - -class StableDiffusionXLModel(DiffusersModel): - # TODO: check that configs overwriten properly - class DiffusersConfig(ModelConfigBase): - model_format: Literal[StableDiffusionXLModelFormat.Diffusers] - vae: Optional[str] = Field(None) - variant: ModelVariantType - - class CheckpointConfig(ModelConfigBase): - model_format: Literal[StableDiffusionXLModelFormat.Checkpoint] - vae: Optional[str] = Field(None) - config: str - variant: ModelVariantType - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert base_model in {BaseModelType.StableDiffusionXL, BaseModelType.StableDiffusionXLRefiner} - assert model_type == ModelType.Main - super().__init__( - model_path=model_path, - base_model=BaseModelType.StableDiffusionXL, - model_type=ModelType.Main, - ) - - @classmethod - def probe_config(cls, path: str, **kwargs): - model_format = cls.detect_format(path) - ckpt_config_path = kwargs.get("config", None) - if model_format == StableDiffusionXLModelFormat.Checkpoint: - if ckpt_config_path: - ckpt_config = OmegaConf.load(ckpt_config_path) - in_channels = ckpt_config["model"]["params"]["unet_config"]["params"]["in_channels"] - - else: - checkpoint = read_checkpoint_meta(path) - checkpoint = checkpoint.get("state_dict", checkpoint) - in_channels = checkpoint["model.diffusion_model.input_blocks.0.0.weight"].shape[1] - - elif model_format == StableDiffusionXLModelFormat.Diffusers: - unet_config_path = os.path.join(path, "unet", "config.json") - if os.path.exists(unet_config_path): - with open(unet_config_path, "r") as f: - unet_config = json.loads(f.read()) - in_channels = unet_config["in_channels"] - - else: - raise InvalidModelException(f"{path} is not a recognized Stable Diffusion diffusers model") - - else: - raise NotImplementedError(f"Unknown stable diffusion 2.* format: {model_format}") - - if in_channels == 9: - variant = ModelVariantType.Inpaint - elif in_channels == 5: - variant = ModelVariantType.Depth - elif in_channels == 4: - variant = ModelVariantType.Normal - else: - raise Exception("Unkown stable diffusion 2.* model format") - - if ckpt_config_path is None: - # avoid circular import - from .stable_diffusion import _select_ckpt_config - - ckpt_config_path = _select_ckpt_config(kwargs.get("model_base", BaseModelType.StableDiffusionXL), variant) - - return cls.create_config( - path=path, - model_format=model_format, - config=ckpt_config_path, - variant=variant, - ) - - @classproperty - def save_to_config(cls) -> bool: - return True - - @classmethod - def detect_format(cls, model_path: str): - if os.path.isdir(model_path): - return StableDiffusionXLModelFormat.Diffusers - else: - return StableDiffusionXLModelFormat.Checkpoint - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - # The convert script adapted from the diffusers package uses - # strings for the base model type. To avoid making too many - # source code changes, we simply translate here - if Path(output_path).exists(): - return output_path - - if isinstance(config, cls.CheckpointConfig): - from invokeai.backend.model_management.models.stable_diffusion import _convert_ckpt_and_cache - - # Hack in VAE-fp16 fix - If model sdxl-vae-fp16-fix is installed, - # then we bake it into the converted model unless there is already - # a nonstandard VAE installed. - kwargs = {} - app_config = InvokeAIAppConfig.get_config() - vae_path = app_config.models_path / "sdxl/vae/sdxl-vae-fp16-fix" - if vae_path.exists() and not has_baked_in_sdxl_vae(Path(model_path)): - InvokeAILogger.get_logger().warning("No baked-in VAE detected. Inserting sdxl-vae-fp16-fix.") - kwargs["vae_path"] = vae_path - - return _convert_ckpt_and_cache( - version=base_model, - model_config=config, - output_path=output_path, - use_safetensors=True, - **kwargs, - ) - else: - return model_path diff --git a/invokeai/backend/model_management_OLD/models/stable_diffusion.py b/invokeai/backend/model_management_OLD/models/stable_diffusion.py deleted file mode 100644 index a38a44fccf..0000000000 --- a/invokeai/backend/model_management_OLD/models/stable_diffusion.py +++ /dev/null @@ -1,337 +0,0 @@ -import json -import os -from enum import Enum -from pathlib import Path -from typing import Literal, Optional, Union - -from diffusers import StableDiffusionInpaintPipeline, StableDiffusionPipeline -from omegaconf import OmegaConf -from pydantic import Field - -import invokeai.backend.util.logging as logger -from invokeai.app.services.config import InvokeAIAppConfig - -from .base import ( - BaseModelType, - DiffusersModel, - InvalidModelException, - ModelConfigBase, - ModelNotFoundException, - ModelType, - ModelVariantType, - SilenceWarnings, - classproperty, - read_checkpoint_meta, -) -from .sdxl import StableDiffusionXLModel - - -class StableDiffusion1ModelFormat(str, Enum): - Checkpoint = "checkpoint" - Diffusers = "diffusers" - - -class StableDiffusion1Model(DiffusersModel): - class DiffusersConfig(ModelConfigBase): - model_format: Literal[StableDiffusion1ModelFormat.Diffusers] - vae: Optional[str] = Field(None) - variant: ModelVariantType - - class CheckpointConfig(ModelConfigBase): - model_format: Literal[StableDiffusion1ModelFormat.Checkpoint] - vae: Optional[str] = Field(None) - config: str - variant: ModelVariantType - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert base_model == BaseModelType.StableDiffusion1 - assert model_type == ModelType.Main - super().__init__( - model_path=model_path, - base_model=BaseModelType.StableDiffusion1, - model_type=ModelType.Main, - ) - - @classmethod - def probe_config(cls, path: str, **kwargs): - model_format = cls.detect_format(path) - ckpt_config_path = kwargs.get("config", None) - if model_format == StableDiffusion1ModelFormat.Checkpoint: - if ckpt_config_path: - ckpt_config = OmegaConf.load(ckpt_config_path) - ckpt_config["model"]["params"]["unet_config"]["params"]["in_channels"] - - else: - checkpoint = read_checkpoint_meta(path) - checkpoint = checkpoint.get("state_dict", checkpoint) - in_channels = checkpoint["model.diffusion_model.input_blocks.0.0.weight"].shape[1] - - elif model_format == StableDiffusion1ModelFormat.Diffusers: - unet_config_path = os.path.join(path, "unet", "config.json") - if os.path.exists(unet_config_path): - with open(unet_config_path, "r") as f: - unet_config = json.loads(f.read()) - in_channels = unet_config["in_channels"] - - else: - raise NotImplementedError(f"{path} is not a supported stable diffusion diffusers format") - - else: - raise NotImplementedError(f"Unknown stable diffusion 1.* format: {model_format}") - - if in_channels == 9: - variant = ModelVariantType.Inpaint - elif in_channels == 4: - variant = ModelVariantType.Normal - else: - raise Exception("Unkown stable diffusion 1.* model format") - - if ckpt_config_path is None: - ckpt_config_path = _select_ckpt_config(BaseModelType.StableDiffusion1, variant) - - return cls.create_config( - path=path, - model_format=model_format, - config=ckpt_config_path, - variant=variant, - ) - - @classproperty - def save_to_config(cls) -> bool: - return True - - @classmethod - def detect_format(cls, model_path: str): - if not os.path.exists(model_path): - raise ModelNotFoundException() - - if os.path.isdir(model_path): - if os.path.exists(os.path.join(model_path, "model_index.json")): - return StableDiffusion1ModelFormat.Diffusers - - if os.path.isfile(model_path): - if any(model_path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt"]): - return StableDiffusion1ModelFormat.Checkpoint - - raise InvalidModelException(f"Not a valid model: {model_path}") - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - if isinstance(config, cls.CheckpointConfig): - return _convert_ckpt_and_cache( - version=BaseModelType.StableDiffusion1, - model_config=config, - load_safety_checker=False, - output_path=output_path, - ) - else: - return model_path - - -class StableDiffusion2ModelFormat(str, Enum): - Checkpoint = "checkpoint" - Diffusers = "diffusers" - - -class StableDiffusion2Model(DiffusersModel): - # TODO: check that configs overwriten properly - class DiffusersConfig(ModelConfigBase): - model_format: Literal[StableDiffusion2ModelFormat.Diffusers] - vae: Optional[str] = Field(None) - variant: ModelVariantType - - class CheckpointConfig(ModelConfigBase): - model_format: Literal[StableDiffusion2ModelFormat.Checkpoint] - vae: Optional[str] = Field(None) - config: str - variant: ModelVariantType - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert base_model == BaseModelType.StableDiffusion2 - assert model_type == ModelType.Main - super().__init__( - model_path=model_path, - base_model=BaseModelType.StableDiffusion2, - model_type=ModelType.Main, - ) - - @classmethod - def probe_config(cls, path: str, **kwargs): - model_format = cls.detect_format(path) - ckpt_config_path = kwargs.get("config", None) - if model_format == StableDiffusion2ModelFormat.Checkpoint: - if ckpt_config_path: - ckpt_config = OmegaConf.load(ckpt_config_path) - ckpt_config["model"]["params"]["unet_config"]["params"]["in_channels"] - - else: - checkpoint = read_checkpoint_meta(path) - checkpoint = checkpoint.get("state_dict", checkpoint) - in_channels = checkpoint["model.diffusion_model.input_blocks.0.0.weight"].shape[1] - - elif model_format == StableDiffusion2ModelFormat.Diffusers: - unet_config_path = os.path.join(path, "unet", "config.json") - if os.path.exists(unet_config_path): - with open(unet_config_path, "r") as f: - unet_config = json.loads(f.read()) - in_channels = unet_config["in_channels"] - - else: - raise Exception("Not supported stable diffusion diffusers format(possibly onnx?)") - - else: - raise NotImplementedError(f"Unknown stable diffusion 2.* format: {model_format}") - - if in_channels == 9: - variant = ModelVariantType.Inpaint - elif in_channels == 5: - variant = ModelVariantType.Depth - elif in_channels == 4: - variant = ModelVariantType.Normal - else: - raise Exception("Unkown stable diffusion 2.* model format") - - if ckpt_config_path is None: - ckpt_config_path = _select_ckpt_config(BaseModelType.StableDiffusion2, variant) - - return cls.create_config( - path=path, - model_format=model_format, - config=ckpt_config_path, - variant=variant, - ) - - @classproperty - def save_to_config(cls) -> bool: - return True - - @classmethod - def detect_format(cls, model_path: str): - if not os.path.exists(model_path): - raise ModelNotFoundException() - - if os.path.isdir(model_path): - if os.path.exists(os.path.join(model_path, "model_index.json")): - return StableDiffusion2ModelFormat.Diffusers - - if os.path.isfile(model_path): - if any(model_path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt"]): - return StableDiffusion2ModelFormat.Checkpoint - - raise InvalidModelException(f"Not a valid model: {model_path}") - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - if isinstance(config, cls.CheckpointConfig): - return _convert_ckpt_and_cache( - version=BaseModelType.StableDiffusion2, - model_config=config, - output_path=output_path, - ) - else: - return model_path - - -# TODO: rework -# pass precision - currently defaulting to fp16 -def _convert_ckpt_and_cache( - version: BaseModelType, - model_config: Union[ - StableDiffusion1Model.CheckpointConfig, - StableDiffusion2Model.CheckpointConfig, - StableDiffusionXLModel.CheckpointConfig, - ], - output_path: str, - use_save_model: bool = False, - **kwargs, -) -> str: - """ - Convert the checkpoint model indicated in mconfig into a - diffusers, cache it to disk, and return Path to converted - file. If already on disk then just returns Path. - """ - app_config = InvokeAIAppConfig.get_config() - - weights = app_config.models_path / model_config.path - config_file = app_config.root_path / model_config.config - output_path = Path(output_path) - variant = model_config.variant - pipeline_class = StableDiffusionInpaintPipeline if variant == "inpaint" else StableDiffusionPipeline - - # return cached version if it exists - if output_path.exists(): - return output_path - - # to avoid circular import errors - from ...util.devices import choose_torch_device, torch_dtype - from ..convert_ckpt_to_diffusers import convert_ckpt_to_diffusers - - model_base_to_model_type = { - BaseModelType.StableDiffusion1: "FrozenCLIPEmbedder", - BaseModelType.StableDiffusion2: "FrozenOpenCLIPEmbedder", - BaseModelType.StableDiffusionXL: "SDXL", - BaseModelType.StableDiffusionXLRefiner: "SDXL-Refiner", - } - logger.info(f"Converting {weights} to diffusers format") - with SilenceWarnings(): - convert_ckpt_to_diffusers( - weights, - output_path, - model_type=model_base_to_model_type[version], - model_version=version, - model_variant=model_config.variant, - original_config_file=config_file, - extract_ema=True, - scan_needed=True, - pipeline_class=pipeline_class, - from_safetensors=weights.suffix == ".safetensors", - precision=torch_dtype(choose_torch_device()), - **kwargs, - ) - return output_path - - -def _select_ckpt_config(version: BaseModelType, variant: ModelVariantType): - ckpt_configs = { - BaseModelType.StableDiffusion1: { - ModelVariantType.Normal: "v1-inference.yaml", - ModelVariantType.Inpaint: "v1-inpainting-inference.yaml", - }, - BaseModelType.StableDiffusion2: { - ModelVariantType.Normal: "v2-inference-v.yaml", # best guess, as we can't differentiate with base(512) - ModelVariantType.Inpaint: "v2-inpainting-inference.yaml", - ModelVariantType.Depth: "v2-midas-inference.yaml", - }, - BaseModelType.StableDiffusionXL: { - ModelVariantType.Normal: "sd_xl_base.yaml", - ModelVariantType.Inpaint: None, - ModelVariantType.Depth: None, - }, - BaseModelType.StableDiffusionXLRefiner: { - ModelVariantType.Normal: "sd_xl_refiner.yaml", - ModelVariantType.Inpaint: None, - ModelVariantType.Depth: None, - }, - } - - app_config = InvokeAIAppConfig.get_config() - try: - config_path = app_config.legacy_conf_path / ckpt_configs[version][variant] - if config_path.is_relative_to(app_config.root_path): - config_path = config_path.relative_to(app_config.root_path) - return str(config_path) - - except Exception: - return None diff --git a/invokeai/backend/model_management_OLD/models/stable_diffusion_onnx.py b/invokeai/backend/model_management_OLD/models/stable_diffusion_onnx.py deleted file mode 100644 index 2d0dd22c43..0000000000 --- a/invokeai/backend/model_management_OLD/models/stable_diffusion_onnx.py +++ /dev/null @@ -1,150 +0,0 @@ -from enum import Enum -from typing import Literal - -from diffusers import OnnxRuntimeModel - -from .base import ( - BaseModelType, - DiffusersModel, - IAIOnnxRuntimeModel, - ModelConfigBase, - ModelType, - ModelVariantType, - SchedulerPredictionType, - classproperty, -) - - -class StableDiffusionOnnxModelFormat(str, Enum): - Olive = "olive" - Onnx = "onnx" - - -class ONNXStableDiffusion1Model(DiffusersModel): - class Config(ModelConfigBase): - model_format: Literal[StableDiffusionOnnxModelFormat.Onnx] - variant: ModelVariantType - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert base_model == BaseModelType.StableDiffusion1 - assert model_type == ModelType.ONNX - super().__init__( - model_path=model_path, - base_model=BaseModelType.StableDiffusion1, - model_type=ModelType.ONNX, - ) - - for child_name, child_type in self.child_types.items(): - if child_type is OnnxRuntimeModel: - self.child_types[child_name] = IAIOnnxRuntimeModel - - # TODO: check that no optimum models provided - - @classmethod - def probe_config(cls, path: str, **kwargs): - model_format = cls.detect_format(path) - in_channels = 4 # TODO: - - if in_channels == 9: - variant = ModelVariantType.Inpaint - elif in_channels == 4: - variant = ModelVariantType.Normal - else: - raise Exception("Unkown stable diffusion 1.* model format") - - return cls.create_config( - path=path, - model_format=model_format, - variant=variant, - ) - - @classproperty - def save_to_config(cls) -> bool: - return True - - @classmethod - def detect_format(cls, model_path: str): - # TODO: Detect onnx vs olive - return StableDiffusionOnnxModelFormat.Onnx - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - return model_path - - -class ONNXStableDiffusion2Model(DiffusersModel): - # TODO: check that configs overwriten properly - class Config(ModelConfigBase): - model_format: Literal[StableDiffusionOnnxModelFormat.Onnx] - variant: ModelVariantType - prediction_type: SchedulerPredictionType - upcast_attention: bool - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert base_model == BaseModelType.StableDiffusion2 - assert model_type == ModelType.ONNX - super().__init__( - model_path=model_path, - base_model=BaseModelType.StableDiffusion2, - model_type=ModelType.ONNX, - ) - - for child_name, child_type in self.child_types.items(): - if child_type is OnnxRuntimeModel: - self.child_types[child_name] = IAIOnnxRuntimeModel - # TODO: check that no optimum models provided - - @classmethod - def probe_config(cls, path: str, **kwargs): - model_format = cls.detect_format(path) - in_channels = 4 # TODO: - - if in_channels == 9: - variant = ModelVariantType.Inpaint - elif in_channels == 5: - variant = ModelVariantType.Depth - elif in_channels == 4: - variant = ModelVariantType.Normal - else: - raise Exception("Unkown stable diffusion 2.* model format") - - if variant == ModelVariantType.Normal: - prediction_type = SchedulerPredictionType.VPrediction - upcast_attention = True - - else: - prediction_type = SchedulerPredictionType.Epsilon - upcast_attention = False - - return cls.create_config( - path=path, - model_format=model_format, - variant=variant, - prediction_type=prediction_type, - upcast_attention=upcast_attention, - ) - - @classproperty - def save_to_config(cls) -> bool: - return True - - @classmethod - def detect_format(cls, model_path: str): - # TODO: Detect onnx vs olive - return StableDiffusionOnnxModelFormat.Onnx - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - return model_path diff --git a/invokeai/backend/model_management_OLD/models/t2i_adapter.py b/invokeai/backend/model_management_OLD/models/t2i_adapter.py deleted file mode 100644 index 4adb9901f9..0000000000 --- a/invokeai/backend/model_management_OLD/models/t2i_adapter.py +++ /dev/null @@ -1,102 +0,0 @@ -import os -from enum import Enum -from typing import Literal, Optional - -import torch -from diffusers import T2IAdapter - -from invokeai.backend.model_management.models.base import ( - BaseModelType, - EmptyConfigLoader, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelNotFoundException, - ModelType, - SubModelType, - calc_model_size_by_data, - calc_model_size_by_fs, - classproperty, -) - - -class T2IAdapterModelFormat(str, Enum): - Diffusers = "diffusers" - - -class T2IAdapterModel(ModelBase): - class DiffusersConfig(ModelConfigBase): - model_format: Literal[T2IAdapterModelFormat.Diffusers] - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert model_type == ModelType.T2IAdapter - super().__init__(model_path, base_model, model_type) - - config = EmptyConfigLoader.load_config(self.model_path, config_name="config.json") - - model_class_name = config.get("_class_name", None) - if model_class_name not in {"T2IAdapter"}: - raise InvalidModelException(f"Invalid T2I-Adapter model. Unknown _class_name: '{model_class_name}'.") - - self.model_class = self._hf_definition_to_type(["diffusers", model_class_name]) - self.model_size = calc_model_size_by_fs(self.model_path) - - def get_size(self, child_type: Optional[SubModelType] = None): - if child_type is not None: - raise ValueError(f"T2I-Adapters do not have child models. Invalid child type: '{child_type}'.") - return self.model_size - - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ) -> T2IAdapter: - if child_type is not None: - raise ValueError(f"T2I-Adapters do not have child models. Invalid child type: '{child_type}'.") - - model = None - for variant in ["fp16", None]: - try: - model = self.model_class.from_pretrained( - self.model_path, - torch_dtype=torch_dtype, - variant=variant, - ) - break - except Exception: - pass - if not model: - raise ModelNotFoundException() - - # Calculate a more accurate size after loading the model into memory. - self.model_size = calc_model_size_by_data(model) - return model - - @classproperty - def save_to_config(cls) -> bool: - return False - - @classmethod - def detect_format(cls, path: str): - if not os.path.exists(path): - raise ModelNotFoundException(f"Model not found at '{path}'.") - - if os.path.isdir(path): - if os.path.exists(os.path.join(path, "config.json")): - return T2IAdapterModelFormat.Diffusers - - raise InvalidModelException(f"Unsupported T2I-Adapter format: '{path}'.") - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - format = cls.detect_format(model_path) - if format == T2IAdapterModelFormat.Diffusers: - return model_path - else: - raise ValueError(f"Unsupported format: '{format}'.") diff --git a/invokeai/backend/model_management_OLD/models/textual_inversion.py b/invokeai/backend/model_management_OLD/models/textual_inversion.py deleted file mode 100644 index 99358704b8..0000000000 --- a/invokeai/backend/model_management_OLD/models/textual_inversion.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -from typing import Optional - -import torch - -# TODO: naming -from ..lora import TextualInversionModel as TextualInversionModelRaw -from .base import ( - BaseModelType, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelNotFoundException, - ModelType, - SubModelType, - classproperty, -) - - -class TextualInversionModel(ModelBase): - # model_size: int - - class Config(ModelConfigBase): - model_format: None - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert model_type == ModelType.TextualInversion - super().__init__(model_path, base_model, model_type) - - self.model_size = os.path.getsize(self.model_path) - - def get_size(self, child_type: Optional[SubModelType] = None): - if child_type is not None: - raise Exception("There is no child models in textual inversion") - return self.model_size - - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ): - if child_type is not None: - raise Exception("There is no child models in textual inversion") - - checkpoint_path = self.model_path - if os.path.isdir(checkpoint_path): - checkpoint_path = os.path.join(checkpoint_path, "learned_embeds.bin") - - if not os.path.exists(checkpoint_path): - raise ModelNotFoundException() - - model = TextualInversionModelRaw.from_checkpoint( - file_path=checkpoint_path, - dtype=torch_dtype, - ) - - self.model_size = model.embedding.nelement() * model.embedding.element_size() - return model - - @classproperty - def save_to_config(cls) -> bool: - return False - - @classmethod - def detect_format(cls, path: str): - if not os.path.exists(path): - raise ModelNotFoundException() - - if os.path.isdir(path): - if os.path.exists(os.path.join(path, "learned_embeds.bin")): - return None # diffusers-ti - - if os.path.isfile(path): - if any(path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt", "bin"]): - return None - - raise InvalidModelException(f"Not a valid model: {path}") - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, - base_model: BaseModelType, - ) -> str: - return model_path diff --git a/invokeai/backend/model_management_OLD/models/vae.py b/invokeai/backend/model_management_OLD/models/vae.py deleted file mode 100644 index 8cc37e67a7..0000000000 --- a/invokeai/backend/model_management_OLD/models/vae.py +++ /dev/null @@ -1,179 +0,0 @@ -import os -from enum import Enum -from pathlib import Path -from typing import Optional - -import safetensors -import torch -from omegaconf import OmegaConf - -from invokeai.app.services.config import InvokeAIAppConfig - -from .base import ( - BaseModelType, - EmptyConfigLoader, - InvalidModelException, - ModelBase, - ModelConfigBase, - ModelNotFoundException, - ModelType, - ModelVariantType, - SubModelType, - calc_model_size_by_data, - calc_model_size_by_fs, - classproperty, -) - - -class VaeModelFormat(str, Enum): - Checkpoint = "checkpoint" - Diffusers = "diffusers" - - -class VaeModel(ModelBase): - # vae_class: Type - # model_size: int - - class Config(ModelConfigBase): - model_format: VaeModelFormat - - def __init__(self, model_path: str, base_model: BaseModelType, model_type: ModelType): - assert model_type == ModelType.Vae - super().__init__(model_path, base_model, model_type) - - try: - config = EmptyConfigLoader.load_config(self.model_path, config_name="config.json") - # config = json.loads(os.path.join(self.model_path, "config.json")) - except Exception: - raise Exception("Invalid vae model! (config.json not found or invalid)") - - try: - vae_class_name = config.get("_class_name", "AutoencoderKL") - self.vae_class = self._hf_definition_to_type(["diffusers", vae_class_name]) - self.model_size = calc_model_size_by_fs(self.model_path) - except Exception: - raise Exception("Invalid vae model! (Unkown vae type)") - - def get_size(self, child_type: Optional[SubModelType] = None): - if child_type is not None: - raise Exception("There is no child models in vae model") - return self.model_size - - def get_model( - self, - torch_dtype: Optional[torch.dtype], - child_type: Optional[SubModelType] = None, - ): - if child_type is not None: - raise Exception("There is no child models in vae model") - - model = self.vae_class.from_pretrained( - self.model_path, - torch_dtype=torch_dtype, - ) - # calc more accurate size - self.model_size = calc_model_size_by_data(model) - return model - - @classproperty - def save_to_config(cls) -> bool: - return False - - @classmethod - def detect_format(cls, path: str): - if not os.path.exists(path): - raise ModelNotFoundException(f"Does not exist as local file: {path}") - - if os.path.isdir(path): - if os.path.exists(os.path.join(path, "config.json")): - return VaeModelFormat.Diffusers - - if os.path.isfile(path): - if any(path.endswith(f".{ext}") for ext in ["safetensors", "ckpt", "pt"]): - return VaeModelFormat.Checkpoint - - raise InvalidModelException(f"Not a valid model: {path}") - - @classmethod - def convert_if_required( - cls, - model_path: str, - output_path: str, - config: ModelConfigBase, # empty config or config of parent model - base_model: BaseModelType, - ) -> str: - if cls.detect_format(model_path) == VaeModelFormat.Checkpoint: - return _convert_vae_ckpt_and_cache( - weights_path=model_path, - output_path=output_path, - base_model=base_model, - model_config=config, - ) - else: - return model_path - - -# TODO: rework -def _convert_vae_ckpt_and_cache( - weights_path: str, - output_path: str, - base_model: BaseModelType, - model_config: ModelConfigBase, -) -> str: - """ - Convert the VAE indicated in mconfig into a diffusers AutoencoderKL - object, cache it to disk, and return Path to converted - file. If already on disk then just returns Path. - """ - app_config = InvokeAIAppConfig.get_config() - weights_path = app_config.root_dir / weights_path - output_path = Path(output_path) - - """ - this size used only in when tiling enabled to separate input in tiles - sizes in configs from stable diffusion githubs(1 and 2) set to 256 - on huggingface it: - 1.5 - 512 - 1.5-inpainting - 256 - 2-inpainting - 512 - 2-depth - 256 - 2-base - 512 - 2 - 768 - 2.1-base - 768 - 2.1 - 768 - """ - image_size = 512 - - # return cached version if it exists - if output_path.exists(): - return output_path - - if base_model in {BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2}: - from .stable_diffusion import _select_ckpt_config - - # all sd models use same vae settings - config_file = _select_ckpt_config(base_model, ModelVariantType.Normal) - else: - raise Exception(f"Vae conversion not supported for model type: {base_model}") - - # this avoids circular import error - from ..convert_ckpt_to_diffusers import convert_ldm_vae_to_diffusers - - if weights_path.suffix == ".safetensors": - checkpoint = safetensors.torch.load_file(weights_path, device="cpu") - else: - checkpoint = torch.load(weights_path, map_location="cpu") - - # sometimes weights are hidden under "state_dict", and sometimes not - if "state_dict" in checkpoint: - checkpoint = checkpoint["state_dict"] - - config = OmegaConf.load(app_config.root_path / config_file) - - vae_model = convert_ldm_vae_to_diffusers( - checkpoint=checkpoint, - vae_config=config, - image_size=image_size, - ) - vae_model.save_pretrained(output_path, safe_serialization=True) - return output_path diff --git a/invokeai/backend/model_management_OLD/seamless.py b/invokeai/backend/model_management_OLD/seamless.py deleted file mode 100644 index fb9112b56d..0000000000 --- a/invokeai/backend/model_management_OLD/seamless.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -from contextlib import contextmanager -from typing import Callable, List, Union - -import torch.nn as nn -from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL -from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel - - -def _conv_forward_asymmetric(self, input, weight, bias): - """ - Patch for Conv2d._conv_forward that supports asymmetric padding - """ - working = nn.functional.pad(input, self.asymmetric_padding["x"], mode=self.asymmetric_padding_mode["x"]) - working = nn.functional.pad(working, self.asymmetric_padding["y"], mode=self.asymmetric_padding_mode["y"]) - return nn.functional.conv2d( - working, - weight, - bias, - self.stride, - nn.modules.utils._pair(0), - self.dilation, - self.groups, - ) - - -@contextmanager -def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]): - # Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor - to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = [] - try: - # Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence - skipped_layers = 1 - for m_name, m in model.named_modules(): - if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): - continue - - if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name: - # down_blocks.1.resnets.1.conv1 - _, block_num, _, resnet_num, submodule_name = m_name.split(".") - block_num = int(block_num) - resnet_num = int(resnet_num) - - if block_num >= len(model.down_blocks) - skipped_layers: - continue - - # Skip the second resnet (could be configurable) - if resnet_num > 0: - continue - - # Skip Conv2d layers (could be configurable) - if submodule_name == "conv2": - continue - - m.asymmetric_padding_mode = {} - m.asymmetric_padding = {} - m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" - m.asymmetric_padding["x"] = ( - m._reversed_padding_repeated_twice[0], - m._reversed_padding_repeated_twice[1], - 0, - 0, - ) - m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" - m.asymmetric_padding["y"] = ( - 0, - 0, - m._reversed_padding_repeated_twice[2], - m._reversed_padding_repeated_twice[3], - ) - - to_restore.append((m, m._conv_forward)) - m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) - - yield - - finally: - for module, orig_conv_forward in to_restore: - module._conv_forward = orig_conv_forward - if hasattr(module, "asymmetric_padding_mode"): - del module.asymmetric_padding_mode - if hasattr(module, "asymmetric_padding"): - del module.asymmetric_padding diff --git a/invokeai/backend/model_management_OLD/util.py b/invokeai/backend/model_management_OLD/util.py deleted file mode 100644 index f4737d9f0b..0000000000 --- a/invokeai/backend/model_management_OLD/util.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) 2023 The InvokeAI Development Team -"""Utilities used by the Model Manager""" - - -def lora_token_vector_length(checkpoint: dict) -> int: - """ - Given a checkpoint in memory, return the lora token vector length - - :param checkpoint: The checkpoint - """ - - def _get_shape_1(key: str, tensor, checkpoint) -> int: - lora_token_vector_length = None - - if "." not in key: - return lora_token_vector_length # wrong key format - model_key, lora_key = key.split(".", 1) - - # check lora/locon - if lora_key == "lora_down.weight": - lora_token_vector_length = tensor.shape[1] - - # check loha (don't worry about hada_t1/hada_t2 as it used only in 4d shapes) - elif lora_key in ["hada_w1_b", "hada_w2_b"]: - lora_token_vector_length = tensor.shape[1] - - # check lokr (don't worry about lokr_t2 as it used only in 4d shapes) - elif "lokr_" in lora_key: - if model_key + ".lokr_w1" in checkpoint: - _lokr_w1 = checkpoint[model_key + ".lokr_w1"] - elif model_key + "lokr_w1_b" in checkpoint: - _lokr_w1 = checkpoint[model_key + ".lokr_w1_b"] - else: - return lora_token_vector_length # unknown format - - if model_key + ".lokr_w2" in checkpoint: - _lokr_w2 = checkpoint[model_key + ".lokr_w2"] - elif model_key + "lokr_w2_b" in checkpoint: - _lokr_w2 = checkpoint[model_key + ".lokr_w2_b"] - else: - return lora_token_vector_length # unknown format - - lora_token_vector_length = _lokr_w1.shape[1] * _lokr_w2.shape[1] - - elif lora_key == "diff": - lora_token_vector_length = tensor.shape[1] - - # ia3 can be detected only by shape[0] in text encoder - elif lora_key == "weight" and "lora_unet_" not in model_key: - lora_token_vector_length = tensor.shape[0] - - return lora_token_vector_length - - lora_token_vector_length = None - lora_te1_length = None - lora_te2_length = None - for key, tensor in checkpoint.items(): - if key.startswith("lora_unet_") and ("_attn2_to_k." in key or "_attn2_to_v." in key): - lora_token_vector_length = _get_shape_1(key, tensor, checkpoint) - elif key.startswith("lora_unet_") and ( - "time_emb_proj.lora_down" in key - ): # recognizes format at https://civitai.com/models/224641 - lora_token_vector_length = _get_shape_1(key, tensor, checkpoint) - elif key.startswith("lora_te") and "_self_attn_" in key: - tmp_length = _get_shape_1(key, tensor, checkpoint) - if key.startswith("lora_te_"): - lora_token_vector_length = tmp_length - elif key.startswith("lora_te1_"): - lora_te1_length = tmp_length - elif key.startswith("lora_te2_"): - lora_te2_length = tmp_length - - if lora_te1_length is not None and lora_te2_length is not None: - lora_token_vector_length = lora_te1_length + lora_te2_length - - if lora_token_vector_length is not None: - break - - return lora_token_vector_length diff --git a/invokeai/backend/model_manager/__init__.py b/invokeai/backend/model_manager/__init__.py index 98cc5054c7..88356d0468 100644 --- a/invokeai/backend/model_manager/__init__.py +++ b/invokeai/backend/model_manager/__init__.py @@ -1,5 +1,4 @@ """Re-export frequently-used symbols from the Model Manager backend.""" - from .config import ( AnyModel, AnyModelConfig, @@ -33,3 +32,42 @@ __all__ = [ "SchedulerPredictionType", "SubModelType", ] + +########## to help populate the openapi_schema with format enums for each config ########### +# This code is no longer necessary? +# leave it here just in case +# +# import inspect +# from enum import Enum +# from typing import Any, Iterable, Dict, get_args, Set +# def _expand(something: Any) -> Iterable[type]: +# if isinstance(something, type): +# yield something +# else: +# for x in get_args(something): +# for y in _expand(x): +# yield y + +# def _find_format(cls: type) -> Iterable[Enum]: +# if hasattr(inspect, "get_annotations"): +# fields = inspect.get_annotations(cls) +# else: +# fields = cls.__annotations__ +# if "format" in fields: +# for x in get_args(fields["format"]): +# yield x +# for parent_class in cls.__bases__: +# for x in _find_format(parent_class): +# yield x +# return None + +# def get_model_config_formats() -> Dict[str, Set[Enum]]: +# result: Dict[str, Set[Enum]] = {} +# for model_config in _expand(AnyModelConfig): +# for field in _find_format(model_config): +# if field is None: +# continue +# if not result.get(model_config.__qualname__): +# result[model_config.__qualname__] = set() +# result[model_config.__qualname__].add(field) +# return result diff --git a/invokeai/backend/model_manager/load/__init__.py b/invokeai/backend/model_manager/load/__init__.py index a3a840b625..a0421017db 100644 --- a/invokeai/backend/model_manager/load/__init__.py +++ b/invokeai/backend/model_manager/load/__init__.py @@ -6,12 +6,22 @@ from importlib import import_module from pathlib import Path from .convert_cache.convert_cache_default import ModelConvertCache -from .load_base import AnyModelLoader, LoadedModel +from .load_base import LoadedModel, ModelLoaderBase +from .load_default import ModelLoader from .model_cache.model_cache_default import ModelCache +from .model_loader_registry import ModelLoaderRegistry, ModelLoaderRegistryBase # This registers the subclasses that implement loaders of specific model types loaders = [x.stem for x in Path(Path(__file__).parent, "model_loaders").glob("*.py") if x.stem != "__init__"] for module in loaders: import_module(f"{__package__}.model_loaders.{module}") -__all__ = ["AnyModelLoader", "LoadedModel", "ModelCache", "ModelConvertCache"] +__all__ = [ + "LoadedModel", + "ModelCache", + "ModelConvertCache", + "ModelLoaderBase", + "ModelLoader", + "ModelLoaderRegistryBase", + "ModelLoaderRegistry", +] diff --git a/invokeai/backend/model_manager/load/load_base.py b/invokeai/backend/model_manager/load/load_base.py index 4c5e899aa3..b8ce56eb16 100644 --- a/invokeai/backend/model_manager/load/load_base.py +++ b/invokeai/backend/model_manager/load/load_base.py @@ -1,37 +1,22 @@ # Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team """ Base class for model loading in InvokeAI. - -Use like this: - - loader = AnyModelLoader(...) - loaded_model = loader.get_model('019ab39adfa1840455') - with loaded_model as model: # context manager moves model into VRAM - # do something with loaded_model """ -import hashlib from abc import ABC, abstractmethod from dataclasses import dataclass from logging import Logger from pathlib import Path -from typing import Any, Callable, Dict, Optional, Tuple, Type +from typing import Any, Optional from invokeai.app.services.config import InvokeAIAppConfig from invokeai.backend.model_manager.config import ( AnyModel, AnyModelConfig, - BaseModelType, - ModelConfigBase, - ModelFormat, - ModelType, SubModelType, - VaeCheckpointConfig, - VaeDiffusersConfig, ) from invokeai.backend.model_manager.load.convert_cache.convert_cache_base import ModelConvertCacheBase from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase, ModelLockerBase -from invokeai.backend.util.logging import InvokeAILogger @dataclass @@ -56,6 +41,14 @@ class LoadedModel: return self._locker.model +# TODO(MM2): +# Some "intermediary" subclasses in the ModelLoaderBase class hierarchy define methods that their subclasses don't +# know about. I think the problem may be related to this class being an ABC. +# +# For example, GenericDiffusersLoader defines `get_hf_load_class()`, and StableDiffusionDiffusersModel attempts to +# call it. However, the method is not defined in the ABC, so it is not guaranteed to be implemented. + + class ModelLoaderBase(ABC): """Abstract base class for loading models into RAM/VRAM.""" @@ -71,7 +64,7 @@ class ModelLoaderBase(ABC): pass @abstractmethod - def load_model(self, model_config: ModelConfigBase, submodel_type: Optional[SubModelType] = None) -> LoadedModel: + def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel: """ Return a model given its confguration. @@ -90,106 +83,3 @@ class ModelLoaderBase(ABC): ) -> int: """Return size in bytes of the model, calculated before loading.""" pass - - -# TO DO: Better name? -class AnyModelLoader: - """This class manages the model loaders and invokes the correct one to load a model of given base and type.""" - - # this tracks the loader subclasses - _registry: Dict[str, Type[ModelLoaderBase]] = {} - _logger: Logger = InvokeAILogger.get_logger() - - def __init__( - self, - app_config: InvokeAIAppConfig, - logger: Logger, - ram_cache: ModelCacheBase[AnyModel], - convert_cache: ModelConvertCacheBase, - ): - """Initialize AnyModelLoader with its dependencies.""" - self._app_config = app_config - self._logger = logger - self._ram_cache = ram_cache - self._convert_cache = convert_cache - - @property - def ram_cache(self) -> ModelCacheBase[AnyModel]: - """Return the RAM cache associated used by the loaders.""" - return self._ram_cache - - @property - def convert_cache(self) -> ModelConvertCacheBase: - """Return the convert cache associated used by the loaders.""" - return self._convert_cache - - def load_model(self, model_config: ModelConfigBase, submodel_type: Optional[SubModelType] = None) -> LoadedModel: - """ - Return a model given its configuration. - - :param key: model key, as known to the config backend - :param submodel_type: an ModelType enum indicating the portion of - the model to retrieve (e.g. ModelType.Vae) - """ - implementation, model_config, submodel_type = self.__class__.get_implementation(model_config, submodel_type) - return implementation( - app_config=self._app_config, - logger=self._logger, - ram_cache=self._ram_cache, - convert_cache=self._convert_cache, - ).load_model(model_config, submodel_type) - - @staticmethod - def _to_registry_key(base: BaseModelType, type: ModelType, format: ModelFormat) -> str: - return "-".join([base.value, type.value, format.value]) - - @classmethod - def get_implementation( - cls, config: ModelConfigBase, submodel_type: Optional[SubModelType] - ) -> Tuple[Type[ModelLoaderBase], ModelConfigBase, Optional[SubModelType]]: - """Get subclass of ModelLoaderBase registered to handle base and type.""" - # We have to handle VAE overrides here because this will change the model type and the corresponding implementation returned - conf2, submodel_type = cls._handle_subtype_overrides(config, submodel_type) - - key1 = cls._to_registry_key(conf2.base, conf2.type, conf2.format) # for a specific base type - key2 = cls._to_registry_key(BaseModelType.Any, conf2.type, conf2.format) # with wildcard Any - implementation = cls._registry.get(key1) or cls._registry.get(key2) - if not implementation: - raise NotImplementedError( - f"No subclass of LoadedModel is registered for base={config.base}, type={config.type}, format={config.format}" - ) - return implementation, conf2, submodel_type - - @classmethod - def _handle_subtype_overrides( - cls, config: ModelConfigBase, submodel_type: Optional[SubModelType] - ) -> Tuple[ModelConfigBase, Optional[SubModelType]]: - if submodel_type == SubModelType.Vae and hasattr(config, "vae") and config.vae is not None: - model_path = Path(config.vae) - config_class = ( - VaeCheckpointConfig if model_path.suffix in [".pt", ".safetensors", ".ckpt"] else VaeDiffusersConfig - ) - hash = hashlib.md5(model_path.as_posix().encode("utf-8")).hexdigest() - new_conf = config_class(path=model_path.as_posix(), name=model_path.stem, base=config.base, key=hash) - submodel_type = None - else: - new_conf = config - return new_conf, submodel_type - - @classmethod - def register( - cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any - ) -> Callable[[Type[ModelLoaderBase]], Type[ModelLoaderBase]]: - """Define a decorator which registers the subclass of loader.""" - - def decorator(subclass: Type[ModelLoaderBase]) -> Type[ModelLoaderBase]: - cls._logger.debug(f"Registering class {subclass.__name__} to load models of type {base}/{type}/{format}") - key = cls._to_registry_key(base, type, format) - if key in cls._registry: - raise Exception( - f"{subclass.__name__} is trying to register as a loader for {base}/{type}/{format}, but this type of model has already been registered by {cls._registry[key].__name__}" - ) - cls._registry[key] = subclass - return subclass - - return decorator diff --git a/invokeai/backend/model_manager/load/load_default.py b/invokeai/backend/model_manager/load/load_default.py index 79c9311de1..642cffaf4b 100644 --- a/invokeai/backend/model_manager/load/load_default.py +++ b/invokeai/backend/model_manager/load/load_default.py @@ -1,13 +1,9 @@ # Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team """Default implementation of model loading in InvokeAI.""" -import sys from logging import Logger from pathlib import Path -from typing import Any, Dict, Optional, Tuple - -from diffusers import ModelMixin -from diffusers.configuration_utils import ConfigMixin +from typing import Optional, Tuple from invokeai.app.services.config import InvokeAIAppConfig from invokeai.backend.model_manager import ( @@ -25,17 +21,6 @@ from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_ from invokeai.backend.util.devices import choose_torch_device, torch_dtype -class ConfigLoader(ConfigMixin): - """Subclass of ConfigMixin for loading diffusers configuration files.""" - - @classmethod - def load_config(cls, *args: Any, **kwargs: Any) -> Dict[str, Any]: - """Load a diffusrs ConfigMixin configuration.""" - cls.config_name = kwargs.pop("config_name") - # Diffusers doesn't provide typing info - return super().load_config(*args, **kwargs) # type: ignore - - # TO DO: The loader is not thread safe! class ModelLoader(ModelLoaderBase): """Default implementation of ModelLoaderBase.""" @@ -137,43 +122,6 @@ class ModelLoader(ModelLoaderBase): variant=config.repo_variant if hasattr(config, "repo_variant") else None, ) - def _load_diffusers_config(self, model_path: Path, config_name: str = "config.json") -> Dict[str, Any]: - return ConfigLoader.load_config(model_path, config_name=config_name) - - # TO DO: Add exception handling - def _hf_definition_to_type(self, module: str, class_name: str) -> ModelMixin: # fix with correct type - if module in ["diffusers", "transformers"]: - res_type = sys.modules[module] - else: - res_type = sys.modules["diffusers"].pipelines - result: ModelMixin = getattr(res_type, class_name) - return result - - # TO DO: Add exception handling - def _get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelType] = None) -> ModelMixin: - if submodel_type: - try: - config = self._load_diffusers_config(model_path, config_name="model_index.json") - module, class_name = config[submodel_type.value] - return self._hf_definition_to_type(module=module, class_name=class_name) - except KeyError as e: - raise InvalidModelConfigException( - f'The "{submodel_type}" submodel is not available for this model.' - ) from e - else: - try: - config = self._load_diffusers_config(model_path, config_name="config.json") - class_name = config.get("_class_name", None) - if class_name: - return self._hf_definition_to_type(module="diffusers", class_name=class_name) - if config.get("model_type", None) == "clip_vision_model": - class_name = config.get("architectures")[0] - return self._hf_definition_to_type(module="transformers", class_name=class_name) - if not class_name: - raise InvalidModelConfigException("Unable to decifer Load Class based on given config.json") - except KeyError as e: - raise InvalidModelConfigException("An expected config.json file is missing from this model.") from e - # This needs to be implemented in subclasses that handle checkpoints def _convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Path) -> Path: raise NotImplementedError diff --git a/invokeai/backend/model_manager/load/memory_snapshot.py b/invokeai/backend/model_manager/load/memory_snapshot.py index 209d7166f3..195e39361b 100644 --- a/invokeai/backend/model_manager/load/memory_snapshot.py +++ b/invokeai/backend/model_manager/load/memory_snapshot.py @@ -55,7 +55,7 @@ class MemorySnapshot: vram = None try: - malloc_info = LibcUtil().mallinfo2() # type: ignore + malloc_info = LibcUtil().mallinfo2() except (OSError, AttributeError): # OSError: This is expected in environments that do not have the 'libc.so.6' shared library. # AttributeError: This is expected in environments that have `libc.so.6` but do not have the `mallinfo2` (e.g. glibc < 2.33) diff --git a/invokeai/backend/model_manager/load/model_loader_registry.py b/invokeai/backend/model_manager/load/model_loader_registry.py new file mode 100644 index 0000000000..ce1110e749 --- /dev/null +++ b/invokeai/backend/model_manager/load/model_loader_registry.py @@ -0,0 +1,122 @@ +# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Development team +""" +This module implements a system in which model loaders register the +type, base and format of models that they know how to load. + +Use like this: + + cls, model_config, submodel_type = ModelLoaderRegistry.get_implementation(model_config, submodel_type) # type: ignore + loaded_model = cls( + app_config=app_config, + logger=logger, + ram_cache=ram_cache, + convert_cache=convert_cache + ).load_model(model_config, submodel_type) + +""" +import hashlib +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Callable, Dict, Optional, Tuple, Type + +from ..config import ( + AnyModelConfig, + BaseModelType, + ModelConfigBase, + ModelFormat, + ModelType, + SubModelType, + VaeCheckpointConfig, + VaeDiffusersConfig, +) +from . import ModelLoaderBase + + +class ModelLoaderRegistryBase(ABC): + """This class allows model loaders to register their type, base and format.""" + + @classmethod + @abstractmethod + def register( + cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any + ) -> Callable[[Type[ModelLoaderBase]], Type[ModelLoaderBase]]: + """Define a decorator which registers the subclass of loader.""" + + @classmethod + @abstractmethod + def get_implementation( + cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] + ) -> Tuple[Type[ModelLoaderBase], ModelConfigBase, Optional[SubModelType]]: + """ + Get subclass of ModelLoaderBase registered to handle base and type. + + Parameters: + :param config: Model configuration record, as returned by ModelRecordService + :param submodel_type: Submodel to fetch (main models only) + :return: tuple(loader_class, model_config, submodel_type) + + Note that the returned model config may be different from one what passed + in, in the event that a submodel type is provided. + """ + + +class ModelLoaderRegistry: + """ + This class allows model loaders to register their type, base and format. + """ + + _registry: Dict[str, Type[ModelLoaderBase]] = {} + + @classmethod + def register( + cls, type: ModelType, format: ModelFormat, base: BaseModelType = BaseModelType.Any + ) -> Callable[[Type[ModelLoaderBase]], Type[ModelLoaderBase]]: + """Define a decorator which registers the subclass of loader.""" + + def decorator(subclass: Type[ModelLoaderBase]) -> Type[ModelLoaderBase]: + key = cls._to_registry_key(base, type, format) + if key in cls._registry: + raise Exception( + f"{subclass.__name__} is trying to register as a loader for {base}/{type}/{format}, but this type of model has already been registered by {cls._registry[key].__name__}" + ) + cls._registry[key] = subclass + return subclass + + return decorator + + @classmethod + def get_implementation( + cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] + ) -> Tuple[Type[ModelLoaderBase], ModelConfigBase, Optional[SubModelType]]: + """Get subclass of ModelLoaderBase registered to handle base and type.""" + # We have to handle VAE overrides here because this will change the model type and the corresponding implementation returned + conf2, submodel_type = cls._handle_subtype_overrides(config, submodel_type) + + key1 = cls._to_registry_key(conf2.base, conf2.type, conf2.format) # for a specific base type + key2 = cls._to_registry_key(BaseModelType.Any, conf2.type, conf2.format) # with wildcard Any + implementation = cls._registry.get(key1) or cls._registry.get(key2) + if not implementation: + raise NotImplementedError( + f"No subclass of LoadedModel is registered for base={config.base}, type={config.type}, format={config.format}" + ) + return implementation, conf2, submodel_type + + @classmethod + def _handle_subtype_overrides( + cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] + ) -> Tuple[ModelConfigBase, Optional[SubModelType]]: + if submodel_type == SubModelType.Vae and hasattr(config, "vae") and config.vae is not None: + model_path = Path(config.vae) + config_class = ( + VaeCheckpointConfig if model_path.suffix in [".pt", ".safetensors", ".ckpt"] else VaeDiffusersConfig + ) + hash = hashlib.md5(model_path.as_posix().encode("utf-8")).hexdigest() + new_conf = config_class(path=model_path.as_posix(), name=model_path.stem, base=config.base, key=hash) + submodel_type = None + else: + new_conf = config + return new_conf, submodel_type + + @staticmethod + def _to_registry_key(base: BaseModelType, type: ModelType, format: ModelFormat) -> str: + return "-".join([base.value, type.value, format.value]) diff --git a/invokeai/backend/model_manager/load/model_loaders/controlnet.py b/invokeai/backend/model_manager/load/model_loaders/controlnet.py index d446d07933..43393f5a84 100644 --- a/invokeai/backend/model_manager/load/model_loaders/controlnet.py +++ b/invokeai/backend/model_manager/load/model_loaders/controlnet.py @@ -13,13 +13,13 @@ from invokeai.backend.model_manager import ( ModelType, ) from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_controlnet_to_diffusers -from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from .. import ModelLoaderRegistry from .generic_diffusers import GenericDiffusersLoader -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Diffusers) -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Checkpoint) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ControlNet, format=ModelFormat.Checkpoint) class ControlnetLoader(GenericDiffusersLoader): """Class to load ControlNet models.""" diff --git a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py index 114e317f3c..9a9b25aec5 100644 --- a/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py +++ b/invokeai/backend/model_manager/load/model_loaders/generic_diffusers.py @@ -1,24 +1,27 @@ # Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team """Class for simple diffusers model loading in InvokeAI.""" +import sys from pathlib import Path -from typing import Optional +from typing import Any, Dict, Optional + +from diffusers import ConfigMixin, ModelMixin from invokeai.backend.model_manager import ( AnyModel, BaseModelType, + InvalidModelConfigException, ModelFormat, ModelRepoVariant, ModelType, SubModelType, ) -from ..load_base import AnyModelLoader -from ..load_default import ModelLoader +from .. import ModelLoader, ModelLoaderRegistry -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers) class GenericDiffusersLoader(ModelLoader): """Class to load simple diffusers models.""" @@ -28,9 +31,60 @@ class GenericDiffusersLoader(ModelLoader): model_variant: Optional[ModelRepoVariant] = None, submodel_type: Optional[SubModelType] = None, ) -> AnyModel: - model_class = self._get_hf_load_class(model_path) + model_class = self.get_hf_load_class(model_path) if submodel_type is not None: raise Exception(f"There are no submodels in models of type {model_class}") variant = model_variant.value if model_variant else None result: AnyModel = model_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, variant=variant) # type: ignore return result + + # TO DO: Add exception handling + def get_hf_load_class(self, model_path: Path, submodel_type: Optional[SubModelType] = None) -> ModelMixin: + """Given the model path and submodel, returns the diffusers ModelMixin subclass needed to load.""" + if submodel_type: + try: + config = self._load_diffusers_config(model_path, config_name="model_index.json") + module, class_name = config[submodel_type.value] + result = self._hf_definition_to_type(module=module, class_name=class_name) + except KeyError as e: + raise InvalidModelConfigException( + f'The "{submodel_type}" submodel is not available for this model.' + ) from e + else: + try: + config = self._load_diffusers_config(model_path, config_name="config.json") + class_name = config.get("_class_name", None) + if class_name: + result = self._hf_definition_to_type(module="diffusers", class_name=class_name) + if config.get("model_type", None) == "clip_vision_model": + class_name = config.get("architectures") + assert class_name is not None + result = self._hf_definition_to_type(module="transformers", class_name=class_name[0]) + if not class_name: + raise InvalidModelConfigException("Unable to decifer Load Class based on given config.json") + except KeyError as e: + raise InvalidModelConfigException("An expected config.json file is missing from this model.") from e + return result + + # TO DO: Add exception handling + def _hf_definition_to_type(self, module: str, class_name: str) -> ModelMixin: # fix with correct type + if module in ["diffusers", "transformers"]: + res_type = sys.modules[module] + else: + res_type = sys.modules["diffusers"].pipelines + result: ModelMixin = getattr(res_type, class_name) + return result + + def _load_diffusers_config(self, model_path: Path, config_name: str = "config.json") -> Dict[str, Any]: + return ConfigLoader.load_config(model_path, config_name=config_name) + + +class ConfigLoader(ConfigMixin): + """Subclass of ConfigMixin for loading diffusers configuration files.""" + + @classmethod + def load_config(cls, *args: Any, **kwargs: Any) -> Dict[str, Any]: + """Load a diffusrs ConfigMixin configuration.""" + cls.config_name = kwargs.pop("config_name") + # Diffusers doesn't provide typing info + return super().load_config(*args, **kwargs) # type: ignore diff --git a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py index 27ced41c1e..7d25e9d218 100644 --- a/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py +++ b/invokeai/backend/model_manager/load/model_loaders/ip_adapter.py @@ -15,11 +15,10 @@ from invokeai.backend.model_manager import ( ModelType, SubModelType, ) -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.model_manager.load import ModelLoader, ModelLoaderRegistry -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.InvokeAI) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.IPAdapter, format=ModelFormat.InvokeAI) class IPAdapterInvokeAILoader(ModelLoader): """Class to load IP Adapter diffusers models.""" diff --git a/invokeai/backend/model_manager/load/model_loaders/lora.py b/invokeai/backend/model_manager/load/model_loaders/lora.py index 6ff2dcc918..fe804ef565 100644 --- a/invokeai/backend/model_manager/load/model_loaders/lora.py +++ b/invokeai/backend/model_manager/load/model_loaders/lora.py @@ -18,13 +18,13 @@ from invokeai.backend.model_manager import ( SubModelType, ) from invokeai.backend.model_manager.load.convert_cache import ModelConvertCacheBase -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader from invokeai.backend.model_manager.load.model_cache.model_cache_base import ModelCacheBase +from .. import ModelLoader, ModelLoaderRegistry -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Diffusers) -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Lycoris) + +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Lora, format=ModelFormat.Lycoris) class LoraLoader(ModelLoader): """Class to load LoRA models.""" diff --git a/invokeai/backend/model_manager/load/model_loaders/onnx.py b/invokeai/backend/model_manager/load/model_loaders/onnx.py index 935a6b7c95..38f0274acc 100644 --- a/invokeai/backend/model_manager/load/model_loaders/onnx.py +++ b/invokeai/backend/model_manager/load/model_loaders/onnx.py @@ -13,13 +13,14 @@ from invokeai.backend.model_manager import ( ModelType, SubModelType, ) -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader + +from .. import ModelLoaderRegistry +from .generic_diffusers import GenericDiffusersLoader -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Onnx) -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Olive) -class OnnyxDiffusersModel(ModelLoader): +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Onnx) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.ONNX, format=ModelFormat.Olive) +class OnnyxDiffusersModel(GenericDiffusersLoader): """Class to load onnx models.""" def _load_model( @@ -30,7 +31,7 @@ class OnnyxDiffusersModel(ModelLoader): ) -> AnyModel: if not submodel_type is not None: raise Exception("A submodel type must be provided when loading onnx pipelines.") - load_class = self._get_hf_load_class(model_path, submodel_type) + load_class = self.get_hf_load_class(model_path, submodel_type) variant = model_variant.value if model_variant else None model_path = model_path / submodel_type.value result: AnyModel = load_class.from_pretrained( diff --git a/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py index 23b4e1fccd..5884f84e8d 100644 --- a/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py +++ b/invokeai/backend/model_manager/load/model_loaders/stable_diffusion.py @@ -19,13 +19,14 @@ from invokeai.backend.model_manager import ( ) from invokeai.backend.model_manager.config import MainCheckpointConfig from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ckpt_to_diffusers -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader + +from .. import ModelLoaderRegistry +from .generic_diffusers import GenericDiffusersLoader -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Diffusers) -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Checkpoint) -class StableDiffusionDiffusersModel(ModelLoader): +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Main, format=ModelFormat.Checkpoint) +class StableDiffusionDiffusersModel(GenericDiffusersLoader): """Class to load main models.""" model_base_to_model_type = { @@ -43,7 +44,7 @@ class StableDiffusionDiffusersModel(ModelLoader): ) -> AnyModel: if not submodel_type is not None: raise Exception("A submodel type must be provided when loading main pipelines.") - load_class = self._get_hf_load_class(model_path, submodel_type) + load_class = self.get_hf_load_class(model_path, submodel_type) variant = model_variant.value if model_variant else None model_path = model_path / submodel_type.value result: AnyModel = load_class.from_pretrained( diff --git a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py index 9476747960..094d4d7c5c 100644 --- a/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py +++ b/invokeai/backend/model_manager/load/model_loaders/textual_inversion.py @@ -5,7 +5,6 @@ from pathlib import Path from typing import Optional, Tuple -from invokeai.backend.textual_inversion import TextualInversionModelRaw from invokeai.backend.model_manager import ( AnyModel, AnyModelConfig, @@ -15,12 +14,15 @@ from invokeai.backend.model_manager import ( ModelType, SubModelType, ) -from invokeai.backend.model_manager.load.load_base import AnyModelLoader -from invokeai.backend.model_manager.load.load_default import ModelLoader +from invokeai.backend.textual_inversion import TextualInversionModelRaw + +from .. import ModelLoader, ModelLoaderRegistry -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFile) -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFolder) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFile) +@ModelLoaderRegistry.register( + base=BaseModelType.Any, type=ModelType.TextualInversion, format=ModelFormat.EmbeddingFolder +) class TextualInversionLoader(ModelLoader): """Class to load TI models.""" diff --git a/invokeai/backend/model_manager/load/model_loaders/vae.py b/invokeai/backend/model_manager/load/model_loaders/vae.py index 3983ea7595..7ade1494eb 100644 --- a/invokeai/backend/model_manager/load/model_loaders/vae.py +++ b/invokeai/backend/model_manager/load/model_loaders/vae.py @@ -14,14 +14,14 @@ from invokeai.backend.model_manager import ( ModelType, ) from invokeai.backend.model_manager.convert_ckpt_to_diffusers import convert_ldm_vae_to_diffusers -from invokeai.backend.model_manager.load.load_base import AnyModelLoader +from .. import ModelLoaderRegistry from .generic_diffusers import GenericDiffusersLoader -@AnyModelLoader.register(base=BaseModelType.Any, type=ModelType.Vae, format=ModelFormat.Diffusers) -@AnyModelLoader.register(base=BaseModelType.StableDiffusion1, type=ModelType.Vae, format=ModelFormat.Checkpoint) -@AnyModelLoader.register(base=BaseModelType.StableDiffusion2, type=ModelType.Vae, format=ModelFormat.Checkpoint) +@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.Vae, format=ModelFormat.Diffusers) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion1, type=ModelType.Vae, format=ModelFormat.Checkpoint) +@ModelLoaderRegistry.register(base=BaseModelType.StableDiffusion2, type=ModelType.Vae, format=ModelFormat.Checkpoint) class VaeLoader(GenericDiffusersLoader): """Class to load VAE models.""" diff --git a/invokeai/backend/model_manager/load/optimizations.py b/invokeai/backend/model_manager/load/optimizations.py index a46d262175..030fcfa639 100644 --- a/invokeai/backend/model_manager/load/optimizations.py +++ b/invokeai/backend/model_manager/load/optimizations.py @@ -1,16 +1,16 @@ from contextlib import contextmanager +from typing import Any, Generator import torch -def _no_op(*args, **kwargs): +def _no_op(*args: Any, **kwargs: Any) -> None: pass @contextmanager -def skip_torch_weight_init(): - """A context manager that monkey-patches several of the common torch layers (torch.nn.Linear, torch.nn.Conv1d, etc.) - to skip weight initialization. +def skip_torch_weight_init() -> Generator[None, None, None]: + """Monkey patch several of the common torch layers (torch.nn.Linear, torch.nn.Conv1d, etc.) to skip weight initialization. By default, `torch.nn.Linear` and `torch.nn.ConvNd` layers initialize their weights (according to a particular distribution) when __init__ is called. This weight initialization step can take a significant amount of time, and is @@ -18,13 +18,14 @@ def skip_torch_weight_init(): monkey-patches common torch layers to skip the weight initialization step. """ torch_modules = [torch.nn.Linear, torch.nn.modules.conv._ConvNd, torch.nn.Embedding] - saved_functions = [m.reset_parameters for m in torch_modules] + saved_functions = [hasattr(m, "reset_parameters") and m.reset_parameters for m in torch_modules] try: for torch_module in torch_modules: + assert hasattr(torch_module, "reset_parameters") torch_module.reset_parameters = _no_op - yield None finally: for torch_module, saved_function in zip(torch_modules, saved_functions, strict=True): + assert hasattr(torch_module, "reset_parameters") torch_module.reset_parameters = saved_function diff --git a/invokeai/backend/model_manager/merge.py b/invokeai/backend/model_manager/merge.py index 108f1f0e6f..7063cb907d 100644 --- a/invokeai/backend/model_manager/merge.py +++ b/invokeai/backend/model_manager/merge.py @@ -13,7 +13,7 @@ from typing import Any, List, Optional, Set import torch from diffusers import AutoPipelineForText2Image -from diffusers import logging as dlogging +from diffusers.utils import logging as dlogging from invokeai.app.services.model_install import ModelInstallServiceBase from invokeai.backend.util.devices import choose_torch_device, torch_dtype @@ -76,7 +76,7 @@ class ModelMerger(object): custom_pipeline="checkpoint_merger", torch_dtype=dtype, variant=variant, - ) + ) # type: ignore merged_pipe = pipe.merge( pretrained_model_name_or_path_list=model_paths, alpha=alpha, diff --git a/invokeai/backend/model_manager/metadata/metadata_base.py b/invokeai/backend/model_manager/metadata/metadata_base.py index 5c3afcdc96..6e410d8222 100644 --- a/invokeai/backend/model_manager/metadata/metadata_base.py +++ b/invokeai/backend/model_manager/metadata/metadata_base.py @@ -54,8 +54,8 @@ class LicenseRestrictions(BaseModel): AllowDifferentLicense: bool = Field( description="if true, derivatives of this model be redistributed under a different license", default=False ) - AllowCommercialUse: CommercialUsage = Field( - description="Type of commercial use allowed or 'No' if no commercial use is allowed.", default_factory=set + AllowCommercialUse: Optional[CommercialUsage] = Field( + description="Type of commercial use allowed or 'No' if no commercial use is allowed.", default=None ) @@ -139,7 +139,10 @@ class CivitaiMetadata(ModelMetadataWithFiles): @property def allow_commercial_use(self) -> bool: """Return True if commercial use is allowed.""" - return self.restrictions.AllowCommercialUse != CommercialUsage("None") + if self.restrictions.AllowCommercialUse is None: + return False + else: + return self.restrictions.AllowCommercialUse != CommercialUsage("None") @property def allow_derivatives(self) -> bool: diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index d511ffa875..7de4289466 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -8,7 +8,6 @@ import torch from picklescan.scanner import scan_file_path import invokeai.backend.util.logging as logger -from .util.model_util import lora_token_vector_length, read_checkpoint_meta from invokeai.backend.util.util import SilenceWarnings from .config import ( @@ -23,6 +22,7 @@ from .config import ( SchedulerPredictionType, ) from .hash import FastModelHash +from .util.model_util import lora_token_vector_length, read_checkpoint_meta CkptType = Dict[str, Any] @@ -53,6 +53,7 @@ LEGACY_CONFIGS: Dict[BaseModelType, Dict[ModelVariantType, Union[str, Dict[Sched }, } + class ProbeBase(object): """Base class for probes.""" diff --git a/invokeai/backend/model_manager/search.py b/invokeai/backend/model_manager/search.py index 0ead22b743..f7ef2e049d 100644 --- a/invokeai/backend/model_manager/search.py +++ b/invokeai/backend/model_manager/search.py @@ -116,9 +116,9 @@ class ModelSearch(ModelSearchBase): # returns all models that have 'anime' in the path """ - models_found: Optional[Set[Path]] = Field(default=None) - scanned_dirs: Optional[Set[Path]] = Field(default=None) - pruned_paths: Optional[Set[Path]] = Field(default=None) + models_found: Set[Path] = Field(default_factory=set) + scanned_dirs: Set[Path] = Field(default_factory=set) + pruned_paths: Set[Path] = Field(default_factory=set) def search_started(self) -> None: self.models_found = set() diff --git a/invokeai/backend/model_manager/util/libc_util.py b/invokeai/backend/model_manager/util/libc_util.py index 1fbcae0a93..ef1ac2f8a4 100644 --- a/invokeai/backend/model_manager/util/libc_util.py +++ b/invokeai/backend/model_manager/util/libc_util.py @@ -35,7 +35,7 @@ class Struct_mallinfo2(ctypes.Structure): ("keepcost", ctypes.c_size_t), ] - def __str__(self): + def __str__(self) -> str: s = "" s += f"{'arena': <10}= {(self.arena/2**30):15.5f} # Non-mmapped space allocated (GB) (uordblks + fordblks)\n" s += f"{'ordblks': <10}= {(self.ordblks): >15} # Number of free chunks\n" @@ -62,7 +62,7 @@ class LibcUtil: TODO: Improve cross-OS compatibility of this class. """ - def __init__(self): + def __init__(self) -> None: self._libc = ctypes.cdll.LoadLibrary("libc.so.6") def mallinfo2(self) -> Struct_mallinfo2: @@ -72,4 +72,5 @@ class LibcUtil: """ mallinfo2 = self._libc.mallinfo2 mallinfo2.restype = Struct_mallinfo2 - return mallinfo2() + result: Struct_mallinfo2 = mallinfo2() + return result diff --git a/invokeai/backend/model_manager/util/model_util.py b/invokeai/backend/model_manager/util/model_util.py index 6847a40878..2e448520e5 100644 --- a/invokeai/backend/model_manager/util/model_util.py +++ b/invokeai/backend/model_manager/util/model_util.py @@ -1,12 +1,15 @@ """Utilities for parsing model files, used mostly by probe.py""" import json -import torch -from typing import Union from pathlib import Path +from typing import Dict, Optional, Union + +import safetensors +import torch from picklescan.scanner import scan_file_path -def _fast_safetensors_reader(path: str): + +def _fast_safetensors_reader(path: str) -> Dict[str, torch.Tensor]: checkpoint = {} device = torch.device("meta") with open(path, "rb") as f: @@ -37,10 +40,12 @@ def _fast_safetensors_reader(path: str): return checkpoint -def read_checkpoint_meta(path: Union[str, Path], scan: bool = False): + +def read_checkpoint_meta(path: Union[str, Path], scan: bool = False) -> Dict[str, torch.Tensor]: if str(path).endswith(".safetensors"): try: - checkpoint = _fast_safetensors_reader(path) + path_str = path.as_posix() if isinstance(path, Path) else path + checkpoint = _fast_safetensors_reader(path_str) except Exception: # TODO: create issue for support "meta"? checkpoint = safetensors.torch.load_file(path, device="cpu") @@ -52,14 +57,15 @@ def read_checkpoint_meta(path: Union[str, Path], scan: bool = False): checkpoint = torch.load(path, map_location=torch.device("meta")) return checkpoint -def lora_token_vector_length(checkpoint: dict) -> int: + +def lora_token_vector_length(checkpoint: Dict[str, torch.Tensor]) -> Optional[int]: """ Given a checkpoint in memory, return the lora token vector length :param checkpoint: The checkpoint """ - def _get_shape_1(key: str, tensor, checkpoint) -> int: + def _get_shape_1(key: str, tensor: torch.Tensor, checkpoint: Dict[str, torch.Tensor]) -> Optional[int]: lora_token_vector_length = None if "." not in key: diff --git a/invokeai/backend/onnx/onnx_runtime.py b/invokeai/backend/onnx/onnx_runtime.py index 9b2096abdf..8916865dd5 100644 --- a/invokeai/backend/onnx/onnx_runtime.py +++ b/invokeai/backend/onnx/onnx_runtime.py @@ -8,6 +8,7 @@ import numpy as np import onnx from onnx import numpy_helper from onnxruntime import InferenceSession, SessionOptions, get_available_providers + from ..raw_model import RawModel ONNX_WEIGHTS_NAME = "model.onnx" @@ -15,7 +16,7 @@ ONNX_WEIGHTS_NAME = "model.onnx" # NOTE FROM LS: This was copied from Stalker's original implementation. # I have not yet gone through and fixed all the type hints -class IAIOnnxRuntimeModel: +class IAIOnnxRuntimeModel(RawModel): class _tensor_access: def __init__(self, model): # type: ignore self.model = model diff --git a/invokeai/backend/raw_model.py b/invokeai/backend/raw_model.py index 2e224d538b..d0dc50c456 100644 --- a/invokeai/backend/raw_model.py +++ b/invokeai/backend/raw_model.py @@ -10,5 +10,6 @@ The term 'raw' was introduced to describe a wrapper around a torch.nn.Module that adds additional methods and attributes. """ + class RawModel: """Base class for 'Raw' model wrappers.""" diff --git a/invokeai/backend/stable_diffusion/seamless.py b/invokeai/backend/stable_diffusion/seamless.py index bfdf9e0c53..fb9112b56d 100644 --- a/invokeai/backend/stable_diffusion/seamless.py +++ b/invokeai/backend/stable_diffusion/seamless.py @@ -1,10 +1,11 @@ from __future__ import annotations from contextlib import contextmanager -from typing import List, Union +from typing import Callable, List, Union import torch.nn as nn -from diffusers.models import AutoencoderKL, UNet2DConditionModel +from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL +from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel def _conv_forward_asymmetric(self, input, weight, bias): @@ -26,70 +27,51 @@ def _conv_forward_asymmetric(self, input, weight, bias): @contextmanager def set_seamless(model: Union[UNet2DConditionModel, AutoencoderKL], seamless_axes: List[str]): + # Callable: (input: Tensor, weight: Tensor, bias: Optional[Tensor]) -> Tensor + to_restore: list[tuple[nn.Conv2d | nn.ConvTranspose2d, Callable]] = [] try: - to_restore = [] - + # Hard coded to skip down block layers, allowing for seamless tiling at the expense of prompt adherence + skipped_layers = 1 for m_name, m in model.named_modules(): - if isinstance(model, UNet2DConditionModel): - if ".attentions." in m_name: + if not isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): + continue + + if isinstance(model, UNet2DConditionModel) and m_name.startswith("down_blocks.") and ".resnets." in m_name: + # down_blocks.1.resnets.1.conv1 + _, block_num, _, resnet_num, submodule_name = m_name.split(".") + block_num = int(block_num) + resnet_num = int(resnet_num) + + if block_num >= len(model.down_blocks) - skipped_layers: continue - if ".resnets." in m_name: - if ".conv2" in m_name: - continue - if ".conv_shortcut" in m_name: - continue - - """ - if isinstance(model, UNet2DConditionModel): - if False and ".upsamplers." in m_name: + # Skip the second resnet (could be configurable) + if resnet_num > 0: continue - if False and ".downsamplers." in m_name: + # Skip Conv2d layers (could be configurable) + if submodule_name == "conv2": continue - if True and ".resnets." in m_name: - if True and ".conv1" in m_name: - if False and "down_blocks" in m_name: - continue - if False and "mid_block" in m_name: - continue - if False and "up_blocks" in m_name: - continue + m.asymmetric_padding_mode = {} + m.asymmetric_padding = {} + m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" + m.asymmetric_padding["x"] = ( + m._reversed_padding_repeated_twice[0], + m._reversed_padding_repeated_twice[1], + 0, + 0, + ) + m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" + m.asymmetric_padding["y"] = ( + 0, + 0, + m._reversed_padding_repeated_twice[2], + m._reversed_padding_repeated_twice[3], + ) - if True and ".conv2" in m_name: - continue - - if True and ".conv_shortcut" in m_name: - continue - - if True and ".attentions." in m_name: - continue - - if False and m_name in ["conv_in", "conv_out"]: - continue - """ - - if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)): - m.asymmetric_padding_mode = {} - m.asymmetric_padding = {} - m.asymmetric_padding_mode["x"] = "circular" if ("x" in seamless_axes) else "constant" - m.asymmetric_padding["x"] = ( - m._reversed_padding_repeated_twice[0], - m._reversed_padding_repeated_twice[1], - 0, - 0, - ) - m.asymmetric_padding_mode["y"] = "circular" if ("y" in seamless_axes) else "constant" - m.asymmetric_padding["y"] = ( - 0, - 0, - m._reversed_padding_repeated_twice[2], - m._reversed_padding_repeated_twice[3], - ) - - to_restore.append((m, m._conv_forward)) - m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) + to_restore.append((m, m._conv_forward)) + m._conv_forward = _conv_forward_asymmetric.__get__(m, nn.Conv2d) yield diff --git a/invokeai/backend/textual_inversion.py b/invokeai/backend/textual_inversion.py index 9a4fa0b540..f7390979bb 100644 --- a/invokeai/backend/textual_inversion.py +++ b/invokeai/backend/textual_inversion.py @@ -8,8 +8,10 @@ from compel.embeddings_provider import BaseTextualInversionManager from safetensors.torch import load_file from transformers import CLIPTokenizer from typing_extensions import Self + from .raw_model import RawModel + class TextualInversionModelRaw(RawModel): embedding: torch.Tensor # [n, 768]|[n, 1280] embedding_2: Optional[torch.Tensor] = None # [n, 768]|[n, 1280] - for SDXL models diff --git a/invokeai/backend/util/test_utils.py b/invokeai/backend/util/test_utils.py index a3def182c8..0d76c4633c 100644 --- a/invokeai/backend/util/test_utils.py +++ b/invokeai/backend/util/test_utils.py @@ -42,7 +42,7 @@ def install_and_load_model( # If the requested model is already installed, return its LoadedModel with contextlib.suppress(UnknownModelException): # TODO: Replace with wrapper call - loaded_model: LoadedModel = model_manager.load.load_model_by_attr( + loaded_model: LoadedModel = model_manager.load_model_by_attr( model_name=model_name, base_model=base_model, model_type=model_type ) return loaded_model @@ -53,7 +53,7 @@ def install_and_load_model( assert job.complete try: - loaded_model = model_manager.load.load_model_by_config(job.config_out) + loaded_model = model_manager.load_model_by_config(job.config_out) return loaded_model except UnknownModelException as e: raise Exception( diff --git a/tests/backend/model_manager/model_loading/test_model_load.py b/tests/backend/model_manager/model_loading/test_model_load.py index 38d9b8afb8..c1fde504ea 100644 --- a/tests/backend/model_manager/model_loading/test_model_load.py +++ b/tests/backend/model_manager/model_loading/test_model_load.py @@ -4,18 +4,27 @@ Test model loading from pathlib import Path -from invokeai.app.services.model_install import ModelInstallServiceBase -from invokeai.app.services.model_load import ModelLoadServiceBase +from invokeai.app.services.model_manager import ModelManagerServiceBase from invokeai.backend.textual_inversion import TextualInversionModelRaw from tests.backend.model_manager.model_manager_fixtures import * # noqa F403 -def test_loading(mm2_installer: ModelInstallServiceBase, mm2_loader: ModelLoadServiceBase, embedding_file: Path): - store = mm2_installer.record_store + +def test_loading(mm2_model_manager: ModelManagerServiceBase, embedding_file: Path): + store = mm2_model_manager.store matches = store.search_by_attr(model_name="test_embedding") assert len(matches) == 0 - key = mm2_installer.register_path(embedding_file) - loaded_model = mm2_loader.load_model_by_config(store.get_model(key)) + key = mm2_model_manager.install.register_path(embedding_file) + loaded_model = mm2_model_manager.load_model_by_config(store.get_model(key)) assert loaded_model is not None assert loaded_model.config.key == key with loaded_model as model: assert isinstance(model, TextualInversionModelRaw) + loaded_model_2 = mm2_model_manager.load_model_by_key(key) + assert loaded_model.config.key == loaded_model_2.config.key + + loaded_model_3 = mm2_model_manager.load_model_by_attr( + model_name=loaded_model.config.name, + model_type=loaded_model.config.type, + base_model=loaded_model.config.base, + ) + assert loaded_model.config.key == loaded_model_3.config.key diff --git a/tests/backend/model_manager/model_manager_fixtures.py b/tests/backend/model_manager/model_manager_fixtures.py index 5f7f44c018..df54e2f926 100644 --- a/tests/backend/model_manager/model_manager_fixtures.py +++ b/tests/backend/model_manager/model_manager_fixtures.py @@ -6,17 +6,17 @@ from pathlib import Path from typing import Any, Dict, List import pytest -from pytest import FixtureRequest from pydantic import BaseModel +from pytest import FixtureRequest from requests.sessions import Session from requests_testadapter import TestAdapter, TestSession from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.download import DownloadQueueServiceBase, DownloadQueueService +from invokeai.app.services.download import DownloadQueueService, DownloadQueueServiceBase from invokeai.app.services.events.events_base import EventServiceBase -from invokeai.app.services.model_manager import ModelManagerServiceBase, ModelManagerService -from invokeai.app.services.model_load import ModelLoadServiceBase, ModelLoadService from invokeai.app.services.model_install import ModelInstallService, ModelInstallServiceBase +from invokeai.app.services.model_load import ModelLoadService, ModelLoadServiceBase +from invokeai.app.services.model_manager import ModelManagerService, ModelManagerServiceBase from invokeai.app.services.model_metadata import ModelMetadataStoreBase, ModelMetadataStoreSQL from invokeai.app.services.model_records import ModelRecordServiceBase, ModelRecordServiceSQL from invokeai.backend.model_manager.config import ( @@ -95,9 +95,7 @@ def mm2_app_config(mm2_root_dir: Path) -> InvokeAIAppConfig: @pytest.fixture -def mm2_download_queue(mm2_session: Session, - request: FixtureRequest - ) -> DownloadQueueServiceBase: +def mm2_download_queue(mm2_session: Session, request: FixtureRequest) -> DownloadQueueServiceBase: download_queue = DownloadQueueService(requests_session=mm2_session) download_queue.start() @@ -107,30 +105,34 @@ def mm2_download_queue(mm2_session: Session, request.addfinalizer(stop_queue) return download_queue + @pytest.fixture def mm2_metadata_store(mm2_record_store: ModelRecordServiceSQL) -> ModelMetadataStoreBase: return mm2_record_store.metadata_store + @pytest.fixture def mm2_loader(mm2_app_config: InvokeAIAppConfig, mm2_record_store: ModelRecordServiceBase) -> ModelLoadServiceBase: ram_cache = ModelCache( logger=InvokeAILogger.get_logger(), max_cache_size=mm2_app_config.ram_cache_size, - max_vram_cache_size=mm2_app_config.vram_cache_size + max_vram_cache_size=mm2_app_config.vram_cache_size, ) convert_cache = ModelConvertCache(mm2_app_config.models_convert_cache_path) - return ModelLoadService(app_config=mm2_app_config, - record_store=mm2_record_store, - ram_cache=ram_cache, - convert_cache=convert_cache, - ) + return ModelLoadService( + app_config=mm2_app_config, + ram_cache=ram_cache, + convert_cache=convert_cache, + ) + @pytest.fixture -def mm2_installer(mm2_app_config: InvokeAIAppConfig, - mm2_download_queue: DownloadQueueServiceBase, - mm2_session: Session, - request: FixtureRequest, - ) -> ModelInstallServiceBase: +def mm2_installer( + mm2_app_config: InvokeAIAppConfig, + mm2_download_queue: DownloadQueueServiceBase, + mm2_session: Session, + request: FixtureRequest, +) -> ModelInstallServiceBase: logger = InvokeAILogger.get_logger() db = create_mock_sqlite_database(mm2_app_config, logger) events = DummyEventService() @@ -213,15 +215,13 @@ def mm2_record_store(mm2_app_config: InvokeAIAppConfig) -> ModelRecordServiceBas store.add_model("test_config_5", raw5) return store + @pytest.fixture -def mm2_model_manager(mm2_record_store: ModelRecordServiceBase, - mm2_installer: ModelInstallServiceBase, - mm2_loader: ModelLoadServiceBase) -> ModelManagerServiceBase: - return ModelManagerService( - store=mm2_record_store, - install=mm2_installer, - load=mm2_loader - ) +def mm2_model_manager( + mm2_record_store: ModelRecordServiceBase, mm2_installer: ModelInstallServiceBase, mm2_loader: ModelLoadServiceBase +) -> ModelManagerServiceBase: + return ModelManagerService(store=mm2_record_store, install=mm2_installer, load=mm2_loader) + @pytest.fixture def mm2_session(embedding_file: Path, diffusers_dir: Path) -> Session: @@ -306,5 +306,3 @@ def mm2_session(embedding_file: Path, diffusers_dir: Path) -> Session: ), ) return sess - - diff --git a/tests/backend/model_manager/test_lora.py b/tests/backend/model_manager/test_lora.py index e124bb68ef..114a4cfdcf 100644 --- a/tests/backend/model_manager/test_lora.py +++ b/tests/backend/model_manager/test_lora.py @@ -5,8 +5,8 @@ import pytest import torch -from invokeai.backend.model_patcher import ModelPatcher from invokeai.backend.lora import LoRALayer, LoRAModelRaw +from invokeai.backend.model_patcher import ModelPatcher @pytest.mark.parametrize( diff --git a/tests/backend/model_manager/test_memory_snapshot.py b/tests/backend/model_manager/test_memory_snapshot.py index 87ec8c34ee..d31ae79b66 100644 --- a/tests/backend/model_manager/test_memory_snapshot.py +++ b/tests/backend/model_manager/test_memory_snapshot.py @@ -1,7 +1,8 @@ import pytest -from invokeai.backend.model_manager.util.libc_util import Struct_mallinfo2 from invokeai.backend.model_manager.load.memory_snapshot import MemorySnapshot, get_pretty_snapshot_diff +from invokeai.backend.model_manager.util.libc_util import Struct_mallinfo2 + def test_memory_snapshot_capture(): """Smoke test of MemorySnapshot.capture().""" From fc2082259542985c259d962449714debd0349833 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Mon, 19 Feb 2024 00:56:27 +0530 Subject: [PATCH 147/411] fix: Alpha channel causing issue with DW Processor --- .../app/invocations/controlnet_image_processors.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 8542134fff..5b15981caa 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -40,12 +40,7 @@ from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.image_util.depth_anything import DepthAnythingDetector from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector -from .baseinvocation import ( - BaseInvocation, - BaseInvocationOutput, - invocation, - invocation_output, -) +from .baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output CONTROLNET_MODE_VALUES = Literal["balanced", "more_prompt", "more_control", "unbalanced"] CONTROLNET_RESIZE_VALUES = Literal[ @@ -593,9 +588,7 @@ class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation): depth_anything_detector = DepthAnythingDetector() depth_anything_detector.load_model(model_size=self.model_size) - if image.mode == "RGBA": - image = image.convert("RGB") - + image = image.convert("RGB") if image.mode != "RGB" else image processed_image = depth_anything_detector(image=image, resolution=self.resolution, offload=self.offload) return processed_image @@ -615,7 +608,8 @@ class DWOpenposeImageProcessorInvocation(ImageProcessorInvocation): draw_hands: bool = InputField(default=False) image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) - def run_processor(self, image): + def run_processor(self, image: Image.Image): + image = image.convert("RGB") if image.mode != "RGB" else image dw_openpose = DWOpenposeDetector() processed_image = dw_openpose( image, From 43d94c8108784e1d13fe6ccfce98d9b9b5eaf22f Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 18 Feb 2024 15:42:58 -0500 Subject: [PATCH 148/411] feat(nodes): format option for get_image method Also default CNet preprocessors to "RGB" --- .../app/invocations/controlnet_image_processors.py | 2 +- invokeai/app/services/shared/invocation_context.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 5b15981caa..1ef5352db6 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -144,7 +144,7 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithBoard): return image def invoke(self, context: InvocationContext) -> ImageOutput: - raw_image = context.images.get_pil(self.image.image_name) + raw_image = context.images.get_pil(self.image.image_name, "RGB") # image type should be PIL.PngImagePlugin.PngImageFile ? processed_image = self.run_processor(raw_image) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 1395427a97..d217a865af 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -194,13 +194,20 @@ class ImagesInterface(InvocationContextInterface): node_id=self._context_data.invocation.id, ) - def get_pil(self, image_name: str) -> Image: + def get_pil(self, image_name: str, format: str | None = None) -> Image: """ Gets an image as a PIL Image object. :param image_name: The name of the image to get. + :param format: The color format to convert the image to. If None, the original format is used. """ - return self._services.images.get_pil_image(image_name) + image = self._services.images.get_pil_image(image_name) + if format and format != image.mode: + try: + image = image.convert(format) + except ValueError: + self._services.logger.warning(f"Could not convert image from {image.mode} to {format}. Using original format.") + return image def get_metadata(self, image_name: str) -> Optional[MetadataField]: """ From 92394ab75107a5209bb5300d24fe8a861d59c846 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 18 Feb 2024 16:29:31 -0500 Subject: [PATCH 149/411] fix(nodes): canny preprocessor uses RGBA again --- .../app/invocations/controlnet_image_processors.py | 10 +++++++++- .../app/invocations/custom_nodes/InvokeAI_DemoFusion | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) create mode 160000 invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 1ef5352db6..797ea62f7c 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -143,8 +143,12 @@ class ImageProcessorInvocation(BaseInvocation, WithMetadata, WithBoard): # superclass just passes through image without processing return image + def load_image(self, context: InvocationContext) -> Image.Image: + # allows override for any special formatting specific to the preprocessor + return context.images.get_pil(self.image.image_name, "RGB") + def invoke(self, context: InvocationContext) -> ImageOutput: - raw_image = context.images.get_pil(self.image.image_name, "RGB") + raw_image = self.load_image(context) # image type should be PIL.PngImagePlugin.PngImageFile ? processed_image = self.run_processor(raw_image) @@ -181,6 +185,10 @@ class CannyImageProcessorInvocation(ImageProcessorInvocation): default=200, ge=0, le=255, description="The high threshold of the Canny pixel gradient (0-255)" ) + def load_image(self, context: InvocationContext) -> Image.Image: + # Keep alpha channel for Canny processing to detect edges of transparent areas + return context.images.get_pil(self.image.image_name, "RGBA") + def run_processor(self, image): canny_processor = CannyDetector() processed_image = canny_processor(image, self.low_threshold, self.high_threshold) diff --git a/invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion b/invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion new file mode 160000 index 0000000000..aae207914f --- /dev/null +++ b/invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion @@ -0,0 +1 @@ +Subproject commit aae207914f08f77324691ae984fae6dabb0b8976 From 2d007ce532a1e06f2f23293de3412b9ea2008f33 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 18 Feb 2024 16:48:05 -0500 Subject: [PATCH 150/411] fix: removed custom module --- invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion | 1 - 1 file changed, 1 deletion(-) delete mode 160000 invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion diff --git a/invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion b/invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion deleted file mode 160000 index aae207914f..0000000000 --- a/invokeai/app/invocations/custom_nodes/InvokeAI_DemoFusion +++ /dev/null @@ -1 +0,0 @@ -Subproject commit aae207914f08f77324691ae984fae6dabb0b8976 From 965867151bc5194824847d0151ac0642ba609e21 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 18 Feb 2024 16:56:46 -0500 Subject: [PATCH 151/411] chore(invocations): use IMAGE_MODES constant literal --- invokeai/app/invocations/constants.py | 3 +++ invokeai/app/invocations/image.py | 4 +--- invokeai/app/services/shared/invocation_context.py | 11 ++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/invokeai/app/invocations/constants.py b/invokeai/app/invocations/constants.py index 795e7a3b60..fca5a2ec7f 100644 --- a/invokeai/app/invocations/constants.py +++ b/invokeai/app/invocations/constants.py @@ -12,3 +12,6 @@ The ratio of image:latent dimensions is LATENT_SCALE_FACTOR:1, or 8:1. SCHEDULER_NAME_VALUES = Literal[tuple(SCHEDULER_MAP.keys())] """A literal type representing the valid scheduler names.""" + +IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] +"""A literal type for PIL image modes supported by Invoke""" \ No newline at end of file diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index f5ad5515a6..1f3b5b7368 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -16,6 +16,7 @@ from invokeai.app.invocations.fields import ( WithMetadata, ) from invokeai.app.invocations.primitives import ImageOutput +from invokeai.app.invocations.constants import IMAGE_MODES from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark @@ -263,9 +264,6 @@ class ImageChannelInvocation(BaseInvocation, WithMetadata, WithBoard): return ImageOutput.build(image_dto) -IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] - - @invocation( "img_conv", title="Convert Image Mode", diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index d217a865af..2383785ad4 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -6,6 +6,7 @@ from PIL.Image import Image from torch import Tensor from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata +from invokeai.app.invocations.constants import IMAGE_MODES from invokeai.app.services.boards.boards_common import BoardDTO from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin @@ -194,19 +195,19 @@ class ImagesInterface(InvocationContextInterface): node_id=self._context_data.invocation.id, ) - def get_pil(self, image_name: str, format: str | None = None) -> Image: + def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image: """ Gets an image as a PIL Image object. :param image_name: The name of the image to get. - :param format: The color format to convert the image to. If None, the original format is used. + :param mode: The color mode to convert the image to. If None, the original mode is used. """ image = self._services.images.get_pil_image(image_name) - if format and format != image.mode: + if mode and mode != image.mode: try: - image = image.convert(format) + image = image.convert(mode) except ValueError: - self._services.logger.warning(f"Could not convert image from {image.mode} to {format}. Using original format.") + self._services.logger.warning(f"Could not convert image from {image.mode} to {mode}. Using original mode instead.") return image def get_metadata(self, image_name: str) -> Optional[MetadataField]: From 56ac2104e3cb3c03006f903226a2db9e4a0ad9bc Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 18 Feb 2024 23:10:38 -0500 Subject: [PATCH 152/411] chore(invocations): remove redundant RGB conversions --- invokeai/app/invocations/controlnet_image_processors.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 797ea62f7c..1e998e4b61 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -423,10 +423,6 @@ class MediapipeFaceProcessorInvocation(ImageProcessorInvocation): min_confidence: float = InputField(default=0.5, ge=0, le=1, description="Minimum confidence for face detection") def run_processor(self, image): - # MediaPipeFaceDetector throws an error if image has alpha channel - # so convert to RGB if needed - if image.mode == "RGBA": - image = image.convert("RGB") mediapipe_face_processor = MediapipeFaceDetector() processed_image = mediapipe_face_processor(image, max_faces=self.max_faces, min_confidence=self.min_confidence) return processed_image @@ -595,8 +591,7 @@ class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation): def run_processor(self, image: Image.Image): depth_anything_detector = DepthAnythingDetector() depth_anything_detector.load_model(model_size=self.model_size) - - image = image.convert("RGB") if image.mode != "RGB" else image + processed_image = depth_anything_detector(image=image, resolution=self.resolution, offload=self.offload) return processed_image @@ -617,7 +612,6 @@ class DWOpenposeImageProcessorInvocation(ImageProcessorInvocation): image_resolution: int = InputField(default=512, ge=0, description=FieldDescriptions.image_res) def run_processor(self, image: Image.Image): - image = image.convert("RGB") if image.mode != "RGB" else image dw_openpose = DWOpenposeDetector() processed_image = dw_openpose( image, From cd070d8be9daf3992ce6c080015749fa35a3488a Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 18 Feb 2024 23:11:36 -0500 Subject: [PATCH 153/411] chore: ruff formatting --- invokeai/app/invocations/constants.py | 2 +- invokeai/app/invocations/controlnet_image_processors.py | 2 +- invokeai/app/invocations/image.py | 2 +- invokeai/app/services/shared/invocation_context.py | 6 ++++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/invokeai/app/invocations/constants.py b/invokeai/app/invocations/constants.py index fca5a2ec7f..cebe0eb30f 100644 --- a/invokeai/app/invocations/constants.py +++ b/invokeai/app/invocations/constants.py @@ -14,4 +14,4 @@ SCHEDULER_NAME_VALUES = Literal[tuple(SCHEDULER_MAP.keys())] """A literal type representing the valid scheduler names.""" IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] -"""A literal type for PIL image modes supported by Invoke""" \ No newline at end of file +"""A literal type for PIL image modes supported by Invoke""" diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 1e998e4b61..8774f2fb27 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -591,7 +591,7 @@ class DepthAnythingImageProcessorInvocation(ImageProcessorInvocation): def run_processor(self, image: Image.Image): depth_anything_detector = DepthAnythingDetector() depth_anything_detector.load_model(model_size=self.model_size) - + processed_image = depth_anything_detector(image=image, resolution=self.resolution, offload=self.offload) return processed_image diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 1f3b5b7368..a0c41161c3 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -7,6 +7,7 @@ import cv2 import numpy from PIL import Image, ImageChops, ImageFilter, ImageOps +from invokeai.app.invocations.constants import IMAGE_MODES from invokeai.app.invocations.fields import ( ColorField, FieldDescriptions, @@ -16,7 +17,6 @@ from invokeai.app.invocations.fields import ( WithMetadata, ) from invokeai.app.invocations.primitives import ImageOutput -from invokeai.app.invocations.constants import IMAGE_MODES from invokeai.app.services.image_records.image_records_common import ImageCategory from invokeai.app.services.shared.invocation_context import InvocationContext from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 2383785ad4..43ecb2c543 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Optional from PIL.Image import Image from torch import Tensor -from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata from invokeai.app.invocations.constants import IMAGE_MODES +from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata from invokeai.app.services.boards.boards_common import BoardDTO from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin @@ -207,7 +207,9 @@ class ImagesInterface(InvocationContextInterface): try: image = image.convert(mode) except ValueError: - self._services.logger.warning(f"Could not convert image from {image.mode} to {mode}. Using original mode instead.") + self._services.logger.warning( + f"Could not convert image from {image.mode} to {mode}. Using original mode instead." + ) return image def get_metadata(self, image_name: str) -> Optional[MetadataField]: From 1242cb4f8511933f0fe2e86409dc97df1fa64b94 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Sun, 18 Feb 2024 23:15:28 -0500 Subject: [PATCH 154/411] one more redundant RGB convert removed --- invokeai/app/invocations/controlnet_image_processors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/app/invocations/controlnet_image_processors.py b/invokeai/app/invocations/controlnet_image_processors.py index 8774f2fb27..9eba3acdca 100644 --- a/invokeai/app/invocations/controlnet_image_processors.py +++ b/invokeai/app/invocations/controlnet_image_processors.py @@ -552,7 +552,6 @@ class ColorMapImageProcessorInvocation(ImageProcessorInvocation): color_map_tile_size: int = InputField(default=64, ge=0, description=FieldDescriptions.tile_size) def run_processor(self, image: Image.Image): - image = image.convert("RGB") np_image = np.array(image, dtype=np.uint8) height, width = np_image.shape[:2] From af2117dc0c1f13030cdb26be033da44948a2d50f Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 18 Feb 2024 18:02:58 -0500 Subject: [PATCH 155/411] remove errant def that was crashing invokeai-configure --- .../backend/install/invokeai_configure.py | 25 ++++++++----------- invokeai/frontend/install/model_install.py | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/invokeai/backend/install/invokeai_configure.py b/invokeai/backend/install/invokeai_configure.py index 4dfa2b070c..ac3e583de3 100755 --- a/invokeai/backend/install/invokeai_configure.py +++ b/invokeai/backend/install/invokeai_configure.py @@ -84,6 +84,8 @@ _, MAX_VRAM = torch.cuda.mem_get_info() if HAS_CUDA else (0.0, 0.0) MAX_VRAM /= GB MAX_RAM = psutil.virtual_memory().total / GB +FORCE_FULL_PRECISION = False + INIT_FILE_PREAMBLE = """# InvokeAI initialization file # This is the InvokeAI initialization file, which contains command-line default values. # Feel free to edit. If anything goes wrong, you can re-initialize this file by deleting @@ -112,9 +114,6 @@ then run one of the following commands to start InvokeAI. Web UI: invokeai-web -Command-line client: - invokeai - If you installed using an installation script, run: {config.root_path}/invoke.{"bat" if sys.platform == "win32" else "sh"} @@ -408,7 +407,7 @@ Use cursor arrows to make a checkbox selection, and space to toggle. begin_entry_at=3, max_height=2, relx=30, - max_width=56, + max_width=80, scroll_exit=True, ) self.add_widget_intelligent( @@ -664,7 +663,6 @@ https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/LICENS generation_options = [GENERATION_OPT_CHOICES[x] for x in self.generation_options.value] for v in GENERATION_OPT_CHOICES: setattr(new_opts, v, v in generation_options) - return new_opts @@ -695,9 +693,6 @@ class EditOptApplication(npyscreen.NPSAppManaged): cycle_widgets=False, ) - def new_opts(self) -> Namespace: - return self.options.marshall_arguments() - def default_ramcache() -> float: """Run a heuristic for the default RAM cache based on installed RAM.""" @@ -712,6 +707,7 @@ def default_ramcache() -> float: def default_startup_options(init_file: Path) -> InvokeAIAppConfig: opts = InvokeAIAppConfig.get_config() opts.ram = default_ramcache() + opts.precision = "float32" if FORCE_FULL_PRECISION else choose_precision(torch.device(choose_torch_device())) return opts @@ -760,7 +756,8 @@ def initialize_rootdir(root: Path, yes_to_all: bool = False): def run_console_ui( program_opts: Namespace, initfile: Path, install_helper: InstallHelper ) -> Tuple[Optional[Namespace], Optional[InstallSelections]]: - invokeai_opts = default_startup_options(initfile) + first_time = not (config.root_path / "invokeai.yaml").exists() + invokeai_opts = default_startup_options(initfile) if first_time else config invokeai_opts.root = program_opts.root if not set_min_terminal_size(MIN_COLS, MIN_LINES): @@ -773,7 +770,7 @@ def run_console_ui( if editApp.user_cancelled: return (None, None) else: - return (editApp.new_opts(), editApp.install_selections) + return (editApp.new_opts, editApp.install_selections) # ------------------------------------- @@ -785,7 +782,7 @@ def write_opts(opts: InvokeAIAppConfig, init_file: Path) -> None: new_config = InvokeAIAppConfig.get_config() new_config.root = config.root - for key, value in opts.model_dump().items(): + for key, value in vars(opts).items(): if hasattr(new_config, key): setattr(new_config, key, value) @@ -869,7 +866,8 @@ def migrate_if_needed(opt: Namespace, root: Path) -> bool: # ------------------------------------- -def main(): +def main() -> None: + global FORCE_FULL_PRECISION # FIXME parser = argparse.ArgumentParser(description="InvokeAI model downloader") parser.add_argument( "--skip-sd-weights", @@ -921,17 +919,16 @@ def main(): help="path to root of install directory", ) opt = parser.parse_args() - invoke_args = [] if opt.root: invoke_args.extend(["--root", opt.root]) if opt.full_precision: invoke_args.extend(["--precision", "float32"]) config.parse_args(invoke_args) - config.precision = "float32" if opt.full_precision else choose_precision(torch.device(choose_torch_device())) logger = InvokeAILogger().get_logger(config=config) errors = set() + FORCE_FULL_PRECISION = opt.full_precision # FIXME global try: # if we do a root migration/upgrade, then we are keeping previous diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index 20b630dfc6..3a4d66ae0a 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -43,7 +43,7 @@ from invokeai.frontend.install.widgets import ( warnings.filterwarnings("ignore", category=UserWarning) # noqa: E402 config = InvokeAIAppConfig.get_config() logger = InvokeAILogger.get_logger("ModelInstallService") -logger.setLevel("WARNING") +# logger.setLevel("WARNING") # logger.setLevel('DEBUG') # build a table mapping all non-printable characters to None From 731860c332a00286a4f54383e5ff3224b56478a9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 11:22:08 +1100 Subject: [PATCH 156/411] feat(nodes): JIT graph nodes validation We use pydantic to validate a union of valid invocations when instantiating a graph. Previously, we constructed the union while creating the `Graph` class. This introduces a dependency on the order of imports. For example, consider a setup where we have 3 invocations in the app: - Python executes the module where `FirstInvocation` is defined, registering `FirstInvocation`. - Python executes the module where `SecondInvocation` is defined, registering `SecondInvocation`. - Python executes the module where `Graph` is defined. A union of invocations is created and used to define the `Graph.nodes` field. The union contains `FirstInvocation` and `SecondInvocation`. - Python executes the module where `ThirdInvocation` is defined, registering `ThirdInvocation`. - A graph is created that includes `ThirdInvocation`. Pydantic validates the graph using the union, which does not know about `ThirdInvocation`, raising a `ValidationError` about an unknown invocation type. This scenario has been particularly problematic in tests, where we may create invocations dynamically. The test files have to be structured in such a way that the imports happen in the right order. It's a major pain. This PR refactors the validation of graph nodes to resolve this issue: - `BaseInvocation` gets a new method `get_typeadapter`. This builds a pydantic `TypeAdapter` for the union of all registered invocations, caching it after the first call. - `Graph.nodes`'s type is widened to `dict[str, BaseInvocation]`. This actually is a nice bonus, because we get better type hints whenever we reference `some_graph.nodes`. - A "plain" field validator takes over the validation logic for `Graph.nodes`. "Plain" validators totally override pydantic's own validation logic. The validator grabs the `TypeAdapter` from `BaseInvocation`, then validates each node with it. The validation is identical to the previous implementation - we get the same errors. `BaseInvocationOutput` gets the same treatment. --- invokeai/app/invocations/baseinvocation.py | 45 ++++++++++++++++------ invokeai/app/invocations/compel.py | 2 +- invokeai/app/services/shared/graph.py | 41 +++++++++++++------- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 3243714937..5edae5342d 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -8,13 +8,26 @@ import warnings from abc import ABC, abstractmethod from enum import Enum from inspect import signature -from types import UnionType -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Literal, Optional, Type, TypeVar, Union, cast +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + ClassVar, + Iterable, + Literal, + Optional, + Type, + TypeVar, + Union, + cast, +) import semver -from pydantic import BaseModel, ConfigDict, Field, create_model +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, create_model from pydantic.fields import FieldInfo from pydantic_core import PydanticUndefined +from typing_extensions import TypeAliasType from invokeai.app.invocations.fields import ( FieldKind, @@ -84,6 +97,7 @@ class BaseInvocationOutput(BaseModel): """ _output_classes: ClassVar[set[BaseInvocationOutput]] = set() + _typeadapter: ClassVar[Optional[TypeAdapter[Any]]] = None @classmethod def register_output(cls, output: BaseInvocationOutput) -> None: @@ -96,10 +110,14 @@ class BaseInvocationOutput(BaseModel): return cls._output_classes @classmethod - def get_outputs_union(cls) -> UnionType: - """Gets a union of all invocation outputs.""" - outputs_union = Union[tuple(cls._output_classes)] # type: ignore [valid-type] - return outputs_union # type: ignore [return-value] + def get_typeadapter(cls) -> TypeAdapter[Any]: + """Gets a pydantc TypeAdapter for the union of all invocation output types.""" + if not cls._typeadapter: + InvocationOutputsUnion = TypeAliasType( + "InvocationOutputsUnion", Annotated[Union[tuple(cls._output_classes)], Field(discriminator="type")] + ) + cls._typeadapter = TypeAdapter(InvocationOutputsUnion) + return cls._typeadapter @classmethod def get_output_types(cls) -> Iterable[str]: @@ -148,6 +166,7 @@ class BaseInvocation(ABC, BaseModel): """ _invocation_classes: ClassVar[set[BaseInvocation]] = set() + _typeadapter: ClassVar[Optional[TypeAdapter[Any]]] = None @classmethod def get_type(cls) -> str: @@ -160,10 +179,14 @@ class BaseInvocation(ABC, BaseModel): cls._invocation_classes.add(invocation) @classmethod - def get_invocations_union(cls) -> UnionType: - """Gets a union of all invocation types.""" - invocations_union = Union[tuple(cls._invocation_classes)] # type: ignore [valid-type] - return invocations_union # type: ignore [return-value] + def get_typeadapter(cls) -> TypeAdapter[Any]: + """Gets a pydantc TypeAdapter for the union of all invocation types.""" + if not cls._typeadapter: + InvocationsUnion = TypeAliasType( + "InvocationsUnion", Annotated[Union[tuple(cls._invocation_classes)], Field(discriminator="type")] + ) + cls._typeadapter = TypeAdapter(InvocationsUnion) + return cls._typeadapter @classmethod def get_invocations(cls) -> Iterable[BaseInvocation]: diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 517da4375e..47be380626 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -417,7 +417,7 @@ class ClipSkipInvocation(BaseInvocation): """Skip layers in clip text_encoder model.""" clip: ClipField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP") - skipped_layers: int = InputField(default=0, description=FieldDescriptions.skipped_layers) + skipped_layers: int = InputField(default=0, ge=0, description=FieldDescriptions.skipped_layers) def invoke(self, context: InvocationContext) -> ClipSkipInvocationOutput: self.clip.skipped_layers += self.skipped_layers diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 3df230f5ee..3066af0e50 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -2,10 +2,15 @@ import copy import itertools -from typing import Annotated, Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints +from typing import Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints import networkx as nx -from pydantic import BaseModel, ConfigDict, field_validator, model_validator +from pydantic import ( + BaseModel, + ConfigDict, + field_validator, + model_validator, +) from pydantic.fields import Field # Importing * is bad karma but needed here for node detection @@ -260,21 +265,24 @@ class CollectInvocation(BaseInvocation): return CollectInvocationOutput(collection=copy.copy(self.collection)) -InvocationsUnion: Any = BaseInvocation.get_invocations_union() -InvocationOutputsUnion: Any = BaseInvocationOutput.get_outputs_union() - - class Graph(BaseModel): id: str = Field(description="The id of this graph", default_factory=uuid_string) # TODO: use a list (and never use dict in a BaseModel) because pydantic/fastapi hates me - nodes: dict[str, Annotated[InvocationsUnion, Field(discriminator="type")]] = Field( - description="The nodes in this graph", default_factory=dict - ) + nodes: dict[str, BaseInvocation] = Field(description="The nodes in this graph", default_factory=dict) edges: list[Edge] = Field( description="The connections between nodes and their fields in this graph", default_factory=list, ) + @field_validator("nodes", mode="plain") + @classmethod + def validate_nodes(cls, v: dict[str, Any]): + nodes: dict[str, BaseInvocation] = {} + typeadapter = BaseInvocation.get_typeadapter() + for node_id, node in v.items(): + nodes[node_id] = typeadapter.validate_python(node) + return nodes + def add_node(self, node: BaseInvocation) -> None: """Adds a node to a graph @@ -824,9 +832,7 @@ class GraphExecutionState(BaseModel): ) # The results of executed nodes - results: dict[str, Annotated[InvocationOutputsUnion, Field(discriminator="type")]] = Field( - description="The results of node executions", default_factory=dict - ) + results: dict[str, BaseInvocationOutput] = Field(description="The results of node executions", default_factory=dict) # Errors raised when executing nodes errors: dict[str, str] = Field(description="Errors raised when executing nodes", default_factory=dict) @@ -843,6 +849,15 @@ class GraphExecutionState(BaseModel): default_factory=dict, ) + @field_validator("results", mode="plain") + @classmethod + def validate_results(cls, v: dict[str, BaseInvocationOutput]): + results: dict[str, BaseInvocationOutput] = {} + typeadapter = BaseInvocationOutput.get_typeadapter() + for result_id, result in v.items(): + results[result_id] = typeadapter.validate_python(result) + return results + @field_validator("graph") def graph_is_valid(cls, v: Graph): """Validates that the graph is valid""" @@ -1247,6 +1262,6 @@ class LibraryGraph(BaseModel): return values -GraphInvocation.model_rebuild(force=True) Graph.model_rebuild(force=True) +GraphInvocation.model_rebuild(force=True) GraphExecutionState.model_rebuild(force=True) From b79ae3a10132aeb3b9a338af6bd5cb22557ca709 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 18:19:22 +1100 Subject: [PATCH 157/411] fix(nodes): fix OpenAPI schema generation The change to `Graph.nodes` and `GraphExecutionState.results` validation requires some fanagling to get the OpenAPI schema generation to work. See new comments for a details. --- invokeai/app/api_app.py | 3 +- invokeai/app/services/shared/graph.py | 90 +++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 149d47fb96..65607c436a 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -151,6 +151,8 @@ def custom_openapi() -> dict[str, Any]: # TODO: note that we assume the schema_key here is the TYPE.__name__ # This could break in some cases, figure out a better way to do it output_type_titles[schema_key] = output_schema["title"] + openapi_schema["components"]["schemas"][schema_key] = output_schema + openapi_schema["components"]["schemas"][schema_key]["class"] = "output" # Add Node Editor UI helper schemas ui_config_schemas = models_json_schema( @@ -173,7 +175,6 @@ def custom_openapi() -> dict[str, Any]: outputs_ref = {"$ref": f"#/components/schemas/{output_type_title}"} invoker_schema["output"] = outputs_ref invoker_schema["class"] = "invocation" - openapi_schema["components"]["schemas"][f"{output_type_title}"]["class"] = "output" # This code no longer seems to be necessary? # Leave it here just in case diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 3066af0e50..1b53f64222 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -2,16 +2,19 @@ import copy import itertools -from typing import Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints +from typing import Annotated, Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints import networkx as nx from pydantic import ( BaseModel, ConfigDict, + GetJsonSchemaHandler, field_validator, model_validator, ) from pydantic.fields import Field +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema # Importing * is bad karma but needed here for node detection from invokeai.app.invocations import * # noqa: F401 F403 @@ -277,12 +280,61 @@ class Graph(BaseModel): @field_validator("nodes", mode="plain") @classmethod def validate_nodes(cls, v: dict[str, Any]): + """Validates the nodes in the graph by retrieving a union of all node types and validating each node.""" + + # Invocations register themselves as their python modules are executed. The union of all invocations is + # constructed at runtime. We use pydantic to validate `Graph.nodes` using that union. + # + # It's possible that when `graph.py` is executed, not all invocation-containing modules will have executed. If + # we construct the invocation union as `graph.py` is executed, we may miss some invocations. Those missing + # invocations will cause a graph to fail if they are used. + # + # We can get around this by validating the nodes in the graph using a "plain" validator, which overrides the + # pydantic validation entirely. This allows us to validate the nodes using the union of invocations at runtime. + # + # This same pattern is used in `GraphExecutionState`. + nodes: dict[str, BaseInvocation] = {} typeadapter = BaseInvocation.get_typeadapter() for node_id, node in v.items(): nodes[node_id] = typeadapter.validate_python(node) return nodes + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: + # We use a "plain" validator to validate the nodes in the graph. Pydantic is unable to create a JSON Schema for + # fields that use "plain" validators, so we have to hack around this. Also, we need to add all invocations to + # the generated schema as options for the `nodes` field. + # + # The workaround is to create a new BaseModel that has the same fields as `Graph` but without the validator and + # with the invocation union as the type for the `nodes` field. Pydantic then generates the JSON Schema as + # expected. + # + # You might be tempted to do something like this: + # + # ```py + # cloned_model = create_model(cls.__name__, __base__=cls, nodes=...) + # delattr(cloned_model, "validate_nodes") + # cloned_model.model_rebuild(force=True) + # json_schema = handler(cloned_model.__pydantic_core_schema__) + # ``` + # + # Unfortunately, this does not work. Calling `handler` here results in infinite recursion as pydantic attempts + # to build the JSON Schema for the cloned model. Instead, we have to manually clone the model. + # + # This same pattern is used in `GraphExecutionState`. + + class Graph(BaseModel): + id: Optional[str] = Field(default=None, description="The id of this graph") + nodes: dict[ + str, Annotated[Union[tuple(BaseInvocation._invocation_classes)], Field(discriminator="type")] + ] = Field(description="The nodes in this graph") + edges: list[Edge] = Field(description="The connections between nodes and their fields in this graph") + + json_schema = handler(Graph.__pydantic_core_schema__) + json_schema = handler.resolve_ref_schema(json_schema) + return json_schema + def add_node(self, node: BaseInvocation) -> None: """Adds a node to a graph @@ -852,6 +904,9 @@ class GraphExecutionState(BaseModel): @field_validator("results", mode="plain") @classmethod def validate_results(cls, v: dict[str, BaseInvocationOutput]): + """Validates the results in the GES by retrieving a union of all output types and validating each result.""" + + # See the comment in `Graph.validate_nodes` for an explanation of this logic. results: dict[str, BaseInvocationOutput] = {} typeadapter = BaseInvocationOutput.get_typeadapter() for result_id, result in v.items(): @@ -864,6 +919,34 @@ class GraphExecutionState(BaseModel): v.validate_self() return v + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> JsonSchemaValue: + # See the comment in `Graph.__get_pydantic_json_schema__` for an explanation of this logic. + class GraphExecutionState(BaseModel): + """Tracks the state of a graph execution""" + + id: str = Field(description="The id of the execution state") + graph: Graph = Field(description="The graph being executed") + execution_graph: Graph = Field(description="The expanded graph of activated and executed nodes") + executed: set[str] = Field(description="The set of node ids that have been executed") + executed_history: list[str] = Field( + description="The list of node ids that have been executed, in order of execution" + ) + results: dict[ + str, Annotated[Union[tuple(BaseInvocationOutput._output_classes)], Field(discriminator="type")] + ] = Field(description="The results of node executions") + errors: dict[str, str] = Field(description="Errors raised when executing nodes") + prepared_source_mapping: dict[str, str] = Field( + description="The map of prepared nodes to original graph nodes" + ) + source_prepared_mapping: dict[str, set[str]] = Field( + description="The map of original graph nodes to prepared nodes" + ) + + json_schema = handler(GraphExecutionState.__pydantic_core_schema__) + json_schema = handler.resolve_ref_schema(json_schema) + return json_schema + model_config = ConfigDict( json_schema_extra={ "required": [ @@ -1260,8 +1343,3 @@ class LibraryGraph(BaseModel): ) return values - - -Graph.model_rebuild(force=True) -GraphInvocation.model_rebuild(force=True) -GraphExecutionState.model_rebuild(force=True) From 641d235102d786c7d3556e1403fbfc8102f6ef38 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:56:13 +1100 Subject: [PATCH 158/411] tidy(nodes): remove GraphInvocation `GraphInvocation` is a node that can contain a whole graph. It is removed for a number of reasons: 1. This feature was unused (the UI doesn't support it) and there is no plan for it to be used. The use-case it served is known in other node execution engines as "node groups" or "blocks" - a self-contained group of nodes, which has group inputs and outputs. This is a planned feature that will be handled client-side. 2. It adds substantial complexity to the graph processing logic. It's probably not enough to have a measurable performance impact but it does make it harder to work in the graph logic. 3. It allows for graphs to be recursive, and the improved invocations union handling does not play well with it. Actually, it works fine within `graph.py` but not in the tests for some reason. I do not understand why. There's probably a workaround, but I took this as encouragement to remove `GraphInvocation` from the app since we don't use it. --- invokeai/app/services/shared/graph.py | 292 +++++++------------------- tests/aa_nodes/test_invoker.py | 13 +- tests/aa_nodes/test_node_graph.py | 168 ++++++++------- tests/aa_nodes/test_session_queue.py | 41 ++-- 4 files changed, 178 insertions(+), 336 deletions(-) diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 1b53f64222..4df9f0c4b0 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -184,10 +184,6 @@ class NodeIdMismatchError(ValueError): pass -class InvalidSubGraphError(ValueError): - pass - - class CyclicalGraphError(ValueError): pass @@ -196,25 +192,6 @@ class UnknownGraphValidationError(ValueError): pass -# TODO: Create and use an Empty output? -@invocation_output("graph_output") -class GraphInvocationOutput(BaseInvocationOutput): - pass - - -# TODO: Fill this out and move to invocations -@invocation("graph", version="1.0.0") -class GraphInvocation(BaseInvocation): - """Execute a graph""" - - # TODO: figure out how to create a default here - graph: "Graph" = InputField(description="The graph to run", default=None) - - def invoke(self, context: InvocationContext) -> GraphInvocationOutput: - """Invoke with provided services and return outputs.""" - return GraphInvocationOutput() - - @invocation_output("iterate_output") class IterateInvocationOutput(BaseInvocationOutput): """Used to connect iteration outputs. Will be expanded to a specific output.""" @@ -346,41 +323,21 @@ class Graph(BaseModel): self.nodes[node.id] = node - def _get_graph_and_node(self, node_path: str) -> tuple["Graph", str]: - """Returns the graph and node id for a node path.""" - # Materialized graphs may have nodes at the top level - if node_path in self.nodes: - return (self, node_path) - - node_id = node_path if "." not in node_path else node_path[: node_path.index(".")] - if node_id not in self.nodes: - raise NodeNotFoundError(f"Node {node_path} not found in graph") - - node = self.nodes[node_id] - - if not isinstance(node, GraphInvocation): - # There's more node path left but this isn't a graph - failure - raise NodeNotFoundError("Node path terminated early at a non-graph node") - - return node.graph._get_graph_and_node(node_path[node_path.index(".") + 1 :]) - - def delete_node(self, node_path: str) -> None: + def delete_node(self, node_id: str) -> None: """Deletes a node from a graph""" try: - graph, node_id = self._get_graph_and_node(node_path) - # Delete edges for this node - input_edges = self._get_input_edges_and_graphs(node_path) - output_edges = self._get_output_edges_and_graphs(node_path) + input_edges = self._get_input_edges(node_id) + output_edges = self._get_output_edges(node_id) - for edge_graph, _, edge in input_edges: - edge_graph.delete_edge(edge) + for edge in input_edges: + self.delete_edge(edge) - for edge_graph, _, edge in output_edges: - edge_graph.delete_edge(edge) + for edge in output_edges: + self.delete_edge(edge) - del graph.nodes[node_id] + del self.nodes[node_id] except NodeNotFoundError: pass # Ignore, not doesn't exist (should this throw?) @@ -430,13 +387,6 @@ class Graph(BaseModel): if k != v.id: raise NodeIdMismatchError(f"Node ids must match, got {k} and {v.id}") - # Validate all subgraphs - for gn in (n for n in self.nodes.values() if isinstance(n, GraphInvocation)): - try: - gn.graph.validate_self() - except Exception as e: - raise InvalidSubGraphError(f"Subgraph {gn.id} is invalid") from e - # Validate that all edges match nodes and fields in the graph for edge in self.edges: source_node = self.nodes.get(edge.source.node_id, None) @@ -498,7 +448,6 @@ class Graph(BaseModel): except ( DuplicateNodeIdError, NodeIdMismatchError, - InvalidSubGraphError, NodeNotFoundError, NodeFieldNotFoundError, CyclicalGraphError, @@ -519,7 +468,7 @@ class Graph(BaseModel): def _validate_edge(self, edge: Edge): """Validates that a new edge doesn't create a cycle in the graph""" - # Validate that the nodes exist (edges may contain node paths, so we can't just check for nodes directly) + # Validate that the nodes exist try: from_node = self.get_node(edge.source.node_id) to_node = self.get_node(edge.destination.node_id) @@ -586,171 +535,90 @@ class Graph(BaseModel): f"Collector input type does not match collector output type: {edge.source.node_id}.{edge.source.field} to {edge.destination.node_id}.{edge.destination.field}" ) - def has_node(self, node_path: str) -> bool: + def has_node(self, node_id: str) -> bool: """Determines whether or not a node exists in the graph.""" try: - n = self.get_node(node_path) - if n is not None: - return True - else: - return False + _ = self.get_node(node_id) + return True except NodeNotFoundError: return False - def get_node(self, node_path: str) -> BaseInvocation: - """Gets a node from the graph using a node path.""" - # Materialized graphs may have nodes at the top level - graph, node_id = self._get_graph_and_node(node_path) - return graph.nodes[node_id] + def get_node(self, node_id: str) -> BaseInvocation: + """Gets a node from the graph.""" + try: + return self.nodes[node_id] + except KeyError as e: + raise NodeNotFoundError(f"Node {node_id} not found in graph") from e - def _get_node_path(self, node_id: str, prefix: Optional[str] = None) -> str: - return node_id if prefix is None or prefix == "" else f"{prefix}.{node_id}" - - def update_node(self, node_path: str, new_node: BaseInvocation) -> None: + def update_node(self, node_id: str, new_node: BaseInvocation) -> None: """Updates a node in the graph.""" - graph, node_id = self._get_graph_and_node(node_path) - node = graph.nodes[node_id] + node = self.nodes[node_id] # Ensure the node type matches the new node if type(node) is not type(new_node): - raise TypeError(f"Node {node_path} is type {type(node)} but new node is type {type(new_node)}") + raise TypeError(f"Node {node_id} is type {type(node)} but new node is type {type(new_node)}") # Ensure the new id is either the same or is not in the graph - prefix = None if "." not in node_path else node_path[: node_path.rindex(".")] - new_path = self._get_node_path(new_node.id, prefix=prefix) - if new_node.id != node.id and self.has_node(new_path): - raise NodeAlreadyInGraphError("Node with id {new_node.id} already exists in graph") + if new_node.id != node.id and self.has_node(new_node.id): + raise NodeAlreadyInGraphError(f"Node with id {new_node.id} already exists in graph") # Set the new node in the graph - graph.nodes[new_node.id] = new_node + self.nodes[new_node.id] = new_node if new_node.id != node.id: - input_edges = self._get_input_edges_and_graphs(node_path) - output_edges = self._get_output_edges_and_graphs(node_path) + input_edges = self._get_input_edges(node_id) + output_edges = self._get_output_edges(node_id) # Delete node and all edges - graph.delete_node(node_path) + self.delete_node(node_id) # Create new edges for each input and output - for graph, _, edge in input_edges: - # Remove the graph prefix from the node path - new_graph_node_path = ( - new_node.id - if "." not in edge.destination.node_id - else f'{edge.destination.node_id[edge.destination.node_id.rindex("."):]}.{new_node.id}' - ) - graph.add_edge( + for edge in input_edges: + self.add_edge( Edge( source=edge.source, - destination=EdgeConnection(node_id=new_graph_node_path, field=edge.destination.field), + destination=EdgeConnection(node_id=new_node.id, field=edge.destination.field), ) ) - for graph, _, edge in output_edges: - # Remove the graph prefix from the node path - new_graph_node_path = ( - new_node.id - if "." not in edge.source.node_id - else f'{edge.source.node_id[edge.source.node_id.rindex("."):]}.{new_node.id}' - ) - graph.add_edge( + for edge in output_edges: + self.add_edge( Edge( - source=EdgeConnection(node_id=new_graph_node_path, field=edge.source.field), + source=EdgeConnection(node_id=new_node.id, field=edge.source.field), destination=edge.destination, ) ) - def _get_input_edges(self, node_path: str, field: Optional[str] = None) -> list[Edge]: - """Gets all input edges for a node""" - edges = self._get_input_edges_and_graphs(node_path) + def _get_input_edges(self, node_id: str, field: Optional[str] = None) -> list[Edge]: + """Gets all input edges for a node. If field is provided, only edges to that field are returned.""" - # Filter to edges that match the field - filtered_edges = (e for e in edges if field is None or e[2].destination.field == field) + edges = [e for e in self.edges if e.destination.node_id == node_id] - # Create full node paths for each edge - return [ - Edge( - source=EdgeConnection( - node_id=self._get_node_path(e.source.node_id, prefix=prefix), - field=e.source.field, - ), - destination=EdgeConnection( - node_id=self._get_node_path(e.destination.node_id, prefix=prefix), - field=e.destination.field, - ), - ) - for _, prefix, e in filtered_edges - ] + if field is None: + return edges - def _get_input_edges_and_graphs( - self, node_path: str, prefix: Optional[str] = None - ) -> list[tuple["Graph", Union[str, None], Edge]]: - """Gets all input edges for a node along with the graph they are in and the graph's path""" - edges = [] + filtered_edges = [e for e in edges if e.destination.field == field] - # Return any input edges that appear in this graph - edges.extend([(self, prefix, e) for e in self.edges if e.destination.node_id == node_path]) + return filtered_edges - node_id = node_path if "." not in node_path else node_path[: node_path.index(".")] - node = self.nodes[node_id] + def _get_output_edges(self, node_id: str, field: Optional[str] = None) -> list[Edge]: + """Gets all output edges for a node. If field is provided, only edges from that field are returned.""" + edges = [e for e in self.edges if e.source.node_id == node_id] - if isinstance(node, GraphInvocation): - graph = node.graph - graph_path = node.id if prefix is None or prefix == "" else self._get_node_path(node.id, prefix=prefix) - graph_edges = graph._get_input_edges_and_graphs(node_path[(len(node_id) + 1) :], prefix=graph_path) - edges.extend(graph_edges) + if field is None: + return edges - return edges + filtered_edges = [e for e in edges if e.source.field == field] - def _get_output_edges(self, node_path: str, field: str) -> list[Edge]: - """Gets all output edges for a node""" - edges = self._get_output_edges_and_graphs(node_path) - - # Filter to edges that match the field - filtered_edges = (e for e in edges if e[2].source.field == field) - - # Create full node paths for each edge - return [ - Edge( - source=EdgeConnection( - node_id=self._get_node_path(e.source.node_id, prefix=prefix), - field=e.source.field, - ), - destination=EdgeConnection( - node_id=self._get_node_path(e.destination.node_id, prefix=prefix), - field=e.destination.field, - ), - ) - for _, prefix, e in filtered_edges - ] - - def _get_output_edges_and_graphs( - self, node_path: str, prefix: Optional[str] = None - ) -> list[tuple["Graph", Union[str, None], Edge]]: - """Gets all output edges for a node along with the graph they are in and the graph's path""" - edges = [] - - # Return any input edges that appear in this graph - edges.extend([(self, prefix, e) for e in self.edges if e.source.node_id == node_path]) - - node_id = node_path if "." not in node_path else node_path[: node_path.index(".")] - node = self.nodes[node_id] - - if isinstance(node, GraphInvocation): - graph = node.graph - graph_path = node.id if prefix is None or prefix == "" else self._get_node_path(node.id, prefix=prefix) - graph_edges = graph._get_output_edges_and_graphs(node_path[(len(node_id) + 1) :], prefix=graph_path) - edges.extend(graph_edges) - - return edges + return filtered_edges def _is_iterator_connection_valid( self, - node_path: str, + node_id: str, new_input: Optional[EdgeConnection] = None, new_output: Optional[EdgeConnection] = None, ) -> bool: - inputs = [e.source for e in self._get_input_edges(node_path, "collection")] - outputs = [e.destination for e in self._get_output_edges(node_path, "item")] + inputs = [e.source for e in self._get_input_edges(node_id, "collection")] + outputs = [e.destination for e in self._get_output_edges(node_id, "item")] if new_input is not None: inputs.append(new_input) @@ -778,12 +646,12 @@ class Graph(BaseModel): def _is_collector_connection_valid( self, - node_path: str, + node_id: str, new_input: Optional[EdgeConnection] = None, new_output: Optional[EdgeConnection] = None, ) -> bool: - inputs = [e.source for e in self._get_input_edges(node_path, "item")] - outputs = [e.destination for e in self._get_output_edges(node_path, "collection")] + inputs = [e.source for e in self._get_input_edges(node_id, "item")] + outputs = [e.destination for e in self._get_output_edges(node_id, "collection")] if new_input is not None: inputs.append(new_input) @@ -839,27 +707,17 @@ class Graph(BaseModel): g.add_edges_from({(e.source.node_id, e.destination.node_id) for e in self.edges}) return g - def nx_graph_flat(self, nx_graph: Optional[nx.DiGraph] = None, prefix: Optional[str] = None) -> nx.DiGraph: + def nx_graph_flat(self, nx_graph: Optional[nx.DiGraph] = None) -> nx.DiGraph: """Returns a flattened NetworkX DiGraph, including all subgraphs (but not with iterations expanded)""" g = nx_graph or nx.DiGraph() # Add all nodes from this graph except graph/iteration nodes - g.add_nodes_from( - [ - self._get_node_path(n.id, prefix) - for n in self.nodes.values() - if not isinstance(n, GraphInvocation) and not isinstance(n, IterateInvocation) - ] - ) - - # Expand graph nodes - for sgn in (gn for gn in self.nodes.values() if isinstance(gn, GraphInvocation)): - g = sgn.graph.nx_graph_flat(g, self._get_node_path(sgn.id, prefix)) + g.add_nodes_from([n.id for n in self.nodes.values() if not isinstance(n, IterateInvocation)]) # TODO: figure out if iteration nodes need to be expanded unique_edges = {(e.source.node_id, e.destination.node_id) for e in self.edges} - g.add_edges_from([(self._get_node_path(e[0], prefix), self._get_node_path(e[1], prefix)) for e in unique_edges]) + g.add_edges_from([(e[0], e[1]) for e in unique_edges]) return g @@ -1017,17 +875,17 @@ class GraphExecutionState(BaseModel): """Returns true if the graph has any errors""" return len(self.errors) > 0 - def _create_execution_node(self, node_path: str, iteration_node_map: list[tuple[str, str]]) -> list[str]: + def _create_execution_node(self, node_id: str, iteration_node_map: list[tuple[str, str]]) -> list[str]: """Prepares an iteration node and connects all edges, returning the new node id""" - node = self.graph.get_node(node_path) + node = self.graph.get_node(node_id) self_iteration_count = -1 # If this is an iterator node, we must create a copy for each iteration if isinstance(node, IterateInvocation): # Get input collection edge (should error if there are no inputs) - input_collection_edge = next(iter(self.graph._get_input_edges(node_path, "collection"))) + input_collection_edge = next(iter(self.graph._get_input_edges(node_id, "collection"))) input_collection_prepared_node_id = next( n[1] for n in iteration_node_map if n[0] == input_collection_edge.source.node_id ) @@ -1041,7 +899,7 @@ class GraphExecutionState(BaseModel): return new_nodes # Get all input edges - input_edges = self.graph._get_input_edges(node_path) + input_edges = self.graph._get_input_edges(node_id) # Create new edges for this iteration # For collect nodes, this may contain multiple inputs to the same field @@ -1068,10 +926,10 @@ class GraphExecutionState(BaseModel): # Add to execution graph self.execution_graph.add_node(new_node) - self.prepared_source_mapping[new_node.id] = node_path - if node_path not in self.source_prepared_mapping: - self.source_prepared_mapping[node_path] = set() - self.source_prepared_mapping[node_path].add(new_node.id) + self.prepared_source_mapping[new_node.id] = node_id + if node_id not in self.source_prepared_mapping: + self.source_prepared_mapping[node_id] = set() + self.source_prepared_mapping[node_id].add(new_node.id) # Add new edges to execution graph for edge in new_edges: @@ -1175,13 +1033,13 @@ class GraphExecutionState(BaseModel): def _get_iteration_node( self, - source_node_path: str, + source_node_id: str, graph: nx.DiGraph, execution_graph: nx.DiGraph, prepared_iterator_nodes: list[str], ) -> Optional[str]: """Gets the prepared version of the specified source node that matches every iteration specified""" - prepared_nodes = self.source_prepared_mapping[source_node_path] + prepared_nodes = self.source_prepared_mapping[source_node_id] if len(prepared_nodes) == 1: return next(iter(prepared_nodes)) @@ -1192,7 +1050,7 @@ class GraphExecutionState(BaseModel): # Filter to only iterator nodes that are a parent of the specified node, in tuple format (prepared, source) iterator_source_node_mapping = [(n, self.prepared_source_mapping[n]) for n in prepared_iterator_nodes] - parent_iterators = [itn for itn in iterator_source_node_mapping if nx.has_path(graph, itn[1], source_node_path)] + parent_iterators = [itn for itn in iterator_source_node_mapping if nx.has_path(graph, itn[1], source_node_id)] return next( (n for n in prepared_nodes if all(nx.has_path(execution_graph, pit[0], n) for pit in parent_iterators)), @@ -1261,19 +1119,19 @@ class GraphExecutionState(BaseModel): def add_node(self, node: BaseInvocation) -> None: self.graph.add_node(node) - def update_node(self, node_path: str, new_node: BaseInvocation) -> None: - if not self._is_node_updatable(node_path): + def update_node(self, node_id: str, new_node: BaseInvocation) -> None: + if not self._is_node_updatable(node_id): raise NodeAlreadyExecutedError( - f"Node {node_path} has already been prepared or executed and cannot be updated" + f"Node {node_id} has already been prepared or executed and cannot be updated" ) - self.graph.update_node(node_path, new_node) + self.graph.update_node(node_id, new_node) - def delete_node(self, node_path: str) -> None: - if not self._is_node_updatable(node_path): + def delete_node(self, node_id: str) -> None: + if not self._is_node_updatable(node_id): raise NodeAlreadyExecutedError( - f"Node {node_path} has already been prepared or executed and cannot be deleted" + f"Node {node_id} has already been prepared or executed and cannot be deleted" ) - self.graph.delete_node(node_path) + self.graph.delete_node(node_id) def add_edge(self, edge: Edge) -> None: if not self._is_node_updatable(edge.destination.node_id): diff --git a/tests/aa_nodes/test_invoker.py b/tests/aa_nodes/test_invoker.py index f67b5a2ac5..38fcf859a5 100644 --- a/tests/aa_nodes/test_invoker.py +++ b/tests/aa_nodes/test_invoker.py @@ -23,7 +23,7 @@ from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService from invokeai.app.services.invoker import Invoker from invokeai.app.services.session_queue.session_queue_common import DEFAULT_QUEUE_ID -from invokeai.app.services.shared.graph import Graph, GraphExecutionState, GraphInvocation +from invokeai.app.services.shared.graph import Graph, GraphExecutionState @pytest.fixture @@ -35,17 +35,6 @@ def simple_graph(): return g -@pytest.fixture -def graph_with_subgraph(): - sub_g = Graph() - sub_g.add_node(PromptTestInvocation(id="1", prompt="Banana sushi")) - sub_g.add_node(TextToImageTestInvocation(id="2")) - sub_g.add_edge(create_edge("1", "prompt", "2", "prompt")) - g = Graph() - g.add_node(GraphInvocation(id="1", graph=sub_g)) - return g - - # This must be defined here to avoid issues with the dynamic creation of the union of all invocation types # Defining it in a separate module will cause the union to be incomplete, and pydantic will not validate # the test invocations. diff --git a/tests/aa_nodes/test_node_graph.py b/tests/aa_nodes/test_node_graph.py index 12a181f392..94682962ad 100644 --- a/tests/aa_nodes/test_node_graph.py +++ b/tests/aa_nodes/test_node_graph.py @@ -8,8 +8,6 @@ from invokeai.app.invocations.baseinvocation import ( invocation, invocation_output, ) -from invokeai.app.invocations.image import ShowImageInvocation -from invokeai.app.invocations.math import AddInvocation, SubtractInvocation from invokeai.app.invocations.primitives import ( FloatCollectionInvocation, FloatInvocation, @@ -17,13 +15,11 @@ from invokeai.app.invocations.primitives import ( StringInvocation, ) from invokeai.app.invocations.upscale import ESRGANInvocation -from invokeai.app.services.shared.default_graphs import create_text_to_image from invokeai.app.services.shared.graph import ( CollectInvocation, Edge, EdgeConnection, Graph, - GraphInvocation, InvalidEdgeError, IterateInvocation, NodeAlreadyInGraphError, @@ -425,19 +421,19 @@ def test_graph_invalid_if_edges_reference_missing_nodes(): assert g.is_valid() is False -def test_graph_invalid_if_subgraph_invalid(): - g = Graph() - n1 = GraphInvocation(id="1") - n1.graph = Graph() +# def test_graph_invalid_if_subgraph_invalid(): +# g = Graph() +# n1 = GraphInvocation(id="1") +# n1.graph = Graph() - n1_1 = TextToImageTestInvocation(id="2", prompt="Banana sushi") - n1.graph.nodes[n1_1.id] = n1_1 - e1 = create_edge("1", "image", "2", "image") - n1.graph.edges.append(e1) +# n1_1 = TextToImageTestInvocation(id="2", prompt="Banana sushi") +# n1.graph.nodes[n1_1.id] = n1_1 +# e1 = create_edge("1", "image", "2", "image") +# n1.graph.edges.append(e1) - g.nodes[n1.id] = n1 +# g.nodes[n1.id] = n1 - assert g.is_valid() is False +# assert g.is_valid() is False def test_graph_invalid_if_has_cycle(): @@ -466,108 +462,108 @@ def test_graph_invalid_with_invalid_connection(): assert g.is_valid() is False -# TODO: Subgraph operations -def test_graph_gets_subgraph_node(): - g = Graph() - n1 = GraphInvocation(id="1") - n1.graph = Graph() +# # TODO: Subgraph operations +# def test_graph_gets_subgraph_node(): +# g = Graph() +# n1 = GraphInvocation(id="1") +# n1.graph = Graph() - n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") - n1.graph.add_node(n1_1) +# n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") +# n1.graph.add_node(n1_1) - g.add_node(n1) +# g.add_node(n1) - result = g.get_node("1.1") +# result = g.get_node("1.1") - assert result is not None - assert result.id == "1" - assert result == n1_1 +# assert result is not None +# assert result.id == "1" +# assert result == n1_1 -def test_graph_expands_subgraph(): - g = Graph() - n1 = GraphInvocation(id="1") - n1.graph = Graph() +# def test_graph_expands_subgraph(): +# g = Graph() +# n1 = GraphInvocation(id="1") +# n1.graph = Graph() - n1_1 = AddInvocation(id="1", a=1, b=2) - n1_2 = SubtractInvocation(id="2", b=3) - n1.graph.add_node(n1_1) - n1.graph.add_node(n1_2) - n1.graph.add_edge(create_edge("1", "value", "2", "a")) +# n1_1 = AddInvocation(id="1", a=1, b=2) +# n1_2 = SubtractInvocation(id="2", b=3) +# n1.graph.add_node(n1_1) +# n1.graph.add_node(n1_2) +# n1.graph.add_edge(create_edge("1", "value", "2", "a")) - g.add_node(n1) +# g.add_node(n1) - n2 = AddInvocation(id="2", b=5) - g.add_node(n2) - g.add_edge(create_edge("1.2", "value", "2", "a")) +# n2 = AddInvocation(id="2", b=5) +# g.add_node(n2) +# g.add_edge(create_edge("1.2", "value", "2", "a")) - dg = g.nx_graph_flat() - assert set(dg.nodes) == {"1.1", "1.2", "2"} - assert set(dg.edges) == {("1.1", "1.2"), ("1.2", "2")} +# dg = g.nx_graph_flat() +# assert set(dg.nodes) == {"1.1", "1.2", "2"} +# assert set(dg.edges) == {("1.1", "1.2"), ("1.2", "2")} -def test_graph_subgraph_t2i(): - g = Graph() - n1 = GraphInvocation(id="1") +# def test_graph_subgraph_t2i(): +# g = Graph() +# n1 = GraphInvocation(id="1") - # Get text to image default graph - lg = create_text_to_image() - n1.graph = lg.graph +# # Get text to image default graph +# lg = create_text_to_image() +# n1.graph = lg.graph - g.add_node(n1) +# g.add_node(n1) - n2 = IntegerInvocation(id="2", value=512) - n3 = IntegerInvocation(id="3", value=256) +# n2 = IntegerInvocation(id="2", value=512) +# n3 = IntegerInvocation(id="3", value=256) - g.add_node(n2) - g.add_node(n3) +# g.add_node(n2) +# g.add_node(n3) - g.add_edge(create_edge("2", "value", "1.width", "value")) - g.add_edge(create_edge("3", "value", "1.height", "value")) +# g.add_edge(create_edge("2", "value", "1.width", "value")) +# g.add_edge(create_edge("3", "value", "1.height", "value")) - n4 = ShowImageInvocation(id="4") - g.add_node(n4) - g.add_edge(create_edge("1.8", "image", "4", "image")) +# n4 = ShowImageInvocation(id="4") +# g.add_node(n4) +# g.add_edge(create_edge("1.8", "image", "4", "image")) - # Validate - dg = g.nx_graph_flat() - assert set(dg.nodes) == {"1.width", "1.height", "1.seed", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "2", "3", "4"} - expected_edges = [(f"1.{e.source.node_id}", f"1.{e.destination.node_id}") for e in lg.graph.edges] - expected_edges.extend([("2", "1.width"), ("3", "1.height"), ("1.8", "4")]) - print(expected_edges) - print(list(dg.edges)) - assert set(dg.edges) == set(expected_edges) +# # Validate +# dg = g.nx_graph_flat() +# assert set(dg.nodes) == {"1.width", "1.height", "1.seed", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "2", "3", "4"} +# expected_edges = [(f"1.{e.source.node_id}", f"1.{e.destination.node_id}") for e in lg.graph.edges] +# expected_edges.extend([("2", "1.width"), ("3", "1.height"), ("1.8", "4")]) +# print(expected_edges) +# print(list(dg.edges)) +# assert set(dg.edges) == set(expected_edges) -def test_graph_fails_to_get_missing_subgraph_node(): - g = Graph() - n1 = GraphInvocation(id="1") - n1.graph = Graph() +# def test_graph_fails_to_get_missing_subgraph_node(): +# g = Graph() +# n1 = GraphInvocation(id="1") +# n1.graph = Graph() - n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") - n1.graph.add_node(n1_1) +# n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") +# n1.graph.add_node(n1_1) - g.add_node(n1) +# g.add_node(n1) - with pytest.raises(NodeNotFoundError): - _ = g.get_node("1.2") +# with pytest.raises(NodeNotFoundError): +# _ = g.get_node("1.2") -def test_graph_fails_to_enumerate_non_subgraph_node(): - g = Graph() - n1 = GraphInvocation(id="1") - n1.graph = Graph() +# def test_graph_fails_to_enumerate_non_subgraph_node(): +# g = Graph() +# n1 = GraphInvocation(id="1") +# n1.graph = Graph() - n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") - n1.graph.add_node(n1_1) +# n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") +# n1.graph.add_node(n1_1) - g.add_node(n1) +# g.add_node(n1) - n2 = ESRGANInvocation(id="2") - g.add_node(n2) +# n2 = ESRGANInvocation(id="2") +# g.add_node(n2) - with pytest.raises(NodeNotFoundError): - _ = g.get_node("2.1") +# with pytest.raises(NodeNotFoundError): +# _ = g.get_node("2.1") def test_graph_gets_networkx_graph(): diff --git a/tests/aa_nodes/test_session_queue.py b/tests/aa_nodes/test_session_queue.py index b15bb9df36..bfe6444de8 100644 --- a/tests/aa_nodes/test_session_queue.py +++ b/tests/aa_nodes/test_session_queue.py @@ -8,10 +8,9 @@ from invokeai.app.services.session_queue.session_queue_common import ( NodeFieldValue, calc_session_count, create_session_nfv_tuples, - populate_graph, prepare_values_to_insert, ) -from invokeai.app.services.shared.graph import Graph, GraphExecutionState, GraphInvocation +from invokeai.app.services.shared.graph import Graph, GraphExecutionState from tests.aa_nodes.test_nodes import PromptTestInvocation @@ -39,28 +38,28 @@ def batch_graph() -> Graph: return g -def test_populate_graph_with_subgraph(): - g1 = Graph() - g1.add_node(PromptTestInvocation(id="1", prompt="Banana sushi")) - g1.add_node(PromptTestInvocation(id="2", prompt="Banana sushi")) - n1 = PromptTestInvocation(id="1", prompt="Banana snake") - subgraph = Graph() - subgraph.add_node(n1) - g1.add_node(GraphInvocation(id="3", graph=subgraph)) +# def test_populate_graph_with_subgraph(): +# g1 = Graph() +# g1.add_node(PromptTestInvocation(id="1", prompt="Banana sushi")) +# g1.add_node(PromptTestInvocation(id="2", prompt="Banana sushi")) +# n1 = PromptTestInvocation(id="1", prompt="Banana snake") +# subgraph = Graph() +# subgraph.add_node(n1) +# g1.add_node(GraphInvocation(id="3", graph=subgraph)) - nfvs = [ - NodeFieldValue(node_path="1", field_name="prompt", value="Strawberry sushi"), - NodeFieldValue(node_path="2", field_name="prompt", value="Strawberry sunday"), - NodeFieldValue(node_path="3.1", field_name="prompt", value="Strawberry snake"), - ] +# nfvs = [ +# NodeFieldValue(node_path="1", field_name="prompt", value="Strawberry sushi"), +# NodeFieldValue(node_path="2", field_name="prompt", value="Strawberry sunday"), +# NodeFieldValue(node_path="3.1", field_name="prompt", value="Strawberry snake"), +# ] - g2 = populate_graph(g1, nfvs) +# g2 = populate_graph(g1, nfvs) - # do not mutate g1 - assert g1 is not g2 - assert g2.get_node("1").prompt == "Strawberry sushi" - assert g2.get_node("2").prompt == "Strawberry sunday" - assert g2.get_node("3.1").prompt == "Strawberry snake" +# # do not mutate g1 +# assert g1 is not g2 +# assert g2.get_node("1").prompt == "Strawberry sushi" +# assert g2.get_node("2").prompt == "Strawberry sunday" +# assert g2.get_node("3.1").prompt == "Strawberry snake" def test_create_sessions_from_batch_with_runs(batch_data_collection, batch_graph): From 0b81703c9f5b570388fa1e09171897d16a15e3b0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:59:21 +1100 Subject: [PATCH 159/411] tidy(nodes): move node tests to parent dir Thanks to the resolution of the import vs union issue, we can put tests anywhere. --- tests/aa_nodes/__init__.py | 0 tests/{aa_nodes => }/test_graph_execution_state.py | 0 tests/{aa_nodes => }/test_invoker.py | 0 tests/{aa_nodes => }/test_node_graph.py | 0 tests/{aa_nodes => }/test_nodes.py | 0 tests/{aa_nodes => }/test_session_queue.py | 3 ++- 6 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 tests/aa_nodes/__init__.py rename tests/{aa_nodes => }/test_graph_execution_state.py (100%) rename tests/{aa_nodes => }/test_invoker.py (100%) rename tests/{aa_nodes => }/test_node_graph.py (100%) rename tests/{aa_nodes => }/test_nodes.py (100%) rename tests/{aa_nodes => }/test_session_queue.py (99%) diff --git a/tests/aa_nodes/__init__.py b/tests/aa_nodes/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/aa_nodes/test_graph_execution_state.py b/tests/test_graph_execution_state.py similarity index 100% rename from tests/aa_nodes/test_graph_execution_state.py rename to tests/test_graph_execution_state.py diff --git a/tests/aa_nodes/test_invoker.py b/tests/test_invoker.py similarity index 100% rename from tests/aa_nodes/test_invoker.py rename to tests/test_invoker.py diff --git a/tests/aa_nodes/test_node_graph.py b/tests/test_node_graph.py similarity index 100% rename from tests/aa_nodes/test_node_graph.py rename to tests/test_node_graph.py diff --git a/tests/aa_nodes/test_nodes.py b/tests/test_nodes.py similarity index 100% rename from tests/aa_nodes/test_nodes.py rename to tests/test_nodes.py diff --git a/tests/aa_nodes/test_session_queue.py b/tests/test_session_queue.py similarity index 99% rename from tests/aa_nodes/test_session_queue.py rename to tests/test_session_queue.py index bfe6444de8..48b980539c 100644 --- a/tests/aa_nodes/test_session_queue.py +++ b/tests/test_session_queue.py @@ -11,7 +11,8 @@ from invokeai.app.services.session_queue.session_queue_common import ( prepare_values_to_insert, ) from invokeai.app.services.shared.graph import Graph, GraphExecutionState -from tests.aa_nodes.test_nodes import PromptTestInvocation + +from .test_nodes import PromptTestInvocation @pytest.fixture From e93bd1539252b735739b624539784179df506c23 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:00:58 +1100 Subject: [PATCH 160/411] tidy(nodes): remove LibraryGraphs The workflow library supersedes this unused feature. --- .../app/services/shared/default_graphs.py | 92 ------------------- invokeai/app/services/shared/graph.py | 56 ----------- 2 files changed, 148 deletions(-) delete mode 100644 invokeai/app/services/shared/default_graphs.py diff --git a/invokeai/app/services/shared/default_graphs.py b/invokeai/app/services/shared/default_graphs.py deleted file mode 100644 index 7e62c6d0a1..0000000000 --- a/invokeai/app/services/shared/default_graphs.py +++ /dev/null @@ -1,92 +0,0 @@ -from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC - -from ...invocations.compel import CompelInvocation -from ...invocations.image import ImageNSFWBlurInvocation -from ...invocations.latent import DenoiseLatentsInvocation, LatentsToImageInvocation -from ...invocations.noise import NoiseInvocation -from ...invocations.primitives import IntegerInvocation -from .graph import Edge, EdgeConnection, ExposedNodeInput, ExposedNodeOutput, Graph, LibraryGraph - -default_text_to_image_graph_id = "539b2af5-2b4d-4d8c-8071-e54a3255fc74" - - -def create_text_to_image() -> LibraryGraph: - graph = Graph( - nodes={ - "width": IntegerInvocation(id="width", value=512), - "height": IntegerInvocation(id="height", value=512), - "seed": IntegerInvocation(id="seed", value=-1), - "3": NoiseInvocation(id="3"), - "4": CompelInvocation(id="4"), - "5": CompelInvocation(id="5"), - "6": DenoiseLatentsInvocation(id="6"), - "7": LatentsToImageInvocation(id="7"), - "8": ImageNSFWBlurInvocation(id="8"), - }, - edges=[ - Edge( - source=EdgeConnection(node_id="width", field="value"), - destination=EdgeConnection(node_id="3", field="width"), - ), - Edge( - source=EdgeConnection(node_id="height", field="value"), - destination=EdgeConnection(node_id="3", field="height"), - ), - Edge( - source=EdgeConnection(node_id="seed", field="value"), - destination=EdgeConnection(node_id="3", field="seed"), - ), - Edge( - source=EdgeConnection(node_id="3", field="noise"), - destination=EdgeConnection(node_id="6", field="noise"), - ), - Edge( - source=EdgeConnection(node_id="6", field="latents"), - destination=EdgeConnection(node_id="7", field="latents"), - ), - Edge( - source=EdgeConnection(node_id="4", field="conditioning"), - destination=EdgeConnection(node_id="6", field="positive_conditioning"), - ), - Edge( - source=EdgeConnection(node_id="5", field="conditioning"), - destination=EdgeConnection(node_id="6", field="negative_conditioning"), - ), - Edge( - source=EdgeConnection(node_id="7", field="image"), - destination=EdgeConnection(node_id="8", field="image"), - ), - ], - ) - return LibraryGraph( - id=default_text_to_image_graph_id, - name="t2i", - description="Converts text to an image", - graph=graph, - exposed_inputs=[ - ExposedNodeInput(node_path="4", field="prompt", alias="positive_prompt"), - ExposedNodeInput(node_path="5", field="prompt", alias="negative_prompt"), - ExposedNodeInput(node_path="width", field="value", alias="width"), - ExposedNodeInput(node_path="height", field="value", alias="height"), - ExposedNodeInput(node_path="seed", field="value", alias="seed"), - ], - exposed_outputs=[ExposedNodeOutput(node_path="8", field="image", alias="image")], - ) - - -def create_system_graphs(graph_library: ItemStorageABC[LibraryGraph]) -> list[LibraryGraph]: - """Creates the default system graphs, or adds new versions if the old ones don't match""" - - # TODO: Uncomment this when we are ready to fix this up to prevent breaking changes - graphs: list[LibraryGraph] = [] - - text_to_image = graph_library.get(default_text_to_image_graph_id) - - # TODO: Check if the graph is the same as the default one, and if not, update it - # if text_to_image is None: - text_to_image = create_text_to_image() - graph_library.set(text_to_image) - - graphs.append(text_to_image) - - return graphs diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 4df9f0c4b0..5380c2e795 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -10,7 +10,6 @@ from pydantic import ( ConfigDict, GetJsonSchemaHandler, field_validator, - model_validator, ) from pydantic.fields import Field from pydantic.json_schema import JsonSchemaValue @@ -1146,58 +1145,3 @@ class GraphExecutionState(BaseModel): f"Destination node {edge.destination.node_id} has already been prepared or executed and cannot have a source edge deleted" ) self.graph.delete_edge(edge) - - -class ExposedNodeInput(BaseModel): - node_path: str = Field(description="The node path to the node with the input") - field: str = Field(description="The field name of the input") - alias: str = Field(description="The alias of the input") - - -class ExposedNodeOutput(BaseModel): - node_path: str = Field(description="The node path to the node with the output") - field: str = Field(description="The field name of the output") - alias: str = Field(description="The alias of the output") - - -class LibraryGraph(BaseModel): - id: str = Field(description="The unique identifier for this library graph", default_factory=uuid_string) - graph: Graph = Field(description="The graph") - name: str = Field(description="The name of the graph") - description: str = Field(description="The description of the graph") - exposed_inputs: list[ExposedNodeInput] = Field(description="The inputs exposed by this graph", default_factory=list) - exposed_outputs: list[ExposedNodeOutput] = Field( - description="The outputs exposed by this graph", default_factory=list - ) - - @field_validator("exposed_inputs", "exposed_outputs") - def validate_exposed_aliases(cls, v: list[Union[ExposedNodeInput, ExposedNodeOutput]]): - if len(v) != len({i.alias for i in v}): - raise ValueError("Duplicate exposed alias") - return v - - @model_validator(mode="after") - def validate_exposed_nodes(cls, values): - graph = values.graph - - # Validate exposed inputs - for exposed_input in values.exposed_inputs: - if not graph.has_node(exposed_input.node_path): - raise ValueError(f"Exposed input node {exposed_input.node_path} does not exist") - node = graph.get_node(exposed_input.node_path) - if get_input_field(node, exposed_input.field) is None: - raise ValueError( - f"Exposed input field {exposed_input.field} does not exist on node {exposed_input.node_path}" - ) - - # Validate exposed outputs - for exposed_output in values.exposed_outputs: - if not graph.has_node(exposed_output.node_path): - raise ValueError(f"Exposed output node {exposed_output.node_path} does not exist") - node = graph.get_node(exposed_output.node_path) - if get_output_field(node, exposed_output.field) is None: - raise ValueError( - f"Exposed output field {exposed_output.field} does not exist on node {exposed_output.node_path}" - ) - - return values From 7e71effa170e706bd136f9f8bed7b813679e9186 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:02:37 +1100 Subject: [PATCH 161/411] tidy(nodes): remove no-op model_config Because we now customize the JSON Schema creation for GraphExecutionState, the model_config did nothing. --- invokeai/app/services/shared/graph.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 5380c2e795..e3941d9ca3 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -7,7 +7,6 @@ from typing import Annotated, Any, Optional, TypeVar, Union, get_args, get_origi import networkx as nx from pydantic import ( BaseModel, - ConfigDict, GetJsonSchemaHandler, field_validator, ) @@ -804,22 +803,6 @@ class GraphExecutionState(BaseModel): json_schema = handler.resolve_ref_schema(json_schema) return json_schema - model_config = ConfigDict( - json_schema_extra={ - "required": [ - "id", - "graph", - "execution_graph", - "executed", - "executed_history", - "results", - "errors", - "prepared_source_mapping", - "source_prepared_mapping", - ] - } - ) - def next(self) -> Optional[BaseInvocation]: """Gets the next node ready to execute.""" From 67daa127e3393c2f4514e0712b496533268f0d09 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:02:51 +1100 Subject: [PATCH 162/411] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 165 ++++++++---------- 1 file changed, 72 insertions(+), 93 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 40fc262be2..47a257ffe6 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -2011,8 +2011,9 @@ export type components = { /** * CLIP * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + * @default null */ - clip?: components["schemas"]["ClipField"] | null; + clip: components["schemas"]["ClipField"] | null; /** * type * @default clip_skip_output @@ -3264,6 +3265,7 @@ export type components = { /** * Masked Latents Name * @description The name of the masked image latents + * @default null */ masked_latents_name?: string | null; }; @@ -4211,14 +4213,14 @@ export type components = { * Nodes * @description The nodes in this graph */ - nodes?: { - [key: string]: components["schemas"]["ControlNetInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["GraphInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CvInpaintInvocation"]; + nodes: { + [key: string]: components["schemas"]["ImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CompelInvocation"]; }; /** * Edges * @description The connections between nodes and their fields in this graph */ - edges?: components["schemas"]["Edge"][]; + edges: components["schemas"]["Edge"][]; }; /** * GraphExecutionState @@ -4249,7 +4251,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["SchedulerOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["GraphInvocationOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["String2Output"] | components["schemas"]["IntegerOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["IterateInvocationOutput"]; + [key: string]: components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ControlOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["LatentsCollectionOutput"]; }; /** * Errors @@ -4273,46 +4275,6 @@ export type components = { [key: string]: string[]; }; }; - /** - * GraphInvocation - * @description Execute a graph - */ - GraphInvocation: { - /** - * Id - * @description The id of this instance of an invocation. Must be unique among all instances of invocations. - */ - id: string; - /** - * Is Intermediate - * @description Whether or not this is an intermediate invocation. - * @default false - */ - is_intermediate?: boolean; - /** - * Use Cache - * @description Whether or not to use the cache - * @default true - */ - use_cache?: boolean; - /** @description The graph to run */ - graph?: components["schemas"]["Graph"]; - /** - * type - * @default graph - * @constant - */ - type: "graph"; - }; - /** GraphInvocationOutput */ - GraphInvocationOutput: { - /** - * type - * @default graph_output - * @constant - */ - type: "graph_output"; - }; /** * HFModelSource * @description A HuggingFace repo_id with optional variant, sub-folder and access token. @@ -6218,6 +6180,7 @@ export type components = { /** * Seed * @description Seed used to generate this latents + * @default null */ seed?: number | null; }; @@ -6631,7 +6594,10 @@ export type components = { * @description Key of model as returned by ModelRecordServiceBase.get_model() */ key: string; - /** @description Info to load submodel */ + /** + * @description Info to load submodel + * @default null + */ submodel_type?: components["schemas"]["SubModelType"] | null; /** * Weight @@ -6697,13 +6663,15 @@ export type components = { /** * UNet * @description UNet (scheduler, LoRAs) + * @default null */ - unet?: components["schemas"]["UNetField"] | null; + unet: components["schemas"]["UNetField"] | null; /** * CLIP * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + * @default null */ - clip?: components["schemas"]["ClipField"] | null; + clip: components["schemas"]["ClipField"] | null; /** * type * @default lora_loader_output @@ -7420,7 +7388,10 @@ export type components = { * @description Key of model as returned by ModelRecordServiceBase.get_model() */ key: string; - /** @description Info to load submodel */ + /** + * @description Info to load submodel + * @default null + */ submodel_type?: components["schemas"]["SubModelType"] | null; }; /** @@ -8794,18 +8765,21 @@ export type components = { /** * UNet * @description UNet (scheduler, LoRAs) + * @default null */ - unet?: components["schemas"]["UNetField"] | null; + unet: components["schemas"]["UNetField"] | null; /** * CLIP 1 * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + * @default null */ - clip?: components["schemas"]["ClipField"] | null; + clip: components["schemas"]["ClipField"] | null; /** * CLIP 2 * @description CLIP (tokenizer, text encoder, LoRAs) and skipped layer count + * @default null */ - clip2?: components["schemas"]["ClipField"] | null; + clip2: components["schemas"]["ClipField"] | null; /** * type * @default sdxl_lora_loader_output @@ -9202,13 +9176,15 @@ export type components = { /** * UNet * @description UNet (scheduler, LoRAs) + * @default null */ - unet?: components["schemas"]["UNetField"] | null; + unet: components["schemas"]["UNetField"] | null; /** * VAE * @description VAE + * @default null */ - vae?: components["schemas"]["VaeField"] | null; + vae: components["schemas"]["VaeField"] | null; /** * type * @default seamless_output @@ -10397,7 +10373,10 @@ export type components = { * @description Axes("x" and "y") to which apply seamless */ seamless_axes?: string[]; - /** @description FreeU configuration */ + /** + * @description FreeU configuration + * @default null + */ freeu_config?: components["schemas"]["FreeUConfig"] | null; }; /** @@ -11112,48 +11091,12 @@ export type components = { * @enum {string} */ UIType: "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_MainModel" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; - /** - * VaeModelFormat - * @description An enumeration. - * @enum {string} - */ - VaeModelFormat: "checkpoint" | "diffusers"; - /** - * T2IAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - T2IAdapterModelFormat: "diffusers"; - /** - * StableDiffusionXLModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion1ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusionOnnxModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; /** * ControlNetModelFormat * @description An enumeration. * @enum {string} */ ControlNetModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** * LoRAModelFormat * @description An enumeration. @@ -11161,17 +11104,53 @@ export type components = { */ LoRAModelFormat: "lycoris" | "diffusers"; /** - * CLIPVisionModelFormat + * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ - CLIPVisionModelFormat: "diffusers"; + StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; /** * IPAdapterModelFormat * @description An enumeration. * @enum {string} */ IPAdapterModelFormat: "invokeai"; + /** + * T2IAdapterModelFormat + * @description An enumeration. + * @enum {string} + */ + T2IAdapterModelFormat: "diffusers"; + /** + * StableDiffusion1ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; + /** + * CLIPVisionModelFormat + * @description An enumeration. + * @enum {string} + */ + CLIPVisionModelFormat: "diffusers"; + /** + * StableDiffusionOnnxModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionOnnxModelFormat: "olive" | "onnx"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + /** + * VaeModelFormat + * @description An enumeration. + * @enum {string} + */ + VaeModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; From da9991e36130875968c3edbc407656d11ec4ab7c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:08:22 +1100 Subject: [PATCH 163/411] tidy(nodes): remove commented tests --- tests/test_node_graph.py | 119 ------------------------------------ tests/test_session_queue.py | 24 -------- 2 files changed, 143 deletions(-) diff --git a/tests/test_node_graph.py b/tests/test_node_graph.py index 94682962ad..87a4948af4 100644 --- a/tests/test_node_graph.py +++ b/tests/test_node_graph.py @@ -421,21 +421,6 @@ def test_graph_invalid_if_edges_reference_missing_nodes(): assert g.is_valid() is False -# def test_graph_invalid_if_subgraph_invalid(): -# g = Graph() -# n1 = GraphInvocation(id="1") -# n1.graph = Graph() - -# n1_1 = TextToImageTestInvocation(id="2", prompt="Banana sushi") -# n1.graph.nodes[n1_1.id] = n1_1 -# e1 = create_edge("1", "image", "2", "image") -# n1.graph.edges.append(e1) - -# g.nodes[n1.id] = n1 - -# assert g.is_valid() is False - - def test_graph_invalid_if_has_cycle(): g = Graph() n1 = ESRGANInvocation(id="1") @@ -462,110 +447,6 @@ def test_graph_invalid_with_invalid_connection(): assert g.is_valid() is False -# # TODO: Subgraph operations -# def test_graph_gets_subgraph_node(): -# g = Graph() -# n1 = GraphInvocation(id="1") -# n1.graph = Graph() - -# n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") -# n1.graph.add_node(n1_1) - -# g.add_node(n1) - -# result = g.get_node("1.1") - -# assert result is not None -# assert result.id == "1" -# assert result == n1_1 - - -# def test_graph_expands_subgraph(): -# g = Graph() -# n1 = GraphInvocation(id="1") -# n1.graph = Graph() - -# n1_1 = AddInvocation(id="1", a=1, b=2) -# n1_2 = SubtractInvocation(id="2", b=3) -# n1.graph.add_node(n1_1) -# n1.graph.add_node(n1_2) -# n1.graph.add_edge(create_edge("1", "value", "2", "a")) - -# g.add_node(n1) - -# n2 = AddInvocation(id="2", b=5) -# g.add_node(n2) -# g.add_edge(create_edge("1.2", "value", "2", "a")) - -# dg = g.nx_graph_flat() -# assert set(dg.nodes) == {"1.1", "1.2", "2"} -# assert set(dg.edges) == {("1.1", "1.2"), ("1.2", "2")} - - -# def test_graph_subgraph_t2i(): -# g = Graph() -# n1 = GraphInvocation(id="1") - -# # Get text to image default graph -# lg = create_text_to_image() -# n1.graph = lg.graph - -# g.add_node(n1) - -# n2 = IntegerInvocation(id="2", value=512) -# n3 = IntegerInvocation(id="3", value=256) - -# g.add_node(n2) -# g.add_node(n3) - -# g.add_edge(create_edge("2", "value", "1.width", "value")) -# g.add_edge(create_edge("3", "value", "1.height", "value")) - -# n4 = ShowImageInvocation(id="4") -# g.add_node(n4) -# g.add_edge(create_edge("1.8", "image", "4", "image")) - -# # Validate -# dg = g.nx_graph_flat() -# assert set(dg.nodes) == {"1.width", "1.height", "1.seed", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "2", "3", "4"} -# expected_edges = [(f"1.{e.source.node_id}", f"1.{e.destination.node_id}") for e in lg.graph.edges] -# expected_edges.extend([("2", "1.width"), ("3", "1.height"), ("1.8", "4")]) -# print(expected_edges) -# print(list(dg.edges)) -# assert set(dg.edges) == set(expected_edges) - - -# def test_graph_fails_to_get_missing_subgraph_node(): -# g = Graph() -# n1 = GraphInvocation(id="1") -# n1.graph = Graph() - -# n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") -# n1.graph.add_node(n1_1) - -# g.add_node(n1) - -# with pytest.raises(NodeNotFoundError): -# _ = g.get_node("1.2") - - -# def test_graph_fails_to_enumerate_non_subgraph_node(): -# g = Graph() -# n1 = GraphInvocation(id="1") -# n1.graph = Graph() - -# n1_1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") -# n1.graph.add_node(n1_1) - -# g.add_node(n1) - -# n2 = ESRGANInvocation(id="2") -# g.add_node(n2) - -# with pytest.raises(NodeNotFoundError): -# _ = g.get_node("2.1") - - def test_graph_gets_networkx_graph(): g = Graph() n1 = TextToImageTestInvocation(id="1", prompt="Banana sushi") diff --git a/tests/test_session_queue.py b/tests/test_session_queue.py index 48b980539c..bf26b9b002 100644 --- a/tests/test_session_queue.py +++ b/tests/test_session_queue.py @@ -39,30 +39,6 @@ def batch_graph() -> Graph: return g -# def test_populate_graph_with_subgraph(): -# g1 = Graph() -# g1.add_node(PromptTestInvocation(id="1", prompt="Banana sushi")) -# g1.add_node(PromptTestInvocation(id="2", prompt="Banana sushi")) -# n1 = PromptTestInvocation(id="1", prompt="Banana snake") -# subgraph = Graph() -# subgraph.add_node(n1) -# g1.add_node(GraphInvocation(id="3", graph=subgraph)) - -# nfvs = [ -# NodeFieldValue(node_path="1", field_name="prompt", value="Strawberry sushi"), -# NodeFieldValue(node_path="2", field_name="prompt", value="Strawberry sunday"), -# NodeFieldValue(node_path="3.1", field_name="prompt", value="Strawberry snake"), -# ] - -# g2 = populate_graph(g1, nfvs) - -# # do not mutate g1 -# assert g1 is not g2 -# assert g2.get_node("1").prompt == "Strawberry sushi" -# assert g2.get_node("2").prompt == "Strawberry sunday" -# assert g2.get_node("3.1").prompt == "Strawberry snake" - - def test_create_sessions_from_batch_with_runs(batch_data_collection, batch_graph): b = Batch(graph=batch_graph, data=batch_data_collection, runs=2) t = list(create_session_nfv_tuples(batch=b, maximum=1000)) From 725c03cf87a2d937c714770b83a74a419de3e0de Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 01:41:04 +1100 Subject: [PATCH 164/411] refactor(nodes): merge processors Consolidate graph processing logic into session processor. With graphs as the unit of work, and the session queue distributing graphs, we no longer need the invocation queue or processor. Instead, the session processor dequeues the next session and processes it in a simple loop, greatly simplifying the app. - Remove `graph_execution_manager` service. - Remove `queue` (invocation queue) service. - Remove `processor` (invocation processor) service. - Remove queue-related logic from `Invoker`. It now only starts and stops the services, providing them with access to other services. - Remove unused `invocation_retrieval_error` and `session_retrieval_error` events, these are no longer needed. - Clean up stats service now that it is less coupled to the rest of the app. - Refactor cancellation logic - cancellations now originate from session queue (i.e. HTTP cancel endpoint) and are emitted as events. Processor gets the events and sets the canceled event. Access to this event is provided to the invocation context for e.g. the step callback. - Remove `sessions` router; it provided access to `graph_executions` but that no longer exists. --- invokeai/app/api/dependencies.py | 10 - invokeai/app/api/routers/sessions.py | 276 ------------------ invokeai/app/api_app.py | 3 - invokeai/app/services/events/events_base.py | 48 +-- .../services/invocation_processor/__init__.py | 0 .../invocation_processor_base.py | 5 - .../invocation_processor_common.py | 15 - .../invocation_processor_default.py | 241 --------------- .../app/services/invocation_queue/__init__.py | 0 .../invocation_queue/invocation_queue_base.py | 26 -- .../invocation_queue_common.py | 23 -- .../invocation_queue_memory.py | 44 --- invokeai/app/services/invocation_services.py | 10 - .../invocation_stats/invocation_stats_base.py | 10 +- .../invocation_stats_default.py | 43 +-- invokeai/app/services/invoker.py | 52 ---- .../services/model_load/model_load_default.py | 3 - .../session_processor_common.py | 14 + .../session_processor_default.py | 249 +++++++++++----- .../session_queue/session_queue_sqlite.py | 5 +- .../app/services/shared/invocation_context.py | 20 +- invokeai/app/util/step_callback.py | 9 +- 22 files changed, 227 insertions(+), 879 deletions(-) delete mode 100644 invokeai/app/api/routers/sessions.py delete mode 100644 invokeai/app/services/invocation_processor/__init__.py delete mode 100644 invokeai/app/services/invocation_processor/invocation_processor_base.py delete mode 100644 invokeai/app/services/invocation_processor/invocation_processor_common.py delete mode 100644 invokeai/app/services/invocation_processor/invocation_processor_default.py delete mode 100644 invokeai/app/services/invocation_queue/__init__.py delete mode 100644 invokeai/app/services/invocation_queue/invocation_queue_base.py delete mode 100644 invokeai/app/services/invocation_queue/invocation_queue_common.py delete mode 100644 invokeai/app/services/invocation_queue/invocation_queue_memory.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 8e79b26e2d..a9132516a8 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -4,7 +4,6 @@ from logging import Logger import torch -from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache from invokeai.app.services.shared.sqlite.sqlite_util import init_db @@ -22,8 +21,6 @@ from ..services.image_files.image_files_disk import DiskImageFileStorage from ..services.image_records.image_records_sqlite import SqliteImageRecordStorage from ..services.images.images_default import ImageService from ..services.invocation_cache.invocation_cache_memory import MemoryInvocationCache -from ..services.invocation_processor.invocation_processor_default import DefaultInvocationProcessor -from ..services.invocation_queue.invocation_queue_memory import MemoryInvocationQueue from ..services.invocation_services import InvocationServices from ..services.invocation_stats.invocation_stats_default import InvocationStatsService from ..services.invoker import Invoker @@ -33,7 +30,6 @@ from ..services.model_records import ModelRecordServiceSQL from ..services.names.names_default import SimpleNameService from ..services.session_processor.session_processor_default import DefaultSessionProcessor from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue -from ..services.shared.graph import GraphExecutionState from ..services.urls.urls_default import LocalUrlService from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage from .events import FastAPIEventService @@ -85,7 +81,6 @@ class ApiDependencies: board_records = SqliteBoardRecordStorage(db=db) boards = BoardService() events = FastAPIEventService(event_handler_id) - graph_execution_manager = ItemStorageMemory[GraphExecutionState]() image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) @@ -105,8 +100,6 @@ class ApiDependencies: ) names = SimpleNameService() performance_statistics = InvocationStatsService() - processor = DefaultInvocationProcessor() - queue = MemoryInvocationQueue() session_processor = DefaultSessionProcessor() session_queue = SqliteSessionQueue(db=db) urls = LocalUrlService() @@ -119,7 +112,6 @@ class ApiDependencies: boards=boards, configuration=configuration, events=events, - graph_execution_manager=graph_execution_manager, image_files=image_files, image_records=image_records, images=images, @@ -129,8 +121,6 @@ class ApiDependencies: download_queue=download_queue_service, names=names, performance_statistics=performance_statistics, - processor=processor, - queue=queue, session_processor=session_processor, session_queue=session_queue, urls=urls, diff --git a/invokeai/app/api/routers/sessions.py b/invokeai/app/api/routers/sessions.py deleted file mode 100644 index fb850d0b2b..0000000000 --- a/invokeai/app/api/routers/sessions.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - - -from fastapi import HTTPException, Path -from fastapi.routing import APIRouter - -from ...services.shared.graph import GraphExecutionState -from ..dependencies import ApiDependencies - -session_router = APIRouter(prefix="/v1/sessions", tags=["sessions"]) - - -# @session_router.post( -# "/", -# operation_id="create_session", -# responses={ -# 200: {"model": GraphExecutionState}, -# 400: {"description": "Invalid json"}, -# }, -# deprecated=True, -# ) -# async def create_session( -# queue_id: str = Query(default="", description="The id of the queue to associate the session with"), -# graph: Optional[Graph] = Body(default=None, description="The graph to initialize the session with"), -# ) -> GraphExecutionState: -# """Creates a new session, optionally initializing it with an invocation graph""" -# session = ApiDependencies.invoker.create_execution_state(queue_id=queue_id, graph=graph) -# return session - - -# @session_router.get( -# "/", -# operation_id="list_sessions", -# responses={200: {"model": PaginatedResults[GraphExecutionState]}}, -# deprecated=True, -# ) -# async def list_sessions( -# page: int = Query(default=0, description="The page of results to get"), -# per_page: int = Query(default=10, description="The number of results per page"), -# query: str = Query(default="", description="The query string to search for"), -# ) -> PaginatedResults[GraphExecutionState]: -# """Gets a list of sessions, optionally searching""" -# if query == "": -# result = ApiDependencies.invoker.services.graph_execution_manager.list(page, per_page) -# else: -# result = ApiDependencies.invoker.services.graph_execution_manager.search(query, page, per_page) -# return result - - -@session_router.get( - "/{session_id}", - operation_id="get_session", - responses={ - 200: {"model": GraphExecutionState}, - 404: {"description": "Session not found"}, - }, -) -async def get_session( - session_id: str = Path(description="The id of the session to get"), -) -> GraphExecutionState: - """Gets a session""" - session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) - if session is None: - raise HTTPException(status_code=404) - else: - return session - - -# @session_router.post( -# "/{session_id}/nodes", -# operation_id="add_node", -# responses={ -# 200: {"model": str}, -# 400: {"description": "Invalid node or link"}, -# 404: {"description": "Session not found"}, -# }, -# deprecated=True, -# ) -# async def add_node( -# session_id: str = Path(description="The id of the session"), -# node: Annotated[Union[BaseInvocation.get_invocations()], Field(discriminator="type")] = Body( # type: ignore -# description="The node to add" -# ), -# ) -> str: -# """Adds a node to the graph""" -# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) -# if session is None: -# raise HTTPException(status_code=404) - -# try: -# session.add_node(node) -# ApiDependencies.invoker.services.graph_execution_manager.set( -# session -# ) # TODO: can this be done automatically, or add node through an API? -# return session.id -# except NodeAlreadyExecutedError: -# raise HTTPException(status_code=400) -# except IndexError: -# raise HTTPException(status_code=400) - - -# @session_router.put( -# "/{session_id}/nodes/{node_path}", -# operation_id="update_node", -# responses={ -# 200: {"model": GraphExecutionState}, -# 400: {"description": "Invalid node or link"}, -# 404: {"description": "Session not found"}, -# }, -# deprecated=True, -# ) -# async def update_node( -# session_id: str = Path(description="The id of the session"), -# node_path: str = Path(description="The path to the node in the graph"), -# node: Annotated[Union[BaseInvocation.get_invocations()], Field(discriminator="type")] = Body( # type: ignore -# description="The new node" -# ), -# ) -> GraphExecutionState: -# """Updates a node in the graph and removes all linked edges""" -# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) -# if session is None: -# raise HTTPException(status_code=404) - -# try: -# session.update_node(node_path, node) -# ApiDependencies.invoker.services.graph_execution_manager.set( -# session -# ) # TODO: can this be done automatically, or add node through an API? -# return session -# except NodeAlreadyExecutedError: -# raise HTTPException(status_code=400) -# except IndexError: -# raise HTTPException(status_code=400) - - -# @session_router.delete( -# "/{session_id}/nodes/{node_path}", -# operation_id="delete_node", -# responses={ -# 200: {"model": GraphExecutionState}, -# 400: {"description": "Invalid node or link"}, -# 404: {"description": "Session not found"}, -# }, -# deprecated=True, -# ) -# async def delete_node( -# session_id: str = Path(description="The id of the session"), -# node_path: str = Path(description="The path to the node to delete"), -# ) -> GraphExecutionState: -# """Deletes a node in the graph and removes all linked edges""" -# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) -# if session is None: -# raise HTTPException(status_code=404) - -# try: -# session.delete_node(node_path) -# ApiDependencies.invoker.services.graph_execution_manager.set( -# session -# ) # TODO: can this be done automatically, or add node through an API? -# return session -# except NodeAlreadyExecutedError: -# raise HTTPException(status_code=400) -# except IndexError: -# raise HTTPException(status_code=400) - - -# @session_router.post( -# "/{session_id}/edges", -# operation_id="add_edge", -# responses={ -# 200: {"model": GraphExecutionState}, -# 400: {"description": "Invalid node or link"}, -# 404: {"description": "Session not found"}, -# }, -# deprecated=True, -# ) -# async def add_edge( -# session_id: str = Path(description="The id of the session"), -# edge: Edge = Body(description="The edge to add"), -# ) -> GraphExecutionState: -# """Adds an edge to the graph""" -# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) -# if session is None: -# raise HTTPException(status_code=404) - -# try: -# session.add_edge(edge) -# ApiDependencies.invoker.services.graph_execution_manager.set( -# session -# ) # TODO: can this be done automatically, or add node through an API? -# return session -# except NodeAlreadyExecutedError: -# raise HTTPException(status_code=400) -# except IndexError: -# raise HTTPException(status_code=400) - - -# # TODO: the edge being in the path here is really ugly, find a better solution -# @session_router.delete( -# "/{session_id}/edges/{from_node_id}/{from_field}/{to_node_id}/{to_field}", -# operation_id="delete_edge", -# responses={ -# 200: {"model": GraphExecutionState}, -# 400: {"description": "Invalid node or link"}, -# 404: {"description": "Session not found"}, -# }, -# deprecated=True, -# ) -# async def delete_edge( -# session_id: str = Path(description="The id of the session"), -# from_node_id: str = Path(description="The id of the node the edge is coming from"), -# from_field: str = Path(description="The field of the node the edge is coming from"), -# to_node_id: str = Path(description="The id of the node the edge is going to"), -# to_field: str = Path(description="The field of the node the edge is going to"), -# ) -> GraphExecutionState: -# """Deletes an edge from the graph""" -# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) -# if session is None: -# raise HTTPException(status_code=404) - -# try: -# edge = Edge( -# source=EdgeConnection(node_id=from_node_id, field=from_field), -# destination=EdgeConnection(node_id=to_node_id, field=to_field), -# ) -# session.delete_edge(edge) -# ApiDependencies.invoker.services.graph_execution_manager.set( -# session -# ) # TODO: can this be done automatically, or add node through an API? -# return session -# except NodeAlreadyExecutedError: -# raise HTTPException(status_code=400) -# except IndexError: -# raise HTTPException(status_code=400) - - -# @session_router.put( -# "/{session_id}/invoke", -# operation_id="invoke_session", -# responses={ -# 200: {"model": None}, -# 202: {"description": "The invocation is queued"}, -# 400: {"description": "The session has no invocations ready to invoke"}, -# 404: {"description": "Session not found"}, -# }, -# deprecated=True, -# ) -# async def invoke_session( -# queue_id: str = Query(description="The id of the queue to associate the session with"), -# session_id: str = Path(description="The id of the session to invoke"), -# all: bool = Query(default=False, description="Whether or not to invoke all remaining invocations"), -# ) -> Response: -# """Invokes a session""" -# session = ApiDependencies.invoker.services.graph_execution_manager.get(session_id) -# if session is None: -# raise HTTPException(status_code=404) - -# if session.is_complete(): -# raise HTTPException(status_code=400) - -# ApiDependencies.invoker.invoke(queue_id, session, invoke_all=all) -# return Response(status_code=202) - - -# @session_router.delete( -# "/{session_id}/invoke", -# operation_id="cancel_session_invoke", -# responses={202: {"description": "The invocation is canceled"}}, -# deprecated=True, -# ) -# async def cancel_session_invoke( -# session_id: str = Path(description="The id of the session to cancel"), -# ) -> Response: -# """Invokes a session""" -# ApiDependencies.invoker.cancel(session_id) -# return Response(status_code=202) diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py index 65607c436a..f6b08ddba6 100644 --- a/invokeai/app/api_app.py +++ b/invokeai/app/api_app.py @@ -50,7 +50,6 @@ if True: # hack to make flake8 happy with imports coming after setting up the c images, model_manager, session_queue, - sessions, utilities, workflows, ) @@ -110,8 +109,6 @@ async def shutdown_event() -> None: # Include all routers -app.include_router(sessions.session_router, prefix="/api") - app.include_router(utilities.utilities_router, prefix="/api") app.include_router(model_manager.model_manager_router, prefix="/api") app.include_router(download_queue.download_queue_router, prefix="/api") diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 90d9068b88..5355fe2298 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union -from invokeai.app.services.invocation_processor.invocation_processor_common import ProgressImage +from invokeai.app.services.session_processor.session_processor_common import ProgressImage from invokeai.app.services.session_queue.session_queue_common import ( BatchStatus, EnqueueBatchResult, @@ -204,52 +204,6 @@ class EventServiceBase: }, ) - def emit_session_retrieval_error( - self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - error_type: str, - error: str, - ) -> None: - """Emitted when session retrieval fails""" - self.__emit_queue_event( - event_name="session_retrieval_error", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "error_type": error_type, - "error": error, - }, - ) - - def emit_invocation_retrieval_error( - self, - queue_id: str, - queue_item_id: int, - queue_batch_id: str, - graph_execution_state_id: str, - node_id: str, - error_type: str, - error: str, - ) -> None: - """Emitted when invocation retrieval fails""" - self.__emit_queue_event( - event_name="invocation_retrieval_error", - payload={ - "queue_id": queue_id, - "queue_item_id": queue_item_id, - "queue_batch_id": queue_batch_id, - "graph_execution_state_id": graph_execution_state_id, - "node_id": node_id, - "error_type": error_type, - "error": error, - }, - ) - def emit_session_canceled( self, queue_id: str, diff --git a/invokeai/app/services/invocation_processor/__init__.py b/invokeai/app/services/invocation_processor/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/invokeai/app/services/invocation_processor/invocation_processor_base.py b/invokeai/app/services/invocation_processor/invocation_processor_base.py deleted file mode 100644 index 7947a201dd..0000000000 --- a/invokeai/app/services/invocation_processor/invocation_processor_base.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class InvocationProcessorABC(ABC): # noqa: B024 - pass diff --git a/invokeai/app/services/invocation_processor/invocation_processor_common.py b/invokeai/app/services/invocation_processor/invocation_processor_common.py deleted file mode 100644 index 347f6c7323..0000000000 --- a/invokeai/app/services/invocation_processor/invocation_processor_common.py +++ /dev/null @@ -1,15 +0,0 @@ -from pydantic import BaseModel, Field - - -class ProgressImage(BaseModel): - """The progress image sent intermittently during processing""" - - width: int = Field(description="The effective width of the image in pixels") - height: int = Field(description="The effective height of the image in pixels") - dataURL: str = Field(description="The image data as a b64 data URL") - - -class CanceledException(Exception): - """Execution canceled by user.""" - - pass diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py deleted file mode 100644 index d2ebe235e6..0000000000 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ /dev/null @@ -1,241 +0,0 @@ -import time -import traceback -from contextlib import suppress -from threading import BoundedSemaphore, Event, Thread -from typing import Optional - -import invokeai.backend.util.logging as logger -from invokeai.app.services.invocation_queue.invocation_queue_common import InvocationQueueItem -from invokeai.app.services.invocation_stats.invocation_stats_common import ( - GESStatsNotFoundError, -) -from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context -from invokeai.app.util.profiler import Profiler - -from ..invoker import Invoker -from .invocation_processor_base import InvocationProcessorABC -from .invocation_processor_common import CanceledException - - -class DefaultInvocationProcessor(InvocationProcessorABC): - __invoker_thread: Thread - __stop_event: Event - __invoker: Invoker - __threadLimit: BoundedSemaphore - - def start(self, invoker: Invoker) -> None: - # if we do want multithreading at some point, we could make this configurable - self.__threadLimit = BoundedSemaphore(1) - self.__invoker = invoker - self.__stop_event = Event() - self.__invoker_thread = Thread( - name="invoker_processor", - target=self.__process, - kwargs={"stop_event": self.__stop_event}, - ) - self.__invoker_thread.daemon = True # TODO: make async and do not use threads - self.__invoker_thread.start() - - def stop(self, *args, **kwargs) -> None: - self.__stop_event.set() - - def __process(self, stop_event: Event): - try: - self.__threadLimit.acquire() - queue_item: Optional[InvocationQueueItem] = None - - profiler = ( - Profiler( - logger=self.__invoker.services.logger, - output_dir=self.__invoker.services.configuration.profiles_path, - prefix=self.__invoker.services.configuration.profile_prefix, - ) - if self.__invoker.services.configuration.profile_graphs - else None - ) - - def stats_cleanup(graph_execution_state_id: str) -> None: - if profiler: - profile_path = profiler.stop() - stats_path = profile_path.with_suffix(".json") - self.__invoker.services.performance_statistics.dump_stats( - graph_execution_state_id=graph_execution_state_id, output_path=stats_path - ) - with suppress(GESStatsNotFoundError): - self.__invoker.services.performance_statistics.log_stats(graph_execution_state_id) - self.__invoker.services.performance_statistics.reset_stats(graph_execution_state_id) - - while not stop_event.is_set(): - try: - queue_item = self.__invoker.services.queue.get() - except Exception as e: - self.__invoker.services.logger.error("Exception while getting from queue:\n%s" % e) - - if not queue_item: # Probably stopping - # do not hammer the queue - time.sleep(0.5) - continue - - if profiler and profiler.profile_id != queue_item.graph_execution_state_id: - profiler.start(profile_id=queue_item.graph_execution_state_id) - - try: - graph_execution_state = self.__invoker.services.graph_execution_manager.get( - queue_item.graph_execution_state_id - ) - except Exception as e: - self.__invoker.services.logger.error("Exception while retrieving session:\n%s" % e) - self.__invoker.services.events.emit_session_retrieval_error( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=queue_item.graph_execution_state_id, - error_type=e.__class__.__name__, - error=traceback.format_exc(), - ) - continue - - try: - invocation = graph_execution_state.execution_graph.get_node(queue_item.invocation_id) - except Exception as e: - self.__invoker.services.logger.error("Exception while retrieving invocation:\n%s" % e) - self.__invoker.services.events.emit_invocation_retrieval_error( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=queue_item.graph_execution_state_id, - node_id=queue_item.invocation_id, - error_type=e.__class__.__name__, - error=traceback.format_exc(), - ) - continue - - # get the source node id to provide to clients (the prepared node id is not as useful) - source_node_id = graph_execution_state.prepared_source_mapping[invocation.id] - - # Send starting event - self.__invoker.services.events.emit_invocation_started( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=graph_execution_state.id, - node=invocation.model_dump(), - source_node_id=source_node_id, - ) - - # Invoke - try: - graph_id = graph_execution_state.id - with self.__invoker.services.performance_statistics.collect_stats(invocation, graph_id): - # use the internal invoke_internal(), which wraps the node's invoke() method, - # which handles a few things: - # - nodes that require a value, but get it only from a connection - # - referencing the invocation cache instead of executing the node - context_data = InvocationContextData( - invocation=invocation, - session_id=graph_id, - workflow=queue_item.workflow, - source_node_id=source_node_id, - queue_id=queue_item.session_queue_id, - queue_item_id=queue_item.session_queue_item_id, - batch_id=queue_item.session_queue_batch_id, - ) - context = build_invocation_context( - services=self.__invoker.services, - context_data=context_data, - ) - outputs = invocation.invoke_internal(context=context, services=self.__invoker.services) - - # Check queue to see if this is canceled, and skip if so - if self.__invoker.services.queue.is_canceled(graph_execution_state.id): - continue - - # Save outputs and history - graph_execution_state.complete(invocation.id, outputs) - - # Save the state changes - self.__invoker.services.graph_execution_manager.set(graph_execution_state) - - # Send complete event - self.__invoker.services.events.emit_invocation_complete( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=graph_execution_state.id, - node=invocation.model_dump(), - source_node_id=source_node_id, - result=outputs.model_dump(), - ) - - except KeyboardInterrupt: - pass - - except CanceledException: - stats_cleanup(graph_execution_state.id) - pass - - except Exception as e: - error = traceback.format_exc() - logger.error(error) - - # Save error - graph_execution_state.set_node_error(invocation.id, error) - - # Save the state changes - self.__invoker.services.graph_execution_manager.set(graph_execution_state) - - self.__invoker.services.logger.error("Error while invoking:\n%s" % e) - # Send error event - self.__invoker.services.events.emit_invocation_error( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=graph_execution_state.id, - node=invocation.model_dump(), - source_node_id=source_node_id, - error_type=e.__class__.__name__, - error=error, - ) - pass - - # Check queue to see if this is canceled, and skip if so - if self.__invoker.services.queue.is_canceled(graph_execution_state.id): - continue - - # Queue any further commands if invoking all - is_complete = graph_execution_state.is_complete() - if queue_item.invoke_all and not is_complete: - try: - self.__invoker.invoke( - session_queue_batch_id=queue_item.session_queue_batch_id, - session_queue_item_id=queue_item.session_queue_item_id, - session_queue_id=queue_item.session_queue_id, - graph_execution_state=graph_execution_state, - workflow=queue_item.workflow, - invoke_all=True, - ) - except Exception as e: - self.__invoker.services.logger.error("Error while invoking:\n%s" % e) - self.__invoker.services.events.emit_invocation_error( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=graph_execution_state.id, - node=invocation.model_dump(), - source_node_id=source_node_id, - error_type=e.__class__.__name__, - error=traceback.format_exc(), - ) - elif is_complete: - self.__invoker.services.events.emit_graph_execution_complete( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=graph_execution_state.id, - ) - stats_cleanup(graph_execution_state.id) - - except KeyboardInterrupt: - pass # Log something? KeyboardInterrupt is probably not going to be seen by the processor - finally: - self.__threadLimit.release() diff --git a/invokeai/app/services/invocation_queue/__init__.py b/invokeai/app/services/invocation_queue/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/invokeai/app/services/invocation_queue/invocation_queue_base.py b/invokeai/app/services/invocation_queue/invocation_queue_base.py deleted file mode 100644 index 09f4875c5f..0000000000 --- a/invokeai/app/services/invocation_queue/invocation_queue_base.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - -from abc import ABC, abstractmethod -from typing import Optional - -from .invocation_queue_common import InvocationQueueItem - - -class InvocationQueueABC(ABC): - """Abstract base class for all invocation queues""" - - @abstractmethod - def get(self) -> InvocationQueueItem: - pass - - @abstractmethod - def put(self, item: Optional[InvocationQueueItem]) -> None: - pass - - @abstractmethod - def cancel(self, graph_execution_state_id: str) -> None: - pass - - @abstractmethod - def is_canceled(self, graph_execution_state_id: str) -> bool: - pass diff --git a/invokeai/app/services/invocation_queue/invocation_queue_common.py b/invokeai/app/services/invocation_queue/invocation_queue_common.py deleted file mode 100644 index 696f6a981d..0000000000 --- a/invokeai/app/services/invocation_queue/invocation_queue_common.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - -import time -from typing import Optional - -from pydantic import BaseModel, Field - -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID - - -class InvocationQueueItem(BaseModel): - graph_execution_state_id: str = Field(description="The ID of the graph execution state") - invocation_id: str = Field(description="The ID of the node being invoked") - session_queue_id: str = Field(description="The ID of the session queue from which this invocation queue item came") - session_queue_item_id: int = Field( - description="The ID of session queue item from which this invocation queue item came" - ) - session_queue_batch_id: str = Field( - description="The ID of the session batch from which this invocation queue item came" - ) - workflow: Optional[WorkflowWithoutID] = Field(description="The workflow associated with this queue item") - invoke_all: bool = Field(default=False) - timestamp: float = Field(default_factory=time.time) diff --git a/invokeai/app/services/invocation_queue/invocation_queue_memory.py b/invokeai/app/services/invocation_queue/invocation_queue_memory.py deleted file mode 100644 index 8d6fff7052..0000000000 --- a/invokeai/app/services/invocation_queue/invocation_queue_memory.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - -import time -from queue import Queue -from typing import Optional - -from .invocation_queue_base import InvocationQueueABC -from .invocation_queue_common import InvocationQueueItem - - -class MemoryInvocationQueue(InvocationQueueABC): - __queue: Queue - __cancellations: dict[str, float] - - def __init__(self): - self.__queue = Queue() - self.__cancellations = {} - - def get(self) -> InvocationQueueItem: - item = self.__queue.get() - - while ( - isinstance(item, InvocationQueueItem) - and item.graph_execution_state_id in self.__cancellations - and self.__cancellations[item.graph_execution_state_id] > item.timestamp - ): - item = self.__queue.get() - - # Clear old items - for graph_execution_state_id in list(self.__cancellations.keys()): - if self.__cancellations[graph_execution_state_id] < item.timestamp: - del self.__cancellations[graph_execution_state_id] - - return item - - def put(self, item: Optional[InvocationQueueItem]) -> None: - self.__queue.put(item) - - def cancel(self, graph_execution_state_id: str) -> None: - if graph_execution_state_id not in self.__cancellations: - self.__cancellations[graph_execution_state_id] = time.time() - - def is_canceled(self, graph_execution_state_id: str) -> bool: - return graph_execution_state_id in self.__cancellations diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 0a1fa1e922..04fe71a3eb 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -23,15 +23,11 @@ if TYPE_CHECKING: from .image_records.image_records_base import ImageRecordStorageBase from .images.images_base import ImageServiceABC from .invocation_cache.invocation_cache_base import InvocationCacheBase - from .invocation_processor.invocation_processor_base import InvocationProcessorABC - from .invocation_queue.invocation_queue_base import InvocationQueueABC from .invocation_stats.invocation_stats_base import InvocationStatsServiceBase - from .item_storage.item_storage_base import ItemStorageABC from .model_manager.model_manager_base import ModelManagerServiceBase from .names.names_base import NameServiceBase from .session_processor.session_processor_base import SessionProcessorBase from .session_queue.session_queue_base import SessionQueueBase - from .shared.graph import GraphExecutionState from .urls.urls_base import UrlServiceBase from .workflow_records.workflow_records_base import WorkflowRecordsStorageBase @@ -47,16 +43,13 @@ class InvocationServices: board_records: "BoardRecordStorageBase", configuration: "InvokeAIAppConfig", events: "EventServiceBase", - graph_execution_manager: "ItemStorageABC[GraphExecutionState]", images: "ImageServiceABC", image_files: "ImageFileStorageBase", image_records: "ImageRecordStorageBase", logger: "Logger", model_manager: "ModelManagerServiceBase", download_queue: "DownloadQueueServiceBase", - processor: "InvocationProcessorABC", performance_statistics: "InvocationStatsServiceBase", - queue: "InvocationQueueABC", session_queue: "SessionQueueBase", session_processor: "SessionProcessorBase", invocation_cache: "InvocationCacheBase", @@ -72,16 +65,13 @@ class InvocationServices: self.board_records = board_records self.configuration = configuration self.events = events - self.graph_execution_manager = graph_execution_manager self.images = images self.image_files = image_files self.image_records = image_records self.logger = logger self.model_manager = model_manager self.download_queue = download_queue - self.processor = processor self.performance_statistics = performance_statistics - self.queue = queue self.session_queue = session_queue self.session_processor = session_processor self.invocation_cache = invocation_cache diff --git a/invokeai/app/services/invocation_stats/invocation_stats_base.py b/invokeai/app/services/invocation_stats/invocation_stats_base.py index ec8a453323..b28220e74c 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_base.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_base.py @@ -3,7 +3,7 @@ Usage: -statistics = InvocationStatsService(graph_execution_manager) +statistics = InvocationStatsService() with statistics.collect_stats(invocation, graph_execution_state.id): ... execute graphs... statistics.log_stats() @@ -60,12 +60,8 @@ class InvocationStatsServiceBase(ABC): pass @abstractmethod - def reset_stats(self, graph_execution_state_id: str) -> None: - """ - Reset all statistics for the indicated graph. - :param graph_execution_state_id: The id of the session whose stats to reset. - :raises GESStatsNotFoundError: if the graph isn't tracked in the stats. - """ + def reset_stats(self): + """Reset all stored statistics.""" pass @abstractmethod diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index 486a1ca5b3..06a5b675c3 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -10,7 +10,6 @@ import torch import invokeai.backend.util.logging as logger from invokeai.app.invocations.baseinvocation import BaseInvocation from invokeai.app.services.invoker import Invoker -from invokeai.app.services.item_storage.item_storage_common import ItemNotFoundError from invokeai.backend.model_manager.load.model_cache import CacheStats from .invocation_stats_base import InvocationStatsServiceBase @@ -51,9 +50,6 @@ class InvocationStatsService(InvocationStatsServiceBase): self._stats[graph_execution_state_id] = GraphExecutionStats() self._cache_stats[graph_execution_state_id] = CacheStats() - # Prune stale stats. There should be none since we're starting a new graph, but just in case. - self._prune_stale_stats() - # Record state before the invocation. start_time = time.time() start_ram = psutil.Process().memory_info().rss @@ -78,42 +74,9 @@ class InvocationStatsService(InvocationStatsServiceBase): ) self._stats[graph_execution_state_id].add_node_execution_stats(node_stats) - def _prune_stale_stats(self) -> None: - """Check all graphs being tracked and prune any that have completed/errored. - - This shouldn't be necessary, but we don't have totally robust upstream handling of graph completions/errors, so - for now we call this function periodically to prevent them from accumulating. - """ - to_prune: list[str] = [] - for graph_execution_state_id in self._stats: - try: - graph_execution_state = self._invoker.services.graph_execution_manager.get(graph_execution_state_id) - except ItemNotFoundError: - # TODO(ryand): What would cause this? Should this exception just be allowed to propagate? - logger.warning(f"Failed to get graph state for {graph_execution_state_id}.") - continue - - if not graph_execution_state.is_complete(): - # The graph is still running, don't prune it. - continue - - to_prune.append(graph_execution_state_id) - - for graph_execution_state_id in to_prune: - del self._stats[graph_execution_state_id] - del self._cache_stats[graph_execution_state_id] - - if len(to_prune) > 0: - logger.info(f"Pruned stale graph stats for {to_prune}.") - - def reset_stats(self, graph_execution_state_id: str): - try: - del self._stats[graph_execution_state_id] - del self._cache_stats[graph_execution_state_id] - except KeyError as e: - raise GESStatsNotFoundError( - f"Attempted to clear statistics for unknown graph {graph_execution_state_id}: {e}." - ) from e + def reset_stats(self): + self._stats = {} + self._cache_stats = {} def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary: graph_stats_summary = self._get_graph_summary(graph_execution_state_id) diff --git a/invokeai/app/services/invoker.py b/invokeai/app/services/invoker.py index a04c6f2059..527afb37f4 100644 --- a/invokeai/app/services/invoker.py +++ b/invokeai/app/services/invoker.py @@ -1,12 +1,7 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from typing import Optional -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID - -from .invocation_queue.invocation_queue_common import InvocationQueueItem from .invocation_services import InvocationServices -from .shared.graph import Graph, GraphExecutionState class Invoker: @@ -18,51 +13,6 @@ class Invoker: self.services = services self._start() - def invoke( - self, - session_queue_id: str, - session_queue_item_id: int, - session_queue_batch_id: str, - graph_execution_state: GraphExecutionState, - workflow: Optional[WorkflowWithoutID] = None, - invoke_all: bool = False, - ) -> Optional[str]: - """Determines the next node to invoke and enqueues it, preparing if needed. - Returns the id of the queued node, or `None` if there are no nodes left to enqueue.""" - - # Get the next invocation - invocation = graph_execution_state.next() - if not invocation: - return None - - # Save the execution state - self.services.graph_execution_manager.set(graph_execution_state) - - # Queue the invocation - self.services.queue.put( - InvocationQueueItem( - session_queue_id=session_queue_id, - session_queue_item_id=session_queue_item_id, - session_queue_batch_id=session_queue_batch_id, - graph_execution_state_id=graph_execution_state.id, - invocation_id=invocation.id, - workflow=workflow, - invoke_all=invoke_all, - ) - ) - - return invocation.id - - def create_execution_state(self, graph: Optional[Graph] = None) -> GraphExecutionState: - """Creates a new execution state for the given graph""" - new_state = GraphExecutionState(graph=Graph() if graph is None else graph) - self.services.graph_execution_manager.set(new_state) - return new_state - - def cancel(self, graph_execution_state_id: str) -> None: - """Cancels the given execution state""" - self.services.queue.cancel(graph_execution_state_id) - def __start_service(self, service) -> None: # Call start() method on any services that have it start_op = getattr(service, "start", None) @@ -85,5 +35,3 @@ class Invoker: # First stop all services for service in vars(self.services): self.__stop_service(getattr(self.services, service)) - - self.services.queue.put(None) diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index 15c6283d8a..24ab10b427 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -4,7 +4,6 @@ from typing import Optional, Type from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_manager import AnyModel, AnyModelConfig, SubModelType @@ -95,8 +94,6 @@ class ModelLoadService(ModelLoadServiceBase): ) -> None: if not self._invoker: return - if self._invoker.services.queue.is_canceled(context_data.session_id): - raise CanceledException() if not loaded: self._invoker.services.events.emit_model_load_started( diff --git a/invokeai/app/services/session_processor/session_processor_common.py b/invokeai/app/services/session_processor/session_processor_common.py index 00195a773f..0ca51de517 100644 --- a/invokeai/app/services/session_processor/session_processor_common.py +++ b/invokeai/app/services/session_processor/session_processor_common.py @@ -4,3 +4,17 @@ from pydantic import BaseModel, Field class SessionProcessorStatus(BaseModel): is_started: bool = Field(description="Whether the session processor is started") is_processing: bool = Field(description="Whether a session is being processed") + + +class CanceledException(Exception): + """Execution canceled by user.""" + + pass + + +class ProgressImage(BaseModel): + """The progress image sent intermittently during processing""" + + width: int = Field(description="The effective width of the image in pixels") + height: int = Field(description="The effective height of the image in pixels") + dataURL: str = Field(description="The image data as a b64 data URL") diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 32e94a305d..dd34c78252 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -1,4 +1,5 @@ import traceback +from contextlib import suppress from threading import BoundedSemaphore, Thread from threading import Event as ThreadEvent from typing import Optional @@ -7,7 +8,11 @@ from fastapi_events.handlers.local import local_handler from fastapi_events.typing import Event as FastAPIEvent from invokeai.app.services.events.events_base import EventServiceBase +from invokeai.app.services.invocation_stats.invocation_stats_common import GESStatsNotFoundError +from invokeai.app.services.session_processor.session_processor_common import CanceledException from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem +from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context +from invokeai.app.util.profiler import Profiler from ..invoker import Invoker from .session_processor_base import SessionProcessorBase @@ -19,123 +24,237 @@ THREAD_LIMIT = 1 class DefaultSessionProcessor(SessionProcessorBase): def start(self, invoker: Invoker) -> None: - self.__invoker: Invoker = invoker - self.__queue_item: Optional[SessionQueueItem] = None + self._invoker: Invoker = invoker + self._queue_item: Optional[SessionQueueItem] = None - self.__resume_event = ThreadEvent() - self.__stop_event = ThreadEvent() - self.__poll_now_event = ThreadEvent() + self._resume_event = ThreadEvent() + self._stop_event = ThreadEvent() + self._poll_now_event = ThreadEvent() + self._cancel_event = ThreadEvent() local_handler.register(event_name=EventServiceBase.queue_event, _func=self._on_queue_event) - self.__threadLimit = BoundedSemaphore(THREAD_LIMIT) - self.__thread = Thread( + self._thread_limit = BoundedSemaphore(THREAD_LIMIT) + self._thread = Thread( name="session_processor", - target=self.__process, + target=self._process, kwargs={ - "stop_event": self.__stop_event, - "poll_now_event": self.__poll_now_event, - "resume_event": self.__resume_event, + "stop_event": self._stop_event, + "poll_now_event": self._poll_now_event, + "resume_event": self._resume_event, + "cancel_event": self._cancel_event, }, ) - self.__thread.start() + self._thread.start() def stop(self, *args, **kwargs) -> None: - self.__stop_event.set() + self._stop_event.set() def _poll_now(self) -> None: - self.__poll_now_event.set() + self._poll_now_event.set() async def _on_queue_event(self, event: FastAPIEvent) -> None: event_name = event[1]["event"] - # This was a match statement, but match is not supported on python 3.9 - if event_name in [ - "graph_execution_state_complete", - "invocation_error", - "session_retrieval_error", - "invocation_retrieval_error", - ]: - self.__queue_item = None - self._poll_now() - elif ( - event_name == "session_canceled" - and self.__queue_item is not None - and self.__queue_item.session_id == event[1]["data"]["graph_execution_state_id"] - ): - self.__queue_item = None + if event_name == "session_canceled" or event_name == "queue_cleared": + # These both mean we should cancel the current session. + self._cancel_event.set() self._poll_now() elif event_name == "batch_enqueued": self._poll_now() - elif event_name == "queue_cleared": - self.__queue_item = None - self._poll_now() def resume(self) -> SessionProcessorStatus: - if not self.__resume_event.is_set(): - self.__resume_event.set() + if not self._resume_event.is_set(): + self._resume_event.set() return self.get_status() def pause(self) -> SessionProcessorStatus: - if self.__resume_event.is_set(): - self.__resume_event.clear() + if self._resume_event.is_set(): + self._resume_event.clear() return self.get_status() def get_status(self) -> SessionProcessorStatus: return SessionProcessorStatus( - is_started=self.__resume_event.is_set(), - is_processing=self.__queue_item is not None, + is_started=self._resume_event.is_set(), + is_processing=self._queue_item is not None, ) - def __process( + def _process( self, stop_event: ThreadEvent, poll_now_event: ThreadEvent, resume_event: ThreadEvent, + cancel_event: ThreadEvent, ): + # Outermost processor try block; any unhandled exception is a fatal processor error try: + self._thread_limit.acquire() stop_event.clear() resume_event.set() - self.__threadLimit.acquire() - queue_item: Optional[SessionQueueItem] = None + cancel_event.clear() + + # If profiling is enabled, create a profiler. The same profiler will be used for all sessions. Internally, + # the profiler will create a new profile for each session. + profiler = ( + Profiler( + logger=self._invoker.services.logger, + output_dir=self._invoker.services.configuration.profiles_path, + prefix=self._invoker.services.configuration.profile_prefix, + ) + if self._invoker.services.configuration.profile_graphs + else None + ) + + # Helper function to stop the profiler and save the stats + def stats_cleanup(graph_execution_state_id: str) -> None: + if profiler: + profile_path = profiler.stop() + stats_path = profile_path.with_suffix(".json") + self._invoker.services.performance_statistics.dump_stats( + graph_execution_state_id=graph_execution_state_id, output_path=stats_path + ) + # We'll get a GESStatsNotFoundError if we try to log stats for an untracked graph, but in the processor + # we don't care about that - suppress the error. + with suppress(GESStatsNotFoundError): + self._invoker.services.performance_statistics.log_stats(graph_execution_state_id) + self._invoker.services.performance_statistics.reset_stats() + while not stop_event.is_set(): poll_now_event.clear() + # Middle processor try block; any unhandled exception is a non-fatal processor error try: - # do not dequeue if there is already a session running - if self.__queue_item is None and resume_event.is_set(): - queue_item = self.__invoker.services.session_queue.dequeue() + # Get the next session to process + self._queue_item = self._invoker.services.session_queue.dequeue() + if self._queue_item is not None and resume_event.is_set(): + self._invoker.services.logger.debug(f"Executing queue item {self._queue_item.item_id}") + cancel_event.clear() - if queue_item is not None: - self.__invoker.services.logger.debug(f"Executing queue item {queue_item.item_id}") - self.__queue_item = queue_item - self.__invoker.services.graph_execution_manager.set(queue_item.session) - self.__invoker.invoke( - session_queue_batch_id=queue_item.batch_id, - session_queue_id=queue_item.queue_id, - session_queue_item_id=queue_item.item_id, - graph_execution_state=queue_item.session, - workflow=queue_item.workflow, - invoke_all=True, + # If profiling is enabled, start the profiler + if profiler is not None: + profiler.start(profile_id=self._queue_item.session_id) + + # Prepare invocations and take the first + invocation = self._queue_item.session.next() + + # Loop over invocations until the session is complete or canceled + while invocation is not None and not cancel_event.is_set(): + # get the source node id to provide to clients (the prepared node id is not as useful) + source_node_id = self._queue_item.session.prepared_source_mapping[invocation.id] + + # Send starting event + self._invoker.services.events.emit_invocation_started( + queue_batch_id=self._queue_item.batch_id, + queue_item_id=self._queue_item.item_id, + queue_id=self._queue_item.queue_id, + graph_execution_state_id=self._queue_item.session_id, + node=invocation.model_dump(), + source_node_id=source_node_id, ) - queue_item = None - if queue_item is None: - self.__invoker.services.logger.debug("Waiting for next polling interval or event") + # Innermost processor try block; any unhandled exception is an invocation error & will fail the graph + try: + with self._invoker.services.performance_statistics.collect_stats( + invocation, self._queue_item.session.id + ): + # Build invocation context (the node-facing API) + context_data = InvocationContextData( + invocation=invocation, + source_node_id=source_node_id, + session_id=self._queue_item.session.id, + workflow=self._queue_item.workflow, + queue_id=self._queue_item.queue_id, + queue_item_id=self._queue_item.item_id, + batch_id=self._queue_item.batch_id, + ) + context = build_invocation_context( + context_data=context_data, + services=self._invoker.services, + cancel_event=self._cancel_event, + ) + + # Invoke the node + outputs = invocation.invoke_internal( + context=context, services=self._invoker.services + ) + + # Save outputs and history + self._queue_item.session.complete(invocation.id, outputs) + + # Send complete event + self._invoker.services.events.emit_invocation_complete( + queue_batch_id=self._queue_item.batch_id, + queue_item_id=self._queue_item.item_id, + queue_id=self._queue_item.queue_id, + graph_execution_state_id=self._queue_item.session.id, + node=invocation.model_dump(), + source_node_id=source_node_id, + result=outputs.model_dump(), + ) + + except KeyboardInterrupt: + pass + + except CanceledException: + pass + + except Exception as e: + error = traceback.format_exc() + + # Save error + self._queue_item.session.set_node_error(invocation.id, error) + self._invoker.services.logger.error("Error while invoking:\n%s" % e) + + # Send error event + self._invoker.services.events.emit_invocation_error( + queue_batch_id=self._queue_item.session_id, + queue_item_id=self._queue_item.item_id, + queue_id=self._queue_item.queue_id, + graph_execution_state_id=self._queue_item.session.id, + node=invocation.model_dump(), + source_node_id=source_node_id, + error_type=e.__class__.__name__, + error=error, + ) + pass + + if self._queue_item.session.is_complete() or cancel_event.is_set(): + # Send complete event + self._invoker.services.events.emit_graph_execution_complete( + queue_batch_id=self._queue_item.batch_id, + queue_item_id=self._queue_item.item_id, + queue_id=self._queue_item.queue_id, + graph_execution_state_id=self._queue_item.session.id, + ) + # Save the stats and stop the profiler if it's running + stats_cleanup(self._queue_item.session.id) + invocation = None + else: + # Prepare the next invocation + invocation = self._queue_item.session.next() + + # The session is complete, immediately poll for next session + self._queue_item = None + poll_now_event.set() + else: + # The queue was empty, wait for next polling interval or event to try again + self._invoker.services.logger.debug("Waiting for next polling interval or event") poll_now_event.wait(POLLING_INTERVAL) continue except Exception as e: - self.__invoker.services.logger.error(f"Error in session processor: {e}") - if queue_item is not None: - self.__invoker.services.session_queue.cancel_queue_item( - queue_item.item_id, error=traceback.format_exc() + # Non-fatal error in processor, cancel the queue item and wait for next polling interval or event + self._invoker.services.logger.error(f"Error in session processor: {e}") + if self._queue_item is not None: + self._invoker.services.session_queue.cancel_queue_item( + self._queue_item.item_id, error=traceback.format_exc() ) poll_now_event.wait(POLLING_INTERVAL) continue except Exception as e: - self.__invoker.services.logger.error(f"Fatal Error in session processor: {e}") + # Fatal error in processor, log and pass - we're done here + self._invoker.services.logger.error(f"Fatal Error in session processor: {e}") pass finally: stop_event.clear() poll_now_event.clear() - self.__queue_item = None - self.__threadLimit.release() + self._queue_item = None + self._thread_limit.release() diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py index 64642690e9..7af9f0e08c 100644 --- a/invokeai/app/services/session_queue/session_queue_sqlite.py +++ b/invokeai/app/services/session_queue/session_queue_sqlite.py @@ -60,7 +60,7 @@ class SqliteSessionQueue(SessionQueueBase): # This was a match statement, but match is not supported on python 3.9 if event_name == "graph_execution_state_complete": await self._handle_complete_event(event) - elif event_name in ["invocation_error", "session_retrieval_error", "invocation_retrieval_error"]: + elif event_name == "invocation_error": await self._handle_error_event(event) elif event_name == "session_canceled": await self._handle_cancel_event(event) @@ -429,7 +429,6 @@ class SqliteSessionQueue(SessionQueueBase): if queue_item.status not in ["canceled", "failed", "completed"]: status = "failed" if error is not None else "canceled" queue_item = self._set_queue_item_status(item_id=item_id, status=status, error=error) # type: ignore [arg-type] # mypy seems to not narrow the Literals here - self.__invoker.services.queue.cancel(queue_item.session_id) self.__invoker.services.events.emit_session_canceled( queue_item_id=queue_item.item_id, queue_id=queue_item.queue_id, @@ -471,7 +470,6 @@ class SqliteSessionQueue(SessionQueueBase): ) self.__conn.commit() if current_queue_item is not None and current_queue_item.batch_id in batch_ids: - self.__invoker.services.queue.cancel(current_queue_item.session_id) self.__invoker.services.events.emit_session_canceled( queue_item_id=current_queue_item.item_id, queue_id=current_queue_item.queue_id, @@ -523,7 +521,6 @@ class SqliteSessionQueue(SessionQueueBase): ) self.__conn.commit() if current_queue_item is not None and current_queue_item.queue_id == queue_id: - self.__invoker.services.queue.cancel(current_queue_item.session_id) self.__invoker.services.events.emit_session_canceled( queue_item_id=current_queue_item.item_id, queue_id=current_queue_item.queue_id, diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 43ecb2c543..4606bd9e03 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,6 +1,7 @@ +import threading from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Callable, Optional from PIL.Image import Image from torch import Tensor @@ -370,6 +371,12 @@ class ConfigInterface(InvocationContextInterface): class UtilInterface(InvocationContextInterface): + def __init__( + self, services: InvocationServices, context_data: InvocationContextData, is_canceled: Callable[[], bool] + ) -> None: + super().__init__(services, context_data) + self._is_canceled = is_canceled + def sd_step_callback(self, intermediate_state: PipelineIntermediateState, base_model: BaseModelType) -> None: """ The step callback emits a progress event with the current step, the total number of @@ -390,8 +397,8 @@ class UtilInterface(InvocationContextInterface): context_data=self._context_data, intermediate_state=intermediate_state, base_model=base_model, - invocation_queue=self._services.queue, events=self._services.events, + is_canceled=self._is_canceled, ) @@ -412,6 +419,7 @@ class InvocationContext: boards: BoardsInterface, context_data: InvocationContextData, services: InvocationServices, + is_canceled: Callable[[], bool], ) -> None: self.images = images """Provides methods to save, get and update images and their metadata.""" @@ -433,11 +441,13 @@ class InvocationContext: """Provides data about the current queue item and invocation. This is an internal API and may change without warning.""" self._services = services """Provides access to the full application services. This is an internal API and may change without warning.""" + self._is_canceled = is_canceled def build_invocation_context( services: InvocationServices, context_data: InvocationContextData, + cancel_event: threading.Event, ) -> InvocationContext: """ Builds the invocation context for a specific invocation execution. @@ -446,12 +456,15 @@ def build_invocation_context( :param invocation_context_data: The invocation context data. """ + def is_canceled() -> bool: + return cancel_event.is_set() + logger = LoggerInterface(services=services, context_data=context_data) images = ImagesInterface(services=services, context_data=context_data) tensors = TensorsInterface(services=services, context_data=context_data) models = ModelsInterface(services=services, context_data=context_data) config = ConfigInterface(services=services, context_data=context_data) - util = UtilInterface(services=services, context_data=context_data) + util = UtilInterface(services=services, context_data=context_data, is_canceled=is_canceled) conditioning = ConditioningInterface(services=services, context_data=context_data) boards = BoardsInterface(services=services, context_data=context_data) @@ -466,6 +479,7 @@ def build_invocation_context( conditioning=conditioning, services=services, boards=boards, + is_canceled=is_canceled, ) return ctx diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index 33d00ca366..9c9f5254a4 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -1,9 +1,9 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable import torch from PIL import Image -from invokeai.app.services.invocation_processor.invocation_processor_common import CanceledException, ProgressImage +from invokeai.app.services.session_processor.session_processor_common import CanceledException, ProgressImage from invokeai.backend.model_manager.config import BaseModelType from ...backend.stable_diffusion import PipelineIntermediateState @@ -11,7 +11,6 @@ from ...backend.util.util import image_to_dataURL if TYPE_CHECKING: from invokeai.app.services.events.events_base import EventServiceBase - from invokeai.app.services.invocation_queue.invocation_queue_base import InvocationQueueABC from invokeai.app.services.shared.invocation_context import InvocationContextData @@ -34,10 +33,10 @@ def stable_diffusion_step_callback( context_data: "InvocationContextData", intermediate_state: PipelineIntermediateState, base_model: BaseModelType, - invocation_queue: "InvocationQueueABC", events: "EventServiceBase", + is_canceled: Callable[[], bool], ) -> None: - if invocation_queue.is_canceled(context_data.session_id): + if is_canceled(): raise CanceledException # Some schedulers report not only the noisy latents at the current timestep, From 317d076a1acb67d3602f573b7c9c99c9791e70b1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 11:25:00 +1100 Subject: [PATCH 165/411] feat(nodes): promote `is_canceled` to public node API --- invokeai/app/services/shared/invocation_context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 4606bd9e03..317cbdbb23 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -437,11 +437,12 @@ class InvocationContext: """Provides utility methods.""" self.boards = boards """Provides methods to interact with boards.""" + self.is_canceled = is_canceled + """Checks if the current invocation has been canceled.""" self._data = context_data """Provides data about the current queue item and invocation. This is an internal API and may change without warning.""" self._services = services """Provides access to the full application services. This is an internal API and may change without warning.""" - self._is_canceled = is_canceled def build_invocation_context( @@ -457,6 +458,7 @@ def build_invocation_context( """ def is_canceled() -> bool: + """Checks if the current invocation has been canceled.""" return cancel_event.is_set() logger = LoggerInterface(services=services, context_data=context_data) From 0788b6eceefcdaf44012abfcd9944c624f685615 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 11:42:40 +1100 Subject: [PATCH 166/411] chore(nodes): add comments for cancel state --- .../session_processor/session_processor_default.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index dd34c78252..2ff76c06e4 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -192,9 +192,20 @@ class DefaultSessionProcessor(SessionProcessorBase): ) except KeyboardInterrupt: + # TODO(psyche): should we set the cancel event here and/or cancel the queue item? pass except CanceledException: + # When the user cancels the graph, we first set the cancel event. The event is checked + # between invocations, in this loop. Some invocations are long-running, and we need to + # be able to cancel them mid-execution. + # + # For example, denoising is a long-running invocation with many steps. A step callback + # is executed after each step. This step callback checks if the canceled event is set, + # then raises a CanceledException to stop execution immediately. + # + # When we get a CanceledException, we don't need to do anything - just pass and let the + # loop go to its next iteration, and the cancel event will be handled correctly. pass except Exception as e: From 3cfac8b843fd37acb49bf7680b1cb3307422f0f4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 11:42:53 +1100 Subject: [PATCH 167/411] feat(nodes): better invocation error messages --- .../services/session_processor/session_processor_default.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 2ff76c06e4..f235544405 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -213,7 +213,9 @@ class DefaultSessionProcessor(SessionProcessorBase): # Save error self._queue_item.session.set_node_error(invocation.id, error) - self._invoker.services.logger.error("Error while invoking:\n%s" % e) + self._invoker.services.logger.error( + f"Error while invoking session {self._queue_item.session_id}, invocation {invocation.id} ({invocation.get_type()}):\n{e}" + ) # Send error event self._invoker.services.events.emit_invocation_error( From 86c50f2d5b3c15eac98fa8acfcba4d897aeda194 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 11:44:08 +1100 Subject: [PATCH 168/411] tidy(nodes): remove extraneous comments --- invokeai/app/services/shared/invocation_context.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 317cbdbb23..6b6379dc5d 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -388,11 +388,6 @@ class UtilInterface(InvocationContextInterface): :param base_model: The base model for the current denoising step. """ - # The step callback needs access to the events and the invocation queue services, but this - # represents a dangerous level of access. - # - # We wrap the step callback so that nodes do not have direct access to these services. - stable_diffusion_step_callback( context_data=self._context_data, intermediate_state=intermediate_state, @@ -458,7 +453,6 @@ def build_invocation_context( """ def is_canceled() -> bool: - """Checks if the current invocation has been canceled.""" return cancel_event.is_set() logger = LoggerInterface(services=services, context_data=context_data) From 18adcc1dd26804e5b6e73a5b19fcbd25d81bf52e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 11:51:50 +1100 Subject: [PATCH 169/411] feat(nodes): add whole queue_item to InvocationContextData No reason to not have the whole thing in there. --- .../session_processor_default.py | 16 +++++--------- .../app/services/shared/invocation_context.py | 22 ++++++------------- invokeai/app/util/step_callback.py | 10 ++++----- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index f235544405..e49f79bcf3 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -139,7 +139,7 @@ class DefaultSessionProcessor(SessionProcessorBase): # Loop over invocations until the session is complete or canceled while invocation is not None and not cancel_event.is_set(): # get the source node id to provide to clients (the prepared node id is not as useful) - source_node_id = self._queue_item.session.prepared_source_mapping[invocation.id] + source_invocation_id = self._queue_item.session.prepared_source_mapping[invocation.id] # Send starting event self._invoker.services.events.emit_invocation_started( @@ -148,7 +148,7 @@ class DefaultSessionProcessor(SessionProcessorBase): queue_id=self._queue_item.queue_id, graph_execution_state_id=self._queue_item.session_id, node=invocation.model_dump(), - source_node_id=source_node_id, + source_node_id=source_invocation_id, ) # Innermost processor try block; any unhandled exception is an invocation error & will fail the graph @@ -159,12 +159,8 @@ class DefaultSessionProcessor(SessionProcessorBase): # Build invocation context (the node-facing API) context_data = InvocationContextData( invocation=invocation, - source_node_id=source_node_id, - session_id=self._queue_item.session.id, - workflow=self._queue_item.workflow, - queue_id=self._queue_item.queue_id, - queue_item_id=self._queue_item.item_id, - batch_id=self._queue_item.batch_id, + source_invocation_id=source_invocation_id, + queue_item=self._queue_item, ) context = build_invocation_context( context_data=context_data, @@ -187,7 +183,7 @@ class DefaultSessionProcessor(SessionProcessorBase): queue_id=self._queue_item.queue_id, graph_execution_state_id=self._queue_item.session.id, node=invocation.model_dump(), - source_node_id=source_node_id, + source_node_id=source_invocation_id, result=outputs.model_dump(), ) @@ -224,7 +220,7 @@ class DefaultSessionProcessor(SessionProcessorBase): queue_id=self._queue_item.queue_id, graph_execution_state_id=self._queue_item.session.id, node=invocation.model_dump(), - source_node_id=source_node_id, + source_node_id=source_invocation_id, error_type=e.__class__.__name__, error=error, ) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 6b6379dc5d..6b314d10bf 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -13,7 +13,6 @@ from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invocation_services import InvocationServices -from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID from invokeai.app.util.step_callback import stable_diffusion_step_callback from invokeai.backend.model_manager.config import AnyModelConfig, BaseModelType, ModelFormat, ModelType, SubModelType from invokeai.backend.model_manager.load.load_base import LoadedModel @@ -23,6 +22,7 @@ from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Condit if TYPE_CHECKING: from invokeai.app.invocations.baseinvocation import BaseInvocation + from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem """ The InvocationContext provides access to various services and data about the current invocation. @@ -49,20 +49,12 @@ Note: The docstrings are in weird places, but that's where they must be to get I @dataclass class InvocationContextData: + queue_item: "SessionQueueItem" + """The queue item that is being executed.""" invocation: "BaseInvocation" """The invocation that is being executed.""" - session_id: str - """The session that is being executed.""" - queue_id: str - """The queue in which the session is being executed.""" - source_node_id: str - """The ID of the node from which the currently executing invocation was prepared.""" - queue_item_id: int - """The ID of the queue item that is being executed.""" - batch_id: str - """The ID of the batch that is being executed.""" - workflow: Optional[WorkflowWithoutID] = None - """The workflow associated with this queue item, if any.""" + source_invocation_id: str + """The ID of the invocation from which the currently executing invocation was prepared.""" class InvocationContextInterface: @@ -191,8 +183,8 @@ class ImagesInterface(InvocationContextInterface): board_id=board_id_, metadata=metadata_, image_origin=ResourceOrigin.INTERNAL, - workflow=self._context_data.workflow, - session_id=self._context_data.session_id, + workflow=self._context_data.queue_item.workflow, + session_id=self._context_data.queue_item.session_id, node_id=self._context_data.invocation.id, ) diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index 9c9f5254a4..8cb59f5b3a 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -114,12 +114,12 @@ def stable_diffusion_step_callback( dataURL = image_to_dataURL(image, image_format="JPEG") events.emit_generator_progress( - queue_id=context_data.queue_id, - queue_item_id=context_data.queue_item_id, - queue_batch_id=context_data.batch_id, - graph_execution_state_id=context_data.session_id, + queue_id=context_data.queue_item.queue_id, + queue_item_id=context_data.queue_item.item_id, + queue_batch_id=context_data.queue_item.batch_id, + graph_execution_state_id=context_data.queue_item.session_id, node_id=context_data.invocation.id, - source_node_id=context_data.source_node_id, + source_node_id=context_data.source_invocation_id, progress_image=ProgressImage(width=width, height=height, dataURL=dataURL), step=intermediate_state.step, order=intermediate_state.order, From fdac0c3c9ba9dbd52534f8caa1b1492640cf6a5c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 11:54:16 +1100 Subject: [PATCH 170/411] refactor(nodes): move is_canceled to `context.util` --- .../app/services/shared/invocation_context.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 6b314d10bf..994c99dc45 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -1,7 +1,7 @@ import threading from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING, Optional from PIL.Image import Image from torch import Tensor @@ -364,10 +364,14 @@ class ConfigInterface(InvocationContextInterface): class UtilInterface(InvocationContextInterface): def __init__( - self, services: InvocationServices, context_data: InvocationContextData, is_canceled: Callable[[], bool] + self, services: InvocationServices, context_data: InvocationContextData, cancel_event: threading.Event ) -> None: super().__init__(services, context_data) - self._is_canceled = is_canceled + self._cancel_event = cancel_event + + def is_canceled(self) -> bool: + """Checks if the current invocation has been canceled.""" + return self._cancel_event.is_set() def sd_step_callback(self, intermediate_state: PipelineIntermediateState, base_model: BaseModelType) -> None: """ @@ -385,7 +389,7 @@ class UtilInterface(InvocationContextInterface): intermediate_state=intermediate_state, base_model=base_model, events=self._services.events, - is_canceled=self._is_canceled, + is_canceled=self.is_canceled, ) @@ -406,7 +410,6 @@ class InvocationContext: boards: BoardsInterface, context_data: InvocationContextData, services: InvocationServices, - is_canceled: Callable[[], bool], ) -> None: self.images = images """Provides methods to save, get and update images and their metadata.""" @@ -424,8 +427,6 @@ class InvocationContext: """Provides utility methods.""" self.boards = boards """Provides methods to interact with boards.""" - self.is_canceled = is_canceled - """Checks if the current invocation has been canceled.""" self._data = context_data """Provides data about the current queue item and invocation. This is an internal API and may change without warning.""" self._services = services @@ -444,15 +445,12 @@ def build_invocation_context( :param invocation_context_data: The invocation context data. """ - def is_canceled() -> bool: - return cancel_event.is_set() - logger = LoggerInterface(services=services, context_data=context_data) images = ImagesInterface(services=services, context_data=context_data) tensors = TensorsInterface(services=services, context_data=context_data) models = ModelsInterface(services=services, context_data=context_data) config = ConfigInterface(services=services, context_data=context_data) - util = UtilInterface(services=services, context_data=context_data, is_canceled=is_canceled) + util = UtilInterface(services=services, context_data=context_data, cancel_event=cancel_event) conditioning = ConditioningInterface(services=services, context_data=context_data) boards = BoardsInterface(services=services, context_data=context_data) @@ -467,7 +465,6 @@ def build_invocation_context( conditioning=conditioning, services=services, boards=boards, - is_canceled=is_canceled, ) return ctx From ccfe6b6befabeabe6e5956639448c96167eab5f1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 11:56:54 +1100 Subject: [PATCH 171/411] chore(nodes): "context_data" -> "data" Changed within InvocationContext, for brevity. --- .../session_processor_default.py | 4 +- .../app/services/shared/invocation_context.py | 54 +++++++++---------- tests/test_graph_execution_state.py | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index e49f79bcf3..dc08fc8345 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -157,13 +157,13 @@ class DefaultSessionProcessor(SessionProcessorBase): invocation, self._queue_item.session.id ): # Build invocation context (the node-facing API) - context_data = InvocationContextData( + data = InvocationContextData( invocation=invocation, source_invocation_id=source_invocation_id, queue_item=self._queue_item, ) context = build_invocation_context( - context_data=context_data, + data=data, services=self._invoker.services, cancel_event=self._cancel_event, ) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 994c99dc45..f8425523bf 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -58,9 +58,9 @@ class InvocationContextData: class InvocationContextInterface: - def __init__(self, services: InvocationServices, context_data: InvocationContextData) -> None: + def __init__(self, services: InvocationServices, data: InvocationContextData) -> None: self._services = services - self._context_data = context_data + self._data = data class BoardsInterface(InvocationContextInterface): @@ -166,26 +166,26 @@ class ImagesInterface(InvocationContextInterface): metadata_ = None if metadata: metadata_ = metadata - elif isinstance(self._context_data.invocation, WithMetadata): - metadata_ = self._context_data.invocation.metadata + elif isinstance(self._data.invocation, WithMetadata): + metadata_ = self._data.invocation.metadata # If `board_id` is provided directly, use that. Else, use the board provided by `WithBoard`, falling back to None. board_id_ = None if board_id: board_id_ = board_id - elif isinstance(self._context_data.invocation, WithBoard) and self._context_data.invocation.board: - board_id_ = self._context_data.invocation.board.board_id + elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board: + board_id_ = self._data.invocation.board.board_id return self._services.images.create( image=image, - is_intermediate=self._context_data.invocation.is_intermediate, + is_intermediate=self._data.invocation.is_intermediate, image_category=image_category, board_id=board_id_, metadata=metadata_, image_origin=ResourceOrigin.INTERNAL, - workflow=self._context_data.queue_item.workflow, - session_id=self._context_data.queue_item.session_id, - node_id=self._context_data.invocation.id, + workflow=self._data.queue_item.workflow, + session_id=self._data.queue_item.session_id, + node_id=self._data.invocation.id, ) def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image: @@ -285,7 +285,7 @@ class ModelsInterface(InvocationContextInterface): # the event payloads. return self._services.model_manager.load_model_by_key( - key=key, submodel_type=submodel_type, context_data=self._context_data + key=key, submodel_type=submodel_type, context_data=self._data ) def load_by_attrs( @@ -304,7 +304,7 @@ class ModelsInterface(InvocationContextInterface): base_model=base_model, model_type=model_type, submodel=submodel, - context_data=self._context_data, + context_data=self._data, ) def get_config(self, key: str) -> AnyModelConfig: @@ -364,9 +364,9 @@ class ConfigInterface(InvocationContextInterface): class UtilInterface(InvocationContextInterface): def __init__( - self, services: InvocationServices, context_data: InvocationContextData, cancel_event: threading.Event + self, services: InvocationServices, data: InvocationContextData, cancel_event: threading.Event ) -> None: - super().__init__(services, context_data) + super().__init__(services, data) self._cancel_event = cancel_event def is_canceled(self) -> bool: @@ -385,7 +385,7 @@ class UtilInterface(InvocationContextInterface): """ stable_diffusion_step_callback( - context_data=self._context_data, + context_data=self._data, intermediate_state=intermediate_state, base_model=base_model, events=self._services.events, @@ -408,7 +408,7 @@ class InvocationContext: config: ConfigInterface, util: UtilInterface, boards: BoardsInterface, - context_data: InvocationContextData, + data: InvocationContextData, services: InvocationServices, ) -> None: self.images = images @@ -427,7 +427,7 @@ class InvocationContext: """Provides utility methods.""" self.boards = boards """Provides methods to interact with boards.""" - self._data = context_data + self._data = data """Provides data about the current queue item and invocation. This is an internal API and may change without warning.""" self._services = services """Provides access to the full application services. This is an internal API and may change without warning.""" @@ -435,7 +435,7 @@ class InvocationContext: def build_invocation_context( services: InvocationServices, - context_data: InvocationContextData, + data: InvocationContextData, cancel_event: threading.Event, ) -> InvocationContext: """ @@ -445,14 +445,14 @@ def build_invocation_context( :param invocation_context_data: The invocation context data. """ - logger = LoggerInterface(services=services, context_data=context_data) - images = ImagesInterface(services=services, context_data=context_data) - tensors = TensorsInterface(services=services, context_data=context_data) - models = ModelsInterface(services=services, context_data=context_data) - config = ConfigInterface(services=services, context_data=context_data) - util = UtilInterface(services=services, context_data=context_data, cancel_event=cancel_event) - conditioning = ConditioningInterface(services=services, context_data=context_data) - boards = BoardsInterface(services=services, context_data=context_data) + logger = LoggerInterface(services=services, data=data) + images = ImagesInterface(services=services, data=data) + tensors = TensorsInterface(services=services, data=data) + models = ModelsInterface(services=services, data=data) + config = ConfigInterface(services=services, data=data) + util = UtilInterface(services=services, data=data, cancel_event=cancel_event) + conditioning = ConditioningInterface(services=services, data=data) + boards = BoardsInterface(services=services, data=data) ctx = InvocationContext( images=images, @@ -460,7 +460,7 @@ def build_invocation_context( config=config, tensors=tensors, models=models, - context_data=context_data, + data=data, util=util, conditioning=conditioning, services=services, diff --git a/tests/test_graph_execution_state.py b/tests/test_graph_execution_state.py index f839a4a878..9cff502acf 100644 --- a/tests/test_graph_execution_state.py +++ b/tests/test_graph_execution_state.py @@ -86,7 +86,7 @@ def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[B InvocationContext( conditioning=None, config=None, - context_data=None, + data=None, images=None, tensors=None, logger=None, From d53a2a2d4eb7b21d6c84f80b484407da4ee67ff9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:03:43 +1100 Subject: [PATCH 172/411] chore(nodes): better comments for invocation context --- .../app/services/shared/invocation_context.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index f8425523bf..31064a5e7c 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -247,7 +247,7 @@ class ConditioningInterface(InvocationContextInterface): """ Saves a conditioning data object, returning its name. - :param conditioning_context_data: The conditioning data to save. + :param conditioning_data: The conditioning data to save. """ name = self._services.conditioning.save(obj=conditioning_data) @@ -412,25 +412,25 @@ class InvocationContext: services: InvocationServices, ) -> None: self.images = images - """Provides methods to save, get and update images and their metadata.""" + """Methods to save, get and update images and their metadata.""" self.tensors = tensors - """Provides methods to save and get tensors, including image, noise, masks, and masked images.""" + """Methods to save and get tensors, including image, noise, masks, and masked images.""" self.conditioning = conditioning - """Provides methods to save and get conditioning data.""" + """Methods to save and get conditioning data.""" self.models = models - """Provides methods to check if a model exists, get a model, and get a model's info.""" + """Methods to check if a model exists, get a model, and get a model's info.""" self.logger = logger - """Provides access to the app logger.""" + """The app logger.""" self.config = config - """Provides access to the app's config.""" + """The app config.""" self.util = util - """Provides utility methods.""" + """Utility methods, including a method to check if an invocation was canceled and step callbacks.""" self.boards = boards - """Provides methods to interact with boards.""" + """Methods to interact with boards.""" self._data = data - """Provides data about the current queue item and invocation. This is an internal API and may change without warning.""" + """An internal API providing access to data about the current queue item and invocation. You probably shouldn't use this. It may change without warning.""" self._services = services - """Provides access to the full application services. This is an internal API and may change without warning.""" + """An internal API providing access to all application services. You probably shouldn't use this. It may change without warning.""" def build_invocation_context( @@ -441,8 +441,8 @@ def build_invocation_context( """ Builds the invocation context for a specific invocation execution. - :param invocation_services: The invocation services to wrap. - :param invocation_context_data: The invocation context data. + :param services: The invocation services to wrap. + :param data: The invocation context data. """ logger = LoggerInterface(services=services, data=data) From 0788a27a8042fd73cbff2dca435bd52a327e0cb6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:29:08 +1100 Subject: [PATCH 173/411] tests(nodes): fix tests following removal of services --- tests/test_graph_execution_state.py | 89 ++++++--------- tests/test_invoker.py | 163 ---------------------------- 2 files changed, 35 insertions(+), 217 deletions(-) delete mode 100644 tests/test_invoker.py diff --git a/tests/test_graph_execution_state.py b/tests/test_graph_execution_state.py index 9cff502acf..2e88178424 100644 --- a/tests/test_graph_execution_state.py +++ b/tests/test_graph_execution_state.py @@ -1,9 +1,9 @@ import logging +from typing import Optional +from unittest.mock import Mock import pytest -from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory - # This import must happen before other invoke imports or test in other files(!!) break from .test_nodes import ( # isort: split PromptCollectionTestInvocation, @@ -17,8 +17,6 @@ from invokeai.app.invocations.collections import RangeInvocation from invokeai.app.invocations.math import AddInvocation, MultiplyInvocation from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache -from invokeai.app.services.invocation_processor.invocation_processor_default import DefaultInvocationProcessor -from invokeai.app.services.invocation_queue.invocation_queue_memory import MemoryInvocationQueue from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService from invokeai.app.services.shared.graph import ( @@ -28,11 +26,11 @@ from invokeai.app.services.shared.graph import ( IterateInvocation, ) -from .test_invoker import create_edge +from .test_nodes import create_edge @pytest.fixture -def simple_graph(): +def simple_graph() -> Graph: g = Graph() g.add_node(PromptTestInvocation(id="1", prompt="Banana sushi")) g.add_node(TextToImageTestInvocation(id="2")) @@ -47,7 +45,6 @@ def simple_graph(): def mock_services() -> InvocationServices: configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) # NOTE: none of these are actually called by the test invocations - graph_execution_manager = ItemStorageMemory[GraphExecutionState]() return InvocationServices( board_image_records=None, # type: ignore board_images=None, # type: ignore @@ -55,7 +52,6 @@ def mock_services() -> InvocationServices: boards=None, # type: ignore configuration=configuration, events=TestEventService(), - graph_execution_manager=graph_execution_manager, image_files=None, # type: ignore image_records=None, # type: ignore images=None, # type: ignore @@ -65,47 +61,32 @@ def mock_services() -> InvocationServices: download_queue=None, # type: ignore names=None, # type: ignore performance_statistics=InvocationStatsService(), - processor=DefaultInvocationProcessor(), - queue=MemoryInvocationQueue(), session_processor=None, # type: ignore session_queue=None, # type: ignore urls=None, # type: ignore workflow_records=None, # type: ignore - tensors=None, - conditioning=None, + tensors=None, # type: ignore + conditioning=None, # type: ignore ) -def invoke_next(g: GraphExecutionState, services: InvocationServices) -> tuple[BaseInvocation, BaseInvocationOutput]: +def invoke_next(g: GraphExecutionState) -> tuple[Optional[BaseInvocation], Optional[BaseInvocationOutput]]: n = g.next() if n is None: return (None, None) print(f"invoking {n.id}: {type(n)}") - o = n.invoke( - InvocationContext( - conditioning=None, - config=None, - data=None, - images=None, - tensors=None, - logger=None, - models=None, - util=None, - boards=None, - services=None, - ) - ) + o = n.invoke(Mock(InvocationContext)) g.complete(n.id, o) return (n, o) -def test_graph_state_executes_in_order(simple_graph, mock_services): +def test_graph_state_executes_in_order(simple_graph: Graph): g = GraphExecutionState(graph=simple_graph) - n1 = invoke_next(g, mock_services) - n2 = invoke_next(g, mock_services) + n1 = invoke_next(g) + n2 = invoke_next(g) n3 = g.next() assert g.prepared_source_mapping[n1[0].id] == "1" @@ -115,18 +96,18 @@ def test_graph_state_executes_in_order(simple_graph, mock_services): assert n2[0].prompt == n1[0].prompt -def test_graph_is_complete(simple_graph, mock_services): +def test_graph_is_complete(simple_graph: Graph): g = GraphExecutionState(graph=simple_graph) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) + _ = invoke_next(g) + _ = invoke_next(g) _ = g.next() assert g.is_complete() -def test_graph_is_not_complete(simple_graph, mock_services): +def test_graph_is_not_complete(simple_graph: Graph): g = GraphExecutionState(graph=simple_graph) - _ = invoke_next(g, mock_services) + _ = invoke_next(g) _ = g.next() assert not g.is_complete() @@ -135,7 +116,7 @@ def test_graph_is_not_complete(simple_graph, mock_services): # TODO: test completion with iterators/subgraphs -def test_graph_state_expands_iterator(mock_services): +def test_graph_state_expands_iterator(): graph = Graph() graph.add_node(RangeInvocation(id="0", start=0, stop=3, step=1)) graph.add_node(IterateInvocation(id="1")) @@ -147,7 +128,7 @@ def test_graph_state_expands_iterator(mock_services): g = GraphExecutionState(graph=graph) while not g.is_complete(): - invoke_next(g, mock_services) + invoke_next(g) prepared_add_nodes = g.source_prepared_mapping["3"] results = {g.results[n].value for n in prepared_add_nodes} @@ -155,7 +136,7 @@ def test_graph_state_expands_iterator(mock_services): assert results == expected -def test_graph_state_collects(mock_services): +def test_graph_state_collects(): graph = Graph() test_prompts = ["Banana sushi", "Cat sushi"] graph.add_node(PromptCollectionTestInvocation(id="1", collection=list(test_prompts))) @@ -167,19 +148,19 @@ def test_graph_state_collects(mock_services): graph.add_edge(create_edge("3", "prompt", "4", "item")) g = GraphExecutionState(graph=graph) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) - n6 = invoke_next(g, mock_services) + _ = invoke_next(g) + _ = invoke_next(g) + _ = invoke_next(g) + _ = invoke_next(g) + _ = invoke_next(g) + n6 = invoke_next(g) assert isinstance(n6[0], CollectInvocation) assert sorted(g.results[n6[0].id].collection) == sorted(test_prompts) -def test_graph_state_prepares_eagerly(mock_services): +def test_graph_state_prepares_eagerly(): """Tests that all prepareable nodes are prepared""" graph = Graph() @@ -208,7 +189,7 @@ def test_graph_state_prepares_eagerly(mock_services): assert "prompt_iterated" not in g.source_prepared_mapping -def test_graph_executes_depth_first(mock_services): +def test_graph_executes_depth_first(): """Tests that the graph executes depth-first, executing a branch as far as possible before moving to the next branch""" graph = Graph() @@ -222,14 +203,14 @@ def test_graph_executes_depth_first(mock_services): graph.add_edge(create_edge("prompt_iterated", "prompt", "prompt_successor", "prompt")) g = GraphExecutionState(graph=graph) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) - _ = invoke_next(g, mock_services) + _ = invoke_next(g) + _ = invoke_next(g) + _ = invoke_next(g) + _ = invoke_next(g) # Because ordering is not guaranteed, we cannot compare results directly. # Instead, we must count the number of results. - def get_completed_count(g, id): + def get_completed_count(g: GraphExecutionState, id: str): ids = list(g.source_prepared_mapping[id]) completed_ids = [i for i in g.executed if i in ids] return len(completed_ids) @@ -238,17 +219,17 @@ def test_graph_executes_depth_first(mock_services): assert get_completed_count(g, "prompt_iterated") == 1 assert get_completed_count(g, "prompt_successor") == 0 - _ = invoke_next(g, mock_services) + _ = invoke_next(g) assert get_completed_count(g, "prompt_iterated") == 1 assert get_completed_count(g, "prompt_successor") == 1 - _ = invoke_next(g, mock_services) + _ = invoke_next(g) assert get_completed_count(g, "prompt_iterated") == 2 assert get_completed_count(g, "prompt_successor") == 1 - _ = invoke_next(g, mock_services) + _ = invoke_next(g) assert get_completed_count(g, "prompt_iterated") == 2 assert get_completed_count(g, "prompt_successor") == 2 diff --git a/tests/test_invoker.py b/tests/test_invoker.py deleted file mode 100644 index 38fcf859a5..0000000000 --- a/tests/test_invoker.py +++ /dev/null @@ -1,163 +0,0 @@ -import logging -from unittest.mock import Mock - -import pytest - -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory - -# This import must happen before other invoke imports or test in other files(!!) break -from .test_nodes import ( # isort: split - ErrorInvocation, - PromptTestInvocation, - TestEventService, - TextToImageTestInvocation, - create_edge, - wait_until, -) - -from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache -from invokeai.app.services.invocation_processor.invocation_processor_default import DefaultInvocationProcessor -from invokeai.app.services.invocation_queue.invocation_queue_memory import MemoryInvocationQueue -from invokeai.app.services.invocation_services import InvocationServices -from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService -from invokeai.app.services.invoker import Invoker -from invokeai.app.services.session_queue.session_queue_common import DEFAULT_QUEUE_ID -from invokeai.app.services.shared.graph import Graph, GraphExecutionState - - -@pytest.fixture -def simple_graph(): - g = Graph() - g.add_node(PromptTestInvocation(id="1", prompt="Banana sushi")) - g.add_node(TextToImageTestInvocation(id="2")) - g.add_edge(create_edge("1", "prompt", "2", "prompt")) - return g - - -# This must be defined here to avoid issues with the dynamic creation of the union of all invocation types -# Defining it in a separate module will cause the union to be incomplete, and pydantic will not validate -# the test invocations. -@pytest.fixture -def mock_services() -> InvocationServices: - configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) - return InvocationServices( - board_image_records=None, # type: ignore - board_images=None, # type: ignore - board_records=None, # type: ignore - boards=None, # type: ignore - configuration=configuration, - events=TestEventService(), - graph_execution_manager=ItemStorageMemory[GraphExecutionState](), - image_files=None, # type: ignore - image_records=None, # type: ignore - images=None, # type: ignore - invocation_cache=MemoryInvocationCache(max_cache_size=0), - logger=logging, # type: ignore - model_manager=Mock(), # type: ignore - download_queue=None, # type: ignore - names=None, # type: ignore - performance_statistics=InvocationStatsService(), - processor=DefaultInvocationProcessor(), - queue=MemoryInvocationQueue(), - session_processor=None, # type: ignore - session_queue=None, # type: ignore - urls=None, # type: ignore - workflow_records=None, # type: ignore - tensors=None, - conditioning=None, - ) - - -@pytest.fixture() -def mock_invoker(mock_services: InvocationServices) -> Invoker: - return Invoker(services=mock_services) - - -def test_can_create_graph_state(mock_invoker: Invoker): - g = mock_invoker.create_execution_state() - mock_invoker.stop() - - assert g is not None - assert isinstance(g, GraphExecutionState) - - -def test_can_create_graph_state_from_graph(mock_invoker: Invoker, simple_graph): - g = mock_invoker.create_execution_state(graph=simple_graph) - mock_invoker.stop() - - assert g is not None - assert isinstance(g, GraphExecutionState) - assert g.graph == simple_graph - - -# @pytest.mark.xfail(reason = "Requires fixing following the model manager refactor") -def test_can_invoke(mock_invoker: Invoker, simple_graph): - g = mock_invoker.create_execution_state(graph=simple_graph) - invocation_id = mock_invoker.invoke( - session_queue_batch_id="1", - session_queue_item_id=1, - session_queue_id=DEFAULT_QUEUE_ID, - graph_execution_state=g, - ) - assert invocation_id is not None - - def has_executed_any(g: GraphExecutionState): - g = mock_invoker.services.graph_execution_manager.get(g.id) - return len(g.executed) > 0 - - wait_until(lambda: has_executed_any(g), timeout=5, interval=1) - mock_invoker.stop() - - g = mock_invoker.services.graph_execution_manager.get(g.id) - assert len(g.executed) > 0 - - -# @pytest.mark.xfail(reason = "Requires fixing following the model manager refactor") -def test_can_invoke_all(mock_invoker: Invoker, simple_graph): - g = mock_invoker.create_execution_state(graph=simple_graph) - invocation_id = mock_invoker.invoke( - session_queue_batch_id="1", - session_queue_item_id=1, - session_queue_id=DEFAULT_QUEUE_ID, - graph_execution_state=g, - invoke_all=True, - ) - assert invocation_id is not None - - def has_executed_all(g: GraphExecutionState): - g = mock_invoker.services.graph_execution_manager.get(g.id) - return g.is_complete() - - wait_until(lambda: has_executed_all(g), timeout=5, interval=1) - mock_invoker.stop() - - g = mock_invoker.services.graph_execution_manager.get(g.id) - assert g.is_complete() - - -# @pytest.mark.xfail(reason = "Requires fixing following the model manager refactor") -def test_handles_errors(mock_invoker: Invoker): - g = mock_invoker.create_execution_state() - g.graph.add_node(ErrorInvocation(id="1")) - - mock_invoker.invoke( - session_queue_batch_id="1", - session_queue_item_id=1, - session_queue_id=DEFAULT_QUEUE_ID, - graph_execution_state=g, - invoke_all=True, - ) - - def has_executed_all(g: GraphExecutionState): - g = mock_invoker.services.graph_execution_manager.get(g.id) - return g.is_complete() - - wait_until(lambda: has_executed_all(g), timeout=5, interval=1) - mock_invoker.stop() - - g = mock_invoker.services.graph_execution_manager.get(g.id) - assert g.has_error() - assert g.is_complete() - - assert all((i in g.errors for i in g.source_prepared_mapping["1"])) From 16676feea84663b6075a42ef8bd3641ca4d7fc7e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:33:16 +1100 Subject: [PATCH 174/411] feat(nodes): make processor thread limit and polling interval configurable --- .../session_processor_default.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index dc08fc8345..3035a74a5a 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -18,12 +18,9 @@ from ..invoker import Invoker from .session_processor_base import SessionProcessorBase from .session_processor_common import SessionProcessorStatus -POLLING_INTERVAL = 1 -THREAD_LIMIT = 1 - class DefaultSessionProcessor(SessionProcessorBase): - def start(self, invoker: Invoker) -> None: + def start(self, invoker: Invoker, thread_limit: int = 1, polling_interval: int = 1) -> None: self._invoker: Invoker = invoker self._queue_item: Optional[SessionQueueItem] = None @@ -34,7 +31,10 @@ class DefaultSessionProcessor(SessionProcessorBase): local_handler.register(event_name=EventServiceBase.queue_event, _func=self._on_queue_event) - self._thread_limit = BoundedSemaphore(THREAD_LIMIT) + self._thread_limit = thread_limit + self._thread_semaphore = BoundedSemaphore(thread_limit) + self._polling_interval = polling_interval + self._thread = Thread( name="session_processor", target=self._process, @@ -88,7 +88,7 @@ class DefaultSessionProcessor(SessionProcessorBase): ): # Outermost processor try block; any unhandled exception is a fatal processor error try: - self._thread_limit.acquire() + self._thread_semaphore.acquire() stop_event.clear() resume_event.set() cancel_event.clear() @@ -247,7 +247,7 @@ class DefaultSessionProcessor(SessionProcessorBase): else: # The queue was empty, wait for next polling interval or event to try again self._invoker.services.logger.debug("Waiting for next polling interval or event") - poll_now_event.wait(POLLING_INTERVAL) + poll_now_event.wait(self._polling_interval) continue except Exception as e: # Non-fatal error in processor, cancel the queue item and wait for next polling interval or event @@ -256,7 +256,7 @@ class DefaultSessionProcessor(SessionProcessorBase): self._invoker.services.session_queue.cancel_queue_item( self._queue_item.item_id, error=traceback.format_exc() ) - poll_now_event.wait(POLLING_INTERVAL) + poll_now_event.wait(self._polling_interval) continue except Exception as e: # Fatal error in processor, log and pass - we're done here @@ -266,4 +266,4 @@ class DefaultSessionProcessor(SessionProcessorBase): stop_event.clear() poll_now_event.clear() self._queue_item = None - self._thread_limit.release() + self._thread_semaphore.release() From fa39523b110ce76c15a33d2e542eff9ad3fbc290 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 15:58:53 +1100 Subject: [PATCH 175/411] feat(nodes): improved error messages in processor --- .../session_processor/session_processor_default.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 3035a74a5a..7d761e627f 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -249,18 +249,20 @@ class DefaultSessionProcessor(SessionProcessorBase): self._invoker.services.logger.debug("Waiting for next polling interval or event") poll_now_event.wait(self._polling_interval) continue - except Exception as e: + except Exception: # Non-fatal error in processor, cancel the queue item and wait for next polling interval or event - self._invoker.services.logger.error(f"Error in session processor: {e}") + self._invoker.services.logger.error( + f"Non-fatal error in session processor:\n{traceback.format_exc()}" + ) if self._queue_item is not None: self._invoker.services.session_queue.cancel_queue_item( self._queue_item.item_id, error=traceback.format_exc() ) poll_now_event.wait(self._polling_interval) continue - except Exception as e: + except Exception: # Fatal error in processor, log and pass - we're done here - self._invoker.services.logger.error(f"Fatal Error in session processor: {e}") + self._invoker.services.logger.error(f"Fatal Error in session processor:\n{traceback.format_exc()}") pass finally: stop_event.clear() From 0b0cb0ccc66e00a037da4d691fef3b0c643ffa2e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 16:47:55 +1100 Subject: [PATCH 176/411] feat(nodes): making invocation class var in processor --- .../session_processor_default.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 7d761e627f..9ba726bff3 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -7,6 +7,7 @@ from typing import Optional from fastapi_events.handlers.local import local_handler from fastapi_events.typing import Event as FastAPIEvent +from invokeai.app.invocations.baseinvocation import BaseInvocation from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invocation_stats.invocation_stats_common import GESStatsNotFoundError from invokeai.app.services.session_processor.session_processor_common import CanceledException @@ -23,6 +24,7 @@ class DefaultSessionProcessor(SessionProcessorBase): def start(self, invoker: Invoker, thread_limit: int = 1, polling_interval: int = 1) -> None: self._invoker: Invoker = invoker self._queue_item: Optional[SessionQueueItem] = None + self._invocation: Optional[BaseInvocation] = None self._resume_event = ThreadEvent() self._stop_event = ThreadEvent() @@ -134,12 +136,12 @@ class DefaultSessionProcessor(SessionProcessorBase): profiler.start(profile_id=self._queue_item.session_id) # Prepare invocations and take the first - invocation = self._queue_item.session.next() + self._invocation = self._queue_item.session.next() # Loop over invocations until the session is complete or canceled - while invocation is not None and not cancel_event.is_set(): + while self._invocation is not None and not cancel_event.is_set(): # get the source node id to provide to clients (the prepared node id is not as useful) - source_invocation_id = self._queue_item.session.prepared_source_mapping[invocation.id] + source_invocation_id = self._queue_item.session.prepared_source_mapping[self._invocation.id] # Send starting event self._invoker.services.events.emit_invocation_started( @@ -147,18 +149,18 @@ class DefaultSessionProcessor(SessionProcessorBase): queue_item_id=self._queue_item.item_id, queue_id=self._queue_item.queue_id, graph_execution_state_id=self._queue_item.session_id, - node=invocation.model_dump(), + node=self._invocation.model_dump(), source_node_id=source_invocation_id, ) # Innermost processor try block; any unhandled exception is an invocation error & will fail the graph try: with self._invoker.services.performance_statistics.collect_stats( - invocation, self._queue_item.session.id + self._invocation, self._queue_item.session.id ): # Build invocation context (the node-facing API) data = InvocationContextData( - invocation=invocation, + invocation=self._invocation, source_invocation_id=source_invocation_id, queue_item=self._queue_item, ) @@ -169,12 +171,12 @@ class DefaultSessionProcessor(SessionProcessorBase): ) # Invoke the node - outputs = invocation.invoke_internal( + outputs = self._invocation.invoke_internal( context=context, services=self._invoker.services ) # Save outputs and history - self._queue_item.session.complete(invocation.id, outputs) + self._queue_item.session.complete(self._invocation.id, outputs) # Send complete event self._invoker.services.events.emit_invocation_complete( @@ -182,7 +184,7 @@ class DefaultSessionProcessor(SessionProcessorBase): queue_item_id=self._queue_item.item_id, queue_id=self._queue_item.queue_id, graph_execution_state_id=self._queue_item.session.id, - node=invocation.model_dump(), + node=self._invocation.model_dump(), source_node_id=source_invocation_id, result=outputs.model_dump(), ) @@ -208,9 +210,9 @@ class DefaultSessionProcessor(SessionProcessorBase): error = traceback.format_exc() # Save error - self._queue_item.session.set_node_error(invocation.id, error) + self._queue_item.session.set_node_error(self._invocation.id, error) self._invoker.services.logger.error( - f"Error while invoking session {self._queue_item.session_id}, invocation {invocation.id} ({invocation.get_type()}):\n{e}" + f"Error while invoking session {self._queue_item.session_id}, invocation {self._invocation.id} ({self._invocation.get_type()}):\n{e}" ) # Send error event @@ -219,7 +221,7 @@ class DefaultSessionProcessor(SessionProcessorBase): queue_item_id=self._queue_item.item_id, queue_id=self._queue_item.queue_id, graph_execution_state_id=self._queue_item.session.id, - node=invocation.model_dump(), + node=self._invocation.model_dump(), source_node_id=source_invocation_id, error_type=e.__class__.__name__, error=error, @@ -236,10 +238,10 @@ class DefaultSessionProcessor(SessionProcessorBase): ) # Save the stats and stop the profiler if it's running stats_cleanup(self._queue_item.session.id) - invocation = None + self._invocation = None else: # Prepare the next invocation - invocation = self._queue_item.session.next() + self._invocation = self._queue_item.session.next() # The session is complete, immediately poll for next session self._queue_item = None From 8bf9fd34ad19d07d33931bd711aac2191dfaa92f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 18 Feb 2024 16:51:58 +1100 Subject: [PATCH 177/411] fix(nodes): fix model load events was accessing incorrect properties in event data --- .../services/model_load/model_load_default.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py index 24ab10b427..3ff7898c0e 100644 --- a/invokeai/app/services/model_load/model_load_default.py +++ b/invokeai/app/services/model_load/model_load_default.py @@ -97,17 +97,17 @@ class ModelLoadService(ModelLoadServiceBase): if not loaded: self._invoker.services.events.emit_model_load_started( - queue_id=context_data.queue_id, - queue_item_id=context_data.queue_item_id, - queue_batch_id=context_data.batch_id, - graph_execution_state_id=context_data.session_id, + queue_id=context_data.queue_item.queue_id, + queue_item_id=context_data.queue_item.item_id, + queue_batch_id=context_data.queue_item.batch_id, + graph_execution_state_id=context_data.queue_item.session_id, model_config=model_config, ) else: self._invoker.services.events.emit_model_load_completed( - queue_id=context_data.queue_id, - queue_item_id=context_data.queue_item_id, - queue_batch_id=context_data.batch_id, - graph_execution_state_id=context_data.session_id, + queue_id=context_data.queue_item.queue_id, + queue_item_id=context_data.queue_item.item_id, + queue_batch_id=context_data.queue_item.batch_id, + graph_execution_state_id=context_data.queue_item.session_id, model_config=model_config, ) From 763debdeebeb80133cb1be79fb57f279d5513292 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:57:05 +1100 Subject: [PATCH 178/411] fix(nodes): fix typing on stats service context manager --- .../app/services/invocation_stats/invocation_stats_base.py | 4 ++-- .../app/services/invocation_stats/invocation_stats_default.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/invocation_stats/invocation_stats_base.py b/invokeai/app/services/invocation_stats/invocation_stats_base.py index b28220e74c..3266d985fe 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_base.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_base.py @@ -30,7 +30,7 @@ writes to the system log is stored in InvocationServices.performance_statistics. from abc import ABC, abstractmethod from pathlib import Path -from typing import Iterator +from typing import ContextManager from invokeai.app.invocations.baseinvocation import BaseInvocation from invokeai.app.services.invocation_stats.invocation_stats_common import InvocationStatsSummary @@ -50,7 +50,7 @@ class InvocationStatsServiceBase(ABC): self, invocation: BaseInvocation, graph_execution_state_id: str, - ) -> Iterator[None]: + ) -> ContextManager[None]: """ Return a context object that will capture the statistics on the execution of invocaation. Use with: to place around the part of the code that executes the invocation. diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index 06a5b675c3..5a41f1f5d6 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -2,7 +2,7 @@ import json import time from contextlib import contextmanager from pathlib import Path -from typing import Iterator +from typing import Generator import psutil import torch @@ -41,7 +41,7 @@ class InvocationStatsService(InvocationStatsServiceBase): self._invoker = invoker @contextmanager - def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str) -> Iterator[None]: + def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str) -> Generator[None, None, None]: # This is to handle case of the model manager not being initialized, which happens # during some tests. services = self._invoker.services From e3f9da29ba4580c7e9a9504b15d8d00f3846f46d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:09:00 +1100 Subject: [PATCH 179/411] tidy(nodes): clean up profiler/stats in processor, better comments --- .../session_processor_default.py | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index 9ba726bff3..cff7bb6c6c 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -37,6 +37,18 @@ class DefaultSessionProcessor(SessionProcessorBase): self._thread_semaphore = BoundedSemaphore(thread_limit) self._polling_interval = polling_interval + # If profiling is enabled, create a profiler. The same profiler will be used for all sessions. Internally, + # the profiler will create a new profile for each session. + self._profiler = ( + Profiler( + logger=self._invoker.services.logger, + output_dir=self._invoker.services.configuration.profiles_path, + prefix=self._invoker.services.configuration.profile_prefix, + ) + if self._invoker.services.configuration.profile_graphs + else None + ) + self._thread = Thread( name="session_processor", target=self._process, @@ -95,32 +107,6 @@ class DefaultSessionProcessor(SessionProcessorBase): resume_event.set() cancel_event.clear() - # If profiling is enabled, create a profiler. The same profiler will be used for all sessions. Internally, - # the profiler will create a new profile for each session. - profiler = ( - Profiler( - logger=self._invoker.services.logger, - output_dir=self._invoker.services.configuration.profiles_path, - prefix=self._invoker.services.configuration.profile_prefix, - ) - if self._invoker.services.configuration.profile_graphs - else None - ) - - # Helper function to stop the profiler and save the stats - def stats_cleanup(graph_execution_state_id: str) -> None: - if profiler: - profile_path = profiler.stop() - stats_path = profile_path.with_suffix(".json") - self._invoker.services.performance_statistics.dump_stats( - graph_execution_state_id=graph_execution_state_id, output_path=stats_path - ) - # We'll get a GESStatsNotFoundError if we try to log stats for an untracked graph, but in the processor - # we don't care about that - suppress the error. - with suppress(GESStatsNotFoundError): - self._invoker.services.performance_statistics.log_stats(graph_execution_state_id) - self._invoker.services.performance_statistics.reset_stats() - while not stop_event.is_set(): poll_now_event.clear() # Middle processor try block; any unhandled exception is a non-fatal processor error @@ -132,8 +118,8 @@ class DefaultSessionProcessor(SessionProcessorBase): cancel_event.clear() # If profiling is enabled, start the profiler - if profiler is not None: - profiler.start(profile_id=self._queue_item.session_id) + if self._profiler is not None: + self._profiler.start(profile_id=self._queue_item.session_id) # Prepare invocations and take the first self._invocation = self._queue_item.session.next() @@ -228,6 +214,7 @@ class DefaultSessionProcessor(SessionProcessorBase): ) pass + # The session is complete if the all invocations are complete or there was an error if self._queue_item.session.is_complete() or cancel_event.is_set(): # Send complete event self._invoker.services.events.emit_graph_execution_complete( @@ -236,8 +223,20 @@ class DefaultSessionProcessor(SessionProcessorBase): queue_id=self._queue_item.queue_id, graph_execution_state_id=self._queue_item.session.id, ) - # Save the stats and stop the profiler if it's running - stats_cleanup(self._queue_item.session.id) + # If we are profiling, stop the profiler and dump the profile & stats + if self._profiler: + profile_path = self._profiler.stop() + stats_path = profile_path.with_suffix(".json") + self._invoker.services.performance_statistics.dump_stats( + graph_execution_state_id=self._queue_item.session.id, output_path=stats_path + ) + # We'll get a GESStatsNotFoundError if we try to log stats for an untracked graph, but in the processor + # we don't care about that - suppress the error. + with suppress(GESStatsNotFoundError): + self._invoker.services.performance_statistics.log_stats(self._queue_item.session.id) + self._invoker.services.performance_statistics.reset_stats() + + # Set the invocation to None to prepare for the next session self._invocation = None else: # Prepare the next invocation @@ -252,14 +251,18 @@ class DefaultSessionProcessor(SessionProcessorBase): poll_now_event.wait(self._polling_interval) continue except Exception: - # Non-fatal error in processor, cancel the queue item and wait for next polling interval or event + # Non-fatal error in processor self._invoker.services.logger.error( f"Non-fatal error in session processor:\n{traceback.format_exc()}" ) + # Cancel the queue item if self._queue_item is not None: self._invoker.services.session_queue.cancel_queue_item( self._queue_item.item_id, error=traceback.format_exc() ) + # Reset the invocation to None to prepare for the next session + self._invocation = None + # Immediately poll for next queue item poll_now_event.wait(self._polling_interval) continue except Exception: From 89fa36a818d12ef572cc66c81180a96530640e8b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:16:06 +1100 Subject: [PATCH 180/411] chore(nodes): update TODO comment --- .../app/services/session_processor/session_processor_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py index cff7bb6c6c..c0b98220c8 100644 --- a/invokeai/app/services/session_processor/session_processor_default.py +++ b/invokeai/app/services/session_processor/session_processor_default.py @@ -176,7 +176,7 @@ class DefaultSessionProcessor(SessionProcessorBase): ) except KeyboardInterrupt: - # TODO(psyche): should we set the cancel event here and/or cancel the queue item? + # TODO(MM2): Create an event for this pass except CanceledException: From 5b133ad198b8551c7fa0acb330d50025227c4dcf Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Mon, 19 Feb 2024 11:40:53 -0500 Subject: [PATCH 181/411] Add a few convenience targets to Makefile - "test" to run pytests - "frontend-install" to reinstall pnpm's node modeuls --- Makefile | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 10d7a257c5..c3eec094f7 100644 --- a/Makefile +++ b/Makefile @@ -6,33 +6,44 @@ default: help help: @echo Developer commands: @echo - @echo "ruff Run ruff, fixing any safely-fixable errors and formatting" - @echo "ruff-unsafe Run ruff, fixing all fixable errors and formatting" - @echo "mypy Run mypy using the config in pyproject.toml to identify type mismatches and other coding errors" - @echo "mypy-all Run mypy ignoring the config in pyproject.tom but still ignoring missing imports" - @echo "frontend-build Build the frontend in order to run on localhost:9090" - @echo "frontend-dev Run the frontend in developer mode on localhost:5173" - @echo "installer-zip Build the installer .zip file for the current version" - @echo "tag-release Tag the GitHub repository with the current version (use at release time only!)" + @echo "ruff Run ruff, fixing any safely-fixable errors and formatting" + @echo "ruff-unsafe Run ruff, fixing all fixable errors and formatting" + @echo "mypy Run mypy using the config in pyproject.toml to identify type mismatches and other coding errors" + @echo "mypy-all Run mypy ignoring the config in pyproject.tom but still ignoring missing imports" + @echo "test" Run the unit tests. + @echo "frontend-install" Install the pnpm modules needed for the front end + @echo "frontend-build Build the frontend in order to run on localhost:9090" + @echo "frontend-dev Run the frontend in developer mode on localhost:5173" + @echo "installer-zip Build the installer .zip file for the current version" + @echo "tag-release Tag the GitHub repository with the current version (use at release time only!)" # Runs ruff, fixing any safely-fixable errors and formatting ruff: - ruff check . --fix - ruff format . + ruff check . --fix + ruff format . # Runs ruff, fixing all errors it can fix and formatting ruff-unsafe: - ruff check . --fix --unsafe-fixes - ruff format . + ruff check . --fix --unsafe-fixes + ruff format . # Runs mypy, using the config in pyproject.toml mypy: - mypy scripts/invokeai-web.py + mypy scripts/invokeai-web.py # Runs mypy, ignoring the config in pyproject.toml but still ignoring missing (untyped) imports # (many files are ignored by the config, so this is useful for checking all files) mypy-all: - mypy scripts/invokeai-web.py --config-file= --ignore-missing-imports + mypy scripts/invokeai-web.py --config-file= --ignore-missing-imports + +# Run the unit tests +test: + pytest ./tests + +# Install the pnpm modules needed for the front end +frontend-install: + rm -rf invokeai/frontend/web/node_modules + cd invokeai/frontend/web && pnpm install # Build the frontend frontend-build: From ca7e9287108b634b88d9ab1af4d2cf0fc8df7692 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:16:25 +1100 Subject: [PATCH 182/411] fix(ui): fix low-hanging fruit types --- .../listenerMiddleware/listeners/controlNetImageProcessed.ts | 1 + invokeai/frontend/web/src/services/api/endpoints/models.ts | 2 +- invokeai/frontend/web/src/services/api/types.ts | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts index fba274beb8..73de931297 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed.ts @@ -51,6 +51,7 @@ export const addControlNetImageProcessedListener = () => { image: { image_name: ca.controlImage }, }, }, + edges: [], }, runs: 1, }, diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 9a7f108056..57cf7dacfc 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -39,7 +39,7 @@ type UpdateLoRAModelArg = { type UpdateMainModelResponse = paths['/api/v2/models/i/{key}']['patch']['responses']['200']['content']['application/json']; -type ListModelsArg = NonNullable; +type ListModelsArg = NonNullable; type UpdateLoRAModelResponse = UpdateMainModelResponse; diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 4ae2f9b594..aaa70a2684 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -164,7 +164,6 @@ export type IntegerOutput = S['IntegerOutput']; export type IterateInvocationOutput = S['IterateInvocationOutput']; export type CollectInvocationOutput = S['CollectInvocationOutput']; export type LatentsOutput = S['LatentsOutput']; -export type GraphInvocationOutput = S['GraphInvocationOutput']; // Post-image upload actions, controls workflows when images are uploaded From 7e5a85496e63add7f15b239e00495f3d4f66395b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:42:14 +1100 Subject: [PATCH 183/411] chore(ui): bump `@invoke-ai/ui-library` --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/pnpm-lock.yaml | 110 ++++++++++++++++----------- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index cea13350d2..a9f37f76ad 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -57,7 +57,7 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.0.16", - "@invoke-ai/ui-library": "^0.0.18", + "@invoke-ai/ui-library": "^0.0.21", "@mantine/form": "6.0.21", "@nanostores/react": "^0.7.1", "@reduxjs/toolkit": "2.0.1", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 0ec2e47a0c..4f9902299c 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -32,8 +32,8 @@ dependencies: specifier: ^5.0.16 version: 5.0.16 '@invoke-ai/ui-library': - specifier: ^0.0.18 - version: 0.0.18(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.1)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0) + specifier: ^0.0.21 + version: 0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.2)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0) '@mantine/form': specifier: 6.0.21 version: 6.0.21(react@18.2.0) @@ -344,7 +344,7 @@ packages: '@jridgewell/trace-mapping': 0.3.21 dev: true - /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.1): + /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.2): resolution: {integrity: sha512-1yG2MrzUlix6KthjQMCNiHnkXrWwEdFAX6D+HqGJaNu0XvaGul2J+wDNtjsdX+gxiWu1nXXEEOAWlFVYMUf65w==} dependencies: '@zag-js/accordion': 0.32.1 @@ -356,7 +356,7 @@ packages: '@zag-js/color-utils': 0.32.1 '@zag-js/combobox': 0.32.1 '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.1) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) '@zag-js/dialog': 0.32.1 '@zag-js/editable': 0.32.1 '@zag-js/file-upload': 0.32.1 @@ -383,13 +383,13 @@ packages: - '@internationalized/date' dev: false - /@ark-ui/react@1.3.0(@internationalized/date@3.5.1)(react-dom@18.2.0)(react@18.2.0): + /@ark-ui/react@1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JHjNoIX50+mUCTaEGMjfGQWGGi31pKsV646jZJlR/1xohpYJigzg8BvO97cTsVk8fwtur+cm11gz3Nf7f5QUnA==} peerDependencies: react: '>=18.0.0' react-dom: '>=18.0.0' dependencies: - '@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.1) + '@ark-ui/anatomy': 1.3.0(@internationalized/date@3.5.2) '@zag-js/accordion': 0.32.1 '@zag-js/avatar': 0.32.1 '@zag-js/carousel': 0.32.1 @@ -399,7 +399,7 @@ packages: '@zag-js/combobox': 0.32.1 '@zag-js/core': 0.32.1 '@zag-js/date-picker': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.1) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) '@zag-js/dialog': 0.32.1 '@zag-js/editable': 0.32.1 '@zag-js/file-upload': 0.32.1 @@ -1709,6 +1709,13 @@ packages: dependencies: regenerator-runtime: 0.14.1 + /@babel/runtime@7.23.9: + resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: false + /@babel/template@7.22.15: resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} engines: {node: '>=6.9.0'} @@ -1969,7 +1976,7 @@ packages: dependencies: '@chakra-ui/dom-utils': 2.1.0 react: 18.2.0 - react-focus-lock: 2.9.6(@types/react@18.2.48)(react@18.2.0) + react-focus-lock: 2.11.1(@types/react@18.2.48)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -3013,7 +3020,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.1 '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) @@ -3560,16 +3567,16 @@ packages: resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} dev: true - /@internationalized/date@3.5.1: - resolution: {integrity: sha512-LUQIfwU9e+Fmutc/DpRTGXSdgYZLBegi4wygCWDSVmUdLTaMHsQyASDiJtREwanwKuQLq0hY76fCJ9J/9I2xOQ==} + /@internationalized/date@3.5.2: + resolution: {integrity: sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==} dependencies: - '@swc/helpers': 0.5.3 + '@swc/helpers': 0.5.6 dev: false - /@internationalized/number@3.5.0: - resolution: {integrity: sha512-ZY1BW8HT9WKYvaubbuqXbbDdHhOUMfE2zHHFJeTppid0S+pc8HtdIxFxaYMsGjCb4UsF+MEJ4n2TfU7iHnUK8w==} + /@internationalized/number@3.5.1: + resolution: {integrity: sha512-N0fPU/nz15SwR9IbfJ5xaS9Ss/O5h1sVXMZf43vc9mxEG48ovglvvzBjF53aHlq20uoR6c+88CrIXipU/LSzwg==} dependencies: - '@swc/helpers': 0.5.3 + '@swc/helpers': 0.5.6 dev: false /@invoke-ai/eslint-config-react@0.0.13(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-config-prettier@9.1.0)(eslint-plugin-import@2.29.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react-refresh@0.4.5)(eslint-plugin-react@7.33.2)(eslint-plugin-simple-import-sort@10.0.0)(eslint-plugin-storybook@0.6.15)(eslint-plugin-unused-imports@3.0.0)(eslint@8.56.0): @@ -3608,14 +3615,14 @@ packages: prettier: 3.2.4 dev: true - /@invoke-ai/ui-library@0.0.18(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.1)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Yme+2+pzYy3TPb7ZT0hYmBwahH29ZRSVIxLKSexh3BsbJXbTzGssRQU78QvK6Ymxemgbso3P8Rs+IW0zNhQKjQ==} + /@invoke-ai/ui-library@0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.2)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-tCvgkBPDt0gNq+8IcR03e/Mw7R8Mb/SMXTqx3FEIxlTQEo93A/D38dKXeDCzTdx4sQ+sknfB+JLBbHs6sg5hhQ==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 react: ^18.2.0 react-dom: ^18.2.0 dependencies: - '@ark-ui/react': 1.3.0(@internationalized/date@3.5.1)(react-dom@18.2.0)(react@18.2.0) + '@ark-ui/react': 1.3.0(@internationalized/date@3.5.2)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/anatomy': 2.2.2 '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) @@ -3631,11 +3638,11 @@ packages: framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) lodash-es: 4.17.21 nanostores: 0.9.5 - overlayscrollbars: 2.4.7 - overlayscrollbars-react: 0.5.4(overlayscrollbars@2.4.7)(react@18.2.0) + overlayscrollbars: 2.5.0 + overlayscrollbars-react: 0.5.4(overlayscrollbars@2.5.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-i18next: 14.0.1(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0) + react-i18next: 14.0.5(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0) react-icons: 5.0.1(react@18.2.0) react-select: 5.8.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) transitivePeerDependencies: @@ -5634,8 +5641,8 @@ packages: resolution: {integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==} dev: true - /@swc/helpers@0.5.3: - resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==} + /@swc/helpers@0.5.6: + resolution: {integrity: sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==} dependencies: tslib: 2.6.2 dev: false @@ -6754,10 +6761,10 @@ packages: /@zag-js/date-picker@0.32.1: resolution: {integrity: sha512-n/hYmF+/R4+NuyfPRzCgeuLT6LJihKSuKzK29STPWy3sC/tBBHiqhNv1/4UKbatHUJXdBW2XF+N8Rw08RffcFQ==} dependencies: - '@internationalized/date': 3.5.1 + '@internationalized/date': 3.5.2 '@zag-js/anatomy': 0.32.1 '@zag-js/core': 0.32.1 - '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.1) + '@zag-js/date-utils': 0.32.1(@internationalized/date@3.5.2) '@zag-js/dismissable': 0.32.1 '@zag-js/dom-event': 0.32.1 '@zag-js/dom-query': 0.32.1 @@ -6769,12 +6776,12 @@ packages: '@zag-js/utils': 0.32.1 dev: false - /@zag-js/date-utils@0.32.1(@internationalized/date@3.5.1): + /@zag-js/date-utils@0.32.1(@internationalized/date@3.5.2): resolution: {integrity: sha512-dbBDRSVr5pRUw3rXndyGuSshZiWqQI5JQO4D2KIFGkXzorj6WzoOpcO910Z7AdM/9cCAMpCjUrka8d8o9BpJBg==} peerDependencies: '@internationalized/date': '>=3.0.0' dependencies: - '@internationalized/date': 3.5.1 + '@internationalized/date': 3.5.2 dev: false /@zag-js/dialog@0.32.1: @@ -6917,7 +6924,7 @@ packages: /@zag-js/number-input@0.32.1: resolution: {integrity: sha512-atyIOvoMITb4hZtQym7yD6I7grvPW83UeMFO8hCQg3HWwd2zR4+63mouWuyMoWb4QrzVFRVQBaU8OG5xGlknEw==} dependencies: - '@internationalized/number': 3.5.0 + '@internationalized/number': 3.5.1 '@zag-js/anatomy': 0.32.1 '@zag-js/core': 0.32.1 '@zag-js/dom-event': 0.32.1 @@ -9537,8 +9544,8 @@ packages: engines: {node: '>=0.4.0'} dev: true - /focus-lock@1.0.0: - resolution: {integrity: sha512-a8Ge6cdKh9za/GZR/qtigTAk7SrGore56EFcoMshClsh7FLk1zwszc/ltuMfKhx56qeuyL/jWQ4J4axou0iJ9w==} + /focus-lock@1.3.2: + resolution: {integrity: sha512-kFI92jZVqa8rP4Yer2sLNlUDcOdEFxYum2tIIr4eCH0XF+pOmlg0xiY4tkbDmHJXt3phtbJoWs1L6PgUVk97rA==} engines: {node: '>=10'} dependencies: tslib: 2.6.2 @@ -11431,13 +11438,13 @@ packages: react: 18.2.0 dev: false - /overlayscrollbars-react@0.5.4(overlayscrollbars@2.4.7)(react@18.2.0): + /overlayscrollbars-react@0.5.4(overlayscrollbars@2.5.0)(react@18.2.0): resolution: {integrity: sha512-FPKx9XnXovTnI4+2JXig5uEaTLSEJ6svOwPzIfBBXTHBRNsz2+WhYUmfM0K/BNYxjgDEwuPm+NQhEoOA0RoG1g==} peerDependencies: overlayscrollbars: ^2.0.0 react: '>=16.8.0' dependencies: - overlayscrollbars: 2.4.7 + overlayscrollbars: 2.5.0 react: 18.2.0 dev: false @@ -11445,8 +11452,8 @@ packages: resolution: {integrity: sha512-C7tmhetwMv9frEvIT/RfkAVEgbjRNz/Gh2zE8BVmN+jl35GRaAnz73rlGQCMRoC2arpACAXyMNnJkzHb7GBrcA==} dev: false - /overlayscrollbars@2.4.7: - resolution: {integrity: sha512-02X2/nHno35dzebCx+EO2tRDaKAOltZqUKdUqvq3Pt8htCuhJbYi+mjr0CYerVeGRRoZ2Uo6/8XrNg//DJJ+GA==} + /overlayscrollbars@2.5.0: + resolution: {integrity: sha512-CWVC2dwS07XZfLHDm5GmZN1iYggiJ8Vufnvzwt0gwR9Yz1hVckKeTxg7VILZeYVGhDYJHZ1Xc8Xfys5dWZ1qiA==} dev: false /p-limit@2.3.0: @@ -11949,7 +11956,7 @@ packages: peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 react: 18.2.0 dev: false @@ -12035,8 +12042,8 @@ packages: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.9.6(@types/react@18.2.48)(react@18.2.0): - resolution: {integrity: sha512-B7gYnCjHNrNYwY2juS71dHbf0+UpXXojt02svxybj8N5bxceAkzPChKEncHuratjUHkIFNCn06k2qj1DRlzTug==} + /react-focus-lock@2.11.1(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -12044,9 +12051,9 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@types/react': 18.2.48 - focus-lock: 1.0.0 + focus-lock: 1.3.2 prop-types: 15.8.1 react: 18.2.0 react-clientside-effect: 1.2.6(react@18.2.0) @@ -12093,8 +12100,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-i18next@14.0.1(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-TMV8hFismBmpMdIehoFHin/okfvgjFhp723RYgIqB4XyhDobVMyukyM3Z8wtTRmajyFMZrBl/OaaXF2P6WjUAw==} + /react-i18next@14.0.5(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-5+bQSeEtgJrMBABBL5lO7jPdSNAbeAZ+MlFWDw//7FnVacuVu3l9EeWFzBQvZsKy+cihkbThWOAThEdH8YjGEw==} peerDependencies: i18next: '>= 23.2.3' react: '>= 16.8.0' @@ -12106,7 +12113,7 @@ packages: react-native: optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 html-parse-stringify: 3.0.1 i18next: 23.7.16 react: 18.2.0 @@ -12204,6 +12211,23 @@ packages: react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) tslib: 2.6.2 + dev: true + + /react-remove-scroll-bar@2.3.5(@types/react@18.2.48)(react@18.2.0): + resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.48 + react: 18.2.0 + react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) + tslib: 2.6.2 + dev: false /react-remove-scroll@2.5.5(@types/react@18.2.48)(react@18.2.0): resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} @@ -12236,7 +12260,7 @@ packages: dependencies: '@types/react': 18.2.48 react: 18.2.0 - react-remove-scroll-bar: 2.3.4(@types/react@18.2.48)(react@18.2.0) + react-remove-scroll-bar: 2.3.5(@types/react@18.2.48)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) tslib: 2.6.2 use-callback-ref: 1.3.1(@types/react@18.2.48)(react@18.2.0) From a793103d7ac882c1c56c9638deeb23626d92b9fe Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:42:44 +1100 Subject: [PATCH 184/411] fix(ui): get lora select working --- .../web/src/common/hooks/useGroupedModelCombobox.ts | 11 ++++++----- .../web/src/features/lora/components/LoRACard.tsx | 12 ++++++------ .../web/src/features/lora/components/LoRASelect.tsx | 8 ++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index 875ce1f1c4..140cf3eaa6 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -2,15 +2,16 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import type { EntityState } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import type { GroupBase } from 'chakra-react-select'; +import type { ModelIdentifierWithBase } from 'features/nodes/types/common'; import { groupBy, map, reduce } from 'lodash-es'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { AnyModelConfig } from 'services/api/endpoints/models'; import { getModelId } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; type UseGroupedModelComboboxArg = { modelEntities: EntityState | undefined; - selectedModel?: Pick | null; + selectedModel?: ModelIdentifierWithBase | null; onChange: (value: T | null) => void; getIsDisabled?: (model: T) => boolean; isLoading?: boolean; @@ -28,7 +29,7 @@ export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); - const base_model = useAppSelector((s) => s.generation.model?.base_model ?? 'sdxl'); + const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl'); const { modelEntities, selectedModel, getIsDisabled, onChange, isLoading } = arg; const options = useMemo[]>(() => { if (!modelEntities) { @@ -42,8 +43,8 @@ export const useGroupedModelCombobox = ( acc.push({ label, options: val.map((model) => ({ - label: model.model_name, - value: model.id, + label: model.name, + value: model.key, isDisabled: getIsDisabled ? getIsDisabled(model) : false, })), }); diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index 81e0027b2d..71ce145786 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -26,18 +26,18 @@ export const LoRACard = memo((props: LoRACardProps) => { const handleChange = useCallback( (v: number) => { - dispatch(loraWeightChanged({ id: lora.id, weight: v })); + dispatch(loraWeightChanged({ key: lora.key, weight: v })); }, - [dispatch, lora.id] + [dispatch, lora.key] ); const handleSetLoraToggle = useCallback(() => { - dispatch(loraIsEnabledChanged({ id: lora.id, isEnabled: !lora.isEnabled })); - }, [dispatch, lora.id, lora.isEnabled]); + dispatch(loraIsEnabledChanged({ key: lora.key, isEnabled: !lora.isEnabled })); + }, [dispatch, lora.key, lora.isEnabled]); const handleRemoveLora = useCallback(() => { - dispatch(loraRemoved(lora.id)); - }, [dispatch, lora.id]); + dispatch(loraRemoved(lora.key)); + }, [dispatch, lora.key]); return ( diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index 069c557aef..b58751ca5e 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -7,8 +7,8 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { loraAdded, selectLoraSlice } from 'features/lora/store/loraSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { LoRAConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; +import type { LoRAConfig } from 'services/api/types'; const selectAddedLoRAs = createMemoizedSelector(selectLoraSlice, (lora) => lora.loras); @@ -17,11 +17,11 @@ const LoRASelect = () => { const { data, isLoading } = useGetLoRAModelsQuery(); const { t } = useTranslation(); const addedLoRAs = useAppSelector(selectAddedLoRAs); - const currentBaseModel = useAppSelector((s) => s.generation.model?.base_model); + const currentBaseModel = useAppSelector((s) => s.generation.model?.base); const getIsDisabled = (lora: LoRAConfig): boolean => { - const isCompatible = currentBaseModel === lora.base_model; - const isAdded = Boolean(addedLoRAs[lora.id]); + const isCompatible = currentBaseModel === lora.base; + const isAdded = Boolean(addedLoRAs[lora.key]); const hasMainModel = Boolean(currentBaseModel); return !hasMainModel || !isCompatible || isAdded; }; From f870f810d5a068769fff893f16196b964119f42e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:45:05 +1100 Subject: [PATCH 185/411] fix(ui): get embedding select working --- .../frontend/web/src/features/embedding/EmbeddingSelect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx b/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx index 426ddd21e2..fd05edc466 100644 --- a/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx +++ b/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx @@ -18,7 +18,7 @@ export const EmbeddingSelect = memo(({ onSelect, onClose }: EmbeddingSelectProps const getIsDisabled = useCallback( (embedding: TextualInversionConfig): boolean => { - const isCompatible = currentBaseModel === embedding.base_model; + const isCompatible = currentBaseModel === embedding.base; const hasMainModel = Boolean(currentBaseModel); return !hasMainModel || !isCompatible; }, @@ -31,7 +31,7 @@ export const EmbeddingSelect = memo(({ onSelect, onClose }: EmbeddingSelectProps if (!embedding) { return; } - onSelect(embedding.model_name); + onSelect(embedding.key); }, [onSelect] ); From e7e3045a8ac1ed714cf4ccbb6ae16a9f3e3958ed Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:14:08 +1100 Subject: [PATCH 186/411] fix(ui): get vae model select working --- .../common/hooks/useGroupedModelCombobox.ts | 4 +--- .../web/src/common/hooks/useModelCombobox.ts | 12 ++++++------ .../VAEModel/ParamVAEModelSelect.tsx | 18 ++++++++++-------- .../web/src/services/api/endpoints/models.ts | 3 --- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index 140cf3eaa6..fc5bc455ee 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -6,7 +6,6 @@ import type { ModelIdentifierWithBase } from 'features/nodes/types/common'; import { groupBy, map, reduce } from 'lodash-es'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { getModelId } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; type UseGroupedModelComboboxArg = { @@ -58,8 +57,7 @@ export const useGroupedModelCombobox = ( const value = useMemo( () => - options.flatMap((o) => o.options).find((m) => (selectedModel ? m.value === getModelId(selectedModel) : false)) ?? - null, + options.flatMap((o) => o.options).find((m) => (selectedModel ? m.value === selectedModel.key : false)) ?? null, [options, selectedModel] ); diff --git a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts index 07e6aeb34c..e0718d6413 100644 --- a/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useModelCombobox.ts @@ -1,14 +1,14 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import type { EntityState } from '@reduxjs/toolkit'; +import type { ModelIdentifierWithBase } from 'features/nodes/types/common'; import { map } from 'lodash-es'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { AnyModelConfig } from 'services/api/endpoints/models'; -import { getModelId } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; type UseModelComboboxArg = { modelEntities: EntityState | undefined; - selectedModel?: Pick | null; + selectedModel?: ModelIdentifierWithBase | null; onChange: (value: T | null) => void; getIsDisabled?: (model: T) => boolean; optionsFilter?: (model: T) => boolean; @@ -33,14 +33,14 @@ export const useModelCombobox = (arg: UseModelCombobox return map(modelEntities.entities) .filter(optionsFilter) .map((model) => ({ - label: model.model_name, - value: model.id, + label: model.name, + value: model.key, isDisabled: getIsDisabled ? getIsDisabled(model) : false, })); }, [optionsFilter, getIsDisabled, modelEntities]); const value = useMemo( - () => options.find((m) => (selectedModel ? m.value === getModelId(selectedModel) : false)), + () => options.find((m) => (selectedModel ? m.value === selectedModel.key : false)), [options, selectedModel] ); diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index cc0164153d..1810c3ff68 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -7,8 +7,8 @@ import { selectGenerationSlice, vaeSelected } from 'features/parameters/store/ge import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import type { VAEConfig } from 'services/api/endpoints/models'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; +import type { VAEConfig } from 'services/api/types'; const selector = createMemoizedSelector(selectGenerationSlice, (generation) => { const { model, vae } = generation; @@ -22,25 +22,27 @@ const ParamVAEModelSelect = () => { const { data, isLoading } = useGetVaeModelsQuery(); const getIsDisabled = useCallback( (vae: VAEConfig): boolean => { - const isCompatible = model?.base_model === vae.base_model; - const hasMainModel = Boolean(model?.base_model); + const isCompatible = model?.base === vae.base; + const hasMainModel = Boolean(model?.base); return !hasMainModel || !isCompatible; }, - [model?.base_model] + [model?.base] ); const _onChange = useCallback( (vae: VAEConfig | null) => { - dispatch(vaeSelected(vae ? pick(vae, 'base_model', 'model_name') : null)); + dispatch(vaeSelected(vae ? pick(vae, 'key', 'base') : null)); }, [dispatch] ); - const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ + const { options, value, onChange, noOptionsMessage } = useGroupedModelCombobox({ modelEntities: data, onChange: _onChange, - selectedModel: vae ? { ...vae, model_type: 'vae' } : null, + selectedModel: vae ? pick(vae, 'key', 'base') : null, isLoading, getIsDisabled, }); + + console.log(value) return ( @@ -50,7 +52,7 @@ const ParamVAEModelSelect = () => { input; - type UpdateMainModelArg = { base_model: BaseModelType; model_name: string; From e771c5f4670896570b4ae90647c0544a7571b5a2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:16:17 +1100 Subject: [PATCH 187/411] fix(ui): get refiner model select working --- .../SDXLRefiner/ParamSDXLRefinerModelSelect.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx index 4c54251557..e5978ca21b 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx @@ -4,15 +4,16 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; import { refinerModelChanged, selectSdxlSlice } from 'features/sdxl/store/sdxlSlice'; +import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { REFINER_BASE_MODELS } from 'services/api/constants'; -import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/types'; const selectModel = createMemoizedSelector(selectSdxlSlice, (sdxl) => sdxl.refinerModel); -const optionsFilter = (model: MainModelConfig) => model.base_model === 'sdxl-refiner'; +const optionsFilter = (model: MainModelConfig) => model.base === 'sdxl-refiner'; const ParamSDXLRefinerModelSelect = () => { const dispatch = useAppDispatch(); @@ -25,13 +26,7 @@ const ParamSDXLRefinerModelSelect = () => { dispatch(refinerModelChanged(null)); return; } - dispatch( - refinerModelChanged({ - base_model: 'sdxl-refiner', - model_name: model.model_name, - model_type: model.model_type, - }) - ); + dispatch(refinerModelChanged(pick(model, ['key', 'base']))); }, [dispatch] ); From 8afe328af08eb8a68070d75ee87e93bd6c5daf9a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 11:32:58 +1100 Subject: [PATCH 188/411] fix(ui): get workflow editor model selects working --- .../inputs/ControlNetModelFieldInputComponent.tsx | 5 +++-- .../inputs/IPAdapterModelFieldInputComponent.tsx | 5 +++-- .../fields/inputs/LoRAModelFieldInputComponent.tsx | 5 +++-- .../fields/inputs/MainModelFieldInputComponent.tsx | 2 +- .../inputs/RefinerModelFieldInputComponent.tsx | 2 +- .../inputs/SDXLMainModelFieldInputComponent.tsx | 2 +- .../inputs/T2IAdapterModelFieldInputComponent.tsx | 5 +++-- .../fields/inputs/VAEModelFieldInputComponent.tsx | 5 +++-- .../frontend/web/src/features/nodes/types/common.ts | 12 ++++++------ 9 files changed, 24 insertions(+), 19 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx index 53d800e7b6..1951ec60d3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx @@ -3,9 +3,10 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldControlNetModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { ControlNetModelFieldInputInstance, ControlNetModelFieldInputTemplate } from 'features/nodes/types/field'; +import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; -import type { ControlNetConfig } from 'services/api/endpoints/models'; import { useGetControlNetModelsQuery } from 'services/api/endpoints/models'; +import type { ControlNetConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -35,7 +36,7 @@ const ControlNetModelFieldInputComponent = (props: Props) => { const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ modelEntities: data, onChange: _onChange, - selectedModel: field.value ? { ...field.value, model_type: 'controlnet' } : undefined, + selectedModel: field.value ? pick(field.value, ['key', 'base']) : undefined, isLoading, }); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx index 3f195ceb32..137f751fca 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx @@ -3,9 +3,10 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldIPAdapterModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { IPAdapterModelFieldInputInstance, IPAdapterModelFieldInputTemplate } from 'features/nodes/types/field'; +import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; -import type { IPAdapterConfig } from 'services/api/endpoints/models'; import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models'; +import type { IPAdapterConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -35,7 +36,7 @@ const IPAdapterModelFieldInputComponent = ( const { options, value, onChange } = useGroupedModelCombobox({ modelEntities: ipAdapterModels, onChange: _onChange, - selectedModel: field.value ? { ...field.value, model_type: 'ip_adapter' } : undefined, + selectedModel: field.value ? pick(field.value, ['key', 'base']) : undefined, }); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx index eeb07fa08e..5f6318de9e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx @@ -3,9 +3,10 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldLoRAModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { LoRAModelFieldInputInstance, LoRAModelFieldInputTemplate } from 'features/nodes/types/field'; +import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; -import type { LoRAConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; +import type { LoRAConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -34,7 +35,7 @@ const LoRAModelFieldInputComponent = (props: Props) => { const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ modelEntities: data, onChange: _onChange, - selectedModel: field.value ? { ...field.value, model_type: 'lora' } : undefined, + selectedModel: field.value ? pick(field.value, ['key', 'base']) : undefined, isLoading, }); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx index 7ddde08816..1cb0658b81 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/MainModelFieldInputComponent.tsx @@ -6,8 +6,8 @@ import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { MainModelFieldInputInstance, MainModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; import { NON_SDXL_MAIN_MODELS } from 'services/api/constants'; -import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx index 9b5a1138d4..be2b4a4d4f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/RefinerModelFieldInputComponent.tsx @@ -9,8 +9,8 @@ import type { } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; import { REFINER_BASE_MODELS } from 'services/api/constants'; -import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx index cf353619e8..d0d7754606 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/SDXLMainModelFieldInputComponent.tsx @@ -6,8 +6,8 @@ import { fieldMainModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { SDXLMainModelFieldInputInstance, SDXLMainModelFieldInputTemplate } from 'features/nodes/types/field'; import { memo, useCallback } from 'react'; import { SDXL_MAIN_MODELS } from 'services/api/constants'; -import type { MainModelConfig } from 'services/api/endpoints/models'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx index 8402c56343..9115f22c14 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx @@ -3,9 +3,10 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldT2IAdapterModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { T2IAdapterModelFieldInputInstance, T2IAdapterModelFieldInputTemplate } from 'features/nodes/types/field'; +import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; -import type { T2IAdapterConfig } from 'services/api/endpoints/models'; import { useGetT2IAdapterModelsQuery } from 'services/api/endpoints/models'; +import type { T2IAdapterConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -36,7 +37,7 @@ const T2IAdapterModelFieldInputComponent = ( const { options, value, onChange } = useGroupedModelCombobox({ modelEntities: t2iAdapterModels, onChange: _onChange, - selectedModel: field.value ? { ...field.value, model_type: 't2i_adapter' } : undefined, + selectedModel: field.value ? pick(field.value, ['key', 'base']) : undefined, }); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx index af09f2d8f2..87272f48b9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx @@ -4,9 +4,10 @@ import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { SyncModelsIconButton } from 'features/modelManager/components/SyncModels/SyncModelsIconButton'; import { fieldVaeModelValueChanged } from 'features/nodes/store/nodesSlice'; import type { VAEModelFieldInputInstance, VAEModelFieldInputTemplate } from 'features/nodes/types/field'; +import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; -import type { VAEConfig } from 'services/api/endpoints/models'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; +import type { VAEConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -34,7 +35,7 @@ const VAEModelFieldInputComponent = (props: Props) => { const { options, value, onChange, placeholder, noOptionsMessage } = useGroupedModelCombobox({ modelEntities: data, onChange: _onChange, - selectedModel: field.value ? { ...field.value, model_type: 'vae' } : null, + selectedModel: field.value ? pick(field.value, ['key', 'base']) : null, isLoading, }); diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index 891bd29bc8..d5d04deaa5 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -73,7 +73,7 @@ export type BaseModel = z.infer; export type ModelType = z.infer; export type ModelIdentifier = z.infer; export type ModelIdentifierWithBase = z.infer; -export const zMainModelField = zModelFieldBase; +export const zMainModelField = zModelIdentifierWithBase; export type MainModelField = z.infer; export const zSDXLRefinerModelField = zModelIdentifier; @@ -93,23 +93,23 @@ export const zSubModelType = z.enum([ ]); export type SubModelType = z.infer; -export const zVAEModelField = zModelFieldBase; +export const zVAEModelField = zModelIdentifierWithBase; export const zModelInfo = zModelIdentifier.extend({ submodel_type: zSubModelType.nullish(), }); export type ModelInfo = z.infer; -export const zLoRAModelField = zModelFieldBase; +export const zLoRAModelField = zModelIdentifierWithBase; export type LoRAModelField = z.infer; -export const zControlNetModelField = zModelFieldBase; +export const zControlNetModelField = zModelIdentifierWithBase; export type ControlNetModelField = z.infer; -export const zIPAdapterModelField = zModelFieldBase; +export const zIPAdapterModelField = zModelIdentifierWithBase; export type IPAdapterModelField = z.infer; -export const zT2IAdapterModelField = zModelFieldBase; +export const zT2IAdapterModelField = zModelIdentifierWithBase; export type T2IAdapterModelField = z.infer; export const zLoraInfo = zModelInfo.extend({ From 812e24cbd271dd0cf7b84bf0eb87bdedcde581f2 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 7 Jan 2024 19:54:58 -0500 Subject: [PATCH 189/411] groundwork for the bulk_download_service --- .../app/services/bulk_download/__init__.py | 0 .../bulk_download/bulk_download_base.py | 32 +++++++++++++++++++ .../bulk_download/bulk_download_common.py | 21 ++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 invokeai/app/services/bulk_download/__init__.py create mode 100644 invokeai/app/services/bulk_download/bulk_download_base.py create mode 100644 invokeai/app/services/bulk_download/bulk_download_common.py diff --git a/invokeai/app/services/bulk_download/__init__.py b/invokeai/app/services/bulk_download/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py new file mode 100644 index 0000000000..54c8771437 --- /dev/null +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -0,0 +1,32 @@ +from pathlib import Path +from typing import Optional, Union + +from abc import ABC, abstractmethod + +from invokeai.app.services.events.events_base import EventServiceBase +from invokeai.app.services.invoker import Invoker + +class BulkDownloadBase(ABC): + + @abstractmethod + def __init__( + self, + output_folder: Union[str, Path], + event_bus: Optional["EventServiceBase"] = None, + ): + """ + Create BulkDownloadBase object. + + :param output_folder: The path to the output folder where the bulk download files can be temporarily stored. + :param event_bus: InvokeAI event bus for reporting events to. + """ + + @abstractmethod + def start(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> str: + """ + Starts a a bulk download job. + + :param invoker: The Invoker that holds all the services, required to be passed as a parameter to avoid circular dependencies. + :param image_names: A list of image names to include in the zip file. + :param board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. + """ \ No newline at end of file diff --git a/invokeai/app/services/bulk_download/bulk_download_common.py b/invokeai/app/services/bulk_download/bulk_download_common.py new file mode 100644 index 0000000000..3ac1f5bba8 --- /dev/null +++ b/invokeai/app/services/bulk_download/bulk_download_common.py @@ -0,0 +1,21 @@ + +class BulkDownloadException(Exception): + """Exception raised when a bulk download fails.""" + + def __init__(self, message="Bulk download failed"): + super().__init__(message) + self.message = message + +class BulkDownloadTargetException(BulkDownloadException): + """Exception raised when a bulk download target is not found.""" + + def __init__(self, message="The bulk download target was not found"): + super().__init__(message) + self.message = message + +class BulkDownloadParametersException(BulkDownloadException): + """Exception raised when a bulk download parameter is invalid.""" + + def __init__(self, message="The bulk download parameters are invalid, either an array of image names or a board id must be provided"): + super().__init__(message) + self.message = message \ No newline at end of file From f1967c339305a7df0089e81cd4966e5ca29db775 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 7 Jan 2024 19:55:59 -0500 Subject: [PATCH 190/411] adding socket events for bulk download --- invokeai/app/api/sockets.py | 30 ++++++++++++++++++-- invokeai/app/services/events/events_base.py | 31 +++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/invokeai/app/api/sockets.py b/invokeai/app/api/sockets.py index e651e43559..c5d9ace8d2 100644 --- a/invokeai/app/api/sockets.py +++ b/invokeai/app/api/sockets.py @@ -12,16 +12,27 @@ class SocketIO: __sio: AsyncServer __app: ASGIApp + __sub_queue: str = "subscribe_queue" + __unsub_queue: str = "unsubscribe_queue" + + __sub_bulk_download: str = "subscribe_bulk_download" + __unsub_bulk_download: str = "unsubscribe_bulk_download" + + def __init__(self, app: FastAPI): self.__sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*") self.__app = ASGIApp(socketio_server=self.__sio, socketio_path="/ws/socket.io") app.mount("/ws", self.__app) - self.__sio.on("subscribe_queue", handler=self._handle_sub_queue) - self.__sio.on("unsubscribe_queue", handler=self._handle_unsub_queue) + self.__sio.on(self.__sub_queue, handler=self._handle_sub_queue) + self.__sio.on(self.__unsub_queue, handler=self._handle_unsub_queue) local_handler.register(event_name=EventServiceBase.queue_event, _func=self._handle_queue_event) local_handler.register(event_name=EventServiceBase.model_event, _func=self._handle_model_event) + self.__sio.on(self.__sub_bulk_download, handler=self._handle_sub_bulk_download) + self.__sio.on(self.__unsub_bulk_download, handler=self._handle_unsub_bulk_download) + local_handler.register(event_name=EventServiceBase.bulk_download_event, _func=self._handle_bulk_download_event) + async def _handle_queue_event(self, event: Event): await self.__sio.emit( event=event[1]["event"], @@ -39,3 +50,18 @@ class SocketIO: async def _handle_model_event(self, event: Event) -> None: await self.__sio.emit(event=event[1]["event"], data=event[1]["data"]) + + async def _handle_bulk_download_event(self, event: Event): + await self.__sio.emit( + event=event[1]["event"], + data=event[1]["data"], + room=event[1]["data"]["bulk_download_id"], + ) + + async def _handle_sub_bulk_download(self, sid, data, *args, **kwargs): + if "bulk_download_id" in data: + await self.__sio.enter_room(sid, data["bulk_download_id"]) + + async def _handle_unsub_bulk_download(self, sid, data, *args, **kwargs): + if "bulk_download_id" in data: + await self.__sio.leave_room(sid, data["bulk_download_id"]) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 5355fe2298..0a0668b274 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -16,6 +16,7 @@ from invokeai.backend.model_manager import AnyModelConfig class EventServiceBase: queue_event: str = "queue_event" + bulk_download_event: str = "bulk_download_event" download_event: str = "download_event" model_event: str = "model_event" @@ -24,6 +25,14 @@ class EventServiceBase: def dispatch(self, event_name: str, payload: Any) -> None: pass + def _emit_bulk_download_event(self, event_name: str, payload: dict) -> None: + """Bulk download events are emitted to a room with queue_id as the room name""" + payload["timestamp"] = get_timestamp() + self.dispatch( + event_name=EventServiceBase.bulk_download_event, + payload={"event": event_name, "data": payload}, + ) + def __emit_queue_event(self, event_name: str, payload: dict) -> None: """Queue events are emitted to a room with queue_id as the room name""" payload["timestamp"] = get_timestamp() @@ -430,3 +439,25 @@ class EventServiceBase: "error": error, }, ) + + def emit_bulk_download_started(self, bulk_download_id: str) -> None: + """Emitted when a bulk download starts""" + self._emit_bulk_download_event( + event_name="bulk_download_started", + payload={"bulk_download_id": bulk_download_id, } + ) + + def emit_bulk_download_completed(self, bulk_download_id: str, file_path: str) -> None: + """Emitted when a bulk download completes""" + self._emit_bulk_download_event( + event_name="bulk_download_completed", + payload={"bulk_download_id": bulk_download_id, + "file_path": file_path} + ) + + def emit_bulk_download_failed(self, bulk_download_id: str, error: str) -> None: + """Emitted when a bulk download fails""" + self._emit_bulk_download_event( + event_name="bulk_download_failed", + payload={"bulk_download_id": bulk_download_id, "error": error} + ) From 56d2d220a812990a0b28305e2df3153331f913db Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 7 Jan 2024 21:29:42 -0500 Subject: [PATCH 191/411] implementation of bulkdownload background task --- invokeai/app/api/dependencies.py | 3 + invokeai/app/api/routers/images.py | 14 ++- .../bulk_download/bulk_download_base.py | 2 +- .../bulk_download/bulk_download_defauilt.py | 114 ++++++++++++++++++ invokeai/app/services/invocation_services.py | 3 + 5 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 invokeai/app/services/bulk_download/bulk_download_defauilt.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index a9132516a8..ab09d1e5d7 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -15,6 +15,7 @@ from ..services.board_image_records.board_image_records_sqlite import SqliteBoar from ..services.board_images.board_images_default import BoardImagesService from ..services.board_records.board_records_sqlite import SqliteBoardRecordStorage from ..services.boards.boards_default import BoardService +from ..services.bulk_download.bulk_download_defauilt import BulkDownloadService from ..services.config import InvokeAIAppConfig from ..services.download import DownloadQueueService from ..services.image_files.image_files_disk import DiskImageFileStorage @@ -81,6 +82,7 @@ class ApiDependencies: board_records = SqliteBoardRecordStorage(db=db) boards = BoardService() events = FastAPIEventService(event_handler_id) + bulk_download = BulkDownloadService(output_folder=f"{output_folder}", event_bus=events) image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) @@ -110,6 +112,7 @@ class ApiDependencies: board_images=board_images, board_records=board_records, boards=boards, + bulk_download=bulk_download, configuration=configuration, events=events, image_files=image_files, diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index cc60ad1be8..2a8e1e7ec7 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -2,7 +2,7 @@ import io import traceback from typing import Optional -from fastapi import Body, HTTPException, Path, Query, Request, Response, UploadFile +from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, Response, UploadFile from fastapi.responses import FileResponse from fastapi.routing import APIRouter from PIL import Image @@ -10,6 +10,7 @@ from pydantic import BaseModel, Field, ValidationError from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin +from invokeai.app.services.board_records.board_records_common import BoardRecordNotFoundException from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator @@ -372,19 +373,22 @@ async def unstar_images_in_list( except Exception: raise HTTPException(status_code=500, detail="Failed to unstar images") - class ImagesDownloaded(BaseModel): response: Optional[str] = Field( description="If defined, the message to display to the user when images begin downloading" ) -@images_router.post("/download", operation_id="download_images_from_list", response_model=ImagesDownloaded) +@images_router.post("/download", operation_id="download_images_from_list", response_model=ImagesDownloaded, status_code=202) async def download_images_from_list( + background_tasks: BackgroundTasks, image_names: list[str] = Body(description="The list of names of images to download", embed=True), board_id: Optional[str] = Body( default=None, description="The board from which image should be downloaded from", embed=True ), ) -> ImagesDownloaded: - # return ImagesDownloaded(response="Your images are downloading") - raise HTTPException(status_code=501, detail="Endpoint is not yet implemented") + if (image_names is None or len(image_names) == 0) and board_id is None: + raise HTTPException(status_code=400, detail="No images or board id specified.") + background_tasks.add_task(ApiDependencies.invoker.services.bulk_download.handler, ApiDependencies.invoker, image_names, board_id) + return ImagesDownloaded(response="Your images are preparing to be downloaded") + diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 54c8771437..b788020bba 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -22,7 +22,7 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def start(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> str: + def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: """ Starts a a bulk download job. diff --git a/invokeai/app/services/bulk_download/bulk_download_defauilt.py b/invokeai/app/services/bulk_download/bulk_download_defauilt.py new file mode 100644 index 0000000000..ebeaa4be5a --- /dev/null +++ b/invokeai/app/services/bulk_download/bulk_download_defauilt.py @@ -0,0 +1,114 @@ +from pathlib import Path +from typing import Optional, Union +import uuid +from zipfile import ZipFile + +from invokeai.app.services.board_records.board_records_common import BoardRecordNotFoundException +from invokeai.app.services.bulk_download.bulk_download_common import BulkDownloadException +from invokeai.app.services.events.events_base import EventServiceBase +from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException +from invokeai.app.services.invoker import Invoker + +from .bulk_download_base import BulkDownloadBase + +class BulkDownloadService(BulkDownloadBase): + + __output_folder: Path + __bulk_downloads_folder: Path + __event_bus: Optional[EventServiceBase] + + def __init__(self, + output_folder: Union[str, Path], + event_bus: Optional[EventServiceBase] = None,): + """ + Initialize the downloader object. + + :param event_bus: Optional EventService object + """ + self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder) + self.__bulk_downloads_folder = self.__output_folder / "bulk_downloads" + self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) + self.__event_bus = event_bus + + + def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: + """ + Create a zip file containing the images specified by the given image names or board id. + + param: image_names: A list of image names to include in the zip file. + param: board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. + """ + bulk_download_id = str(uuid.uuid4()) + + self._signal_job_started(bulk_download_id) + try: + board_name: Union[str, None] = None + if board_id: + image_names = invoker.services.board_image_records.get_all_board_image_names_for_board(board_id) + if board_id == "none": + board_id = "Uncategorized" + image_names_to_paths: dict[str, str] = self._get_image_name_to_path_map(invoker, image_names) + file_path: str = self._create_zip_file(image_names_to_paths, bulk_download_id) + self._signal_job_completed(bulk_download_id, file_path) + except (ImageRecordNotFoundException, BoardRecordNotFoundException, BulkDownloadException) as e: + self._signal_job_failed(bulk_download_id, e) + except Exception as e: + self._signal_job_failed(bulk_download_id, e) + + def _get_image_name_to_path_map(self, invoker: Invoker, image_names: list[str]) -> dict[str, str]: + """ + Create a map of image names to their paths. + :param image_names: A list of image names. + """ + image_names_to_paths: dict[str, str] = {} + for image_name in image_names: + image_names_to_paths[image_name] = invoker.services.images.get_path(image_name) + return image_names_to_paths + + + + def _create_zip_file(self, image_names_to_paths: dict[str, str], bulk_download_id: str) -> str: + """ + Create a zip file containing the images specified by the given image names or board id. + If download with the same bulk_download_id already exists, it will be overwritten. + """ + + zip_file_path = self.__bulk_downloads_folder / (bulk_download_id + ".zip") + + with ZipFile(zip_file_path, "w") as zip_file: + for image_name, image_path in image_names_to_paths.items(): + zip_file.write(image_path, arcname=image_name) + + return str(zip_file_path) + + + def _signal_job_started(self, bulk_download_id: str) -> None: + """Signal that a bulk download job has started.""" + if self.__event_bus: + assert bulk_download_id is not None + self.__event_bus.emit_bulk_download_started( + bulk_download_id=bulk_download_id, + ) + + + def _signal_job_completed(self, bulk_download_id: str, file_path: str) -> None: + """Signal that a bulk download job has completed.""" + if self.__event_bus: + assert bulk_download_id is not None + assert file_path is not None + self.__event_bus.emit_bulk_download_completed( + bulk_download_id=bulk_download_id, + file_path=file_path, + ) + + def _signal_job_failed(self, bulk_download_id: str, exception: Exception) -> None: + """Signal that a bulk download job has failed.""" + if self.__event_bus: + assert bulk_download_id is not None + assert exception is not None + self.__event_bus.emit_bulk_download_failed( + bulk_download_id=bulk_download_id, + error=str(exception), + ) + + \ No newline at end of file diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index 04fe71a3eb..a560696692 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -16,6 +16,7 @@ if TYPE_CHECKING: from .board_images.board_images_base import BoardImagesServiceABC from .board_records.board_records_base import BoardRecordStorageBase from .boards.boards_base import BoardServiceABC + from .bulk_download.bulk_download_base import BulkDownloadBase from .config import InvokeAIAppConfig from .download import DownloadQueueServiceBase from .events.events_base import EventServiceBase @@ -41,6 +42,7 @@ class InvocationServices: board_image_records: "BoardImageRecordStorageBase", boards: "BoardServiceABC", board_records: "BoardRecordStorageBase", + bulk_download: "BulkDownloadBase", configuration: "InvokeAIAppConfig", events: "EventServiceBase", images: "ImageServiceABC", @@ -63,6 +65,7 @@ class InvocationServices: self.board_image_records = board_image_records self.boards = boards self.board_records = board_records + self.bulk_download = bulk_download self.configuration = configuration self.events = events self.images = images From 7ecc18938ba207e686dc363838436e341befcd17 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 7 Jan 2024 22:17:03 -0500 Subject: [PATCH 192/411] linted and styling --- invokeai/app/api/routers/images.py | 11 ++++--- invokeai/app/api/sockets.py | 1 - .../bulk_download/bulk_download_base.py | 9 +++--- .../bulk_download/bulk_download_common.py | 10 ++++-- .../bulk_download/bulk_download_defauilt.py | 31 +++++++------------ invokeai/app/services/events/events_base.py | 15 +++++---- 6 files changed, 37 insertions(+), 40 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 2a8e1e7ec7..e32f7fb9ee 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -10,7 +10,6 @@ from pydantic import BaseModel, Field, ValidationError from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin -from invokeai.app.services.board_records.board_records_common import BoardRecordNotFoundException from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator @@ -373,13 +372,16 @@ async def unstar_images_in_list( except Exception: raise HTTPException(status_code=500, detail="Failed to unstar images") + class ImagesDownloaded(BaseModel): response: Optional[str] = Field( description="If defined, the message to display to the user when images begin downloading" ) -@images_router.post("/download", operation_id="download_images_from_list", response_model=ImagesDownloaded, status_code=202) +@images_router.post( + "/download", operation_id="download_images_from_list", response_model=ImagesDownloaded, status_code=202 +) async def download_images_from_list( background_tasks: BackgroundTasks, image_names: list[str] = Body(description="The list of names of images to download", embed=True), @@ -389,6 +391,7 @@ async def download_images_from_list( ) -> ImagesDownloaded: if (image_names is None or len(image_names) == 0) and board_id is None: raise HTTPException(status_code=400, detail="No images or board id specified.") - background_tasks.add_task(ApiDependencies.invoker.services.bulk_download.handler, ApiDependencies.invoker, image_names, board_id) + background_tasks.add_task( + ApiDependencies.invoker.services.bulk_download.handler, ApiDependencies.invoker, image_names, board_id + ) return ImagesDownloaded(response="Your images are preparing to be downloaded") - diff --git a/invokeai/app/api/sockets.py b/invokeai/app/api/sockets.py index c5d9ace8d2..463545d9bc 100644 --- a/invokeai/app/api/sockets.py +++ b/invokeai/app/api/sockets.py @@ -18,7 +18,6 @@ class SocketIO: __sub_bulk_download: str = "subscribe_bulk_download" __unsub_bulk_download: str = "unsubscribe_bulk_download" - def __init__(self, app: FastAPI): self.__sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*") self.__app = ASGIApp(socketio_server=self.__sio, socketio_path="/ws/socket.io") diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index b788020bba..fc45aff280 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -1,13 +1,12 @@ +from abc import ABC, abstractmethod from pathlib import Path from typing import Optional, Union -from abc import ABC, abstractmethod - from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker -class BulkDownloadBase(ABC): +class BulkDownloadBase(ABC): @abstractmethod def __init__( self, @@ -25,8 +24,8 @@ class BulkDownloadBase(ABC): def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: """ Starts a a bulk download job. - + :param invoker: The Invoker that holds all the services, required to be passed as a parameter to avoid circular dependencies. :param image_names: A list of image names to include in the zip file. :param board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. - """ \ No newline at end of file + """ diff --git a/invokeai/app/services/bulk_download/bulk_download_common.py b/invokeai/app/services/bulk_download/bulk_download_common.py index 3ac1f5bba8..23a0589daf 100644 --- a/invokeai/app/services/bulk_download/bulk_download_common.py +++ b/invokeai/app/services/bulk_download/bulk_download_common.py @@ -1,4 +1,3 @@ - class BulkDownloadException(Exception): """Exception raised when a bulk download fails.""" @@ -6,6 +5,7 @@ class BulkDownloadException(Exception): super().__init__(message) self.message = message + class BulkDownloadTargetException(BulkDownloadException): """Exception raised when a bulk download target is not found.""" @@ -13,9 +13,13 @@ class BulkDownloadTargetException(BulkDownloadException): super().__init__(message) self.message = message + class BulkDownloadParametersException(BulkDownloadException): """Exception raised when a bulk download parameter is invalid.""" - def __init__(self, message="The bulk download parameters are invalid, either an array of image names or a board id must be provided"): + def __init__( + self, + message="The bulk download parameters are invalid, either an array of image names or a board id must be provided", + ): super().__init__(message) - self.message = message \ No newline at end of file + self.message = message diff --git a/invokeai/app/services/bulk_download/bulk_download_defauilt.py b/invokeai/app/services/bulk_download/bulk_download_defauilt.py index ebeaa4be5a..8321f5069d 100644 --- a/invokeai/app/services/bulk_download/bulk_download_defauilt.py +++ b/invokeai/app/services/bulk_download/bulk_download_defauilt.py @@ -1,6 +1,6 @@ +import uuid from pathlib import Path from typing import Optional, Union -import uuid from zipfile import ZipFile from invokeai.app.services.board_records.board_records_common import BoardRecordNotFoundException @@ -11,15 +11,17 @@ from invokeai.app.services.invoker import Invoker from .bulk_download_base import BulkDownloadBase -class BulkDownloadService(BulkDownloadBase): +class BulkDownloadService(BulkDownloadBase): __output_folder: Path __bulk_downloads_folder: Path __event_bus: Optional[EventServiceBase] - def __init__(self, - output_folder: Union[str, Path], - event_bus: Optional[EventServiceBase] = None,): + def __init__( + self, + output_folder: Union[str, Path], + event_bus: Optional[EventServiceBase] = None, + ): """ Initialize the downloader object. @@ -30,8 +32,7 @@ class BulkDownloadService(BulkDownloadBase): self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) self.__event_bus = event_bus - - def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: + def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: """ Create a zip file containing the images specified by the given image names or board id. @@ -39,10 +40,8 @@ class BulkDownloadService(BulkDownloadBase): param: board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. """ bulk_download_id = str(uuid.uuid4()) - - self._signal_job_started(bulk_download_id) + try: - board_name: Union[str, None] = None if board_id: image_names = invoker.services.board_image_records.get_all_board_image_names_for_board(board_id) if board_id == "none": @@ -64,24 +63,21 @@ class BulkDownloadService(BulkDownloadBase): for image_name in image_names: image_names_to_paths[image_name] = invoker.services.images.get_path(image_name) return image_names_to_paths - - def _create_zip_file(self, image_names_to_paths: dict[str, str], bulk_download_id: str) -> str: """ - Create a zip file containing the images specified by the given image names or board id. + Create a zip file containing the images specified by the given image names or board id. If download with the same bulk_download_id already exists, it will be overwritten. """ zip_file_path = self.__bulk_downloads_folder / (bulk_download_id + ".zip") - + with ZipFile(zip_file_path, "w") as zip_file: for image_name, image_path in image_names_to_paths.items(): zip_file.write(image_path, arcname=image_name) return str(zip_file_path) - def _signal_job_started(self, bulk_download_id: str) -> None: """Signal that a bulk download job has started.""" if self.__event_bus: @@ -90,7 +86,6 @@ class BulkDownloadService(BulkDownloadBase): bulk_download_id=bulk_download_id, ) - def _signal_job_completed(self, bulk_download_id: str, file_path: str) -> None: """Signal that a bulk download job has completed.""" if self.__event_bus: @@ -100,7 +95,7 @@ class BulkDownloadService(BulkDownloadBase): bulk_download_id=bulk_download_id, file_path=file_path, ) - + def _signal_job_failed(self, bulk_download_id: str, exception: Exception) -> None: """Signal that a bulk download job has failed.""" if self.__event_bus: @@ -110,5 +105,3 @@ class BulkDownloadService(BulkDownloadBase): bulk_download_id=bulk_download_id, error=str(exception), ) - - \ No newline at end of file diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 0a0668b274..597a56d944 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -444,20 +444,19 @@ class EventServiceBase: """Emitted when a bulk download starts""" self._emit_bulk_download_event( event_name="bulk_download_started", - payload={"bulk_download_id": bulk_download_id, } + payload={ + "bulk_download_id": bulk_download_id, + }, ) - + def emit_bulk_download_completed(self, bulk_download_id: str, file_path: str) -> None: """Emitted when a bulk download completes""" self._emit_bulk_download_event( - event_name="bulk_download_completed", - payload={"bulk_download_id": bulk_download_id, - "file_path": file_path} + event_name="bulk_download_completed", payload={"bulk_download_id": bulk_download_id, "file_path": file_path} ) - + def emit_bulk_download_failed(self, bulk_download_id: str, error: str) -> None: """Emitted when a bulk download fails""" self._emit_bulk_download_event( - event_name="bulk_download_failed", - payload={"bulk_download_id": bulk_download_id, "error": error} + event_name="bulk_download_failed", payload={"bulk_download_id": bulk_download_id, "error": error} ) From 52b0deb179fe2b60c14f88805c3a1884df61f02c Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sat, 13 Jan 2024 23:35:33 -0500 Subject: [PATCH 193/411] reworking some of the logic to use a default room, adding endpoint to download file on complete --- invokeai/app/api/dependencies.py | 2 +- invokeai/app/api/routers/images.py | 33 +++++++++ .../bulk_download/bulk_download_base.py | 25 +++++++ .../bulk_download/bulk_download_common.py | 3 + ...d_defauilt.py => bulk_download_default.py} | 72 +++++++++++++++---- invokeai/app/services/events/events_base.py | 23 ++++-- 6 files changed, 137 insertions(+), 21 deletions(-) rename invokeai/app/services/bulk_download/{bulk_download_defauilt.py => bulk_download_default.py} (60%) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index ab09d1e5d7..aaa08a2498 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -15,7 +15,7 @@ from ..services.board_image_records.board_image_records_sqlite import SqliteBoar from ..services.board_images.board_images_default import BoardImagesService from ..services.board_records.board_records_sqlite import SqliteBoardRecordStorage from ..services.boards.boards_default import BoardService -from ..services.bulk_download.bulk_download_defauilt import BulkDownloadService +from ..services.bulk_download.bulk_download_default import BulkDownloadService from ..services.config import InvokeAIAppConfig from ..services.download import DownloadQueueService from ..services.image_files.image_files_disk import DiskImageFileStorage diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index e32f7fb9ee..43392dd471 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -395,3 +395,36 @@ async def download_images_from_list( ApiDependencies.invoker.services.bulk_download.handler, ApiDependencies.invoker, image_names, board_id ) return ImagesDownloaded(response="Your images are preparing to be downloaded") + + +@images_router.api_route( + "/download/{bulk_download_item_name}", + methods=["GET"], + operation_id="get_bulk_download_item", + response_class=Response, + responses={ + 200: { + "description": "Return the complete bulk download item", + "content": {"application/zip": {}}, + }, + 404: {"description": "Image not found"}, + }, +) +async def get_bulk_download_item( + bulk_download_item_name: str = Path(description="The bulk_download_item_id of the bulk download item to get"), +) -> FileResponse: + """Gets a bulk download zip file""" + + try: + path = ApiDependencies.invoker.services.bulk_download.get_path(bulk_download_item_name) + + response = FileResponse( + path, + media_type="application/zip", + filename=bulk_download_item_name, + content_disposition_type="inline", + ) + response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}" + return response + except Exception: + raise HTTPException(status_code=404) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index fc45aff280..8a9ea1f3f2 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -29,3 +29,28 @@ class BulkDownloadBase(ABC): :param image_names: A list of image names to include in the zip file. :param board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. """ + + @abstractmethod + def get_path(self, bulk_download_item_id: str) -> str: + """ + Get the path to the bulk download file. + + :param bulk_download_item_id: The ID of the bulk download item. + :return: The path to the bulk download file. + """ + + @abstractmethod + def stop(self, *args, **kwargs) -> None: + """ + Stops the BulkDownloadService and cleans up all the remnants. + + This method is responsible for stopping the BulkDownloadService and performing any necessary cleanup + operations to remove any remnants or resources associated with the service. + + Args: + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + None + """ diff --git a/invokeai/app/services/bulk_download/bulk_download_common.py b/invokeai/app/services/bulk_download/bulk_download_common.py index 23a0589daf..37b80073be 100644 --- a/invokeai/app/services/bulk_download/bulk_download_common.py +++ b/invokeai/app/services/bulk_download/bulk_download_common.py @@ -1,3 +1,6 @@ +DEFAULT_BULK_DOWNLOAD_ID = "default" + + class BulkDownloadException(Exception): """Exception raised when a bulk download fails.""" diff --git a/invokeai/app/services/bulk_download/bulk_download_defauilt.py b/invokeai/app/services/bulk_download/bulk_download_default.py similarity index 60% rename from invokeai/app/services/bulk_download/bulk_download_defauilt.py rename to invokeai/app/services/bulk_download/bulk_download_default.py index 8321f5069d..561fd173a8 100644 --- a/invokeai/app/services/bulk_download/bulk_download_defauilt.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -4,7 +4,11 @@ from typing import Optional, Union from zipfile import ZipFile from invokeai.app.services.board_records.board_records_common import BoardRecordNotFoundException -from invokeai.app.services.bulk_download.bulk_download_common import BulkDownloadException +from invokeai.app.services.bulk_download.bulk_download_common import ( + DEFAULT_BULK_DOWNLOAD_ID, + BulkDownloadException, + BulkDownloadTargetException, +) from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException from invokeai.app.services.invoker import Invoker @@ -32,6 +36,32 @@ class BulkDownloadService(BulkDownloadBase): self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) self.__event_bus = event_bus + def get_path(self, bulk_download_item_name: str) -> str: + """ + Get the path to the bulk download file. + + :param bulk_download_item_name: The name of the bulk download item. + :return: The path to the bulk download file. + """ + path = str(self.__bulk_downloads_folder / bulk_download_item_name) + if not self.validate_path(path): + raise BulkDownloadTargetException() + return path + + def get_bulk_download_item_name(self, bulk_download_item_id: str) -> str: + """ + Get the name of the bulk download item. + + :param bulk_download_item_id: The ID of the bulk download item. + :return: The name of the bulk download item. + """ + return bulk_download_item_id + ".zip" + + def validate_path(self, path: Union[str, Path]) -> bool: + """Validates the path given for a bulk download.""" + path = path if isinstance(path, Path) else Path(path) + return path.exists() + def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: """ Create a zip file containing the images specified by the given image names or board id. @@ -39,7 +69,9 @@ class BulkDownloadService(BulkDownloadBase): param: image_names: A list of image names to include in the zip file. param: board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. """ - bulk_download_id = str(uuid.uuid4()) + bulk_download_id = DEFAULT_BULK_DOWNLOAD_ID + bulk_download_item_id = str(uuid.uuid4()) + self._signal_job_started(bulk_download_id, bulk_download_item_id) try: if board_id: @@ -47,12 +79,12 @@ class BulkDownloadService(BulkDownloadBase): if board_id == "none": board_id = "Uncategorized" image_names_to_paths: dict[str, str] = self._get_image_name_to_path_map(invoker, image_names) - file_path: str = self._create_zip_file(image_names_to_paths, bulk_download_id) - self._signal_job_completed(bulk_download_id, file_path) + bulk_download_item_name: str = self._create_zip_file(image_names_to_paths, bulk_download_item_id) + self._signal_job_completed(bulk_download_id, bulk_download_item_id, bulk_download_item_name) except (ImageRecordNotFoundException, BoardRecordNotFoundException, BulkDownloadException) as e: - self._signal_job_failed(bulk_download_id, e) + self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) except Exception as e: - self._signal_job_failed(bulk_download_id, e) + self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) def _get_image_name_to_path_map(self, invoker: Invoker, image_names: list[str]) -> dict[str, str]: """ @@ -64,44 +96,54 @@ class BulkDownloadService(BulkDownloadBase): image_names_to_paths[image_name] = invoker.services.images.get_path(image_name) return image_names_to_paths - def _create_zip_file(self, image_names_to_paths: dict[str, str], bulk_download_id: str) -> str: + def _create_zip_file(self, image_names_to_paths: dict[str, str], bulk_download_item_id: str) -> str: """ Create a zip file containing the images specified by the given image names or board id. If download with the same bulk_download_id already exists, it will be overwritten. - """ - zip_file_path = self.__bulk_downloads_folder / (bulk_download_id + ".zip") + :return: The name of the zip file. + """ + zip_file_name = bulk_download_item_id + ".zip" + zip_file_path = self.__bulk_downloads_folder / (zip_file_name) with ZipFile(zip_file_path, "w") as zip_file: for image_name, image_path in image_names_to_paths.items(): zip_file.write(image_path, arcname=image_name) - return str(zip_file_path) + return str(zip_file_name) - def _signal_job_started(self, bulk_download_id: str) -> None: + def _signal_job_started(self, bulk_download_id: str, bulk_download_item_id: str) -> None: """Signal that a bulk download job has started.""" if self.__event_bus: assert bulk_download_id is not None self.__event_bus.emit_bulk_download_started( bulk_download_id=bulk_download_id, + bulk_download_item_id=bulk_download_item_id, ) - def _signal_job_completed(self, bulk_download_id: str, file_path: str) -> None: + def _signal_job_completed( + self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + ) -> None: """Signal that a bulk download job has completed.""" if self.__event_bus: assert bulk_download_id is not None - assert file_path is not None + assert bulk_download_item_name is not None self.__event_bus.emit_bulk_download_completed( bulk_download_id=bulk_download_id, - file_path=file_path, + bulk_download_item_id=bulk_download_item_id, + bulk_download_item_name=bulk_download_item_name, ) - def _signal_job_failed(self, bulk_download_id: str, exception: Exception) -> None: + def _signal_job_failed(self, bulk_download_id: str, bulk_download_item_id: str, exception: Exception) -> None: """Signal that a bulk download job has failed.""" if self.__event_bus: assert bulk_download_id is not None assert exception is not None self.__event_bus.emit_bulk_download_failed( bulk_download_id=bulk_download_id, + bulk_download_item_id=bulk_download_item_id, error=str(exception), ) + + def stop(self, *args, **kwargs): + pass diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 597a56d944..3cc3ba2f28 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -440,23 +440,36 @@ class EventServiceBase: }, ) - def emit_bulk_download_started(self, bulk_download_id: str) -> None: + def emit_bulk_download_started(self, bulk_download_id: str, bulk_download_item_id: str) -> None: """Emitted when a bulk download starts""" self._emit_bulk_download_event( event_name="bulk_download_started", payload={ "bulk_download_id": bulk_download_id, + "bulk_download_item_id": bulk_download_item_id, }, ) - def emit_bulk_download_completed(self, bulk_download_id: str, file_path: str) -> None: + def emit_bulk_download_completed( + self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + ) -> None: """Emitted when a bulk download completes""" self._emit_bulk_download_event( - event_name="bulk_download_completed", payload={"bulk_download_id": bulk_download_id, "file_path": file_path} + event_name="bulk_download_completed", + payload={ + "bulk_download_id": bulk_download_id, + "bulk_download_item_id": bulk_download_item_id, + "bulk_download_item_name": bulk_download_item_name, + }, ) - def emit_bulk_download_failed(self, bulk_download_id: str, error: str) -> None: + def emit_bulk_download_failed(self, bulk_download_id: str, bulk_download_item_id: str, error: str) -> None: """Emitted when a bulk download fails""" self._emit_bulk_download_event( - event_name="bulk_download_failed", payload={"bulk_download_id": bulk_download_id, "error": error} + event_name="bulk_download_failed", + payload={ + "bulk_download_id": bulk_download_id, + "bulk_download_item_id": bulk_download_item_id, + "error": error, + }, ) From c43ea9f25c037a692cfdd45c0b7f357b18d30a0f Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 14 Jan 2024 00:21:00 -0500 Subject: [PATCH 194/411] using the board name to download boards --- .../bulk_download/bulk_download_base.py | 4 +- .../bulk_download/bulk_download_default.py | 39 ++++++++++++------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 8a9ea1f3f2..366a5fec5f 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -31,11 +31,11 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def get_path(self, bulk_download_item_id: str) -> str: + def get_path(self, bulk_download_item_name: str) -> str: """ Get the path to the bulk download file. - :param bulk_download_item_id: The ID of the bulk download item. + :param bulk_download_item_name: The name of the bulk download item. :return: The path to the bulk download file. """ diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 561fd173a8..b80b8cc2f5 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -48,15 +48,6 @@ class BulkDownloadService(BulkDownloadBase): raise BulkDownloadTargetException() return path - def get_bulk_download_item_name(self, bulk_download_item_id: str) -> str: - """ - Get the name of the bulk download item. - - :param bulk_download_item_id: The ID of the bulk download item. - :return: The name of the bulk download item. - """ - return bulk_download_item_id + ".zip" - def validate_path(self, path: Union[str, Path]) -> bool: """Validates the path given for a bulk download.""" path = path if isinstance(path, Path) else Path(path) @@ -69,17 +60,27 @@ class BulkDownloadService(BulkDownloadBase): param: image_names: A list of image names to include in the zip file. param: board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. """ - bulk_download_id = DEFAULT_BULK_DOWNLOAD_ID - bulk_download_item_id = str(uuid.uuid4()) - self._signal_job_started(bulk_download_id, bulk_download_item_id) + + bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID + bulk_download_item_id: str = str(uuid.uuid4()) if board_id is None else board_id try: + board_name: str = "" if board_id: image_names = invoker.services.board_image_records.get_all_board_image_names_for_board(board_id) if board_id == "none": board_id = "Uncategorized" + board_name = "Uncategorized" + else: + board_name = invoker.services.board_records.get(board_id).board_name + board_name = self._clean_string_to_path_safe(board_name) + + self._signal_job_started(bulk_download_id, bulk_download_item_id) + image_names_to_paths: dict[str, str] = self._get_image_name_to_path_map(invoker, image_names) - bulk_download_item_name: str = self._create_zip_file(image_names_to_paths, bulk_download_item_id) + bulk_download_item_name: str = self._create_zip_file( + image_names_to_paths, bulk_download_item_id if board_id is None else board_name + ) self._signal_job_completed(bulk_download_id, bulk_download_item_id, bulk_download_item_name) except (ImageRecordNotFoundException, BoardRecordNotFoundException, BulkDownloadException) as e: self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) @@ -112,6 +113,10 @@ class BulkDownloadService(BulkDownloadBase): return str(zip_file_name) + def _clean_string_to_path_safe(self, s: str) -> str: + """Clean a string to be path safe.""" + return "".join([c for c in s if c.isalpha() or c.isdigit() or c == " "]).rstrip() + def _signal_job_started(self, bulk_download_id: str, bulk_download_item_id: str) -> None: """Signal that a bulk download job has started.""" if self.__event_bus: @@ -146,4 +151,10 @@ class BulkDownloadService(BulkDownloadBase): ) def stop(self, *args, **kwargs): - pass + """Stop the bulk download service and delete the files in the bulk download folder.""" + # Get all the files in the bulk downloads folder + files = self.__bulk_downloads_folder.glob("*") + + # Delete all the files + for file in files: + file.unlink() From 7114d64b861ab6161fc41ba5d2677403a14b20bc Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 14 Jan 2024 01:33:43 -0500 Subject: [PATCH 195/411] fixing issue where default board did not return images --- .../bulk_download/bulk_download_default.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index b80b8cc2f5..36d2b350b9 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -10,7 +10,7 @@ from invokeai.app.services.bulk_download.bulk_download_common import ( BulkDownloadTargetException, ) from invokeai.app.services.events.events_base import EventServiceBase -from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException +from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordNotFoundException from invokeai.app.services.invoker import Invoker from .bulk_download_base import BulkDownloadBase @@ -67,7 +67,17 @@ class BulkDownloadService(BulkDownloadBase): try: board_name: str = "" if board_id: - image_names = invoker.services.board_image_records.get_all_board_image_names_for_board(board_id) + # -1 is the default value for limit, which means no limit, is_intermediate only gives us completed images + image_names = [ + img.image_name + for img in invoker.services.images.get_many( + offset=0, + limit=-1, + board_id=board_id, + is_intermediate=False, + categories=[ImageCategory.GENERAL], + ).items + ] if board_id == "none": board_id = "Uncategorized" board_name = "Uncategorized" From 795fbf0e81ec0529ca807cf39f2309bf1f7a51ac Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 14 Jan 2024 03:09:35 -0500 Subject: [PATCH 196/411] refactoring bulkdownload to consider image category --- invokeai/app/api/routers/images.py | 4 +- .../bulk_download/bulk_download_base.py | 13 +++- .../bulk_download/bulk_download_default.py | 62 +++++++++---------- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 43392dd471..236961fa9e 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -391,9 +391,7 @@ async def download_images_from_list( ) -> ImagesDownloaded: if (image_names is None or len(image_names) == 0) and board_id is None: raise HTTPException(status_code=400, detail="No images or board id specified.") - background_tasks.add_task( - ApiDependencies.invoker.services.bulk_download.handler, ApiDependencies.invoker, image_names, board_id - ) + background_tasks.add_task(ApiDependencies.invoker.services.bulk_download.handler, image_names, board_id) return ImagesDownloaded(response="Your images are preparing to be downloaded") diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 366a5fec5f..880345fe98 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -7,6 +7,17 @@ from invokeai.app.services.invoker import Invoker class BulkDownloadBase(ABC): + @abstractmethod + def start(self, invoker: Invoker) -> None: + """ + Starts the BulkDownloadService. + + This method is responsible for starting the BulkDownloadService and performing any necessary initialization + operations to prepare the service for use. + + param: invoker: The Invoker that holds all the services, required to be passed as a parameter to avoid circular dependencies. + """ + @abstractmethod def __init__( self, @@ -21,7 +32,7 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: + def handler(self, image_names: list[str], board_id: Optional[str]) -> None: """ Starts a a bulk download job. diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 36d2b350b9..ffc26dfa54 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -10,7 +10,8 @@ from invokeai.app.services.bulk_download.bulk_download_common import ( BulkDownloadTargetException, ) from invokeai.app.services.events.events_base import EventServiceBase -from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordNotFoundException +from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException +from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invoker import Invoker from .bulk_download_base import BulkDownloadBase @@ -20,6 +21,10 @@ class BulkDownloadService(BulkDownloadBase): __output_folder: Path __bulk_downloads_folder: Path __event_bus: Optional[EventServiceBase] + __invoker: Invoker + + def start(self, invoker: Invoker) -> None: + self.__invoker = invoker def __init__( self, @@ -53,7 +58,7 @@ class BulkDownloadService(BulkDownloadBase): path = path if isinstance(path, Path) else Path(path) return path.exists() - def handler(self, invoker: Invoker, image_names: list[str], board_id: Optional[str]) -> None: + def handler(self, image_names: list[str], board_id: Optional[str]) -> None: """ Create a zip file containing the images specified by the given image names or board id. @@ -64,50 +69,40 @@ class BulkDownloadService(BulkDownloadBase): bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID bulk_download_item_id: str = str(uuid.uuid4()) if board_id is None else board_id + self._signal_job_started(bulk_download_id, bulk_download_item_id) + try: board_name: str = "" + image_dtos: list[ImageDTO] = [] + if board_id: - # -1 is the default value for limit, which means no limit, is_intermediate only gives us completed images - image_names = [ - img.image_name - for img in invoker.services.images.get_many( - offset=0, - limit=-1, - board_id=board_id, - is_intermediate=False, - categories=[ImageCategory.GENERAL], - ).items - ] if board_id == "none": - board_id = "Uncategorized" board_name = "Uncategorized" else: - board_name = invoker.services.board_records.get(board_id).board_name + board_name = self.__invoker.services.board_records.get(board_id).board_name board_name = self._clean_string_to_path_safe(board_name) - self._signal_job_started(bulk_download_id, bulk_download_item_id) - - image_names_to_paths: dict[str, str] = self._get_image_name_to_path_map(invoker, image_names) + # -1 is the default value for limit, which means no limit, is_intermediate only gives us completed images + image_dtos = self.__invoker.services.images.get_many( + offset=0, + limit=-1, + board_id=board_id, + is_intermediate=False, + ).items + else: + image_dtos = [self.__invoker.services.images.get_dto(image_name) for image_name in image_names] bulk_download_item_name: str = self._create_zip_file( - image_names_to_paths, bulk_download_item_id if board_id is None else board_name + image_dtos, bulk_download_item_id if board_id is None else board_name ) self._signal_job_completed(bulk_download_id, bulk_download_item_id, bulk_download_item_name) except (ImageRecordNotFoundException, BoardRecordNotFoundException, BulkDownloadException) as e: self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) except Exception as e: self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) + self.__invoker.services.logger.error("Problem bulk downloading images.") + raise e - def _get_image_name_to_path_map(self, invoker: Invoker, image_names: list[str]) -> dict[str, str]: - """ - Create a map of image names to their paths. - :param image_names: A list of image names. - """ - image_names_to_paths: dict[str, str] = {} - for image_name in image_names: - image_names_to_paths[image_name] = invoker.services.images.get_path(image_name) - return image_names_to_paths - - def _create_zip_file(self, image_names_to_paths: dict[str, str], bulk_download_item_id: str) -> str: + def _create_zip_file(self, image_dtos: list[ImageDTO], bulk_download_item_id: str) -> str: """ Create a zip file containing the images specified by the given image names or board id. If download with the same bulk_download_id already exists, it will be overwritten. @@ -118,11 +113,14 @@ class BulkDownloadService(BulkDownloadBase): zip_file_path = self.__bulk_downloads_folder / (zip_file_name) with ZipFile(zip_file_path, "w") as zip_file: - for image_name, image_path in image_names_to_paths.items(): - zip_file.write(image_path, arcname=image_name) + for image_dto in image_dtos: + image_zip_path = Path(image_dto.image_category.value) / image_dto.image_name + image_path = self.__invoker.services.images.get_path(image_dto.image_name) + zip_file.write(image_path, arcname=image_zip_path) return str(zip_file_name) + # from https://stackoverflow.com/questions/7406102/create-sane-safe-filename-from-any-unsafe-string def _clean_string_to_path_safe(self, s: str) -> str: """Clean a string to be path safe.""" return "".join([c for c in s if c.isalpha() or c.isdigit() or c == " "]).rstrip() From db812133e72579ccfc14bf88ab1519c04b6e0242 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 14 Jan 2024 22:58:07 -0500 Subject: [PATCH 197/411] refactoring dummy event service, DRY principal; adding bulk_download_event to existing invoker tests --- .../services/download/test_download_queue.py | 28 ++------------- .../model_install/test_model_install.py | 1 - tests/fixtures/event_service.py | 34 +++++++++++++++++++ tests/test_graph_execution_state.py | 1 + 4 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/event_service.py diff --git a/tests/app/services/download/test_download_queue.py b/tests/app/services/download/test_download_queue.py index 93a3832b51..34408ac5ae 100644 --- a/tests/app/services/download/test_download_queue.py +++ b/tests/app/services/download/test_download_queue.py @@ -2,19 +2,17 @@ import re import time from pathlib import Path -from typing import Any, Dict, List import pytest -from pydantic import BaseModel from pydantic.networks import AnyHttpUrl from requests.sessions import Session from requests_testadapter import TestAdapter, TestSession from invokeai.app.services.download import DownloadJob, DownloadJobStatus, DownloadQueueService -from invokeai.app.services.events.events_base import EventServiceBase +from tests.fixtures.event_service import DummyEventService # Prevent pytest deprecation warnings -TestAdapter.__test__ = False +TestAdapter.__test__ = False # type: ignore @pytest.fixture @@ -52,28 +50,6 @@ def session() -> Session: return sess -class DummyEvent(BaseModel): - """Dummy Event to use with Dummy Event service.""" - - event_name: str - payload: Dict[str, Any] - - -# A dummy event service for testing event issuing -class DummyEventService(EventServiceBase): - """Dummy event service for testing.""" - - events: List[DummyEvent] - - def __init__(self) -> None: - super().__init__() - self.events = [] - - def dispatch(self, event_name: str, payload: Any) -> None: - """Dispatch an event by appending it to self.events.""" - self.events.append(DummyEvent(event_name=payload["event"], payload=payload["data"])) - - def test_basic_queue_download(tmp_path: Path, session: Session) -> None: events = set() diff --git a/tests/app/services/model_install/test_model_install.py b/tests/app/services/model_install/test_model_install.py index 55f7e86541..14c8ed5c84 100644 --- a/tests/app/services/model_install/test_model_install.py +++ b/tests/app/services/model_install/test_model_install.py @@ -10,7 +10,6 @@ from pydantic import ValidationError from pydantic.networks import Url from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.model_install import ( InstallStatus, LocalModelSource, diff --git a/tests/fixtures/event_service.py b/tests/fixtures/event_service.py new file mode 100644 index 0000000000..71262be3f9 --- /dev/null +++ b/tests/fixtures/event_service.py @@ -0,0 +1,34 @@ +from typing import Any, Dict, List + +import pytest +from pydantic import BaseModel + +from invokeai.app.services.events.events_base import EventServiceBase + + +class DummyEvent(BaseModel): + """Dummy Event to use with Dummy Event service.""" + + event_name: str + payload: Dict[str, Any] + + +# A dummy event service for testing event issuing +class DummyEventService(EventServiceBase): + """Dummy event service for testing.""" + + events: List[DummyEvent] + + def __init__(self) -> None: + super().__init__() + self.events = [] + + def dispatch(self, event_name: str, payload: Any) -> None: + """Dispatch an event by appending it to self.events.""" + self.events.append(DummyEvent(event_name=payload["event"], payload=payload["data"])) + + +@pytest.fixture +def mock_event_service() -> EventServiceBase: + """Create a dummy event service.""" + return DummyEventService() diff --git a/tests/test_graph_execution_state.py b/tests/test_graph_execution_state.py index 2e88178424..9a35037431 100644 --- a/tests/test_graph_execution_state.py +++ b/tests/test_graph_execution_state.py @@ -50,6 +50,7 @@ def mock_services() -> InvocationServices: board_images=None, # type: ignore board_records=None, # type: ignore boards=None, # type: ignore + bulk_download=None, # type: ignore configuration=configuration, events=TestEventService(), image_files=None, # type: ignore From 7d91426d8f499b019f62e047aca2427250587f92 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Mon, 15 Jan 2024 12:59:45 -0500 Subject: [PATCH 198/411] refactoring bulk_download to be better managed --- invokeai/app/api/dependencies.py | 2 +- .../bulk_download/bulk_download_base.py | 8 +----- .../bulk_download/bulk_download_default.py | 26 +++++++++---------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index aaa08a2498..984fd8e267 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -82,7 +82,7 @@ class ApiDependencies: board_records = SqliteBoardRecordStorage(db=db) boards = BoardService() events = FastAPIEventService(event_handler_id) - bulk_download = BulkDownloadService(output_folder=f"{output_folder}", event_bus=events) + bulk_download = BulkDownloadService(output_folder=f"{output_folder}") image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 880345fe98..7a4aa0661c 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod from pathlib import Path from typing import Optional, Union -from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.invoker import Invoker @@ -19,16 +18,11 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def __init__( - self, - output_folder: Union[str, Path], - event_bus: Optional["EventServiceBase"] = None, - ): + def __init__(self, output_folder: Union[str, Path]): """ Create BulkDownloadBase object. :param output_folder: The path to the output folder where the bulk download files can be temporarily stored. - :param event_bus: InvokeAI event bus for reporting events to. """ @abstractmethod diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index ffc26dfa54..a9ea12bfd6 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -1,4 +1,3 @@ -import uuid from pathlib import Path from typing import Optional, Union from zipfile import ZipFile @@ -13,6 +12,7 @@ from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invoker import Invoker +from invokeai.app.util.misc import uuid_string from .bulk_download_base import BulkDownloadBase @@ -20,26 +20,23 @@ from .bulk_download_base import BulkDownloadBase class BulkDownloadService(BulkDownloadBase): __output_folder: Path __bulk_downloads_folder: Path - __event_bus: Optional[EventServiceBase] + __event_bus: EventServiceBase __invoker: Invoker def start(self, invoker: Invoker) -> None: self.__invoker = invoker + self.__event_bus = invoker.services.events def __init__( self, output_folder: Union[str, Path], - event_bus: Optional[EventServiceBase] = None, ): """ Initialize the downloader object. - - :param event_bus: Optional EventService object """ self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder) self.__bulk_downloads_folder = self.__output_folder / "bulk_downloads" self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) - self.__event_bus = event_bus def get_path(self, bulk_download_item_name: str) -> str: """ @@ -67,7 +64,7 @@ class BulkDownloadService(BulkDownloadBase): """ bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID - bulk_download_item_id: str = str(uuid.uuid4()) if board_id is None else board_id + bulk_download_item_id: str = uuid_string() if board_id is None else board_id self._signal_job_started(bulk_download_id, bulk_download_item_id) @@ -76,10 +73,7 @@ class BulkDownloadService(BulkDownloadBase): image_dtos: list[ImageDTO] = [] if board_id: - if board_id == "none": - board_name = "Uncategorized" - else: - board_name = self.__invoker.services.board_records.get(board_id).board_name + board_name = self._get_board_name(board_id) board_name = self._clean_string_to_path_safe(board_name) # -1 is the default value for limit, which means no limit, is_intermediate only gives us completed images @@ -102,6 +96,12 @@ class BulkDownloadService(BulkDownloadBase): self.__invoker.services.logger.error("Problem bulk downloading images.") raise e + def _get_board_name(self, board_id: str) -> str: + if board_id == "none": + return "Uncategorized" + + return self.__invoker.services.board_records.get(board_id).board_name + def _create_zip_file(self, image_dtos: list[ImageDTO], bulk_download_item_id: str) -> str: """ Create a zip file containing the images specified by the given image names or board id. @@ -115,8 +115,8 @@ class BulkDownloadService(BulkDownloadBase): with ZipFile(zip_file_path, "w") as zip_file: for image_dto in image_dtos: image_zip_path = Path(image_dto.image_category.value) / image_dto.image_name - image_path = self.__invoker.services.images.get_path(image_dto.image_name) - zip_file.write(image_path, arcname=image_zip_path) + image_disk_path = self.__invoker.services.images.get_path(image_dto.image_name) + zip_file.write(image_disk_path, arcname=image_zip_path) return str(zip_file_name) From 284ba041bd7a0e39aeb6f031c0ae7dcfa3602311 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Mon, 15 Jan 2024 13:01:06 -0500 Subject: [PATCH 199/411] 97% test coverage on bulk_download --- .../bulk_download/test_bulk_download.py | 319 ++++++++++++++++++ tests/fixtures/event_service.py | 2 +- 2 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 tests/app/services/bulk_download/test_bulk_download.py diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py new file mode 100644 index 0000000000..4f476c21be --- /dev/null +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -0,0 +1,319 @@ +import os +from pathlib import Path +from typing import Any +from zipfile import ZipFile + +import pytest + +from invokeai.app.services.board_records.board_records_common import BoardRecord, BoardRecordNotFoundException +from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage +from invokeai.app.services.bulk_download.bulk_download_common import BulkDownloadTargetException +from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService +from invokeai.app.services.config.config_default import InvokeAIAppConfig +from invokeai.app.services.image_records.image_records_common import ( + ImageCategory, + ImageRecordNotFoundException, + ResourceOrigin, +) +from invokeai.app.services.images.images_common import ImageDTO +from invokeai.app.services.images.images_default import ImageService +from invokeai.app.services.invocation_services import InvocationServices +from invokeai.app.services.invoker import Invoker +from invokeai.app.services.shared.pagination import OffsetPaginatedResults +from invokeai.backend.util.logging import InvokeAILogger +from tests.fixtures.event_service import DummyEventService, mock_event_service # noqa: F401,F811 +from tests.fixtures.sqlite_database import create_mock_sqlite_database + + +@pytest.fixture +def mock_image_dto() -> ImageDTO: + """Create a mock ImageDTO.""" + return ImageDTO( + image_name="mock_image.png", + board_id="12345", + image_url="None", + width=100, + height=100, + thumbnail_url="None", + image_origin=ResourceOrigin.INTERNAL, + image_category=ImageCategory.GENERAL, + created_at="None", + updated_at="None", + starred=False, + has_workflow=False, + is_intermediate=False, + ) + + +@pytest.fixture +def mock_services(mock_event_service: DummyEventService) -> InvocationServices: + configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) + logger = InvokeAILogger.get_logger() + db = create_mock_sqlite_database(configuration, logger) + + return InvocationServices( + board_image_records=None, # type: ignore + board_images=None, # type: ignore + board_records=SqliteBoardRecordStorage(db=db), + boards=None, # type: ignore + bulk_download=None, # type: ignore + configuration=None, # type: ignore + events=mock_event_service, + graph_execution_manager=None, # type: ignore + image_files=None, # type: ignore + image_records=None, # type: ignore + images=ImageService(), + invocation_cache=None, # type: ignore + latents=None, # type: ignore + logger=logger, + model_manager=None, # type: ignore + model_records=None, # type: ignore + download_queue=None, # type: ignore + model_install=None, # type: ignore + names=None, # type: ignore + performance_statistics=None, # type: ignore + processor=None, # type: ignore + queue=None, # type: ignore + session_processor=None, # type: ignore + session_queue=None, # type: ignore + urls=None, # type: ignore + workflow_records=None, # type: ignore + ) + + +@pytest.fixture() +def mock_invoker(mock_services: InvocationServices) -> Invoker: + return Invoker(services=mock_services) + + +def test_get_path_when_file_exists(tmp_path: Path) -> None: + """Test get_path when the file exists.""" + + # Create a directory at tmp_path/bulk_downloads + test_bulk_downloads_dir: Path = tmp_path / "bulk_downloads" + test_bulk_downloads_dir.mkdir(parents=True, exist_ok=True) + + # Create a file at tmp_path/bulk_downloads/test.zip + test_file_path: Path = test_bulk_downloads_dir / "test.zip" + test_file_path.touch() + + bulk_download_service = BulkDownloadService(tmp_path) + assert bulk_download_service.get_path("test.zip") == str(test_file_path) + + +def test_get_path_when_file_does_not_exist(tmp_path: Path) -> None: + """Test get_path when the file does not exist.""" + + bulk_download_service = BulkDownloadService(tmp_path) + with pytest.raises(BulkDownloadTargetException): + bulk_download_service.get_path("test") + + +def test_bulk_downloads_dir_created_at_start(tmp_path: Path) -> None: + """Test that the bulk_downloads directory is created at start.""" + + BulkDownloadService(tmp_path) + assert (tmp_path / "bulk_downloads").exists() + + +def test_handler_image_names(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): + """Test that the handler creates the zip file correctly when given a list of image names.""" + + expected_zip_path, expected_image_path, mock_image_contents = prepare_handler_test( + tmp_path, monkeypatch, mock_image_dto, mock_invoker + ) + + bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service.start(mock_invoker) + bulk_download_service.handler([mock_image_dto.image_name], None) + + assert_handler_success( + expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events + ) + + +def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): + """Test that the handler creates the zip file correctly when given a board id.""" + + expected_zip_path, expected_image_path, mock_image_contents = prepare_handler_test( + tmp_path, monkeypatch, mock_image_dto, mock_invoker + ) + + def mock_board_get(*args, **kwargs): + return BoardRecord(board_id="12345", board_name="test", created_at="None", updated_at="None") + + monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get) + + def mock_get_many(*args, **kwargs): + return OffsetPaginatedResults(limit=-1, total=1, offset=0, items=[mock_image_dto]) + + monkeypatch.setattr(mock_invoker.services.images, "get_many", mock_get_many) + + bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service.start(mock_invoker) + bulk_download_service.handler([], "test") + + assert_handler_success( + expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events + ) + + +def test_handler_board_id_default(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): + """Test that the handler creates the zip file correctly when given a board id.""" + + _, expected_image_path, mock_image_contents = prepare_handler_test( + tmp_path, monkeypatch, mock_image_dto, mock_invoker + ) + expected_zip_path: Path = tmp_path / "bulk_downloads" / "Uncategorized.zip" + + def mock_get_many(*args, **kwargs): + return OffsetPaginatedResults(limit=-1, total=1, offset=0, items=[mock_image_dto]) + + monkeypatch.setattr(mock_invoker.services.images, "get_many", mock_get_many) + + bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service.start(mock_invoker) + bulk_download_service.handler([], "none") + + assert_handler_success( + expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events + ) + + +def prepare_handler_test(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): + """Prepare the test for the handler tests.""" + + def mock_uuid_string(): + return "test" + + # You have to patch the function within the module it's being imported into. This is strange, but it works. + # See http://www.gregreda.com/2021/06/28/mocking-imported-module-function-python/ + monkeypatch.setattr("invokeai.app.services.bulk_download.bulk_download_default.uuid_string", mock_uuid_string) + + expected_zip_path: Path = tmp_path / "bulk_downloads" / "test.zip" + expected_image_path: Path = ( + tmp_path / "bulk_downloads" / mock_image_dto.image_category.value / mock_image_dto.image_name + ) + + # Mock the get_dto method so that when the image dto needs to be retrieved it is returned + def mock_get_dto(*args, **kwargs): + return mock_image_dto + + monkeypatch.setattr(mock_invoker.services.images, "get_dto", mock_get_dto) + + # Create a mock image file so that the contents of the zip file are not empty + mock_image_path: Path = tmp_path / mock_image_dto.image_name + mock_image_contents: str = "Totally an image" + mock_image_path.write_text(mock_image_contents) + + def mock_get_path(*args, **kwargs): + return str(mock_image_path) + + monkeypatch.setattr(mock_invoker.services.images, "get_path", mock_get_path) + + return expected_zip_path, expected_image_path, mock_image_contents + + +def assert_handler_success( + expected_zip_path: Path, + expected_image_path: Path, + mock_image_contents: str, + tmp_path: Path, + event_bus: DummyEventService, +): + """Assert that the handler was successful.""" + # Check that the zip file was created + assert expected_zip_path.exists() + assert expected_zip_path.is_file() + assert expected_zip_path.stat().st_size > 0 + + # Check that the zip contents are expected + with ZipFile(expected_zip_path, "r") as zip_file: + zip_file.extractall(tmp_path / "bulk_downloads") + assert expected_image_path.exists() + assert expected_image_path.is_file() + assert expected_image_path.stat().st_size > 0 + assert expected_image_path.read_text() == mock_image_contents + + # Check that the correct events were emitted + assert len(event_bus.events) == 2 + assert event_bus.events[0].event_name == "bulk_download_started" + assert event_bus.events[1].event_name == "bulk_download_completed" + assert event_bus.events[1].payload["bulk_download_item_name"] == os.path.basename(expected_zip_path) + + +def test_stop(tmp_path: Path) -> None: + """Test that the stop method removes the bulk_downloads directory.""" + + bulk_download_service = BulkDownloadService(tmp_path) + + mock_file: Path = tmp_path / "bulk_downloads" / "test.zip" + mock_file.write_text("contents") + + bulk_download_service.stop() + + assert (tmp_path / "bulk_downloads").exists() + assert len(os.listdir(tmp_path / "bulk_downloads")) == 0 + + +def test_handler_on_image_not_found(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): + """Test that the handler emits an error event when the image is not found.""" + exception: Exception = ImageRecordNotFoundException("Image not found") + + def mock_get_dto(*args, **kwargs): + raise exception + + monkeypatch.setattr(mock_invoker.services.images, "get_dto", mock_get_dto) + + execute_handler_test_on_error(tmp_path, monkeypatch, mock_image_dto, mock_invoker, exception) + + +def test_handler_on_board_not_found(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): + """Test that the handler emits an error event when the image is not found.""" + + exception: Exception = BoardRecordNotFoundException("Image not found") + + def mock_get_board_name(*args, **kwargs): + raise exception + + monkeypatch.setattr(mock_invoker.services.images, "get_dto", mock_get_board_name) + + execute_handler_test_on_error(tmp_path, monkeypatch, mock_image_dto, mock_invoker, exception) + + +def test_handler_on_generic_exception( + tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker +): + """Test that the handler emits an error event when the image is not found.""" + + exception: Exception = Exception("Generic exception") + + def mock_get_board_name(*args, **kwargs): + raise exception + + monkeypatch.setattr(mock_invoker.services.images, "get_dto", mock_get_board_name) + + with pytest.raises(Exception): + execute_handler_test_on_error(tmp_path, monkeypatch, mock_image_dto, mock_invoker, exception) + + event_bus: DummyEventService = mock_invoker.services.events + + assert len(event_bus.events) == 2 + assert event_bus.events[0].event_name == "bulk_download_started" + assert event_bus.events[1].event_name == "bulk_download_failed" + assert event_bus.events[1].payload["error"] == exception.__str__() + + +def execute_handler_test_on_error( + tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker, error: Exception +): + bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service.start(mock_invoker) + bulk_download_service.handler([mock_image_dto.image_name], None) + + event_bus: DummyEventService = mock_invoker.services.events + + assert len(event_bus.events) == 2 + assert event_bus.events[0].event_name == "bulk_download_started" + assert event_bus.events[1].event_name == "bulk_download_failed" + assert event_bus.events[1].payload["error"] == error.__str__() diff --git a/tests/fixtures/event_service.py b/tests/fixtures/event_service.py index 71262be3f9..0a09fa0d64 100644 --- a/tests/fixtures/event_service.py +++ b/tests/fixtures/event_service.py @@ -29,6 +29,6 @@ class DummyEventService(EventServiceBase): @pytest.fixture -def mock_event_service() -> EventServiceBase: +def mock_event_service() -> DummyEventService: """Create a dummy event service.""" return DummyEventService() From 7544b350f32cc6729507cb4ef57a014c4b1b282a Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Mon, 15 Jan 2024 14:37:42 -0500 Subject: [PATCH 200/411] replacing import removed during rebase --- tests/app/services/model_install/test_model_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/app/services/model_install/test_model_install.py b/tests/app/services/model_install/test_model_install.py index 14c8ed5c84..55f7e86541 100644 --- a/tests/app/services/model_install/test_model_install.py +++ b/tests/app/services/model_install/test_model_install.py @@ -10,6 +10,7 @@ from pydantic import ValidationError from pydantic.networks import Url from invokeai.app.services.config import InvokeAIAppConfig +from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.model_install import ( InstallStatus, LocalModelSource, From 79eb871683273ba570de6543a020159fb3b1f611 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Mon, 15 Jan 2024 15:58:43 -0500 Subject: [PATCH 201/411] cleaning up bulk download zip after the response is complete --- invokeai/app/api/routers/images.py | 3 +- .../bulk_download/bulk_download_base.py | 8 ++++ .../bulk_download/bulk_download_default.py | 43 +++++++++++-------- .../bulk_download/test_bulk_download.py | 16 ++++++- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 236961fa9e..d11c89c749 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -409,10 +409,10 @@ async def download_images_from_list( }, ) async def get_bulk_download_item( + background_tasks: BackgroundTasks, bulk_download_item_name: str = Path(description="The bulk_download_item_id of the bulk download item to get"), ) -> FileResponse: """Gets a bulk download zip file""" - try: path = ApiDependencies.invoker.services.bulk_download.get_path(bulk_download_item_name) @@ -423,6 +423,7 @@ async def get_bulk_download_item( content_disposition_type="inline", ) response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}" + background_tasks.add_task(ApiDependencies.invoker.services.bulk_download.delete, bulk_download_item_name) return response except Exception: raise HTTPException(status_code=404) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 7a4aa0661c..a1071f254a 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -59,3 +59,11 @@ class BulkDownloadBase(ABC): Returns: None """ + + @abstractmethod + def delete(self, bulk_download_item_name: str) -> None: + """ + Delete the bulk download file. + + :param bulk_download_item_name: The name of the bulk download item. + """ diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index a9ea12bfd6..a0abb6743a 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -38,23 +38,6 @@ class BulkDownloadService(BulkDownloadBase): self.__bulk_downloads_folder = self.__output_folder / "bulk_downloads" self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) - def get_path(self, bulk_download_item_name: str) -> str: - """ - Get the path to the bulk download file. - - :param bulk_download_item_name: The name of the bulk download item. - :return: The path to the bulk download file. - """ - path = str(self.__bulk_downloads_folder / bulk_download_item_name) - if not self.validate_path(path): - raise BulkDownloadTargetException() - return path - - def validate_path(self, path: Union[str, Path]) -> bool: - """Validates the path given for a bulk download.""" - path = path if isinstance(path, Path) else Path(path) - return path.exists() - def handler(self, image_names: list[str], board_id: Optional[str]) -> None: """ Create a zip file containing the images specified by the given image names or board id. @@ -166,3 +149,29 @@ class BulkDownloadService(BulkDownloadBase): # Delete all the files for file in files: file.unlink() + + def delete(self, bulk_download_item_name: str) -> None: + """ + Delete the bulk download file. + + :param bulk_download_item_name: The name of the bulk download item. + """ + path = self.get_path(bulk_download_item_name) + Path(path).unlink() + + def get_path(self, bulk_download_item_name: str) -> str: + """ + Get the path to the bulk download file. + + :param bulk_download_item_name: The name of the bulk download item. + :return: The path to the bulk download file. + """ + path = str(self.__bulk_downloads_folder / bulk_download_item_name) + if not self.validate_path(path): + raise BulkDownloadTargetException() + return path + + def validate_path(self, path: Union[str, Path]) -> bool: + """Validates the path given for a bulk download.""" + path = path if isinstance(path, Path) else Path(path) + return path.exists() diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 4f476c21be..4c9dc42612 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -293,7 +293,7 @@ def test_handler_on_generic_exception( monkeypatch.setattr(mock_invoker.services.images, "get_dto", mock_get_board_name) - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 execute_handler_test_on_error(tmp_path, monkeypatch, mock_image_dto, mock_invoker, exception) event_bus: DummyEventService = mock_invoker.services.events @@ -317,3 +317,17 @@ def execute_handler_test_on_error( assert event_bus.events[0].event_name == "bulk_download_started" assert event_bus.events[1].event_name == "bulk_download_failed" assert event_bus.events[1].payload["error"] == error.__str__() + + +def test_delete(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): + """Test that the delete method removes the bulk download file.""" + + bulk_download_service = BulkDownloadService(tmp_path) + + mock_file: Path = tmp_path / "bulk_downloads" / "test.zip" + mock_file.write_text("contents") + + bulk_download_service.delete("test.zip") + + assert (tmp_path / "bulk_downloads").exists() + assert len(os.listdir(tmp_path / "bulk_downloads")) == 0 From 39c01a833dbc17a3a03232d2c96bdae3c57c3cdd Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Mon, 15 Jan 2024 19:11:03 -0500 Subject: [PATCH 202/411] adding test coverage for new bulk download routes --- pyproject.toml | 1 + tests/app/routers/test_images.py | 145 ++++++++++++++++++ .../bulk_download/test_bulk_download.py | 2 +- 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 tests/app/routers/test_images.py diff --git a/pyproject.toml b/pyproject.toml index f57607bc0a..5345851951 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,7 @@ dependencies = [ "pytest-cov", "pytest-datadir", "requests_testadapter", + "httpx", ] [project.scripts] diff --git a/tests/app/routers/test_images.py b/tests/app/routers/test_images.py new file mode 100644 index 0000000000..040ae01914 --- /dev/null +++ b/tests/app/routers/test_images.py @@ -0,0 +1,145 @@ +from pathlib import Path +from typing import Any + +import pytest +from fastapi import BackgroundTasks +from fastapi.testclient import TestClient + +from invokeai.app.api.dependencies import ApiDependencies +from invokeai.app.api_app import app +from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage +from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService +from invokeai.app.services.config.config_default import InvokeAIAppConfig +from invokeai.app.services.images.images_default import ImageService +from invokeai.app.services.invocation_services import InvocationServices +from invokeai.app.services.invoker import Invoker +from invokeai.backend.util.logging import InvokeAILogger +from tests.fixtures.sqlite_database import create_mock_sqlite_database + +client = TestClient(app) + + +@pytest.fixture +def mock_services(tmp_path: Path) -> InvocationServices: + configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) + logger = InvokeAILogger.get_logger() + db = create_mock_sqlite_database(configuration, logger) + + return InvocationServices( + board_image_records=None, # type: ignore + board_images=None, # type: ignore + board_records=SqliteBoardRecordStorage(db=db), + boards=None, # type: ignore + bulk_download=BulkDownloadService(tmp_path), + configuration=None, # type: ignore + events=None, # type: ignore + graph_execution_manager=None, # type: ignore + image_files=None, # type: ignore + image_records=None, # type: ignore + images=ImageService(), + invocation_cache=None, # type: ignore + latents=None, # type: ignore + logger=logger, + model_manager=None, # type: ignore + model_records=None, # type: ignore + download_queue=None, # type: ignore + model_install=None, # type: ignore + names=None, # type: ignore + performance_statistics=None, # type: ignore + processor=None, # type: ignore + queue=None, # type: ignore + session_processor=None, # type: ignore + session_queue=None, # type: ignore + urls=None, # type: ignore + workflow_records=None, # type: ignore + ) + + +@pytest.fixture() +def mock_invoker(mock_services: InvocationServices) -> Invoker: + return Invoker(services=mock_services) + + +class MockApiDependencies(ApiDependencies): + invoker: Invoker + + def __init__(self, invoker) -> None: + self.invoker = invoker + + +def test_download_images_from_list(monkeypatch: Any, mock_invoker: Invoker) -> None: + prepare_download_images_test(monkeypatch, mock_invoker) + + response = client.post("/api/v1/images/download", json={"image_names": ["test.png"]}) + + assert response.status_code == 202 + + +def test_download_images_from_board_id_empty_image_name_list(monkeypatch: Any, mock_invoker: Invoker) -> None: + prepare_download_images_test(monkeypatch, mock_invoker) + + response = client.post("/api/v1/images/download", json={"image_names": [], "board_id": "test"}) + + assert response.status_code == 202 + + +def prepare_download_images_test(monkeypatch: Any, mock_invoker: Invoker) -> None: + monkeypatch.setattr("invokeai.app.api.routers.images.ApiDependencies", MockApiDependencies(mock_invoker)) + + def mock_add_task(*args, **kwargs): + return None + + monkeypatch.setattr(BackgroundTasks, "add_task", mock_add_task) + + +def test_download_images_with_empty_image_list_and_no_board_id(monkeypatch: Any, mock_invoker: Invoker) -> None: + prepare_download_images_test(monkeypatch, mock_invoker) + + response = client.post("/api/v1/images/download", json={"image_names": []}) + + assert response.status_code == 400 + + +def test_get_bulk_download_image(tmp_path: Path, monkeypatch: Any, mock_invoker: Invoker) -> None: + mock_file: Path = tmp_path / "test.zip" + mock_file.write_text("contents") + + monkeypatch.setattr(mock_invoker.services.bulk_download, "get_path", lambda x: str(mock_file)) + monkeypatch.setattr("invokeai.app.api.routers.images.ApiDependencies", MockApiDependencies(mock_invoker)) + + def mock_add_task(*args, **kwargs): + return None + + monkeypatch.setattr(BackgroundTasks, "add_task", mock_add_task) + + response = client.get("/api/v1/images/download/test.zip") + + assert response.status_code == 200 + assert response.content == b"contents" + + +def test_get_bulk_download_image_not_found(monkeypatch: Any, mock_invoker: Invoker) -> None: + monkeypatch.setattr("invokeai.app.api.routers.images.ApiDependencies", MockApiDependencies(mock_invoker)) + + def mock_add_task(*args, **kwargs): + return None + + monkeypatch.setattr(BackgroundTasks, "add_task", mock_add_task) + + response = client.get("/api/v1/images/download/test.zip") + + assert response.status_code == 404 + + +def test_get_bulk_download_image_image_deleted_after_response( + monkeypatch: Any, mock_invoker: Invoker, tmp_path: Path +) -> None: + mock_file: Path = tmp_path / "test.zip" + mock_file.write_text("contents") + + monkeypatch.setattr(mock_invoker.services.bulk_download, "get_path", lambda x: str(mock_file)) + monkeypatch.setattr("invokeai.app.api.routers.images.ApiDependencies", MockApiDependencies(mock_invoker)) + + client.get("/api/v1/images/download/test.zip") + + assert not (tmp_path / "test.zip").exists() diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 4c9dc42612..bc6eb8d41c 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -293,7 +293,7 @@ def test_handler_on_generic_exception( monkeypatch.setattr(mock_invoker.services.images, "get_dto", mock_get_board_name) - with pytest.raises(Exception): # noqa: B017 + with pytest.raises(Exception): # noqa: B017 execute_handler_test_on_error(tmp_path, monkeypatch, mock_image_dto, mock_invoker, exception) event_bus: DummyEventService = mock_invoker.services.events From b5ca1643a607e3d1c6a7d9f2b3eb48baf55a376a Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 28 Jan 2024 00:38:01 -0500 Subject: [PATCH 203/411] narrowing bulk_download stop service scope --- .../bulk_download/bulk_download_default.py | 4 ++-- .../bulk_download/test_bulk_download.py | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index a0abb6743a..87966ad622 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -143,8 +143,8 @@ class BulkDownloadService(BulkDownloadBase): def stop(self, *args, **kwargs): """Stop the bulk download service and delete the files in the bulk download folder.""" - # Get all the files in the bulk downloads folder - files = self.__bulk_downloads_folder.glob("*") + # Get all the files in the bulk downloads folder, only .zip files + files = self.__bulk_downloads_folder.glob("*.zip") # Delete all the files for file in files: diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index bc6eb8d41c..184519866a 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -319,7 +319,7 @@ def execute_handler_test_on_error( assert event_bus.events[1].payload["error"] == error.__str__() -def test_delete(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): +def test_delete(tmp_path: Path): """Test that the delete method removes the bulk download file.""" bulk_download_service = BulkDownloadService(tmp_path) @@ -331,3 +331,21 @@ def test_delete(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock assert (tmp_path / "bulk_downloads").exists() assert len(os.listdir(tmp_path / "bulk_downloads")) == 0 + +def test_stop(tmp_path: Path): + """Test that the delete method removes the bulk download file.""" + + bulk_download_service = BulkDownloadService(tmp_path) + + mock_file: Path = tmp_path / "bulk_downloads" / "test.zip" + mock_file.write_text("contents") + + mock_dir: Path = tmp_path / "bulk_downloads" / "test" + mock_dir.mkdir(parents=True, exist_ok=True) + + + bulk_download_service.stop() + + assert (tmp_path / "bulk_downloads").exists() + assert mock_dir.exists() + assert len(os.listdir(tmp_path / "bulk_downloads")) == 1 From d0f3571e59cc1ec28b8d25e031557ca5d46dd5b7 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 28 Jan 2024 01:23:38 -0500 Subject: [PATCH 204/411] returning the bulk_download_item_name on response for possible polling --- invokeai/app/api/routers/images.py | 28 +++++-- .../bulk_download/bulk_download_base.py | 11 ++- .../bulk_download/bulk_download_default.py | 9 ++- tests/app/routers/test_images.py | 22 +++++- .../bulk_download/test_bulk_download.py | 79 ++++++++++++++----- 5 files changed, 116 insertions(+), 33 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index d11c89c749..c12556aed6 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,6 +1,6 @@ import io import traceback -from typing import Optional +from typing import Optional, cast from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, Response, UploadFile from fastapi.responses import FileResponse @@ -13,6 +13,7 @@ from invokeai.app.services.image_records.image_records_common import ImageCatego from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator +from invokeai.app.util.misc import uuid_string from ..dependencies import ApiDependencies @@ -377,6 +378,7 @@ class ImagesDownloaded(BaseModel): response: Optional[str] = Field( description="If defined, the message to display to the user when images begin downloading" ) + bulk_download_item_name: str = Field(description="The bulk download item name of the bulk download item") @images_router.post( @@ -384,15 +386,31 @@ class ImagesDownloaded(BaseModel): ) async def download_images_from_list( background_tasks: BackgroundTasks, - image_names: list[str] = Body(description="The list of names of images to download", embed=True), + image_names: Optional[list[str]] = Body( + default=None, description="The list of names of images to download", embed=True + ), board_id: Optional[str] = Body( default=None, description="The board from which image should be downloaded from", embed=True ), ) -> ImagesDownloaded: if (image_names is None or len(image_names) == 0) and board_id is None: raise HTTPException(status_code=400, detail="No images or board id specified.") - background_tasks.add_task(ApiDependencies.invoker.services.bulk_download.handler, image_names, board_id) - return ImagesDownloaded(response="Your images are preparing to be downloaded") + bulk_download_item_id: str = uuid_string() if board_id is None else board_id + board_name: str = ( + "" if board_id is None else ApiDependencies.invoker.services.board_records.get(board_id).board_name + ) + + # Type narrowing handled above ^, we know that image_names is not None, trying to keep null checks at the boundaries + background_tasks.add_task( + ApiDependencies.invoker.services.bulk_download.handler, + cast(list[str], image_names), + board_id, + bulk_download_item_id, + ) + return ImagesDownloaded( + response="Your images are preparing to be downloaded", + bulk_download_item_name=bulk_download_item_id if board_id is None else board_name + ".zip", + ) @images_router.api_route( @@ -410,7 +428,7 @@ async def download_images_from_list( ) async def get_bulk_download_item( background_tasks: BackgroundTasks, - bulk_download_item_name: str = Path(description="The bulk_download_item_id of the bulk download item to get"), + bulk_download_item_name: str = Path(description="The bulk_download_item_name of the bulk download item to get"), ) -> FileResponse: """Gets a bulk download zip file""" try: diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index a1071f254a..d6b0e62211 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -26,7 +26,7 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def handler(self, image_names: list[str], board_id: Optional[str]) -> None: + def handler(self, image_names: list[str], board_id: Optional[str], bulk_download_item_id: Optional[str]) -> None: """ Starts a a bulk download job. @@ -44,6 +44,15 @@ class BulkDownloadBase(ABC): :return: The path to the bulk download file. """ + @abstractmethod + def get_board_name(self, board_id: str) -> str: + """ + Get the name of the board. + + :param board_id: The ID of the board. + :return: The name of the board. + """ + @abstractmethod def stop(self, *args, **kwargs) -> None: """ diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 87966ad622..be70dea2c1 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -38,7 +38,7 @@ class BulkDownloadService(BulkDownloadBase): self.__bulk_downloads_folder = self.__output_folder / "bulk_downloads" self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) - def handler(self, image_names: list[str], board_id: Optional[str]) -> None: + def handler(self, image_names: list[str], board_id: Optional[str], bulk_download_item_id: Optional[str]) -> None: """ Create a zip file containing the images specified by the given image names or board id. @@ -47,7 +47,8 @@ class BulkDownloadService(BulkDownloadBase): """ bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID - bulk_download_item_id: str = uuid_string() if board_id is None else board_id + if bulk_download_item_id is None: + bulk_download_item_id = uuid_string() if board_id is None else board_id self._signal_job_started(bulk_download_id, bulk_download_item_id) @@ -56,7 +57,7 @@ class BulkDownloadService(BulkDownloadBase): image_dtos: list[ImageDTO] = [] if board_id: - board_name = self._get_board_name(board_id) + board_name = self.get_board_name(board_id) board_name = self._clean_string_to_path_safe(board_name) # -1 is the default value for limit, which means no limit, is_intermediate only gives us completed images @@ -79,7 +80,7 @@ class BulkDownloadService(BulkDownloadBase): self.__invoker.services.logger.error("Problem bulk downloading images.") raise e - def _get_board_name(self, board_id: str) -> str: + def get_board_name(self, board_id: str) -> str: if board_id == "none": return "Uncategorized" diff --git a/tests/app/routers/test_images.py b/tests/app/routers/test_images.py index 040ae01914..a709daf24e 100644 --- a/tests/app/routers/test_images.py +++ b/tests/app/routers/test_images.py @@ -7,6 +7,7 @@ from fastapi.testclient import TestClient from invokeai.app.api.dependencies import ApiDependencies from invokeai.app.api_app import app +from invokeai.app.services.board_records.board_records_common import BoardRecord from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService from invokeai.app.services.config.config_default import InvokeAIAppConfig @@ -70,17 +71,32 @@ class MockApiDependencies(ApiDependencies): def test_download_images_from_list(monkeypatch: Any, mock_invoker: Invoker) -> None: prepare_download_images_test(monkeypatch, mock_invoker) - response = client.post("/api/v1/images/download", json={"image_names": ["test.png"]}) + def mock_uuid_string(): + return "test" + # You have to patch the function within the module it's being imported into. This is strange, but it works. + # See http://www.gregreda.com/2021/06/28/mocking-imported-module-function-python/ + monkeypatch.setattr("invokeai.app.api.routers.images.uuid_string", mock_uuid_string) + + response = client.post("/api/v1/images/download", json={"image_names": ["test.png"]}) + json_response = response.json() assert response.status_code == 202 + assert json_response["bulk_download_item_name"] == "test" def test_download_images_from_board_id_empty_image_name_list(monkeypatch: Any, mock_invoker: Invoker) -> None: + expected_board_name = "test" + + def mock_get(*args, **kwargs): + return BoardRecord(board_id="12345", board_name=expected_board_name, created_at="None", updated_at="None") + + monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_get) prepare_download_images_test(monkeypatch, mock_invoker) - response = client.post("/api/v1/images/download", json={"image_names": [], "board_id": "test"}) - + response = client.post("/api/v1/images/download", json={"board_id": "test"}) + json_response = response.json() assert response.status_code == 202 + assert json_response["bulk_download_item_name"] == "test.zip" def prepare_download_images_test(monkeypatch: Any, mock_invoker: Invoker) -> None: diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 184519866a..7909c44214 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -125,7 +125,7 @@ def test_handler_image_names(tmp_path: Path, monkeypatch: Any, mock_image_dto: I bulk_download_service = BulkDownloadService(tmp_path) bulk_download_service.start(mock_invoker) - bulk_download_service.handler([mock_image_dto.image_name], None) + bulk_download_service.handler([mock_image_dto.image_name], None, None) assert_handler_success( expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events @@ -151,7 +151,7 @@ def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: Imag bulk_download_service = BulkDownloadService(tmp_path) bulk_download_service.start(mock_invoker) - bulk_download_service.handler([], "test") + bulk_download_service.handler([], "test", None) assert_handler_success( expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events @@ -173,7 +173,31 @@ def test_handler_board_id_default(tmp_path: Path, monkeypatch: Any, mock_image_d bulk_download_service = BulkDownloadService(tmp_path) bulk_download_service.start(mock_invoker) - bulk_download_service.handler([], "none") + bulk_download_service.handler([], "none", None) + + assert_handler_success( + expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events + ) + + +def test_handler_bulk_download__item_id_given( + tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker +): + """Test that the handler creates the zip file correctly when given a pregenerated bulk download item id.""" + + _, expected_image_path, mock_image_contents = prepare_handler_test( + tmp_path, monkeypatch, mock_image_dto, mock_invoker + ) + expected_zip_path: Path = tmp_path / "bulk_downloads" / "test_id.zip" + + def mock_get_many(*args, **kwargs): + return OffsetPaginatedResults(limit=-1, total=1, offset=0, items=[mock_image_dto]) + + monkeypatch.setattr(mock_invoker.services.images, "get_many", mock_get_many) + + bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service.start(mock_invoker) + bulk_download_service.handler([mock_image_dto.image_name], None, "test_id") assert_handler_success( expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events @@ -242,20 +266,6 @@ def assert_handler_success( assert event_bus.events[1].payload["bulk_download_item_name"] == os.path.basename(expected_zip_path) -def test_stop(tmp_path: Path) -> None: - """Test that the stop method removes the bulk_downloads directory.""" - - bulk_download_service = BulkDownloadService(tmp_path) - - mock_file: Path = tmp_path / "bulk_downloads" / "test.zip" - mock_file.write_text("contents") - - bulk_download_service.stop() - - assert (tmp_path / "bulk_downloads").exists() - assert len(os.listdir(tmp_path / "bulk_downloads")) == 0 - - def test_handler_on_image_not_found(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): """Test that the handler emits an error event when the image is not found.""" exception: Exception = ImageRecordNotFoundException("Image not found") @@ -309,7 +319,7 @@ def execute_handler_test_on_error( ): bulk_download_service = BulkDownloadService(tmp_path) bulk_download_service.start(mock_invoker) - bulk_download_service.handler([mock_image_dto.image_name], None) + bulk_download_service.handler([mock_image_dto.image_name], None, None) event_bus: DummyEventService = mock_invoker.services.events @@ -319,6 +329,35 @@ def execute_handler_test_on_error( assert event_bus.events[1].payload["error"] == error.__str__() +def test_get_board_name(tmp_path: Path, monkeypatch: Any, mock_invoker: Invoker): + """Test that the get_board_name function returns the correct board name.""" + + expected_board_name = "board1" + + def mock_get(*args, **kwargs): + return BoardRecord(board_id="12345", board_name=expected_board_name, created_at="None", updated_at="None") + + monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_get) + + bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service.start(mock_invoker) + board_name = bulk_download_service.get_board_name("12345") + + assert board_name == expected_board_name + + +def test_get_board_name_default(tmp_path: Path, mock_invoker: Invoker): + """Test that the get_board_name function returns the correct board name.""" + + expected_board_name = "Uncategorized" + + bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service.start(mock_invoker) + board_name = bulk_download_service.get_board_name("none") + + assert board_name == expected_board_name + + def test_delete(tmp_path: Path): """Test that the delete method removes the bulk download file.""" @@ -332,8 +371,9 @@ def test_delete(tmp_path: Path): assert (tmp_path / "bulk_downloads").exists() assert len(os.listdir(tmp_path / "bulk_downloads")) == 0 + def test_stop(tmp_path: Path): - """Test that the delete method removes the bulk download file.""" + """Test that the stop method removes the bulk download file and not any directories.""" bulk_download_service = BulkDownloadService(tmp_path) @@ -343,7 +383,6 @@ def test_stop(tmp_path: Path): mock_dir: Path = tmp_path / "bulk_downloads" / "test" mock_dir.mkdir(parents=True, exist_ok=True) - bulk_download_service.stop() assert (tmp_path / "bulk_downloads").exists() From f15aa562c2343bd91e1f892d16e3105a60a9b886 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 28 Jan 2024 18:59:56 -0500 Subject: [PATCH 205/411] using temp directory for downloads --- invokeai/app/api/routers/images.py | 2 +- .../bulk_download/bulk_download_base.py | 2 +- .../bulk_download/bulk_download_default.py | 18 +++++----- .../bulk_download/test_bulk_download.py | 35 ++++++++++++++----- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index c12556aed6..69a76e4062 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -397,7 +397,7 @@ async def download_images_from_list( raise HTTPException(status_code=400, detail="No images or board id specified.") bulk_download_item_id: str = uuid_string() if board_id is None else board_id board_name: str = ( - "" if board_id is None else ApiDependencies.invoker.services.board_records.get(board_id).board_name + "" if board_id is None else ApiDependencies.invoker.services.bulk_download.get_clean_board_name(board_id) ) # Type narrowing handled above ^, we know that image_names is not None, trying to keep null checks at the boundaries diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index d6b0e62211..89b2e73772 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -45,7 +45,7 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def get_board_name(self, board_id: str) -> str: + def get_clean_board_name(self, board_id: str) -> str: """ Get the name of the board. diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index be70dea2c1..fe76a12333 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -1,4 +1,5 @@ from pathlib import Path +from tempfile import TemporaryDirectory from typing import Optional, Union from zipfile import ZipFile @@ -19,6 +20,7 @@ from .bulk_download_base import BulkDownloadBase class BulkDownloadService(BulkDownloadBase): __output_folder: Path + __temp_directory: TemporaryDirectory __bulk_downloads_folder: Path __event_bus: EventServiceBase __invoker: Invoker @@ -35,7 +37,8 @@ class BulkDownloadService(BulkDownloadBase): Initialize the downloader object. """ self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder) - self.__bulk_downloads_folder = self.__output_folder / "bulk_downloads" + self.__temp_directory = TemporaryDirectory(dir=self.__output_folder) + self.__bulk_downloads_folder = Path(self.__temp_directory.name) / "bulk_downloads" self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) def handler(self, image_names: list[str], board_id: Optional[str], bulk_download_item_id: Optional[str]) -> None: @@ -57,8 +60,7 @@ class BulkDownloadService(BulkDownloadBase): image_dtos: list[ImageDTO] = [] if board_id: - board_name = self.get_board_name(board_id) - board_name = self._clean_string_to_path_safe(board_name) + board_name = self.get_clean_board_name(board_id) # -1 is the default value for limit, which means no limit, is_intermediate only gives us completed images image_dtos = self.__invoker.services.images.get_many( @@ -80,11 +82,11 @@ class BulkDownloadService(BulkDownloadBase): self.__invoker.services.logger.error("Problem bulk downloading images.") raise e - def get_board_name(self, board_id: str) -> str: + def get_clean_board_name(self, board_id: str) -> str: if board_id == "none": return "Uncategorized" - return self.__invoker.services.board_records.get(board_id).board_name + return self._clean_string_to_path_safe(self.__invoker.services.board_records.get(board_id).board_name) def _create_zip_file(self, image_dtos: list[ImageDTO], bulk_download_item_id: str) -> str: """ @@ -145,11 +147,7 @@ class BulkDownloadService(BulkDownloadBase): def stop(self, *args, **kwargs): """Stop the bulk download service and delete the files in the bulk download folder.""" # Get all the files in the bulk downloads folder, only .zip files - files = self.__bulk_downloads_folder.glob("*.zip") - - # Delete all the files - for file in files: - file.unlink() + self.__temp_directory.cleanup() def delete(self, bulk_download_item_name: str) -> None: """ diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 7909c44214..3cd2123232 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from tempfile import TemporaryDirectory from typing import Any from zipfile import ZipFile @@ -86,9 +87,28 @@ def mock_invoker(mock_services: InvocationServices) -> Invoker: return Invoker(services=mock_services) +@pytest.fixture(autouse=True) +def mock_temporary_directory(monkeypatch: Any, tmp_path: Path): + """Mock the TemporaryDirectory class so that it uses the tmp_path fixture.""" + + class MockTemporaryDirectory(TemporaryDirectory): + def __init__(self): + super().__init__(dir=tmp_path) + self.name = tmp_path + + def mock_TemporaryDirectory(*args, **kwargs): + return MockTemporaryDirectory() + + monkeypatch.setattr( + "invokeai.app.services.bulk_download.bulk_download_default.TemporaryDirectory", mock_TemporaryDirectory + ) + + def test_get_path_when_file_exists(tmp_path: Path) -> None: """Test get_path when the file exists.""" + bulk_download_service = BulkDownloadService(tmp_path) + # Create a directory at tmp_path/bulk_downloads test_bulk_downloads_dir: Path = tmp_path / "bulk_downloads" test_bulk_downloads_dir.mkdir(parents=True, exist_ok=True) @@ -97,7 +117,6 @@ def test_get_path_when_file_exists(tmp_path: Path) -> None: test_file_path: Path = test_bulk_downloads_dir / "test.zip" test_file_path.touch() - bulk_download_service = BulkDownloadService(tmp_path) assert bulk_download_service.get_path("test.zip") == str(test_file_path) @@ -164,7 +183,6 @@ def test_handler_board_id_default(tmp_path: Path, monkeypatch: Any, mock_image_d _, expected_image_path, mock_image_contents = prepare_handler_test( tmp_path, monkeypatch, mock_image_dto, mock_invoker ) - expected_zip_path: Path = tmp_path / "bulk_downloads" / "Uncategorized.zip" def mock_get_many(*args, **kwargs): return OffsetPaginatedResults(limit=-1, total=1, offset=0, items=[mock_image_dto]) @@ -175,6 +193,8 @@ def test_handler_board_id_default(tmp_path: Path, monkeypatch: Any, mock_image_d bulk_download_service.start(mock_invoker) bulk_download_service.handler([], "none", None) + expected_zip_path: Path = tmp_path / "bulk_downloads" / "Uncategorized.zip" + assert_handler_success( expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events ) @@ -188,7 +208,6 @@ def test_handler_bulk_download__item_id_given( _, expected_image_path, mock_image_contents = prepare_handler_test( tmp_path, monkeypatch, mock_image_dto, mock_invoker ) - expected_zip_path: Path = tmp_path / "bulk_downloads" / "test_id.zip" def mock_get_many(*args, **kwargs): return OffsetPaginatedResults(limit=-1, total=1, offset=0, items=[mock_image_dto]) @@ -199,6 +218,8 @@ def test_handler_bulk_download__item_id_given( bulk_download_service.start(mock_invoker) bulk_download_service.handler([mock_image_dto.image_name], None, "test_id") + expected_zip_path: Path = tmp_path / "bulk_downloads" / "test_id.zip" + assert_handler_success( expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events ) @@ -341,7 +362,7 @@ def test_get_board_name(tmp_path: Path, monkeypatch: Any, mock_invoker: Invoker) bulk_download_service = BulkDownloadService(tmp_path) bulk_download_service.start(mock_invoker) - board_name = bulk_download_service.get_board_name("12345") + board_name = bulk_download_service.get_clean_board_name("12345") assert board_name == expected_board_name @@ -353,7 +374,7 @@ def test_get_board_name_default(tmp_path: Path, mock_invoker: Invoker): bulk_download_service = BulkDownloadService(tmp_path) bulk_download_service.start(mock_invoker) - board_name = bulk_download_service.get_board_name("none") + board_name = bulk_download_service.get_clean_board_name("none") assert board_name == expected_board_name @@ -385,6 +406,4 @@ def test_stop(tmp_path: Path): bulk_download_service.stop() - assert (tmp_path / "bulk_downloads").exists() - assert mock_dir.exists() - assert len(os.listdir(tmp_path / "bulk_downloads")) == 1 + assert not (tmp_path / "bulk_downloads").exists() From 5f4b406cfe084aa430b7750caa6550d0dc12d6b7 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Fri, 16 Feb 2024 14:10:50 -0500 Subject: [PATCH 206/411] updating imports to satisfy ruff --- tests/app/services/bulk_download/test_bulk_download.py | 2 +- tests/conftest.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 3cd2123232..924385f7e1 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -22,7 +22,7 @@ from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.backend.util.logging import InvokeAILogger -from tests.fixtures.event_service import DummyEventService, mock_event_service # noqa: F401,F811 +from tests.fixtures.event_service import DummyEventService from tests.fixtures.sqlite_database import create_mock_sqlite_database diff --git a/tests/conftest.py b/tests/conftest.py index 1c81600229..85fecfe440 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,2 +1,8 @@ # conftest.py is a special pytest file. Fixtures defined in this file will be accessible to all tests in this directory # without needing to explicitly import them. (https://docs.pytest.org/en/6.2.x/fixture.html) + + +# We import the model_installer and torch_device fixtures here so that they can be used by all tests. Flake8 does not +# play well with fixtures (F401 and F811), so this is cleaner than importing in all files that use these fixtures. +from invokeai.backend.util.test_utils import torch_device # noqa: F401 +from tests.fixtures.event_service import mock_event_service # noqa: F401 From b5a9ed351d8764dc3dfdcfd3176b69adc00b1bf4 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Fri, 16 Feb 2024 15:50:48 -0500 Subject: [PATCH 207/411] moving the responsibility of cleaning up board names to the service not the route --- invokeai/app/api/routers/images.py | 8 +-- .../bulk_download/bulk_download_base.py | 18 ++--- .../bulk_download/bulk_download_default.py | 23 +++--- tests/app/routers/test_images.py | 13 ++-- .../bulk_download/test_bulk_download.py | 71 ++++++++++--------- 5 files changed, 63 insertions(+), 70 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 69a76e4062..d1c64648de 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -13,7 +13,6 @@ from invokeai.app.services.image_records.image_records_common import ImageCatego from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutID, WorkflowWithoutIDValidator -from invokeai.app.util.misc import uuid_string from ..dependencies import ApiDependencies @@ -395,10 +394,7 @@ async def download_images_from_list( ) -> ImagesDownloaded: if (image_names is None or len(image_names) == 0) and board_id is None: raise HTTPException(status_code=400, detail="No images or board id specified.") - bulk_download_item_id: str = uuid_string() if board_id is None else board_id - board_name: str = ( - "" if board_id is None else ApiDependencies.invoker.services.bulk_download.get_clean_board_name(board_id) - ) + bulk_download_item_id: str = ApiDependencies.invoker.services.bulk_download.generate_item_id(board_id) # Type narrowing handled above ^, we know that image_names is not None, trying to keep null checks at the boundaries background_tasks.add_task( @@ -409,7 +405,7 @@ async def download_images_from_list( ) return ImagesDownloaded( response="Your images are preparing to be downloaded", - bulk_download_item_name=bulk_download_item_id if board_id is None else board_name + ".zip", + bulk_download_item_name=bulk_download_item_id + ".zip", ) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 89b2e73772..5199652ad4 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -30,9 +30,9 @@ class BulkDownloadBase(ABC): """ Starts a a bulk download job. - :param invoker: The Invoker that holds all the services, required to be passed as a parameter to avoid circular dependencies. :param image_names: A list of image names to include in the zip file. :param board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. + :param bulk_download_item_id: The bulk_download_item_id that will be used to retrieve the bulk download item when it is prepared, if none is provided a uuid will be generated. """ @abstractmethod @@ -45,12 +45,12 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def get_clean_board_name(self, board_id: str) -> str: + def generate_item_id(self, board_id: Optional[str]) -> str: """ - Get the name of the board. + Generate an item ID for a bulk download item. - :param board_id: The ID of the board. - :return: The name of the board. + :param board_id: The ID of the board whose name is to be included in the item id. + :return: The generated item ID. """ @abstractmethod @@ -61,12 +61,8 @@ class BulkDownloadBase(ABC): This method is responsible for stopping the BulkDownloadService and performing any necessary cleanup operations to remove any remnants or resources associated with the service. - Args: - *args: Variable length argument list. - **kwargs: Arbitrary keyword arguments. - - Returns: - None + :param *args: Variable length argument list. + :param **kwargs: Arbitrary keyword arguments. """ @abstractmethod diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index fe76a12333..406bd7d997 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -50,19 +50,15 @@ class BulkDownloadService(BulkDownloadBase): """ bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID - if bulk_download_item_id is None: - bulk_download_item_id = uuid_string() if board_id is None else board_id + bulk_download_item_id = uuid_string() if bulk_download_item_id is None else bulk_download_item_id self._signal_job_started(bulk_download_id, bulk_download_item_id) try: - board_name: str = "" image_dtos: list[ImageDTO] = [] if board_id: - board_name = self.get_clean_board_name(board_id) - - # -1 is the default value for limit, which means no limit, is_intermediate only gives us completed images + # -1 is the default value for limit, which means no limit, is_intermediate False only gives us completed images image_dtos = self.__invoker.services.images.get_many( offset=0, limit=-1, @@ -71,9 +67,7 @@ class BulkDownloadService(BulkDownloadBase): ).items else: image_dtos = [self.__invoker.services.images.get_dto(image_name) for image_name in image_names] - bulk_download_item_name: str = self._create_zip_file( - image_dtos, bulk_download_item_id if board_id is None else board_name - ) + bulk_download_item_name: str = self._create_zip_file(image_dtos, bulk_download_item_id) self._signal_job_completed(bulk_download_id, bulk_download_item_id, bulk_download_item_name) except (ImageRecordNotFoundException, BoardRecordNotFoundException, BulkDownloadException) as e: self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) @@ -82,7 +76,10 @@ class BulkDownloadService(BulkDownloadBase): self.__invoker.services.logger.error("Problem bulk downloading images.") raise e - def get_clean_board_name(self, board_id: str) -> str: + def generate_item_id(self, board_id: Optional[str]) -> str: + return uuid_string() if board_id is None else self._get_clean_board_name(board_id) + "_" + uuid_string() + + def _get_clean_board_name(self, board_id: str) -> str: if board_id == "none": return "Uncategorized" @@ -109,7 +106,7 @@ class BulkDownloadService(BulkDownloadBase): # from https://stackoverflow.com/questions/7406102/create-sane-safe-filename-from-any-unsafe-string def _clean_string_to_path_safe(self, s: str) -> str: """Clean a string to be path safe.""" - return "".join([c for c in s if c.isalpha() or c.isdigit() or c == " "]).rstrip() + return "".join([c for c in s if c.isalpha() or c.isdigit() or c == " " or c == "_" or c == "-"]).rstrip() def _signal_job_started(self, bulk_download_id: str, bulk_download_item_id: str) -> None: """Signal that a bulk download job has started.""" @@ -166,11 +163,11 @@ class BulkDownloadService(BulkDownloadBase): :return: The path to the bulk download file. """ path = str(self.__bulk_downloads_folder / bulk_download_item_name) - if not self.validate_path(path): + if not self._is_valid_path(path): raise BulkDownloadTargetException() return path - def validate_path(self, path: Union[str, Path]) -> bool: + def _is_valid_path(self, path: Union[str, Path]) -> bool: """Validates the path given for a bulk download.""" path = path if isinstance(path, Path) else Path(path) return path.exists() diff --git a/tests/app/routers/test_images.py b/tests/app/routers/test_images.py index a709daf24e..e8521bf132 100644 --- a/tests/app/routers/test_images.py +++ b/tests/app/routers/test_images.py @@ -71,17 +71,10 @@ class MockApiDependencies(ApiDependencies): def test_download_images_from_list(monkeypatch: Any, mock_invoker: Invoker) -> None: prepare_download_images_test(monkeypatch, mock_invoker) - def mock_uuid_string(): - return "test" - - # You have to patch the function within the module it's being imported into. This is strange, but it works. - # See http://www.gregreda.com/2021/06/28/mocking-imported-module-function-python/ - monkeypatch.setattr("invokeai.app.api.routers.images.uuid_string", mock_uuid_string) - response = client.post("/api/v1/images/download", json={"image_names": ["test.png"]}) json_response = response.json() assert response.status_code == 202 - assert json_response["bulk_download_item_name"] == "test" + assert json_response["bulk_download_item_name"] == "test.zip" def test_download_images_from_board_id_empty_image_name_list(monkeypatch: Any, mock_invoker: Invoker) -> None: @@ -101,6 +94,10 @@ def test_download_images_from_board_id_empty_image_name_list(monkeypatch: Any, m def prepare_download_images_test(monkeypatch: Any, mock_invoker: Invoker) -> None: monkeypatch.setattr("invokeai.app.api.routers.images.ApiDependencies", MockApiDependencies(mock_invoker)) + monkeypatch.setattr( + "invokeai.app.api.routers.images.ApiDependencies.invoker.services.bulk_download.generate_item_id", + lambda arg: "test", + ) def mock_add_task(*args, **kwargs): return None diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 924385f7e1..d70510cd91 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -151,6 +151,42 @@ def test_handler_image_names(tmp_path: Path, monkeypatch: Any, mock_image_dto: I ) +def test_generate_id(monkeypatch: Any): + """Test that the generate_id method generates a unique id.""" + + bulk_download_service = BulkDownloadService("test") + + monkeypatch.setattr("invokeai.app.services.bulk_download.bulk_download_default.uuid_string", lambda: "test") + + assert bulk_download_service.generate_item_id(None) == "test" + + +def test_generate_id_with_board_id(monkeypatch: Any, mock_invoker: Invoker): + """Test that the generate_id method generates a unique id with a board id.""" + + bulk_download_service = BulkDownloadService("test") + bulk_download_service.start(mock_invoker) + + def mock_board_get(*args, **kwargs): + return BoardRecord(board_id="12345", board_name="test_board_name", created_at="None", updated_at="None") + + monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get) + + monkeypatch.setattr("invokeai.app.services.bulk_download.bulk_download_default.uuid_string", lambda: "test") + + assert bulk_download_service.generate_item_id("12345") == "test_board_name_test" + + +def test_generate_id_with_default_board_id(monkeypatch: Any): + """Test that the generate_id method generates a unique id with a board id.""" + + bulk_download_service = BulkDownloadService("test") + + monkeypatch.setattr("invokeai.app.services.bulk_download.bulk_download_default.uuid_string", lambda: "test") + + assert bulk_download_service.generate_item_id("none") == "Uncategorized_test" + + def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker): """Test that the handler creates the zip file correctly when given a board id.""" @@ -159,7 +195,7 @@ def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: Imag ) def mock_board_get(*args, **kwargs): - return BoardRecord(board_id="12345", board_name="test", created_at="None", updated_at="None") + return BoardRecord(board_id="12345", board_name="test_board_name", created_at="None", updated_at="None") monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get) @@ -193,14 +229,14 @@ def test_handler_board_id_default(tmp_path: Path, monkeypatch: Any, mock_image_d bulk_download_service.start(mock_invoker) bulk_download_service.handler([], "none", None) - expected_zip_path: Path = tmp_path / "bulk_downloads" / "Uncategorized.zip" + expected_zip_path: Path = tmp_path / "bulk_downloads" / "test.zip" assert_handler_success( expected_zip_path, expected_image_path, mock_image_contents, tmp_path, mock_invoker.services.events ) -def test_handler_bulk_download__item_id_given( +def test_handler_bulk_download_item_id_given( tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker ): """Test that the handler creates the zip file correctly when given a pregenerated bulk download item id.""" @@ -350,35 +386,6 @@ def execute_handler_test_on_error( assert event_bus.events[1].payload["error"] == error.__str__() -def test_get_board_name(tmp_path: Path, monkeypatch: Any, mock_invoker: Invoker): - """Test that the get_board_name function returns the correct board name.""" - - expected_board_name = "board1" - - def mock_get(*args, **kwargs): - return BoardRecord(board_id="12345", board_name=expected_board_name, created_at="None", updated_at="None") - - monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_get) - - bulk_download_service = BulkDownloadService(tmp_path) - bulk_download_service.start(mock_invoker) - board_name = bulk_download_service.get_clean_board_name("12345") - - assert board_name == expected_board_name - - -def test_get_board_name_default(tmp_path: Path, mock_invoker: Invoker): - """Test that the get_board_name function returns the correct board name.""" - - expected_board_name = "Uncategorized" - - bulk_download_service = BulkDownloadService(tmp_path) - bulk_download_service.start(mock_invoker) - board_name = bulk_download_service.get_clean_board_name("none") - - assert board_name == expected_board_name - - def test_delete(tmp_path: Path): """Test that the delete method removes the bulk download file.""" From 0ab9fe6987453bb082ea823b82689616c8bfa5e6 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Fri, 16 Feb 2024 22:49:38 -0500 Subject: [PATCH 208/411] relocating event_service fixture due to import ordering --- tests/app/services/bulk_download/test_bulk_download.py | 6 ++++++ tests/conftest.py | 1 - tests/fixtures/event_service.py | 7 ------- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index d70510cd91..b7480091d9 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -46,6 +46,12 @@ def mock_image_dto() -> ImageDTO: ) +@pytest.fixture +def mock_event_service() -> DummyEventService: + """Create a dummy event service.""" + return DummyEventService() + + @pytest.fixture def mock_services(mock_event_service: DummyEventService) -> InvocationServices: configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) diff --git a/tests/conftest.py b/tests/conftest.py index 85fecfe440..873ccc13fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,4 +5,3 @@ # We import the model_installer and torch_device fixtures here so that they can be used by all tests. Flake8 does not # play well with fixtures (F401 and F811), so this is cleaner than importing in all files that use these fixtures. from invokeai.backend.util.test_utils import torch_device # noqa: F401 -from tests.fixtures.event_service import mock_event_service # noqa: F401 diff --git a/tests/fixtures/event_service.py b/tests/fixtures/event_service.py index 0a09fa0d64..8f6a45c38f 100644 --- a/tests/fixtures/event_service.py +++ b/tests/fixtures/event_service.py @@ -1,6 +1,5 @@ from typing import Any, Dict, List -import pytest from pydantic import BaseModel from invokeai.app.services.events.events_base import EventServiceBase @@ -26,9 +25,3 @@ class DummyEventService(EventServiceBase): def dispatch(self, event_name: str, payload: Any) -> None: """Dispatch an event by appending it to self.events.""" self.events.append(DummyEvent(event_name=payload["event"], payload=payload["data"])) - - -@pytest.fixture -def mock_event_service() -> DummyEventService: - """Create a dummy event service.""" - return DummyEventService() From 037cac81548a4e0b3076ccbb02f0db1e29dd3a02 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sat, 17 Feb 2024 00:29:05 -0500 Subject: [PATCH 209/411] removing dependency on an output folder, embrace python temp folder for bulk download --- invokeai/app/api/dependencies.py | 2 +- .../bulk_download/bulk_download_base.py | 7 ++--- .../bulk_download/bulk_download_default.py | 9 ++----- tests/app/routers/test_images.py | 2 +- .../bulk_download/test_bulk_download.py | 26 +++++++++---------- 5 files changed, 19 insertions(+), 27 deletions(-) diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index 984fd8e267..95407291ec 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -82,7 +82,7 @@ class ApiDependencies: board_records = SqliteBoardRecordStorage(db=db) boards = BoardService() events = FastAPIEventService(event_handler_id) - bulk_download = BulkDownloadService(output_folder=f"{output_folder}") + bulk_download = BulkDownloadService() image_records = SqliteImageRecordStorage(db=db) images = ImageService() invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 5199652ad4..d889e2ed0e 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod -from pathlib import Path -from typing import Optional, Union +from typing import Optional from invokeai.app.services.invoker import Invoker @@ -18,11 +17,9 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def __init__(self, output_folder: Union[str, Path]): + def __init__(self): """ Create BulkDownloadBase object. - - :param output_folder: The path to the output folder where the bulk download files can be temporarily stored. """ @abstractmethod diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 406bd7d997..4f5bfb087f 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -19,7 +19,6 @@ from .bulk_download_base import BulkDownloadBase class BulkDownloadService(BulkDownloadBase): - __output_folder: Path __temp_directory: TemporaryDirectory __bulk_downloads_folder: Path __event_bus: EventServiceBase @@ -29,15 +28,11 @@ class BulkDownloadService(BulkDownloadBase): self.__invoker = invoker self.__event_bus = invoker.services.events - def __init__( - self, - output_folder: Union[str, Path], - ): + def __init__(self): """ Initialize the downloader object. """ - self.__output_folder: Path = output_folder if isinstance(output_folder, Path) else Path(output_folder) - self.__temp_directory = TemporaryDirectory(dir=self.__output_folder) + self.__temp_directory = TemporaryDirectory() self.__bulk_downloads_folder = Path(self.__temp_directory.name) / "bulk_downloads" self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) diff --git a/tests/app/routers/test_images.py b/tests/app/routers/test_images.py index e8521bf132..67297a116f 100644 --- a/tests/app/routers/test_images.py +++ b/tests/app/routers/test_images.py @@ -31,7 +31,7 @@ def mock_services(tmp_path: Path) -> InvocationServices: board_images=None, # type: ignore board_records=SqliteBoardRecordStorage(db=db), boards=None, # type: ignore - bulk_download=BulkDownloadService(tmp_path), + bulk_download=BulkDownloadService(), configuration=None, # type: ignore events=None, # type: ignore graph_execution_manager=None, # type: ignore diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index b7480091d9..3e8b7fd2eb 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -113,7 +113,7 @@ def mock_temporary_directory(monkeypatch: Any, tmp_path: Path): def test_get_path_when_file_exists(tmp_path: Path) -> None: """Test get_path when the file exists.""" - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() # Create a directory at tmp_path/bulk_downloads test_bulk_downloads_dir: Path = tmp_path / "bulk_downloads" @@ -129,7 +129,7 @@ def test_get_path_when_file_exists(tmp_path: Path) -> None: def test_get_path_when_file_does_not_exist(tmp_path: Path) -> None: """Test get_path when the file does not exist.""" - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() with pytest.raises(BulkDownloadTargetException): bulk_download_service.get_path("test") @@ -137,7 +137,7 @@ def test_get_path_when_file_does_not_exist(tmp_path: Path) -> None: def test_bulk_downloads_dir_created_at_start(tmp_path: Path) -> None: """Test that the bulk_downloads directory is created at start.""" - BulkDownloadService(tmp_path) + BulkDownloadService() assert (tmp_path / "bulk_downloads").exists() @@ -148,7 +148,7 @@ def test_handler_image_names(tmp_path: Path, monkeypatch: Any, mock_image_dto: I tmp_path, monkeypatch, mock_image_dto, mock_invoker ) - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() bulk_download_service.start(mock_invoker) bulk_download_service.handler([mock_image_dto.image_name], None, None) @@ -160,7 +160,7 @@ def test_handler_image_names(tmp_path: Path, monkeypatch: Any, mock_image_dto: I def test_generate_id(monkeypatch: Any): """Test that the generate_id method generates a unique id.""" - bulk_download_service = BulkDownloadService("test") + bulk_download_service = BulkDownloadService() monkeypatch.setattr("invokeai.app.services.bulk_download.bulk_download_default.uuid_string", lambda: "test") @@ -170,7 +170,7 @@ def test_generate_id(monkeypatch: Any): def test_generate_id_with_board_id(monkeypatch: Any, mock_invoker: Invoker): """Test that the generate_id method generates a unique id with a board id.""" - bulk_download_service = BulkDownloadService("test") + bulk_download_service = BulkDownloadService() bulk_download_service.start(mock_invoker) def mock_board_get(*args, **kwargs): @@ -186,7 +186,7 @@ def test_generate_id_with_board_id(monkeypatch: Any, mock_invoker: Invoker): def test_generate_id_with_default_board_id(monkeypatch: Any): """Test that the generate_id method generates a unique id with a board id.""" - bulk_download_service = BulkDownloadService("test") + bulk_download_service = BulkDownloadService() monkeypatch.setattr("invokeai.app.services.bulk_download.bulk_download_default.uuid_string", lambda: "test") @@ -210,7 +210,7 @@ def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: Imag monkeypatch.setattr(mock_invoker.services.images, "get_many", mock_get_many) - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() bulk_download_service.start(mock_invoker) bulk_download_service.handler([], "test", None) @@ -231,7 +231,7 @@ def test_handler_board_id_default(tmp_path: Path, monkeypatch: Any, mock_image_d monkeypatch.setattr(mock_invoker.services.images, "get_many", mock_get_many) - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() bulk_download_service.start(mock_invoker) bulk_download_service.handler([], "none", None) @@ -256,7 +256,7 @@ def test_handler_bulk_download_item_id_given( monkeypatch.setattr(mock_invoker.services.images, "get_many", mock_get_many) - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() bulk_download_service.start(mock_invoker) bulk_download_service.handler([mock_image_dto.image_name], None, "test_id") @@ -380,7 +380,7 @@ def test_handler_on_generic_exception( def execute_handler_test_on_error( tmp_path: Path, monkeypatch: Any, mock_image_dto: ImageDTO, mock_invoker: Invoker, error: Exception ): - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() bulk_download_service.start(mock_invoker) bulk_download_service.handler([mock_image_dto.image_name], None, None) @@ -395,7 +395,7 @@ def execute_handler_test_on_error( def test_delete(tmp_path: Path): """Test that the delete method removes the bulk download file.""" - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() mock_file: Path = tmp_path / "bulk_downloads" / "test.zip" mock_file.write_text("contents") @@ -409,7 +409,7 @@ def test_delete(tmp_path: Path): def test_stop(tmp_path: Path): """Test that the stop method removes the bulk download file and not any directories.""" - bulk_download_service = BulkDownloadService(tmp_path) + bulk_download_service = BulkDownloadService() mock_file: Path = tmp_path / "bulk_downloads" / "test.zip" mock_file.write_text("contents") From a8d7cf4e97b15a371abe5b2441160dd50659a0a3 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sat, 17 Feb 2024 23:53:38 -0500 Subject: [PATCH 210/411] refactoring handlers to do null check --- invokeai/app/api/routers/images.py | 5 ++- .../bulk_download/bulk_download_base.py | 4 ++- .../bulk_download/bulk_download_default.py | 31 +++++++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index d1c64648de..c3504b104d 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,6 +1,6 @@ import io import traceback -from typing import Optional, cast +from typing import Optional from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, Response, UploadFile from fastapi.responses import FileResponse @@ -396,10 +396,9 @@ async def download_images_from_list( raise HTTPException(status_code=400, detail="No images or board id specified.") bulk_download_item_id: str = ApiDependencies.invoker.services.bulk_download.generate_item_id(board_id) - # Type narrowing handled above ^, we know that image_names is not None, trying to keep null checks at the boundaries background_tasks.add_task( ApiDependencies.invoker.services.bulk_download.handler, - cast(list[str], image_names), + image_names, board_id, bulk_download_item_id, ) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index d889e2ed0e..80a2ddfb25 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -23,7 +23,9 @@ class BulkDownloadBase(ABC): """ @abstractmethod - def handler(self, image_names: list[str], board_id: Optional[str], bulk_download_item_id: Optional[str]) -> None: + def handler( + self, image_names: Optional[list[str]], board_id: Optional[str], bulk_download_item_id: Optional[str] + ) -> None: """ Starts a a bulk download job. diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 4f5bfb087f..72bb5a5d52 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -7,6 +7,7 @@ from invokeai.app.services.board_records.board_records_common import BoardRecord from invokeai.app.services.bulk_download.bulk_download_common import ( DEFAULT_BULK_DOWNLOAD_ID, BulkDownloadException, + BulkDownloadParametersException, BulkDownloadTargetException, ) from invokeai.app.services.events.events_base import EventServiceBase @@ -36,7 +37,9 @@ class BulkDownloadService(BulkDownloadBase): self.__bulk_downloads_folder = Path(self.__temp_directory.name) / "bulk_downloads" self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) - def handler(self, image_names: list[str], board_id: Optional[str], bulk_download_item_id: Optional[str]) -> None: + def handler( + self, image_names: Optional[list[str]], board_id: Optional[str], bulk_download_item_id: Optional[str] + ) -> None: """ Create a zip file containing the images specified by the given image names or board id. @@ -53,15 +56,12 @@ class BulkDownloadService(BulkDownloadBase): image_dtos: list[ImageDTO] = [] if board_id: - # -1 is the default value for limit, which means no limit, is_intermediate False only gives us completed images - image_dtos = self.__invoker.services.images.get_many( - offset=0, - limit=-1, - board_id=board_id, - is_intermediate=False, - ).items + image_dtos = self._board_handler(board_id) + elif image_names: + image_dtos = self._image_handler(image_names) else: - image_dtos = [self.__invoker.services.images.get_dto(image_name) for image_name in image_names] + raise BulkDownloadParametersException() + bulk_download_item_name: str = self._create_zip_file(image_dtos, bulk_download_item_id) self._signal_job_completed(bulk_download_id, bulk_download_item_id, bulk_download_item_name) except (ImageRecordNotFoundException, BoardRecordNotFoundException, BulkDownloadException) as e: @@ -71,6 +71,19 @@ class BulkDownloadService(BulkDownloadBase): self.__invoker.services.logger.error("Problem bulk downloading images.") raise e + def _image_handler(self, image_names: list[str]) -> list[ImageDTO]: + return [self.__invoker.services.images.get_dto(image_name) for image_name in image_names] + + def _board_handler(self, board_id: str) -> list[ImageDTO]: + # -1 is the default value for limit, which means no limit, is_intermediate False only gives us completed images + image_dtos = self.__invoker.services.images.get_many( + offset=0, + limit=-1, + board_id=board_id, + is_intermediate=False, + ).items + return image_dtos + def generate_item_id(self, board_id: Optional[str]) -> str: return uuid_string() if board_id is None else self._get_clean_board_name(board_id) + "_" + uuid_string() From e51867756a1fe02e73fd12ca2c327528ee91d33f Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Mon, 19 Feb 2024 13:54:48 -0500 Subject: [PATCH 211/411] adding bulk_download_item_name to socket events --- .../bulk_download/bulk_download_default.py | 24 ++++++++++++++----- invokeai/app/services/events/events_base.py | 10 ++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 72bb5a5d52..4d0d2e7b0f 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -49,8 +49,9 @@ class BulkDownloadService(BulkDownloadBase): bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID bulk_download_item_id = uuid_string() if bulk_download_item_id is None else bulk_download_item_id + bulk_download_item_name = bulk_download_item_id + ".zip" - self._signal_job_started(bulk_download_id, bulk_download_item_id) + self._signal_job_started(bulk_download_id, bulk_download_item_id, bulk_download_item_name) try: image_dtos: list[ImageDTO] = [] @@ -64,10 +65,15 @@ class BulkDownloadService(BulkDownloadBase): bulk_download_item_name: str = self._create_zip_file(image_dtos, bulk_download_item_id) self._signal_job_completed(bulk_download_id, bulk_download_item_id, bulk_download_item_name) - except (ImageRecordNotFoundException, BoardRecordNotFoundException, BulkDownloadException) as e: - self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) + except ( + ImageRecordNotFoundException, + BoardRecordNotFoundException, + BulkDownloadException, + BulkDownloadParametersException, + ) as e: + self._signal_job_failed(bulk_download_id, bulk_download_item_id, bulk_download_item_name, e) except Exception as e: - self._signal_job_failed(bulk_download_id, bulk_download_item_id, e) + self._signal_job_failed(bulk_download_id, bulk_download_item_id, bulk_download_item_name, e) self.__invoker.services.logger.error("Problem bulk downloading images.") raise e @@ -116,13 +122,16 @@ class BulkDownloadService(BulkDownloadBase): """Clean a string to be path safe.""" return "".join([c for c in s if c.isalpha() or c.isdigit() or c == " " or c == "_" or c == "-"]).rstrip() - def _signal_job_started(self, bulk_download_id: str, bulk_download_item_id: str) -> None: + def _signal_job_started( + self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + ) -> None: """Signal that a bulk download job has started.""" if self.__event_bus: assert bulk_download_id is not None self.__event_bus.emit_bulk_download_started( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, + bulk_download_item_name=bulk_download_item_name, ) def _signal_job_completed( @@ -138,7 +147,9 @@ class BulkDownloadService(BulkDownloadBase): bulk_download_item_name=bulk_download_item_name, ) - def _signal_job_failed(self, bulk_download_id: str, bulk_download_item_id: str, exception: Exception) -> None: + def _signal_job_failed( + self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str, exception: Exception + ) -> None: """Signal that a bulk download job has failed.""" if self.__event_bus: assert bulk_download_id is not None @@ -146,6 +157,7 @@ class BulkDownloadService(BulkDownloadBase): self.__event_bus.emit_bulk_download_failed( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, + bulk_download_item_name=bulk_download_item_name, error=str(exception), ) diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py index 3cc3ba2f28..53df14330f 100644 --- a/invokeai/app/services/events/events_base.py +++ b/invokeai/app/services/events/events_base.py @@ -440,13 +440,16 @@ class EventServiceBase: }, ) - def emit_bulk_download_started(self, bulk_download_id: str, bulk_download_item_id: str) -> None: + def emit_bulk_download_started( + self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str + ) -> None: """Emitted when a bulk download starts""" self._emit_bulk_download_event( event_name="bulk_download_started", payload={ "bulk_download_id": bulk_download_id, "bulk_download_item_id": bulk_download_item_id, + "bulk_download_item_name": bulk_download_item_name, }, ) @@ -463,13 +466,16 @@ class EventServiceBase: }, ) - def emit_bulk_download_failed(self, bulk_download_id: str, bulk_download_item_id: str, error: str) -> None: + def emit_bulk_download_failed( + self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str, error: str + ) -> None: """Emitted when a bulk download fails""" self._emit_bulk_download_event( event_name="bulk_download_failed", payload={ "bulk_download_id": bulk_download_id, "bulk_download_item_id": bulk_download_item_id, + "bulk_download_item_name": bulk_download_item_name, "error": error, }, ) From 7f8f182a00b2f8f0b870354a296b827ddacba979 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:47:04 +1100 Subject: [PATCH 212/411] tidy(bulk_download): clean up comments --- .../bulk_download/bulk_download_base.py | 12 ++-------- .../bulk_download/bulk_download_default.py | 23 ------------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index 80a2ddfb25..f085d384a9 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -27,7 +27,7 @@ class BulkDownloadBase(ABC): self, image_names: Optional[list[str]], board_id: Optional[str], bulk_download_item_id: Optional[str] ) -> None: """ - Starts a a bulk download job. + Create a zip file containing the images specified by the given image names or board id. :param image_names: A list of image names to include in the zip file. :param board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. @@ -54,15 +54,7 @@ class BulkDownloadBase(ABC): @abstractmethod def stop(self, *args, **kwargs) -> None: - """ - Stops the BulkDownloadService and cleans up all the remnants. - - This method is responsible for stopping the BulkDownloadService and performing any necessary cleanup - operations to remove any remnants or resources associated with the service. - - :param *args: Variable length argument list. - :param **kwargs: Arbitrary keyword arguments. - """ + """Stops the BulkDownloadService and cleans up.""" @abstractmethod def delete(self, bulk_download_item_name: str) -> None: diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 4d0d2e7b0f..670703db9b 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -30,9 +30,6 @@ class BulkDownloadService(BulkDownloadBase): self.__event_bus = invoker.services.events def __init__(self): - """ - Initialize the downloader object. - """ self.__temp_directory = TemporaryDirectory() self.__bulk_downloads_folder = Path(self.__temp_directory.name) / "bulk_downloads" self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) @@ -40,13 +37,6 @@ class BulkDownloadService(BulkDownloadBase): def handler( self, image_names: Optional[list[str]], board_id: Optional[str], bulk_download_item_id: Optional[str] ) -> None: - """ - Create a zip file containing the images specified by the given image names or board id. - - param: image_names: A list of image names to include in the zip file. - param: board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file. - """ - bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID bulk_download_item_id = uuid_string() if bulk_download_item_id is None else bulk_download_item_id bulk_download_item_name = bulk_download_item_id + ".zip" @@ -162,26 +152,13 @@ class BulkDownloadService(BulkDownloadBase): ) def stop(self, *args, **kwargs): - """Stop the bulk download service and delete the files in the bulk download folder.""" - # Get all the files in the bulk downloads folder, only .zip files self.__temp_directory.cleanup() def delete(self, bulk_download_item_name: str) -> None: - """ - Delete the bulk download file. - - :param bulk_download_item_name: The name of the bulk download item. - """ path = self.get_path(bulk_download_item_name) Path(path).unlink() def get_path(self, bulk_download_item_name: str) -> str: - """ - Get the path to the bulk download file. - - :param bulk_download_item_name: The name of the bulk download item. - :return: The path to the bulk download file. - """ path = str(self.__bulk_downloads_folder / bulk_download_item_name) if not self._is_valid_path(path): raise BulkDownloadTargetException() From bf3b10cb1c96d149433e4a36e7c456dd80940148 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:48:51 +1100 Subject: [PATCH 213/411] tidy(bulk_download): remove extraneous abstract methods `start`, `stop` and `__init__` are not required in implementations of an ABC or service. --- .../bulk_download/bulk_download_base.py | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py index f085d384a9..617b611f56 100644 --- a/invokeai/app/services/bulk_download/bulk_download_base.py +++ b/invokeai/app/services/bulk_download/bulk_download_base.py @@ -1,26 +1,9 @@ from abc import ABC, abstractmethod from typing import Optional -from invokeai.app.services.invoker import Invoker - class BulkDownloadBase(ABC): - @abstractmethod - def start(self, invoker: Invoker) -> None: - """ - Starts the BulkDownloadService. - - This method is responsible for starting the BulkDownloadService and performing any necessary initialization - operations to prepare the service for use. - - param: invoker: The Invoker that holds all the services, required to be passed as a parameter to avoid circular dependencies. - """ - - @abstractmethod - def __init__(self): - """ - Create BulkDownloadBase object. - """ + """Responsible for creating a zip file containing the images specified by the given image names or board id.""" @abstractmethod def handler( @@ -52,10 +35,6 @@ class BulkDownloadBase(ABC): :return: The generated item ID. """ - @abstractmethod - def stop(self, *args, **kwargs) -> None: - """Stops the BulkDownloadService and cleans up.""" - @abstractmethod def delete(self, bulk_download_item_name: str) -> None: """ From 2291122c2b0c1b60ab214fa88c997f59c72abf77 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:50:10 +1100 Subject: [PATCH 214/411] tidy(bulk_download): remove class-level attr annotations These can be misleading as they shadow actual assigned class attributes. This pattern is in the rest of the app but it shouldn't be. --- .../app/services/bulk_download/bulk_download_default.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 670703db9b..b44501a8d9 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -10,7 +10,6 @@ from invokeai.app.services.bulk_download.bulk_download_common import ( BulkDownloadParametersException, BulkDownloadTargetException, ) -from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException from invokeai.app.services.images.images_common import ImageDTO from invokeai.app.services.invoker import Invoker @@ -20,11 +19,6 @@ from .bulk_download_base import BulkDownloadBase class BulkDownloadService(BulkDownloadBase): - __temp_directory: TemporaryDirectory - __bulk_downloads_folder: Path - __event_bus: EventServiceBase - __invoker: Invoker - def start(self, invoker: Invoker) -> None: self.__invoker = invoker self.__event_bus = invoker.services.events From 38af234108f6759a8af9cf4bb9d9e04201d131e9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:52:39 +1100 Subject: [PATCH 215/411] tidy(bulk_download): use single underscore for private attrs Double underscores are used in the app but it doesn't actually do or convey anything that single underscores don't already do. Considered unpythonic except for actual dunder/magic methods. --- .../bulk_download/bulk_download_default.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index b44501a8d9..f8475b8f6e 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -20,13 +20,13 @@ from .bulk_download_base import BulkDownloadBase class BulkDownloadService(BulkDownloadBase): def start(self, invoker: Invoker) -> None: - self.__invoker = invoker - self.__event_bus = invoker.services.events + self._invoker = invoker + self._event_bus = invoker.services.events def __init__(self): - self.__temp_directory = TemporaryDirectory() - self.__bulk_downloads_folder = Path(self.__temp_directory.name) / "bulk_downloads" - self.__bulk_downloads_folder.mkdir(parents=True, exist_ok=True) + self._temp_directory = TemporaryDirectory() + self._bulk_downloads_folder = Path(self._temp_directory.name) / "bulk_downloads" + self._bulk_downloads_folder.mkdir(parents=True, exist_ok=True) def handler( self, image_names: Optional[list[str]], board_id: Optional[str], bulk_download_item_id: Optional[str] @@ -58,15 +58,15 @@ class BulkDownloadService(BulkDownloadBase): self._signal_job_failed(bulk_download_id, bulk_download_item_id, bulk_download_item_name, e) except Exception as e: self._signal_job_failed(bulk_download_id, bulk_download_item_id, bulk_download_item_name, e) - self.__invoker.services.logger.error("Problem bulk downloading images.") + self._invoker.services.logger.error("Problem bulk downloading images.") raise e def _image_handler(self, image_names: list[str]) -> list[ImageDTO]: - return [self.__invoker.services.images.get_dto(image_name) for image_name in image_names] + return [self._invoker.services.images.get_dto(image_name) for image_name in image_names] def _board_handler(self, board_id: str) -> list[ImageDTO]: # -1 is the default value for limit, which means no limit, is_intermediate False only gives us completed images - image_dtos = self.__invoker.services.images.get_many( + image_dtos = self._invoker.services.images.get_many( offset=0, limit=-1, board_id=board_id, @@ -81,7 +81,7 @@ class BulkDownloadService(BulkDownloadBase): if board_id == "none": return "Uncategorized" - return self._clean_string_to_path_safe(self.__invoker.services.board_records.get(board_id).board_name) + return self._clean_string_to_path_safe(self._invoker.services.board_records.get(board_id).board_name) def _create_zip_file(self, image_dtos: list[ImageDTO], bulk_download_item_id: str) -> str: """ @@ -91,12 +91,12 @@ class BulkDownloadService(BulkDownloadBase): :return: The name of the zip file. """ zip_file_name = bulk_download_item_id + ".zip" - zip_file_path = self.__bulk_downloads_folder / (zip_file_name) + zip_file_path = self._bulk_downloads_folder / (zip_file_name) with ZipFile(zip_file_path, "w") as zip_file: for image_dto in image_dtos: image_zip_path = Path(image_dto.image_category.value) / image_dto.image_name - image_disk_path = self.__invoker.services.images.get_path(image_dto.image_name) + image_disk_path = self._invoker.services.images.get_path(image_dto.image_name) zip_file.write(image_disk_path, arcname=image_zip_path) return str(zip_file_name) @@ -110,9 +110,9 @@ class BulkDownloadService(BulkDownloadBase): self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str ) -> None: """Signal that a bulk download job has started.""" - if self.__event_bus: + if self._event_bus: assert bulk_download_id is not None - self.__event_bus.emit_bulk_download_started( + self._event_bus.emit_bulk_download_started( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, bulk_download_item_name=bulk_download_item_name, @@ -122,10 +122,10 @@ class BulkDownloadService(BulkDownloadBase): self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str ) -> None: """Signal that a bulk download job has completed.""" - if self.__event_bus: + if self._event_bus: assert bulk_download_id is not None assert bulk_download_item_name is not None - self.__event_bus.emit_bulk_download_completed( + self._event_bus.emit_bulk_download_completed( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, bulk_download_item_name=bulk_download_item_name, @@ -135,10 +135,10 @@ class BulkDownloadService(BulkDownloadBase): self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str, exception: Exception ) -> None: """Signal that a bulk download job has failed.""" - if self.__event_bus: + if self._event_bus: assert bulk_download_id is not None assert exception is not None - self.__event_bus.emit_bulk_download_failed( + self._event_bus.emit_bulk_download_failed( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, bulk_download_item_name=bulk_download_item_name, @@ -146,14 +146,14 @@ class BulkDownloadService(BulkDownloadBase): ) def stop(self, *args, **kwargs): - self.__temp_directory.cleanup() + self._temp_directory.cleanup() def delete(self, bulk_download_item_name: str) -> None: path = self.get_path(bulk_download_item_name) Path(path).unlink() def get_path(self, bulk_download_item_name: str) -> str: - path = str(self.__bulk_downloads_folder / bulk_download_item_name) + path = str(self._bulk_downloads_folder / bulk_download_item_name) if not self._is_valid_path(path): raise BulkDownloadTargetException() return path From 80c67dd6e0c0d21f4e41f3242f410f8f1116fa23 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:55:13 +1100 Subject: [PATCH 216/411] tidy(bulk_download): nit - use `or` as a coalescing operator Just a bit cleaner. --- invokeai/app/services/bulk_download/bulk_download_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index f8475b8f6e..6c49957a5c 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -32,7 +32,7 @@ class BulkDownloadService(BulkDownloadBase): self, image_names: Optional[list[str]], board_id: Optional[str], bulk_download_item_id: Optional[str] ) -> None: bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID - bulk_download_item_id = uuid_string() if bulk_download_item_id is None else bulk_download_item_id + bulk_download_item_id = bulk_download_item_id or uuid_string() bulk_download_item_name = bulk_download_item_id + ".zip" self._signal_job_started(bulk_download_id, bulk_download_item_id, bulk_download_item_name) From 98441ad08d72f7ff9e791cf744a877c23bd05de6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:58:21 +1100 Subject: [PATCH 217/411] tidy(bulk_download): do not rely on pagination API to get all images for board We can get all images for the board as a list of image names, then pass that to `_image_handler` to get the DTOs, decoupling from the pagination API. --- .../services/bulk_download/bulk_download_default.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 6c49957a5c..9fad0c3443 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -65,14 +65,8 @@ class BulkDownloadService(BulkDownloadBase): return [self._invoker.services.images.get_dto(image_name) for image_name in image_names] def _board_handler(self, board_id: str) -> list[ImageDTO]: - # -1 is the default value for limit, which means no limit, is_intermediate False only gives us completed images - image_dtos = self._invoker.services.images.get_many( - offset=0, - limit=-1, - board_id=board_id, - is_intermediate=False, - ).items - return image_dtos + image_names = self._invoker.services.board_image_records.get_all_board_image_names_for_board(board_id) + return self._image_handler(image_names) def generate_item_id(self, board_id: Optional[str]) -> str: return uuid_string() if board_id is None else self._get_clean_board_name(board_id) + "_" + uuid_string() From cbb997e7d0ee5602f51005f7cebec7baceb1b949 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:05:52 +1100 Subject: [PATCH 218/411] tidy(bulk_download): don't store events service separately Using the invoker object directly leaves no ambiguity as to what `_events_bus` actually is. --- .../services/bulk_download/bulk_download_default.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py index 9fad0c3443..04cec928f4 100644 --- a/invokeai/app/services/bulk_download/bulk_download_default.py +++ b/invokeai/app/services/bulk_download/bulk_download_default.py @@ -21,7 +21,6 @@ from .bulk_download_base import BulkDownloadBase class BulkDownloadService(BulkDownloadBase): def start(self, invoker: Invoker) -> None: self._invoker = invoker - self._event_bus = invoker.services.events def __init__(self): self._temp_directory = TemporaryDirectory() @@ -104,9 +103,9 @@ class BulkDownloadService(BulkDownloadBase): self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str ) -> None: """Signal that a bulk download job has started.""" - if self._event_bus: + if self._invoker: assert bulk_download_id is not None - self._event_bus.emit_bulk_download_started( + self._invoker.services.events.emit_bulk_download_started( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, bulk_download_item_name=bulk_download_item_name, @@ -116,10 +115,10 @@ class BulkDownloadService(BulkDownloadBase): self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str ) -> None: """Signal that a bulk download job has completed.""" - if self._event_bus: + if self._invoker: assert bulk_download_id is not None assert bulk_download_item_name is not None - self._event_bus.emit_bulk_download_completed( + self._invoker.services.events.emit_bulk_download_completed( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, bulk_download_item_name=bulk_download_item_name, @@ -129,10 +128,10 @@ class BulkDownloadService(BulkDownloadBase): self, bulk_download_id: str, bulk_download_item_id: str, bulk_download_item_name: str, exception: Exception ) -> None: """Signal that a bulk download job has failed.""" - if self._event_bus: + if self._invoker: assert bulk_download_id is not None assert exception is not None - self._event_bus.emit_bulk_download_failed( + self._invoker.services.events.emit_bulk_download_failed( bulk_download_id=bulk_download_id, bulk_download_item_id=bulk_download_item_id, bulk_download_item_name=bulk_download_item_name, From 5cba55d670843e727870e95023327a29cec0f87f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 18:49:55 +1100 Subject: [PATCH 219/411] test: clean up & fix tests - Deduplicate the mock invocation services. This is possible now that the import order issue is resolved. - Merge `DummyEventService` into `TestEventService` and update all tests to use `TestEventService`. --- tests/app/routers/test_images.py | 49 ------------- .../bulk_download/test_bulk_download.py | 71 ++++--------------- .../services/download/test_download_queue.py | 6 +- tests/conftest.py | 55 +++++++++++++- tests/fixtures/event_service.py | 27 ------- tests/test_graph_execution_state.py | 39 ---------- tests/test_nodes.py | 15 ++-- 7 files changed, 78 insertions(+), 184 deletions(-) delete mode 100644 tests/fixtures/event_service.py diff --git a/tests/app/routers/test_images.py b/tests/app/routers/test_images.py index 67297a116f..5cb8cf1c37 100644 --- a/tests/app/routers/test_images.py +++ b/tests/app/routers/test_images.py @@ -1,66 +1,17 @@ from pathlib import Path from typing import Any -import pytest from fastapi import BackgroundTasks from fastapi.testclient import TestClient from invokeai.app.api.dependencies import ApiDependencies from invokeai.app.api_app import app from invokeai.app.services.board_records.board_records_common import BoardRecord -from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage -from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.images.images_default import ImageService -from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.invoker import Invoker -from invokeai.backend.util.logging import InvokeAILogger -from tests.fixtures.sqlite_database import create_mock_sqlite_database client = TestClient(app) -@pytest.fixture -def mock_services(tmp_path: Path) -> InvocationServices: - configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) - logger = InvokeAILogger.get_logger() - db = create_mock_sqlite_database(configuration, logger) - - return InvocationServices( - board_image_records=None, # type: ignore - board_images=None, # type: ignore - board_records=SqliteBoardRecordStorage(db=db), - boards=None, # type: ignore - bulk_download=BulkDownloadService(), - configuration=None, # type: ignore - events=None, # type: ignore - graph_execution_manager=None, # type: ignore - image_files=None, # type: ignore - image_records=None, # type: ignore - images=ImageService(), - invocation_cache=None, # type: ignore - latents=None, # type: ignore - logger=logger, - model_manager=None, # type: ignore - model_records=None, # type: ignore - download_queue=None, # type: ignore - model_install=None, # type: ignore - names=None, # type: ignore - performance_statistics=None, # type: ignore - processor=None, # type: ignore - queue=None, # type: ignore - session_processor=None, # type: ignore - session_queue=None, # type: ignore - urls=None, # type: ignore - workflow_records=None, # type: ignore - ) - - -@pytest.fixture() -def mock_invoker(mock_services: InvocationServices) -> Invoker: - return Invoker(services=mock_services) - - class MockApiDependencies(ApiDependencies): invoker: Invoker diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 3e8b7fd2eb..b18f6e038d 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -7,23 +7,17 @@ from zipfile import ZipFile import pytest from invokeai.app.services.board_records.board_records_common import BoardRecord, BoardRecordNotFoundException -from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage from invokeai.app.services.bulk_download.bulk_download_common import BulkDownloadTargetException from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService -from invokeai.app.services.config.config_default import InvokeAIAppConfig from invokeai.app.services.image_records.image_records_common import ( ImageCategory, ImageRecordNotFoundException, ResourceOrigin, ) from invokeai.app.services.images.images_common import ImageDTO -from invokeai.app.services.images.images_default import ImageService -from invokeai.app.services.invocation_services import InvocationServices from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.pagination import OffsetPaginatedResults -from invokeai.backend.util.logging import InvokeAILogger -from tests.fixtures.event_service import DummyEventService -from tests.fixtures.sqlite_database import create_mock_sqlite_database +from tests.test_nodes import TestEventService @pytest.fixture @@ -46,53 +40,6 @@ def mock_image_dto() -> ImageDTO: ) -@pytest.fixture -def mock_event_service() -> DummyEventService: - """Create a dummy event service.""" - return DummyEventService() - - -@pytest.fixture -def mock_services(mock_event_service: DummyEventService) -> InvocationServices: - configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) - logger = InvokeAILogger.get_logger() - db = create_mock_sqlite_database(configuration, logger) - - return InvocationServices( - board_image_records=None, # type: ignore - board_images=None, # type: ignore - board_records=SqliteBoardRecordStorage(db=db), - boards=None, # type: ignore - bulk_download=None, # type: ignore - configuration=None, # type: ignore - events=mock_event_service, - graph_execution_manager=None, # type: ignore - image_files=None, # type: ignore - image_records=None, # type: ignore - images=ImageService(), - invocation_cache=None, # type: ignore - latents=None, # type: ignore - logger=logger, - model_manager=None, # type: ignore - model_records=None, # type: ignore - download_queue=None, # type: ignore - model_install=None, # type: ignore - names=None, # type: ignore - performance_statistics=None, # type: ignore - processor=None, # type: ignore - queue=None, # type: ignore - session_processor=None, # type: ignore - session_queue=None, # type: ignore - urls=None, # type: ignore - workflow_records=None, # type: ignore - ) - - -@pytest.fixture() -def mock_invoker(mock_services: InvocationServices) -> Invoker: - return Invoker(services=mock_services) - - @pytest.fixture(autouse=True) def mock_temporary_directory(monkeypatch: Any, tmp_path: Path): """Mock the TemporaryDirectory class so that it uses the tmp_path fixture.""" @@ -288,6 +235,16 @@ def prepare_handler_test(tmp_path: Path, monkeypatch: Any, mock_image_dto: Image monkeypatch.setattr(mock_invoker.services.images, "get_dto", mock_get_dto) + # This is used when preparing all images for a given board + def mock_get_all_board_image_names_for_board(*args, **kwargs): + return [mock_image_dto.image_name] + + monkeypatch.setattr( + mock_invoker.services.board_image_records, + "get_all_board_image_names_for_board", + mock_get_all_board_image_names_for_board, + ) + # Create a mock image file so that the contents of the zip file are not empty mock_image_path: Path = tmp_path / mock_image_dto.image_name mock_image_contents: str = "Totally an image" @@ -306,7 +263,7 @@ def assert_handler_success( expected_image_path: Path, mock_image_contents: str, tmp_path: Path, - event_bus: DummyEventService, + event_bus: TestEventService, ): """Assert that the handler was successful.""" # Check that the zip file was created @@ -369,7 +326,7 @@ def test_handler_on_generic_exception( with pytest.raises(Exception): # noqa: B017 execute_handler_test_on_error(tmp_path, monkeypatch, mock_image_dto, mock_invoker, exception) - event_bus: DummyEventService = mock_invoker.services.events + event_bus: TestEventService = mock_invoker.services.events assert len(event_bus.events) == 2 assert event_bus.events[0].event_name == "bulk_download_started" @@ -384,7 +341,7 @@ def execute_handler_test_on_error( bulk_download_service.start(mock_invoker) bulk_download_service.handler([mock_image_dto.image_name], None, None) - event_bus: DummyEventService = mock_invoker.services.events + event_bus: TestEventService = mock_invoker.services.events assert len(event_bus.events) == 2 assert event_bus.events[0].event_name == "bulk_download_started" diff --git a/tests/app/services/download/test_download_queue.py b/tests/app/services/download/test_download_queue.py index 34408ac5ae..ff9b193b17 100644 --- a/tests/app/services/download/test_download_queue.py +++ b/tests/app/services/download/test_download_queue.py @@ -9,7 +9,7 @@ from requests.sessions import Session from requests_testadapter import TestAdapter, TestSession from invokeai.app.services.download import DownloadJob, DownloadJobStatus, DownloadQueueService -from tests.fixtures.event_service import DummyEventService +from tests.test_nodes import TestEventService # Prevent pytest deprecation warnings TestAdapter.__test__ = False # type: ignore @@ -101,7 +101,7 @@ def test_errors(tmp_path: Path, session: Session) -> None: def test_event_bus(tmp_path: Path, session: Session) -> None: - event_bus = DummyEventService() + event_bus = TestEventService() queue = DownloadQueueService(requests_session=session, event_bus=event_bus) queue.start() @@ -167,7 +167,7 @@ def test_broken_callbacks(tmp_path: Path, session: Session, capsys) -> None: def test_cancel(tmp_path: Path, session: Session) -> None: - event_bus = DummyEventService() + event_bus = TestEventService() queue = DownloadQueueService(requests_session=session, event_bus=event_bus) queue.start() diff --git a/tests/conftest.py b/tests/conftest.py index 873ccc13fd..a483b7529a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,4 +4,57 @@ # We import the model_installer and torch_device fixtures here so that they can be used by all tests. Flake8 does not # play well with fixtures (F401 and F811), so this is cleaner than importing in all files that use these fixtures. -from invokeai.backend.util.test_utils import torch_device # noqa: F401 +import logging + +import pytest + +from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage +from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage +from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService +from invokeai.app.services.config.config_default import InvokeAIAppConfig +from invokeai.app.services.images.images_default import ImageService +from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache +from invokeai.app.services.invocation_services import InvocationServices +from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService +from invokeai.app.services.invoker import Invoker +from invokeai.backend.util.logging import InvokeAILogger +from tests.fixtures.sqlite_database import create_mock_sqlite_database # noqa: F401 +from tests.test_nodes import TestEventService + + +@pytest.fixture +def mock_services() -> InvocationServices: + configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) + logger = InvokeAILogger.get_logger() + db = create_mock_sqlite_database(configuration, logger) + + # NOTE: none of these are actually called by the test invocations + return InvocationServices( + board_image_records=SqliteBoardImageRecordStorage(db=db), + board_images=None, # type: ignore + board_records=SqliteBoardRecordStorage(db=db), + boards=None, # type: ignore + bulk_download=BulkDownloadService(), + configuration=configuration, + events=TestEventService(), + image_files=None, # type: ignore + image_records=None, # type: ignore + images=ImageService(), + invocation_cache=MemoryInvocationCache(max_cache_size=0), + logger=logging, # type: ignore + model_manager=None, # type: ignore + download_queue=None, # type: ignore + names=None, # type: ignore + performance_statistics=InvocationStatsService(), + session_processor=None, # type: ignore + session_queue=None, # type: ignore + urls=None, # type: ignore + workflow_records=None, # type: ignore + tensors=None, # type: ignore + conditioning=None, # type: ignore + ) + + +@pytest.fixture() +def mock_invoker(mock_services: InvocationServices) -> Invoker: + return Invoker(services=mock_services) diff --git a/tests/fixtures/event_service.py b/tests/fixtures/event_service.py deleted file mode 100644 index 8f6a45c38f..0000000000 --- a/tests/fixtures/event_service.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any, Dict, List - -from pydantic import BaseModel - -from invokeai.app.services.events.events_base import EventServiceBase - - -class DummyEvent(BaseModel): - """Dummy Event to use with Dummy Event service.""" - - event_name: str - payload: Dict[str, Any] - - -# A dummy event service for testing event issuing -class DummyEventService(EventServiceBase): - """Dummy event service for testing.""" - - events: List[DummyEvent] - - def __init__(self) -> None: - super().__init__() - self.events = [] - - def dispatch(self, event_name: str, payload: Any) -> None: - """Dispatch an event by appending it to self.events.""" - self.events.append(DummyEvent(event_name=payload["event"], payload=payload["data"])) diff --git a/tests/test_graph_execution_state.py b/tests/test_graph_execution_state.py index 9a35037431..0bb15b17df 100644 --- a/tests/test_graph_execution_state.py +++ b/tests/test_graph_execution_state.py @@ -1,4 +1,3 @@ -import logging from typing import Optional from unittest.mock import Mock @@ -8,17 +7,12 @@ import pytest from .test_nodes import ( # isort: split PromptCollectionTestInvocation, PromptTestInvocation, - TestEventService, TextToImageTestInvocation, ) from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext from invokeai.app.invocations.collections import RangeInvocation from invokeai.app.invocations.math import AddInvocation, MultiplyInvocation -from invokeai.app.services.config.config_default import InvokeAIAppConfig -from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache -from invokeai.app.services.invocation_services import InvocationServices -from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService from invokeai.app.services.shared.graph import ( CollectInvocation, Graph, @@ -38,39 +32,6 @@ def simple_graph() -> Graph: return g -# This must be defined here to avoid issues with the dynamic creation of the union of all invocation types -# Defining it in a separate module will cause the union to be incomplete, and pydantic will not validate -# the test invocations. -@pytest.fixture -def mock_services() -> InvocationServices: - configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0) - # NOTE: none of these are actually called by the test invocations - return InvocationServices( - board_image_records=None, # type: ignore - board_images=None, # type: ignore - board_records=None, # type: ignore - boards=None, # type: ignore - bulk_download=None, # type: ignore - configuration=configuration, - events=TestEventService(), - image_files=None, # type: ignore - image_records=None, # type: ignore - images=None, # type: ignore - invocation_cache=MemoryInvocationCache(max_cache_size=0), - logger=logging, # type: ignore - model_manager=None, # type: ignore - download_queue=None, # type: ignore - names=None, # type: ignore - performance_statistics=InvocationStatsService(), - session_processor=None, # type: ignore - session_queue=None, # type: ignore - urls=None, # type: ignore - workflow_records=None, # type: ignore - tensors=None, # type: ignore - conditioning=None, # type: ignore - ) - - def invoke_next(g: GraphExecutionState) -> tuple[Optional[BaseInvocation], Optional[BaseInvocationOutput]]: n = g.next() if n is None: diff --git a/tests/test_nodes.py b/tests/test_nodes.py index aab3d9c7b4..e1fe857040 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -1,5 +1,7 @@ from typing import Any, Callable, Union +from pydantic import BaseModel + from invokeai.app.invocations.baseinvocation import ( BaseInvocation, BaseInvocationOutput, @@ -115,25 +117,22 @@ def create_edge(from_id: str, from_field: str, to_id: str, to_field: str) -> Edg ) -class TestEvent: - event_name: str - payload: Any +class TestEvent(BaseModel): __test__ = False # not a pytest test case - def __init__(self, event_name: str, payload: Any): - self.event_name = event_name - self.payload = payload + event_name: str + payload: Any class TestEventService(EventServiceBase): - events: list __test__ = False # not a pytest test case def __init__(self): super().__init__() - self.events = [] + self.events: list[TestEvent] = [] def dispatch(self, event_name: str, payload: Any) -> None: + self.events.append(TestEvent(event_name=payload["event"], payload=payload["data"])) pass From ab94484c6c01faf0d0bd2c0fe912737c544820ef Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Sun, 18 Feb 2024 00:01:15 -0500 Subject: [PATCH 220/411] setting up event listeners for bulk download socket --- .../app/store/nanostores/bulkDownloadId.ts | 9 +++++ .../src/features/system/store/configSlice.ts | 2 +- .../web/src/services/events/actions.ts | 15 ++++++++ .../frontend/web/src/services/events/types.ts | 34 +++++++++++++++++-- .../services/events/util/setEventListeners.ts | 18 ++++++++++ 5 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/nanostores/bulkDownloadId.ts diff --git a/invokeai/frontend/web/src/app/store/nanostores/bulkDownloadId.ts b/invokeai/frontend/web/src/app/store/nanostores/bulkDownloadId.ts new file mode 100644 index 0000000000..5615124493 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/nanostores/bulkDownloadId.ts @@ -0,0 +1,9 @@ +import { atom } from 'nanostores'; + +export const DEFAULT_BULK_DOWNLOAD_ID = 'default'; + +/** + * The download id for a bulk download. Used for socket subscriptions. + */ + +export const $bulkDownloadId = atom(DEFAULT_BULK_DOWNLOAD_ID); diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 1cf62e89c8..94f1f1c64a 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -18,7 +18,7 @@ export const initialConfigState: AppConfig = { shouldUpdateImagesOnConnect: false, shouldFetchMetadataFromApi: false, disabledTabs: [], - disabledFeatures: ['lightbox', 'faceRestore', 'batches', 'bulkDownload'], + disabledFeatures: ['lightbox', 'faceRestore', 'batches'], disabledSDFeatures: ['variation', 'symmetry', 'hires', 'perlinNoise', 'noiseThreshold'], nodesAllowlist: undefined, nodesDenylist: undefined, diff --git a/invokeai/frontend/web/src/services/events/actions.ts b/invokeai/frontend/web/src/services/events/actions.ts index 101e928f79..b80363315e 100644 --- a/invokeai/frontend/web/src/services/events/actions.ts +++ b/invokeai/frontend/web/src/services/events/actions.ts @@ -1,5 +1,8 @@ import { createAction } from '@reduxjs/toolkit'; import type { + BulkDownloadCompletedEvent, + BulkDownloadFailedEvent, + BulkDownloadStartedEvent, GeneratorProgressEvent, GraphExecutionStateCompleteEvent, InvocationCompleteEvent, @@ -64,3 +67,15 @@ export const socketInvocationRetrievalError = createAction<{ export const socketQueueItemStatusChanged = createAction<{ data: QueueItemStatusChangedEvent; }>('socket/socketQueueItemStatusChanged'); + +export const socketBulkDownloadStarted = createAction<{ + data: BulkDownloadStartedEvent; +}>('socket/socketBulkDownloadStarted'); + +export const socketBulkDownloadCompleted = createAction<{ + data: BulkDownloadCompletedEvent; +}>('socket/socketBulkDownloadCompleted'); + +export const socketBulkDownloadFailed = createAction<{ + data: BulkDownloadFailedEvent; +}>('socket/socketBulkDownloadFailed'); diff --git a/invokeai/frontend/web/src/services/events/types.ts b/invokeai/frontend/web/src/services/events/types.ts index 9579b6abc1..092132fea2 100644 --- a/invokeai/frontend/web/src/services/events/types.ts +++ b/invokeai/frontend/web/src/services/events/types.ts @@ -156,7 +156,7 @@ export type InvocationRetrievalErrorEvent = { * * @example socket.on('queue_item_status_changed', (data: QueueItemStatusChangedEvent) => { ... } */ -export type QueueItemStatusChangedEvent = { +export type QueueItemStatusChangedEvent = { queue_id: string; queue_item: { queue_id: string; @@ -191,7 +191,7 @@ export type QueueItemStatusChangedEvent = { failed: number; canceled: number; total: number; - }; + }; }; export type ClientEmitSubscribeQueue = { @@ -202,6 +202,31 @@ export type ClientEmitUnsubscribeQueue = { queue_id: string; }; +export type BulkDownloadStartedEvent = { + bulk_download_id: string; + bulk_download_item_id: string; +}; + +export type BulkDownloadCompletedEvent = { + bulk_download_id: string; + bulk_download_item_id: string; + bulk_download_item_name: string; +}; + +export type BulkDownloadFailedEvent = { + bulk_download_id: string; + bulk_download_item_id: string; + error: string; +} + +export type ClientEmitSubscribeBulkDownload = { + bulk_download_id: string; +}; + +export type ClientEmitUnsubscribeBulkDownload = { + bulk_download_id: string; +}; + export type ServerToClientEvents = { generator_progress: (payload: GeneratorProgressEvent) => void; invocation_complete: (payload: InvocationCompleteEvent) => void; @@ -213,6 +238,9 @@ export type ServerToClientEvents = { session_retrieval_error: (payload: SessionRetrievalErrorEvent) => void; invocation_retrieval_error: (payload: InvocationRetrievalErrorEvent) => void; queue_item_status_changed: (payload: QueueItemStatusChangedEvent) => void; + bulk_download_started: (payload: BulkDownloadStartedEvent) => void; + bulk_download_completed: (payload: BulkDownloadCompletedEvent) => void; + bulk_download_failed: (payload: BulkDownloadFailedEvent) => void; }; export type ClientToServerEvents = { @@ -220,4 +248,6 @@ export type ClientToServerEvents = { disconnect: () => void; subscribe_queue: (payload: ClientEmitSubscribeQueue) => void; unsubscribe_queue: (payload: ClientEmitUnsubscribeQueue) => void; + subscribe_bulk_download: (payload: ClientEmitSubscribeBulkDownload) => void; + unsubscribe_bulk_download: (payload: ClientEmitUnsubscribeBulkDownload) => void; }; diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index c66defee60..d851a185ff 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -1,8 +1,12 @@ +import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId'; import { $queueId } from 'app/store/nanostores/queueId'; import type { AppDispatch } from 'app/store/store'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { + socketBulkDownloadCompleted, + socketBulkDownloadFailed, + socketBulkDownloadStarted, socketConnected, socketDisconnected, socketGeneratorProgress, @@ -34,6 +38,8 @@ export const setEventListeners = (arg: SetEventListenersArg) => { dispatch(socketConnected()); const queue_id = $queueId.get(); socket.emit('subscribe_queue', { queue_id }); + const bulk_download_id = $bulkDownloadId.get(); + socket.emit('subscribe_bulk_download', { bulk_download_id }); }); socket.on('connect_error', (error) => { @@ -150,4 +156,16 @@ export const setEventListeners = (arg: SetEventListenersArg) => { socket.on('queue_item_status_changed', (data) => { dispatch(socketQueueItemStatusChanged({ data })); }); + + socket.on('bulk_download_started', (data) => { + dispatch(socketBulkDownloadStarted({ data })); + }); + + socket.on('bulk_download_completed', (data) => { + dispatch(socketBulkDownloadCompleted({ data })); + }); + + socket.on('bulk_download_failed', (data) => { + dispatch(socketBulkDownloadFailed({ data })); + }); }; From 9e296f6916dfae633b7a833f47b6ae41d6f84240 Mon Sep 17 00:00:00 2001 From: Stefan Tobler Date: Mon, 19 Feb 2024 14:03:26 -0500 Subject: [PATCH 221/411] implementing download for bulk_download events --- invokeai/frontend/web/public/locales/en.json | 2 + .../middleware/listenerMiddleware/index.ts | 4 ++ .../socketio/socketBulkDownloadComplete.ts | 41 +++++++++++++++++++ .../socketio/socketBulkDownloadFailed.ts | 32 +++++++++++++++ .../frontend/web/src/services/events/types.ts | 8 ++-- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 32c707f908..32d3d382bd 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -426,6 +426,8 @@ "downloadSelection": "Download Selection", "preparingDownload": "Preparing Download", "preparingDownloadFailed": "Problem Preparing Download", + "bulkDownloadStarting": "Beginning Download", + "bulkDownloadFailed": "Problem Preparing Download", "problemDeletingImages": "Problem Deleting Images", "problemDeletingImagesDesc": "One or more images could not be deleted" }, 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 322c4eb1ec..07d9bb5df5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -48,6 +48,8 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte import { addModelSelectedListener } from './listeners/modelSelected'; import { addModelsLoadedListener } from './listeners/modelsLoaded'; import { addDynamicPromptsListener } from './listeners/promptChanged'; +import { addBulkDownloadCompleteEventListener } from './listeners/socketio/socketBulkDownloadComplete'; +import { addBulkDownloadFailedEventListener } from './listeners/socketio/socketBulkDownloadFailed'; import { addSocketConnectedEventListener as addSocketConnectedListener } from './listeners/socketio/socketConnected'; import { addSocketDisconnectedEventListener as addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected'; import { addGeneratorProgressEventListener as addGeneratorProgressListener } from './listeners/socketio/socketGeneratorProgress'; @@ -137,6 +139,8 @@ addModelLoadEventListener(); addSessionRetrievalErrorEventListener(); addInvocationRetrievalErrorEventListener(); addSocketQueueItemStatusChangedEventListener(); +addBulkDownloadCompleteEventListener(); +addBulkDownloadFailedEventListener(); // ControlNet addControlNetImageProcessedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts new file mode 100644 index 0000000000..acdb61ff25 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts @@ -0,0 +1,41 @@ +import { logger } from 'app/logging/logger'; +import { addToast } from 'features/system/store/systemSlice'; +import { t } from 'i18next'; +import { socketBulkDownloadCompleted } from 'services/events/actions'; + +import { startAppListening } from '../..'; + +const log = logger('socketio'); + +export const addBulkDownloadCompleteEventListener = () => { + startAppListening({ + actionCreator: socketBulkDownloadCompleted, + effect: async (action, { dispatch }) => { + log.debug(action.payload, 'Bulk download complete'); + + const bulk_download_item_name = action.payload.data.bulk_download_item_name; + + const url = `/api/v1/images/download/${bulk_download_item_name}`; + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = bulk_download_item_name; + document.body.appendChild(a); + a.click(); + + dispatch( + addToast({ + title: t('gallery.bulkDownloadStarting'), + status: 'success', + ...(action.payload + ? { + description: bulk_download_item_name, + duration: null, + isClosable: true, + } + : {}), + }) + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts new file mode 100644 index 0000000000..a9a45c42ae --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts @@ -0,0 +1,32 @@ +import { logger } from 'app/logging/logger'; +import { addToast } from 'features/system/store/systemSlice'; +import { t } from 'i18next'; +import { socketBulkDownloadFailed } from 'services/events/actions'; + +import { startAppListening } from '../..'; + +const log = logger('socketio'); + +export const addBulkDownloadFailedEventListener = () => { + startAppListening({ + actionCreator: socketBulkDownloadFailed, + effect: async (action, { dispatch }) => { + log.debug(action.payload, 'Bulk download error'); + + + dispatch( + addToast({ + title: t('gallery.bulkDownloadFailed'), + status: 'error', + ...(action.payload + ? { + description: action.payload.data.error, + duration: null, + isClosable: true, + } + : {}), + }) + ); + }, + }); +}; diff --git a/invokeai/frontend/web/src/services/events/types.ts b/invokeai/frontend/web/src/services/events/types.ts index 092132fea2..4e68131ee9 100644 --- a/invokeai/frontend/web/src/services/events/types.ts +++ b/invokeai/frontend/web/src/services/events/types.ts @@ -156,7 +156,7 @@ export type InvocationRetrievalErrorEvent = { * * @example socket.on('queue_item_status_changed', (data: QueueItemStatusChangedEvent) => { ... } */ -export type QueueItemStatusChangedEvent = { +export type QueueItemStatusChangedEvent = { queue_id: string; queue_item: { queue_id: string; @@ -191,7 +191,7 @@ export type QueueItemStatusChangedEvent = { failed: number; canceled: number; total: number; - }; + }; }; export type ClientEmitSubscribeQueue = { @@ -205,6 +205,7 @@ export type ClientEmitUnsubscribeQueue = { export type BulkDownloadStartedEvent = { bulk_download_id: string; bulk_download_item_id: string; + bulk_download_item_name: string; }; export type BulkDownloadCompletedEvent = { @@ -216,8 +217,9 @@ export type BulkDownloadCompletedEvent = { export type BulkDownloadFailedEvent = { bulk_download_id: string; bulk_download_item_id: string; + bulk_download_item_name: string; error: string; -} +}; export type ClientEmitSubscribeBulkDownload = { bulk_download_id: string; From a37b60db134c10ab2333da49bb782f112a75d088 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:15:01 +1100 Subject: [PATCH 222/411] feat(bulk_download): update response model, messages --- invokeai/app/api/routers/images.py | 13 ++++++------- .../services/bulk_download/bulk_download_common.py | 5 +---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index c3504b104d..dc8a04b711 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -375,9 +375,11 @@ async def unstar_images_in_list( class ImagesDownloaded(BaseModel): response: Optional[str] = Field( - description="If defined, the message to display to the user when images begin downloading" + default=None, description="The message to display to the user when images begin downloading" + ) + bulk_download_item_name: Optional[str] = Field( + default=None, description="The name of the bulk download item for which events will be emitted" ) - bulk_download_item_name: str = Field(description="The bulk download item name of the bulk download item") @images_router.post( @@ -389,7 +391,7 @@ async def download_images_from_list( default=None, description="The list of names of images to download", embed=True ), board_id: Optional[str] = Body( - default=None, description="The board from which image should be downloaded from", embed=True + default=None, description="The board from which image should be downloaded", embed=True ), ) -> ImagesDownloaded: if (image_names is None or len(image_names) == 0) and board_id is None: @@ -402,10 +404,7 @@ async def download_images_from_list( board_id, bulk_download_item_id, ) - return ImagesDownloaded( - response="Your images are preparing to be downloaded", - bulk_download_item_name=bulk_download_item_id + ".zip", - ) + return ImagesDownloaded(bulk_download_item_name=bulk_download_item_id + ".zip") @images_router.api_route( diff --git a/invokeai/app/services/bulk_download/bulk_download_common.py b/invokeai/app/services/bulk_download/bulk_download_common.py index 37b80073be..68724eb228 100644 --- a/invokeai/app/services/bulk_download/bulk_download_common.py +++ b/invokeai/app/services/bulk_download/bulk_download_common.py @@ -20,9 +20,6 @@ class BulkDownloadTargetException(BulkDownloadException): class BulkDownloadParametersException(BulkDownloadException): """Exception raised when a bulk download parameter is invalid.""" - def __init__( - self, - message="The bulk download parameters are invalid, either an array of image names or a board id must be provided", - ): + def __init__(self, message="No image names or board ID provided"): super().__init__(message) self.message = message From 64908eda553ee4e168ae9a544a1560f99bfd69bf Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:15:14 +1100 Subject: [PATCH 223/411] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 159 ++++++------------ 1 file changed, 52 insertions(+), 107 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 47a257ffe6..2115e79768 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -5,13 +5,6 @@ export type paths = { - "/api/v1/sessions/{session_id}": { - /** - * Get Session - * @description Gets a session - */ - get: operations["get_session"]; - }; "/api/v1/utilities/dynamicprompts": { /** * Parse Dynamicprompts @@ -389,6 +382,13 @@ export type paths = { /** Download Images From List */ post: operations["download_images_from_list"]; }; + "/api/v1/images/download/{bulk_download_item_name}": { + /** + * Get Bulk Download Item + * @description Gets a bulk download zip file + */ + get: operations["get_bulk_download_item"]; + }; "/api/v1/boards/": { /** * List Boards @@ -1152,10 +1152,10 @@ export type components = { * Image Names * @description The list of names of images to download */ - image_names: string[]; + image_names?: string[] | null; /** * Board Id - * @description The board from which image should be downloaded from + * @description The board from which image should be downloaded */ board_id?: string | null; }; @@ -4208,13 +4208,13 @@ export type components = { * Id * @description The id of this graph */ - id?: string; + id?: string | null; /** * Nodes * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["ImageInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CompelInvocation"]; + [key: string]: components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["FaceOffInvocation"]; }; /** * Edges @@ -4251,7 +4251,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["String2Output"] | components["schemas"]["ControlOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["LatentsCollectionOutput"]; + [key: string]: components["schemas"]["ModelLoaderOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["String2Output"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["StringCollectionOutput"]; }; /** * Errors @@ -5674,9 +5674,14 @@ export type components = { ImagesDownloaded: { /** * Response - * @description If defined, the message to display to the user when images begin downloading + * @description The message to display to the user when images begin downloading */ - response: string | null; + response?: string | null; + /** + * Bulk Download Item Name + * @description The name of the bulk download item for which events will be emitted + */ + bulk_download_item_name?: string | null; }; /** ImagesUpdatedFromListResult */ ImagesUpdatedFromListResult: { @@ -6377,7 +6382,7 @@ export type components = { */ AllowDifferentLicense?: boolean; /** @description Type of commercial use allowed or 'No' if no commercial use is allowed. */ - AllowCommercialUse?: components["schemas"]["CommercialUsage"]; + AllowCommercialUse?: components["schemas"]["CommercialUsage"] | null; }; /** * Lineart Anime Processor @@ -11091,66 +11096,6 @@ export type components = { * @enum {string} */ UIType: "SDXLMainModelField" | "SDXLRefinerModelField" | "ONNXModelField" | "VAEModelField" | "LoRAModelField" | "ControlNetModelField" | "IPAdapterModelField" | "SchedulerField" | "AnyField" | "CollectionField" | "CollectionItemField" | "DEPRECATED_Boolean" | "DEPRECATED_Color" | "DEPRECATED_Conditioning" | "DEPRECATED_Control" | "DEPRECATED_Float" | "DEPRECATED_Image" | "DEPRECATED_Integer" | "DEPRECATED_Latents" | "DEPRECATED_String" | "DEPRECATED_BooleanCollection" | "DEPRECATED_ColorCollection" | "DEPRECATED_ConditioningCollection" | "DEPRECATED_ControlCollection" | "DEPRECATED_FloatCollection" | "DEPRECATED_ImageCollection" | "DEPRECATED_IntegerCollection" | "DEPRECATED_LatentsCollection" | "DEPRECATED_StringCollection" | "DEPRECATED_BooleanPolymorphic" | "DEPRECATED_ColorPolymorphic" | "DEPRECATED_ConditioningPolymorphic" | "DEPRECATED_ControlPolymorphic" | "DEPRECATED_FloatPolymorphic" | "DEPRECATED_ImagePolymorphic" | "DEPRECATED_IntegerPolymorphic" | "DEPRECATED_LatentsPolymorphic" | "DEPRECATED_StringPolymorphic" | "DEPRECATED_MainModel" | "DEPRECATED_UNet" | "DEPRECATED_Vae" | "DEPRECATED_CLIP" | "DEPRECATED_Collection" | "DEPRECATED_CollectionItem" | "DEPRECATED_Enum" | "DEPRECATED_WorkflowField" | "DEPRECATED_IsIntermediate" | "DEPRECATED_BoardField" | "DEPRECATED_MetadataItem" | "DEPRECATED_MetadataItemCollection" | "DEPRECATED_MetadataItemPolymorphic" | "DEPRECATED_MetadataDict"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; - /** - * LoRAModelFormat - * @description An enumeration. - * @enum {string} - */ - LoRAModelFormat: "lycoris" | "diffusers"; - /** - * StableDiffusionXLModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; - /** - * IPAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - IPAdapterModelFormat: "invokeai"; - /** - * T2IAdapterModelFormat - * @description An enumeration. - * @enum {string} - */ - T2IAdapterModelFormat: "diffusers"; - /** - * StableDiffusion1ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; - /** - * CLIPVisionModelFormat - * @description An enumeration. - * @enum {string} - */ - CLIPVisionModelFormat: "diffusers"; - /** - * StableDiffusionOnnxModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * VaeModelFormat - * @description An enumeration. - * @enum {string} - */ - VaeModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; @@ -11165,36 +11110,6 @@ export type external = Record; export type operations = { - /** - * Get Session - * @description Gets a session - */ - get_session: { - parameters: { - path: { - /** @description The id of the session to get */ - session_id: string; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - content: { - "application/json": components["schemas"]["GraphExecutionState"]; - }; - }; - /** @description Session not found */ - 404: { - content: never; - }; - /** @description Validation Error */ - 422: { - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; /** * Parse Dynamicprompts * @description Creates a batch process @@ -12451,14 +12366,14 @@ export type operations = { }; /** Download Images From List */ download_images_from_list: { - requestBody: { + requestBody?: { content: { "application/json": components["schemas"]["Body_download_images_from_list"]; }; }; responses: { /** @description Successful Response */ - 200: { + 202: { content: { "application/json": components["schemas"]["ImagesDownloaded"]; }; @@ -12471,6 +12386,36 @@ export type operations = { }; }; }; + /** + * Get Bulk Download Item + * @description Gets a bulk download zip file + */ + get_bulk_download_item: { + parameters: { + path: { + /** @description The bulk_download_item_name of the bulk download item to get */ + bulk_download_item_name: string; + }; + }; + responses: { + /** @description Return the complete bulk download item */ + 200: { + content: { + "application/zip": unknown; + }; + }; + /** @description Image not found */ + 404: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; /** * List Boards * @description Gets a list of boards From c0f0f2f39e2bfd6402f9ca19a3923df31aa9205f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:26:38 +1100 Subject: [PATCH 224/411] feat(ui): revise bulk download listeners - Use a single listener for all of the to keep them in one spot - Use the bulk download item name as a toast id so we can update the existing toasts - Update handling to work with other environments - Move all bulk download handling from components to listener --- invokeai/frontend/web/public/locales/en.json | 9 +- .../middleware/listenerMiddleware/index.ts | 6 +- .../listeners/bulkDownload.ts | 118 ++++++++++++++++++ .../socketio/socketBulkDownloadComplete.ts | 41 ------ .../socketio/socketBulkDownloadFailed.ts | 32 ----- .../components/Boards/BoardContextMenu.tsx | 33 +---- .../MultipleSelectionMenuItems.tsx | 32 +---- 7 files changed, 131 insertions(+), 140 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts delete mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 32d3d382bd..9abf0b80aa 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -424,10 +424,11 @@ "uploads": "Uploads", "deleteSelection": "Delete Selection", "downloadSelection": "Download Selection", - "preparingDownload": "Preparing Download", - "preparingDownloadFailed": "Problem Preparing Download", - "bulkDownloadStarting": "Beginning Download", - "bulkDownloadFailed": "Problem Preparing Download", + "bulkDownloadRequested": "Preparing Download", + "bulkDownloadRequestedDesc": "Your download request is being prepared. This may take a few moments.", + "bulkDownloadRequestFailed": "Problem Preparing Download", + "bulkDownloadStarting": "Download Starting", + "bulkDownloadFailed": "Download Failed", "problemDeletingImages": "Problem Deleting Images", "problemDeletingImagesDesc": "One or more images could not be deleted" }, 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 07d9bb5df5..23e23c1140 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -1,5 +1,6 @@ import type { ListenerEffect, TypedAddListener, TypedStartListening, UnknownAction } from '@reduxjs/toolkit'; import { addListener, createListenerMiddleware } from '@reduxjs/toolkit'; +import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload'; import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked'; import type { AppDispatch, RootState } from 'app/store/store'; @@ -48,8 +49,6 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte import { addModelSelectedListener } from './listeners/modelSelected'; import { addModelsLoadedListener } from './listeners/modelsLoaded'; import { addDynamicPromptsListener } from './listeners/promptChanged'; -import { addBulkDownloadCompleteEventListener } from './listeners/socketio/socketBulkDownloadComplete'; -import { addBulkDownloadFailedEventListener } from './listeners/socketio/socketBulkDownloadFailed'; import { addSocketConnectedEventListener as addSocketConnectedListener } from './listeners/socketio/socketConnected'; import { addSocketDisconnectedEventListener as addSocketDisconnectedListener } from './listeners/socketio/socketDisconnected'; import { addGeneratorProgressEventListener as addGeneratorProgressListener } from './listeners/socketio/socketGeneratorProgress'; @@ -139,8 +138,7 @@ addModelLoadEventListener(); addSessionRetrievalErrorEventListener(); addInvocationRetrievalErrorEventListener(); addSocketQueueItemStatusChangedEventListener(); -addBulkDownloadCompleteEventListener(); -addBulkDownloadFailedEventListener(); +addBulkDownloadListeners(); // ControlNet addControlNetImageProcessedListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.ts new file mode 100644 index 0000000000..39d7e574c2 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/bulkDownload.ts @@ -0,0 +1,118 @@ +import type { UseToastOptions } from '@invoke-ai/ui-library'; +import { createStandaloneToast, theme, TOAST_OPTIONS } from '@invoke-ai/ui-library'; +import { logger } from 'app/logging/logger'; +import { startAppListening } from 'app/store/middleware/listenerMiddleware'; +import { t } from 'i18next'; +import { imagesApi } from 'services/api/endpoints/images'; +import { + socketBulkDownloadCompleted, + socketBulkDownloadFailed, + socketBulkDownloadStarted, +} from 'services/events/actions'; + +const log = logger('images'); + +const { toast } = createStandaloneToast({ + theme: theme, + defaultOptions: TOAST_OPTIONS.defaultOptions, +}); + +export const addBulkDownloadListeners = () => { + startAppListening({ + matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled, + effect: async (action) => { + log.debug(action.payload, 'Bulk download requested'); + + // If we have an item name, we are processing the bulk download locally and should use it as the toast id to + // prevent multiple toasts for the same item. + toast({ + id: action.payload.bulk_download_item_name ?? undefined, + title: t('gallery.bulkDownloadRequested'), + status: 'success', + // Show the response message if it exists, otherwise show the default message + description: action.payload.response || t('gallery.bulkDownloadRequestedDesc'), + duration: null, + isClosable: true, + }); + }, + }); + + startAppListening({ + matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected, + effect: async () => { + log.debug('Bulk download request failed'); + + // There isn't any toast to update if we get this event. + toast({ + title: t('gallery.bulkDownloadRequestFailed'), + status: 'success', + isClosable: true, + }); + }, + }); + + startAppListening({ + actionCreator: socketBulkDownloadStarted, + effect: async (action) => { + // This should always happen immediately after the bulk download request, so we don't need to show a toast here. + log.debug(action.payload.data, 'Bulk download preparation started'); + }, + }); + + startAppListening({ + actionCreator: socketBulkDownloadCompleted, + effect: async (action) => { + log.debug(action.payload.data, 'Bulk download preparation completed'); + + const { bulk_download_item_name } = action.payload.data; + + // TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first + const url = `/api/v1/images/download/${bulk_download_item_name}`; + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = bulk_download_item_name; + document.body.appendChild(a); + a.click(); + + const toastOptions: UseToastOptions = { + id: bulk_download_item_name, + title: t('gallery.bulkDownloadStarting'), + status: 'success', + description: bulk_download_item_name, + duration: 5000, + isClosable: true, + }; + + if (toast.isActive(bulk_download_item_name)) { + toast.update(bulk_download_item_name, toastOptions); + } else { + toast(toastOptions); + } + }, + }); + + startAppListening({ + actionCreator: socketBulkDownloadFailed, + effect: async (action) => { + log.debug(action.payload.data, 'Bulk download preparation failed'); + + const { bulk_download_item_name } = action.payload.data; + + const toastOptions: UseToastOptions = { + id: bulk_download_item_name, + title: t('gallery.bulkDownloadFailed'), + status: 'error', + description: action.payload.data.error, + duration: null, + isClosable: true, + }; + + if (toast.isActive(bulk_download_item_name)) { + toast.update(bulk_download_item_name, toastOptions); + } else { + toast(toastOptions); + } + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts deleted file mode 100644 index acdb61ff25..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadComplete.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { addToast } from 'features/system/store/systemSlice'; -import { t } from 'i18next'; -import { socketBulkDownloadCompleted } from 'services/events/actions'; - -import { startAppListening } from '../..'; - -const log = logger('socketio'); - -export const addBulkDownloadCompleteEventListener = () => { - startAppListening({ - actionCreator: socketBulkDownloadCompleted, - effect: async (action, { dispatch }) => { - log.debug(action.payload, 'Bulk download complete'); - - const bulk_download_item_name = action.payload.data.bulk_download_item_name; - - const url = `/api/v1/images/download/${bulk_download_item_name}`; - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = bulk_download_item_name; - document.body.appendChild(a); - a.click(); - - dispatch( - addToast({ - title: t('gallery.bulkDownloadStarting'), - status: 'success', - ...(action.payload - ? { - description: bulk_download_item_name, - duration: null, - isClosable: true, - } - : {}), - }) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts deleted file mode 100644 index a9a45c42ae..0000000000 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketBulkDownloadFailed.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { logger } from 'app/logging/logger'; -import { addToast } from 'features/system/store/systemSlice'; -import { t } from 'i18next'; -import { socketBulkDownloadFailed } from 'services/events/actions'; - -import { startAppListening } from '../..'; - -const log = logger('socketio'); - -export const addBulkDownloadFailedEventListener = () => { - startAppListening({ - actionCreator: socketBulkDownloadFailed, - effect: async (action, { dispatch }) => { - log.debug(action.payload, 'Bulk download error'); - - - dispatch( - addToast({ - title: t('gallery.bulkDownloadFailed'), - status: 'error', - ...(action.payload - ? { - description: action.payload.data.error, - duration: null, - isClosable: true, - } - : {}), - }) - ); - }, - }); -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx index 490e8eac9e..ad6c37532e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx @@ -5,7 +5,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { autoAddBoardIdChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice'; import type { BoardId } from 'features/gallery/store/types'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { addToast } from 'features/system/store/systemSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDownloadBold, PiPlusBold } from 'react-icons/pi'; @@ -41,35 +40,9 @@ const BoardContextMenu = ({ board, board_id, setBoardToDelete, children }: Props dispatch(autoAddBoardIdChanged(board_id)); }, [board_id, dispatch]); - const handleBulkDownload = useCallback(async () => { - try { - const response = await bulkDownload({ - image_names: [], - board_id: board_id, - }).unwrap(); - - dispatch( - addToast({ - title: t('gallery.preparingDownload'), - status: 'success', - ...(response.response - ? { - description: response.response, - duration: null, - isClosable: true, - } - : {}), - }) - ); - } catch { - dispatch( - addToast({ - title: t('gallery.preparingDownloadFailed'), - status: 'error', - }) - ); - } - }, [t, board_id, bulkDownload, dispatch]); + const handleBulkDownload = useCallback(() => { + bulkDownload({ image_names: [], board_id: board_id }); + }, [board_id, bulkDownload]); const renderMenuFunc = useCallback( () => ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx index e8f71c02f3..7b1fa73472 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/MultipleSelectionMenuItems.tsx @@ -5,7 +5,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { addToast } from 'features/system/store/systemSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiDownloadSimpleBold, PiFoldersBold, PiStarBold, PiStarFill, PiTrashSimpleBold } from 'react-icons/pi'; @@ -44,34 +43,9 @@ const MultipleSelectionMenuItems = () => { unstarImages({ imageDTOs: selection }); }, [unstarImages, selection]); - const handleBulkDownload = useCallback(async () => { - try { - const response = await bulkDownload({ - image_names: selection.map((img) => img.image_name), - }).unwrap(); - - dispatch( - addToast({ - title: t('gallery.preparingDownload'), - status: 'success', - ...(response.response - ? { - description: response.response, - duration: null, - isClosable: true, - } - : {}), - }) - ); - } catch { - dispatch( - addToast({ - title: t('gallery.preparingDownloadFailed'), - status: 'error', - }) - ); - } - }, [t, selection, bulkDownload, dispatch]); + const handleBulkDownload = useCallback(() => { + bulkDownload({ image_names: selection.map((img) => img.image_name) }); + }, [selection, bulkDownload]); const areAllStarred = useMemo(() => { return selection.every((img) => img.starred); From 6a923cce703520f3dddfb6b0226d6d8a7d87f77c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:40:16 +1100 Subject: [PATCH 225/411] feat(ui): do not subscribe to bulk download sio room if baseUrl is set --- .../web/src/services/events/util/setEventListeners.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts index d851a185ff..1f27955ca7 100644 --- a/invokeai/frontend/web/src/services/events/util/setEventListeners.ts +++ b/invokeai/frontend/web/src/services/events/util/setEventListeners.ts @@ -1,3 +1,4 @@ +import { $baseUrl } from 'app/store/nanostores/baseUrl'; import { $bulkDownloadId } from 'app/store/nanostores/bulkDownloadId'; import { $queueId } from 'app/store/nanostores/queueId'; import type { AppDispatch } from 'app/store/store'; @@ -38,8 +39,10 @@ export const setEventListeners = (arg: SetEventListenersArg) => { dispatch(socketConnected()); const queue_id = $queueId.get(); socket.emit('subscribe_queue', { queue_id }); - const bulk_download_id = $bulkDownloadId.get(); - socket.emit('subscribe_bulk_download', { bulk_download_id }); + if (!$baseUrl.get()) { + const bulk_download_id = $bulkDownloadId.get(); + socket.emit('subscribe_bulk_download', { bulk_download_id }); + } }); socket.on('connect_error', (error) => { From e3c23baae9b967496336e6ec342bb9ad7748126a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:44:16 +1100 Subject: [PATCH 226/411] fix(ui): fix package build --- invokeai/frontend/web/tsconfig.node.json | 2 +- invokeai/frontend/web/vite.config.mts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/tsconfig.node.json b/invokeai/frontend/web/tsconfig.node.json index b5936d415c..046964021f 100644 --- a/invokeai/frontend/web/tsconfig.node.json +++ b/invokeai/frontend/web/tsconfig.node.json @@ -5,5 +5,5 @@ "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, - "include": ["vite.config.mts", "config/vite.app.config.mts", "config/vite.package.config.mts", "config/common.mts"] + "include": ["vite.config.mts"] } diff --git a/invokeai/frontend/web/vite.config.mts b/invokeai/frontend/web/vite.config.mts index 32e3e1f64f..ae64b7198d 100644 --- a/invokeai/frontend/web/vite.config.mts +++ b/invokeai/frontend/web/vite.config.mts @@ -26,7 +26,7 @@ export default defineConfig(({ mode }) => { build: { cssCodeSplit: true, lib: { - entry: path.resolve(__dirname, '../src/index.ts'), + entry: path.resolve(__dirname, './src/index.ts'), name: 'InvokeAIUI', fileName: (format) => `invoke-ai-ui.${format}.js`, }, @@ -44,12 +44,12 @@ export default defineConfig(({ mode }) => { }, resolve: { alias: { - app: path.resolve(__dirname, '../src/app'), - assets: path.resolve(__dirname, '../src/assets'), - common: path.resolve(__dirname, '../src/common'), - features: path.resolve(__dirname, '../src/features'), - services: path.resolve(__dirname, '../src/services'), - theme: path.resolve(__dirname, '../src/theme'), + app: path.resolve(__dirname, './src/app'), + assets: path.resolve(__dirname, './src/assets'), + common: path.resolve(__dirname, './src/common'), + features: path.resolve(__dirname, './src/features'), + services: path.resolve(__dirname, './src/services'), + theme: path.resolve(__dirname, './src/theme'), }, }, }; From 34f3a39cc9a6a60ef495e305b5360883d81218d8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:04:43 +1100 Subject: [PATCH 227/411] fix(nodes): fix TI loading --- invokeai/app/invocations/compel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py index 47be380626..50f5322513 100644 --- a/invokeai/app/invocations/compel.py +++ b/invokeai/app/invocations/compel.py @@ -86,7 +86,7 @@ class CompelInvocation(BaseInvocation): for trigger in extract_ti_triggers_from_prompt(self.prompt): name = trigger[1:-1] try: - loaded_model = context.models.load(**self.clip.text_encoder.model_dump()).model + loaded_model = context.models.load(key=name).model assert isinstance(loaded_model, TextualInversionModelRaw) ti_list.append((name, loaded_model)) except UnknownModelException: From 9d9b417432192146c98369029957b29883f48988 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:42:36 +1100 Subject: [PATCH 228/411] fix(ui): use model names in badges --- .../src/features/lora/components/LoRACard.tsx | 6 +- .../web/src/features/nodes/types/common.ts | 8 ++- .../parameters/types/parameterSchemas.ts | 4 +- .../AdvancedSettingsAccordion.tsx | 56 ++++++++++--------- .../GenerationSettingsAccordion.tsx | 31 +++++----- .../web/src/services/api/endpoints/models.ts | 13 +++++ .../api/hooks/useSelectedModelConfig.ts | 14 +++++ .../frontend/web/src/services/api/index.ts | 1 + 8 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index 71ce145786..579d45054b 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -15,14 +15,16 @@ import type { LoRA } from 'features/lora/store/loraSlice'; import { loraIsEnabledChanged, loraRemoved, loraWeightChanged } from 'features/lora/store/loraSlice'; import { memo, useCallback } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; +import { useGetModelConfigQuery } from 'services/api/endpoints/models'; type LoRACardProps = { lora: LoRA; }; export const LoRACard = memo((props: LoRACardProps) => { - const dispatch = useAppDispatch(); const { lora } = props; + const dispatch = useAppDispatch(); + const { data: loraConfig } = useGetModelConfigQuery(lora.key); const handleChange = useCallback( (v: number) => { @@ -44,7 +46,7 @@ export const LoRACard = memo((props: LoRACardProps) => { - {lora.key} + {loraConfig?.name ?? lora.key.substring(0, 8)} diff --git a/invokeai/frontend/web/src/features/nodes/types/common.ts b/invokeai/frontend/web/src/features/nodes/types/common.ts index d5d04deaa5..b195ce4434 100644 --- a/invokeai/frontend/web/src/features/nodes/types/common.ts +++ b/invokeai/frontend/web/src/features/nodes/types/common.ts @@ -67,6 +67,8 @@ export const zModelName = z.string().min(3); export const zModelIdentifier = z.object({ key: z.string().min(1), }); +export const isModelIdentifier = (field: unknown): field is ModelIdentifier => + zModelIdentifier.safeParse(field).success; export const zModelFieldBase = zModelIdentifier; export const zModelIdentifierWithBase = zModelIdentifier.extend({ base: zBaseModel }); export type BaseModel = z.infer; @@ -141,7 +143,7 @@ export type VAEField = z.infer; // #region Control Adapters export const zControlField = z.object({ image: zImageField, - control_model: zControlNetModelField, + control_model: zModelFieldBase, control_weight: z.union([z.number(), z.array(z.number())]).optional(), begin_step_percent: z.number().optional(), end_step_percent: z.number().optional(), @@ -152,7 +154,7 @@ export type ControlField = z.infer; export const zIPAdapterField = z.object({ image: zImageField, - ip_adapter_model: zIPAdapterModelField, + ip_adapter_model: zModelFieldBase, weight: z.number(), begin_step_percent: z.number().optional(), end_step_percent: z.number().optional(), @@ -161,7 +163,7 @@ export type IPAdapterField = z.infer; export const zT2IAdapterField = z.object({ image: zImageField, - t2i_adapter_model: zT2IAdapterModelField, + t2i_adapter_model: zModelFieldBase, weight: z.union([z.number(), z.array(z.number())]).optional(), begin_step_percent: z.number().optional(), end_step_percent: z.number().optional(), diff --git a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts index abd8ee2810..b30d5df147 100644 --- a/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/types/parameterSchemas.ts @@ -4,7 +4,7 @@ import { zControlNetModelField, zIPAdapterModelField, zLoRAModelField, - zMainModelField, + zModelIdentifierWithBase, zSchedulerField, zSDXLRefinerModelField, zT2IAdapterModelField, @@ -105,7 +105,7 @@ export const isParameterAspectRatio = (val: unknown): val is ParameterAspectRati // #endregion // #region Model -export const zParameterModel = zMainModelField.extend({ base: zBaseModel }); +export const zParameterModel = zModelIdentifierWithBase; export type ParameterModel = z.infer; export const isParameterModel = (val: unknown): val is ParameterModel => zParameterModel.safeParse(val).success; // #endregion diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index fc8c54576c..8b10d9bddd 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -1,5 +1,6 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier'; @@ -10,8 +11,9 @@ import ParamVAEModelSelect from 'features/parameters/components/VAEModel/ParamVA import ParamVAEPrecision from 'features/parameters/components/VAEModel/ParamVAEPrecision'; import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useGetModelConfigQuery } from 'services/api/endpoints/models'; const formLabelProps: FormLabelProps = { minW: '9.2rem', @@ -21,31 +23,35 @@ const formLabelProps2: FormLabelProps = { flexGrow: 1, }; -const selectBadges = createMemoizedSelector(selectGenerationSlice, (generation) => { - const badges: (string | number)[] = []; - if (generation.vae) { - // TODO(MM2): Fetch the vae name - let vaeBadge = generation.vae.key; - if (generation.vaePrecision === 'fp16') { - vaeBadge += ` ${generation.vaePrecision}`; - } - badges.push(vaeBadge); - } else if (generation.vaePrecision === 'fp16') { - badges.push(`VAE ${generation.vaePrecision}`); - } - if (generation.clipSkip) { - badges.push(`Skip ${generation.clipSkip}`); - } - if (generation.cfgRescaleMultiplier) { - badges.push(`Rescale ${generation.cfgRescaleMultiplier}`); - } - if (generation.seamlessXAxis || generation.seamlessYAxis) { - badges.push('seamless'); - } - return badges; -}); - export const AdvancedSettingsAccordion = memo(() => { + const vaeKey = useAppSelector((state) => state.generation.vae?.key); + const { data: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken); + const selectBadges = useMemo( + () => + createMemoizedSelector(selectGenerationSlice, (generation) => { + const badges: (string | number)[] = []; + if (vaeConfig) { + let vaeBadge = vaeConfig.name; + if (generation.vaePrecision === 'fp16') { + vaeBadge += ` ${generation.vaePrecision}`; + } + badges.push(vaeBadge); + } else if (generation.vaePrecision === 'fp16') { + badges.push(`VAE ${generation.vaePrecision}`); + } + if (generation.clipSkip) { + badges.push(`Skip ${generation.clipSkip}`); + } + if (generation.cfgRescaleMultiplier) { + badges.push(`Rescale ${generation.cfgRescaleMultiplier}`); + } + if (generation.seamlessXAxis || generation.seamlessYAxis) { + badges.push('seamless'); + } + return badges; + }), + [vaeConfig] + ); const badges = useAppSelector(selectBadges); const { t } = useTranslation(); const { isOpen, onToggle } = useStandaloneAccordionToggle({ 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 cda7dcf6e9..d57e48f11e 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 { } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; +import { EMPTY_ARRAY } from 'app/store/util'; import { LoRAList } from 'features/lora/components/LoRAList'; import LoRASelect from 'features/lora/components/LoRASelect'; import { selectLoraSlice } from 'features/lora/store/loraSlice'; @@ -20,33 +21,31 @@ import ParamCFGScale from 'features/parameters/components/Core/ParamCFGScale'; import ParamScheduler from 'features/parameters/components/Core/ParamScheduler'; import ParamSteps from 'features/parameters/components/Core/ParamSteps'; import ParamMainModelSelect from 'features/parameters/components/MainModel/ParamMainModelSelect'; -import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { filter } from 'lodash-es'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useSelectedModelConfig } from 'services/api/hooks/useSelectedModelConfig'; const formLabelProps: FormLabelProps = { minW: '4rem', }; -const badgesSelector = createMemoizedSelector(selectLoraSlice, selectGenerationSlice, (lora, generation) => { - const enabledLoRAsCount = filter(lora.loras, (l) => !!l.isEnabled).length; - const loraTabBadges = enabledLoRAsCount ? [enabledLoRAsCount] : []; - const accordionBadges: (string | number)[] = []; - // TODO(MM2): fetch model name - if (generation.model) { - accordionBadges.push(generation.model.key); - accordionBadges.push(generation.model.base); - } - - return { loraTabBadges, accordionBadges }; -}); - export const GenerationSettingsAccordion = memo(() => { const { t } = useTranslation(); - const { loraTabBadges, accordionBadges } = useAppSelector(badgesSelector); + const modelConfig = useSelectedModelConfig(); + const selectBadges = useMemo( + () => + createMemoizedSelector(selectLoraSlice, (lora) => { + const enabledLoRAsCount = filter(lora.loras, (l) => !!l.isEnabled).length; + const loraTabBadges = enabledLoRAsCount ? [enabledLoRAsCount] : EMPTY_ARRAY; + const accordionBadges = modelConfig ? [modelConfig.name, modelConfig.base] : EMPTY_ARRAY; + return { loraTabBadges, accordionBadges }; + }), + [modelConfig] + ); + const { loraTabBadges, accordionBadges } = useAppSelector(selectBadges); const { isOpen: isOpenExpander, onToggle: onToggleExpander } = useExpanderToggle({ id: 'generation-settings-advanced', defaultIsOpen: false, diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 46be42d9e5..666e0c707d 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -236,6 +236,18 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: ['Model'], }), + getModelConfig: build.query({ + query: (key) => buildModelsUrl(`i/${key}`), + providesTags: (result) => { + const tags: ApiTagDescription[] = ['Model']; + + if (result) { + tags.push({ type: 'ModelConfig', id: result.key }); + } + + return tags; + }, + }), syncModels: build.mutation({ query: () => { return { @@ -313,6 +325,7 @@ export const modelsApi = api.injectEndpoints({ }); export const { + useGetModelConfigQuery, useGetMainModelsQuery, useGetControlNetModelsQuery, useGetIPAdapterModelsQuery, diff --git a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts new file mode 100644 index 0000000000..4a8d8d72e2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts @@ -0,0 +1,14 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; +import { useGetModelConfigQuery } from 'services/api/endpoints/models'; + +const selectModelKey = createSelector(selectGenerationSlice, (generation) => generation.model?.key); + +export const useSelectedModelConfig = () => { + const key = useAppSelector(selectModelKey); + const { currentData: modelConfig } = useGetModelConfigQuery(key ?? skipToken); + + return modelConfig; +}; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index f32ae34cb7..82f382ee10 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -26,6 +26,7 @@ export const tagTypes = [ 'BatchStatus', 'InvocationCacheStatus', 'Model', + 'ModelConfig', 'T2IAdapterModel', 'MainModel', 'VaeModel', From b59d23d608591d317b5a304b8b2b9bf61d0bb493 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:42:49 +1100 Subject: [PATCH 229/411] fix(ui): handle new model format for metadata --- .../ImageMetadataActions.tsx | 44 +++-- .../ImageMetadataViewer/ImageMetadataItem.tsx | 46 ++++- .../web/src/features/nodes/types/metadata.ts | 4 +- .../parameters/hooks/useRecallParameters.ts | 161 ++++++++++++------ 4 files changed, 178 insertions(+), 77 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx index 5907ba0700..7eec7e1875 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataActions.tsx @@ -1,3 +1,4 @@ +import { isModelIdentifier } from 'features/nodes/types/common'; import type { ControlNetMetadataItem, CoreMetadata, @@ -6,15 +7,10 @@ import type { T2IAdapterMetadataItem, } from 'features/nodes/types/metadata'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; -import { - isParameterControlNetModel, - isParameterLoRAModel, - isParameterT2IAdapterModel, -} from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import ImageMetadataItem from './ImageMetadataItem'; +import ImageMetadataItem, { ModelMetadataItem, VAEMetadataItem } from './ImageMetadataItem'; type Props = { metadata?: CoreMetadata; @@ -147,19 +143,19 @@ const ImageMetadataActions = (props: Props) => { const validControlNets: ControlNetMetadataItem[] = useMemo(() => { return metadata?.controlnets - ? metadata.controlnets.filter((controlnet) => isParameterControlNetModel(controlnet.control_model)) + ? metadata.controlnets.filter((controlnet) => isModelIdentifier(controlnet.control_model)) : []; }, [metadata?.controlnets]); const validIPAdapters: IPAdapterMetadataItem[] = useMemo(() => { return metadata?.ipAdapters - ? metadata.ipAdapters.filter((ipAdapter) => isParameterControlNetModel(ipAdapter.ip_adapter_model)) + ? metadata.ipAdapters.filter((ipAdapter) => isModelIdentifier(ipAdapter.ip_adapter_model)) : []; }, [metadata?.ipAdapters]); const validT2IAdapters: T2IAdapterMetadataItem[] = useMemo(() => { return metadata?.t2iAdapters - ? metadata.t2iAdapters.filter((t2iAdapter) => isParameterT2IAdapterModel(t2iAdapter.t2i_adapter_model)) + ? metadata.t2iAdapters.filter((t2iAdapter) => isModelIdentifier(t2iAdapter.t2i_adapter_model)) : []; }, [metadata?.t2iAdapters]); @@ -209,7 +205,7 @@ const ImageMetadataActions = (props: Props) => { )} {metadata.model !== undefined && metadata.model !== null && metadata.model.key && ( - + )} {metadata.width && ( @@ -220,11 +216,7 @@ const ImageMetadataActions = (props: Props) => { {metadata.scheduler && ( )} - + {metadata.steps && ( )} @@ -264,38 +256,42 @@ const ImageMetadataActions = (props: Props) => { )} {metadata.loras && metadata.loras.map((lora, index) => { - if (isParameterLoRAModel(lora.lora)) { + if (isModelIdentifier(lora.lora)) { return ( - ); } })} {validControlNets.map((controlnet, index) => ( - ))} {validIPAdapters.map((ipAdapter, index) => ( - ))} {validT2IAdapters.map((t2iAdapter, index) => ( - ))} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx index c6dbd16269..7d17a2ad3d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/ImageMetadataItem.tsx @@ -1,8 +1,10 @@ import { ExternalLink, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library'; -import { memo, useCallback } from 'react'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { PiCopyBold } from 'react-icons/pi'; +import { useGetModelConfigQuery } from 'services/api/endpoints/models'; type MetadataItemProps = { isLink?: boolean; @@ -18,8 +20,9 @@ type MetadataItemProps = { */ const ImageMetadataItem = ({ label, value, onClick, isLink, labelPosition, withCopy = false }: MetadataItemProps) => { const { t } = useTranslation(); - - const handleCopy = useCallback(() => navigator.clipboard.writeText(value.toString()), [value]); + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(value?.toString()); + }, [value]); if (!value) { return null; @@ -68,3 +71,40 @@ const ImageMetadataItem = ({ label, value, onClick, isLink, labelPosition, withC }; export default memo(ImageMetadataItem); + +type VAEMetadataItemProps = { + label: string; + modelKey?: string; + onClick: () => void; +}; + +export const VAEMetadataItem = memo(({ label, modelKey, onClick }: VAEMetadataItemProps) => { + const { data: modelConfig } = useGetModelConfigQuery(modelKey ?? skipToken); + + return ( + + ); +}); + +VAEMetadataItem.displayName = 'VAEMetadataItem'; + +type ModelMetadataItemProps = { + label: string; + modelKey?: string; + + extra?: string; + onClick: () => void; +}; + +export const ModelMetadataItem = memo(({ label, modelKey, extra, onClick }: ModelMetadataItemProps) => { + const { data: modelConfig } = useGetModelConfigQuery(modelKey ?? skipToken); + const value = useMemo(() => { + if (modelConfig) { + return `${modelConfig.name}${extra ?? ''}`; + } + return `${modelKey}${extra ?? ''}`; + }, [extra, modelConfig, modelKey]); + return ; +}); + +ModelMetadataItem.displayName = 'ModelMetadataItem'; diff --git a/invokeai/frontend/web/src/features/nodes/types/metadata.ts b/invokeai/frontend/web/src/features/nodes/types/metadata.ts index 0cc30499e3..493a0464b3 100644 --- a/invokeai/frontend/web/src/features/nodes/types/metadata.ts +++ b/invokeai/frontend/web/src/features/nodes/types/metadata.ts @@ -3,8 +3,8 @@ import { z } from 'zod'; import { zControlField, zIPAdapterField, - zLoRAModelField, zMainModelField, + zModelFieldBase, zSDXLRefinerModelField, zT2IAdapterField, zVAEModelField, @@ -15,7 +15,7 @@ import { // - https://github.com/colinhacks/zod/issues/2106 // - https://github.com/colinhacks/zod/issues/2854 export const zLoRAMetadataItem = z.object({ - lora: zLoRAModelField.deepPartial(), + lora: zModelFieldBase.deepPartial(), weight: z.number(), }); const zControlNetMetadataItem = zControlField.deepPartial(); diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts index c8b17816bb..0d464cd9b9 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts @@ -11,6 +11,8 @@ import { } from 'features/controlAdapters/util/buildControlAdapter'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import { loraRecalled, lorasCleared } from 'features/lora/store/loraSlice'; +import type { ModelIdentifier } from 'features/nodes/types/common'; +import { isModelIdentifier } from 'features/nodes/types/common'; import type { ControlNetMetadataItem, CoreMetadata, @@ -37,13 +39,9 @@ import type { ParameterModel } from 'features/parameters/types/parameterSchemas' import { isParameterCFGRescaleMultiplier, isParameterCFGScale, - isParameterControlNetModel, isParameterHeight, isParameterHRFEnabled, isParameterHRFMethod, - isParameterIPAdapterModel, - isParameterLoRAModel, - isParameterModel, isParameterNegativePrompt, isParameterNegativeStylePromptSDXL, isParameterPositivePrompt, @@ -56,7 +54,6 @@ import { isParameterSeed, isParameterSteps, isParameterStrength, - isParameterVAEModel, isParameterWidth, } from 'features/parameters/types/parameterSchemas'; import { @@ -73,15 +70,20 @@ import { import { isNil } from 'lodash-es'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { ALL_BASE_MODELS } from 'services/api/constants'; import { controlNetModelsAdapterSelectors, ipAdapterModelsAdapterSelectors, loraModelsAdapterSelectors, + mainModelsAdapterSelectors, t2iAdapterModelsAdapterSelectors, useGetControlNetModelsQuery, useGetIPAdapterModelsQuery, useGetLoRAModelsQuery, + useGetMainModelsQuery, useGetT2IAdapterModelsQuery, + useGetVaeModelsQuery, + vaeModelsAdapterSelectors, } from 'services/api/endpoints/models'; import type { ImageDTO } from 'services/api/types'; import { v4 as uuidv4 } from 'uuid'; @@ -278,21 +280,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall model with toast - */ - const recallModel = useCallback( - (model: unknown) => { - if (!isParameterModel(model)) { - parameterNotSetToast(); - return; - } - dispatch(modelSelected(model)); - parameterSetToast(); - }, - [dispatch, parameterSetToast, parameterNotSetToast] - ); - /** * Recall scheduler with toast */ @@ -308,25 +295,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall vae model - */ - const recallVaeModel = useCallback( - (vae: unknown) => { - if (!isParameterVAEModel(vae) && !isNil(vae)) { - parameterNotSetToast(); - return; - } - if (isNil(vae)) { - dispatch(vaeSelected(null)); - } else { - dispatch(vaeSelected(vae)); - } - parameterSetToast(); - }, - [dispatch, parameterSetToast, parameterNotSetToast] - ); - /** * Recall steps with toast */ @@ -452,6 +420,95 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); + const { data: mainModels } = useGetMainModelsQuery(ALL_BASE_MODELS); + + const prepareMainModelMetadataItem = useCallback( + (model: ModelIdentifier) => { + const matchingModel = mainModels ? mainModelsAdapterSelectors.selectById(mainModels, model.key) : undefined; + + if (!matchingModel) { + return { model: null, error: 'Model is not installed' }; + } + + return { model: matchingModel, error: null }; + }, + [mainModels] + ); + + /** + * Recall model with toast + */ + const recallModel = useCallback( + (model: unknown) => { + if (!isModelIdentifier(model)) { + parameterNotSetToast(); + return; + } + + const result = prepareMainModelMetadataItem(model); + + if (!result.model) { + parameterNotSetToast(result.error); + return; + } + + dispatch(modelSelected(result.model)); + parameterSetToast(); + }, + [prepareMainModelMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] + ); + + const { data: vaeModels } = useGetVaeModelsQuery(); + + const prepareVAEMetadataItem = useCallback( + (vae: ModelIdentifier, newModel?: ParameterModel) => { + const matchingModel = vaeModels ? vaeModelsAdapterSelectors.selectById(vaeModels, vae.key) : undefined; + if (!matchingModel) { + return { vae: null, error: 'VAE model is not installed' }; + } + const isCompatibleBaseModel = matchingModel?.base === (newModel ?? model)?.base; + + if (!isCompatibleBaseModel) { + return { + vae: null, + error: 'VAE incompatible with currently-selected model', + }; + } + + return { vae: matchingModel, error: null }; + }, + [model, vaeModels] + ); + + /** + * Recall vae model + */ + const recallVaeModel = useCallback( + (vae: unknown) => { + if (!isModelIdentifier(vae) && !isNil(vae)) { + parameterNotSetToast(); + return; + } + + if (isNil(vae)) { + dispatch(vaeSelected(null)); + parameterSetToast(); + return; + } + + const result = prepareVAEMetadataItem(vae); + + if (!result.vae) { + parameterNotSetToast(result.error); + return; + } + + dispatch(vaeSelected(result.vae)); + parameterSetToast(); + }, + [prepareVAEMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] + ); + /** * Recall LoRA with toast */ @@ -460,7 +517,7 @@ export const useRecallParameters = () => { const prepareLoRAMetadataItem = useCallback( (loraMetadataItem: LoRAMetadataItem, newModel?: ParameterModel) => { - if (!isParameterLoRAModel(loraMetadataItem.lora)) { + if (!isModelIdentifier(loraMetadataItem.lora)) { return { lora: null, error: 'Invalid LoRA model' }; } @@ -510,7 +567,7 @@ export const useRecallParameters = () => { const prepareControlNetMetadataItem = useCallback( (controlnetMetadataItem: ControlNetMetadataItem, newModel?: ParameterModel) => { - if (!isParameterControlNetModel(controlnetMetadataItem.control_model)) { + if (!isModelIdentifier(controlnetMetadataItem.control_model)) { return { controlnet: null, error: 'Invalid ControlNet model' }; } @@ -584,7 +641,7 @@ export const useRecallParameters = () => { const prepareT2IAdapterMetadataItem = useCallback( (t2iAdapterMetadataItem: T2IAdapterMetadataItem, newModel?: ParameterModel) => { - if (!isParameterControlNetModel(t2iAdapterMetadataItem.t2i_adapter_model)) { + if (!isModelIdentifier(t2iAdapterMetadataItem.t2i_adapter_model)) { return { controlnet: null, error: 'Invalid ControlNet model' }; } @@ -657,7 +714,7 @@ export const useRecallParameters = () => { const prepareIPAdapterMetadataItem = useCallback( (ipAdapterMetadataItem: IPAdapterMetadataItem, newModel?: ParameterModel) => { - if (!isParameterIPAdapterModel(ipAdapterMetadataItem?.ip_adapter_model)) { + if (!isModelIdentifier(ipAdapterMetadataItem?.ip_adapter_model)) { return { ipAdapter: null, error: 'Invalid IP Adapter model' }; } @@ -762,9 +819,12 @@ export const useRecallParameters = () => { let newModel: ParameterModel | undefined = undefined; - if (isParameterModel(model)) { - newModel = model; - dispatch(modelSelected(model)); + if (isModelIdentifier(model)) { + const result = prepareMainModelMetadataItem(model); + if (result.model) { + dispatch(modelSelected(result.model)); + newModel = result.model; + } } if (isParameterCFGScale(cfg_scale)) { @@ -786,11 +846,14 @@ export const useRecallParameters = () => { if (isParameterScheduler(scheduler)) { dispatch(setScheduler(scheduler)); } - if (isParameterVAEModel(vae) || isNil(vae)) { + if (isModelIdentifier(vae) || isNil(vae)) { if (isNil(vae)) { dispatch(vaeSelected(null)); } else { - dispatch(vaeSelected(vae)); + const result = prepareVAEMetadataItem(vae, newModel); + if (result.vae) { + dispatch(vaeSelected(result.vae)); + } } } @@ -898,6 +961,8 @@ export const useRecallParameters = () => { dispatch, allParameterSetToast, allParameterNotSetToast, + prepareMainModelMetadataItem, + prepareVAEMetadataItem, prepareLoRAMetadataItem, prepareControlNetMetadataItem, prepareIPAdapterMetadataItem, From dbd929df05d1073fd830d6e57d65500c3d969a4e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:43:14 +1100 Subject: [PATCH 230/411] tidy(ui): remove debugging stmt --- .../parameters/components/VAEModel/ParamVAEModelSelect.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index 1810c3ff68..4b9f2764bf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -41,8 +41,6 @@ const ParamVAEModelSelect = () => { isLoading, getIsDisabled, }); - - console.log(value) return ( From 571a86a965e3e6fa7943548b5de600b805a7acc7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:47:46 +1100 Subject: [PATCH 231/411] chore(ui): bump deps Notable updates: - Minor version of RTK includes customizable selectors for RTK Query, so we can remove the patch that was added to ensure only the LRU memoize function was used for perf reasons. Updated to use the LRU memoize function. - Major version of react-resizable-panels. No breaking changes, works great, and you can now resize all panels when dragging at the intersection point of panels. Cool! - Minor (?) version of nanostores. `action` API is removed, we were using it in one spot. Fixed. - @invoke-ai/eslint-config-react has all deps bumped and now has its dependent plugins/configs listed as normal dependencies (as opposed to peer deps). This means we can remove those packages from explicit dev deps. --- invokeai/frontend/web/package.json | 103 +- invokeai/frontend/web/pnpm-lock.yaml | 4090 +++++++++-------- .../store/enhancers/reduxRemember/driver.ts | 8 +- .../frontend/web/src/services/api/index.ts | 12 +- 4 files changed, 2201 insertions(+), 2012 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index a9f37f76ad..743cb1e09d 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -59,52 +59,52 @@ "@fontsource-variable/inter": "^5.0.16", "@invoke-ai/ui-library": "^0.0.21", "@mantine/form": "6.0.21", - "@nanostores/react": "^0.7.1", - "@reduxjs/toolkit": "2.0.1", + "@nanostores/react": "^0.7.2", + "@reduxjs/toolkit": "2.2.1", "@roarr/browser-log-writer": "^1.3.0", "chakra-react-select": "^4.7.6", "compare-versions": "^6.1.0", "dateformat": "^5.0.3", - "framer-motion": "^10.18.0", - "i18next": "^23.7.16", - "i18next-http-backend": "^2.4.2", + "framer-motion": "^11.0.5", + "i18next": "^23.9.0", + "i18next-http-backend": "^2.4.3", "idb-keyval": "^6.2.1", "jsondiffpatch": "^0.6.0", - "konva": "^9.3.1", + "konva": "^9.3.3", "lodash-es": "^4.17.21", - "nanostores": "^0.9.5", + "nanostores": "^0.10.0", "new-github-issue-url": "^1.0.0", - "overlayscrollbars": "^2.4.6", - "overlayscrollbars-react": "^0.5.3", - "query-string": "^8.1.0", + "overlayscrollbars": "^2.5.0", + "overlayscrollbars-react": "^0.5.4", + "query-string": "^8.2.0", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.12", - "react-hook-form": "^7.49.3", - "react-hotkeys-hook": "4.4.4", - "react-i18next": "^14.0.0", + "react-hook-form": "^7.50.1", + "react-hotkeys-hook": "4.5.0", + "react-i18next": "^14.0.5", "react-icons": "^5.0.1", "react-konva": "^18.2.10", "react-redux": "9.1.0", - "react-resizable-panels": "^1.0.9", + "react-resizable-panels": "^2.0.9", "react-select": "5.8.0", "react-textarea-autosize": "^8.5.3", - "react-use": "^17.4.3", - "react-virtuoso": "^4.6.2", - "reactflow": "^11.10.2", + "react-use": "^17.5.0", + "react-virtuoso": "^4.7.0", + "reactflow": "^11.10.4", "redux-dynamic-middlewares": "^2.2.0", "redux-remember": "^5.1.0", "roarr": "^7.21.0", "serialize-error": "^11.0.3", "socket.io-client": "^4.7.4", - "type-fest": "^4.9.0", + "type-fest": "^4.10.2", "use-debounce": "^10.0.0", "use-image": "^1.1.1", "uuid": "^9.0.1", "zod": "^3.22.4", - "zod-validation-error": "^3.0.0" + "zod-validation-error": "^3.0.2" }, "peerDependencies": { "@chakra-ui/react": "^2.8.2", @@ -113,59 +113,44 @@ "ts-toolbelt": "^9.6.0" }, "devDependencies": { - "@arthurgeron/eslint-plugin-react-usememo": "^2.2.3", - "@invoke-ai/eslint-config-react": "^0.0.13", - "@invoke-ai/prettier-config-react": "^0.0.6", - "@storybook/addon-docs": "^7.6.10", - "@storybook/addon-essentials": "^7.6.10", - "@storybook/addon-interactions": "^7.6.10", - "@storybook/addon-links": "^7.6.10", - "@storybook/addon-storysource": "^7.6.10", - "@storybook/blocks": "^7.6.10", - "@storybook/manager-api": "^7.6.10", - "@storybook/react": "^7.6.10", - "@storybook/react-vite": "^7.6.10", - "@storybook/test": "^7.6.10", - "@storybook/theming": "^7.6.10", + "@invoke-ai/eslint-config-react": "^0.0.14", + "@invoke-ai/prettier-config-react": "^0.0.7", + "@storybook/addon-docs": "^7.6.17", + "@storybook/addon-essentials": "^7.6.17", + "@storybook/addon-interactions": "^7.6.17", + "@storybook/addon-links": "^7.6.17", + "@storybook/addon-storysource": "^7.6.17", + "@storybook/blocks": "^7.6.17", + "@storybook/manager-api": "^7.6.17", + "@storybook/react": "^7.6.17", + "@storybook/react-vite": "^7.6.17", + "@storybook/test": "^7.6.17", + "@storybook/theming": "^7.6.17", "@types/dateformat": "^5.0.2", "@types/lodash-es": "^4.17.12", - "@types/node": "^20.11.5", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "@types/uuid": "^9.0.7", - "@typescript-eslint/eslint-plugin": "^6.19.0", - "@typescript-eslint/parser": "^6.19.0", - "@vitejs/plugin-react-swc": "^3.5.0", + "@types/node": "^20.11.19", + "@types/react": "^18.2.57", + "@types/react-dom": "^18.2.19", + "@types/uuid": "^9.0.8", + "@vitejs/plugin-react-swc": "^3.6.0", "concurrently": "^8.2.2", "eslint": "^8.56.0", - "eslint-config-prettier": "^9.1.0", "eslint-plugin-i18next": "^6.0.3", - "eslint-plugin-import": "^2.29.1", "eslint-plugin-path": "^1.2.4", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-simple-import-sort": "^10.0.0", - "eslint-plugin-storybook": "^0.6.15", - "eslint-plugin-unused-imports": "^3.0.0", "madge": "^6.1.0", "openapi-types": "^12.1.3", - "openapi-typescript": "^6.7.3", - "prettier": "^3.2.4", + "openapi-typescript": "^6.7.4", + "prettier": "^3.2.5", "rollup-plugin-visualizer": "^5.12.0", - "storybook": "^7.6.10", + "storybook": "^7.6.17", "ts-toolbelt": "^9.6.0", "tsafe": "^1.6.6", "typescript": "^5.3.3", - "vite": "^5.0.12", - "vite-plugin-css-injected-by-js": "^3.3.1", - "vite-plugin-dts": "^3.7.1", + "vite": "^5.1.3", + "vite-plugin-css-injected-by-js": "^3.4.0", + "vite-plugin-dts": "^3.7.2", "vite-plugin-eslint": "^1.8.1", "vite-tsconfig-paths": "^4.3.1", - "vitest": "^1.2.2" - }, - "pnpm": { - "patchedDependencies": { - "reselect@5.0.1": "patches/reselect@5.0.1.patch" - } + "vitest": "^1.3.1" } } diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 4f9902299c..9e873102e6 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -4,15 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -patchedDependencies: - reselect@5.0.1: - hash: kvbgwzjyy4x4fnh7znyocvb75q - path: patches/reselect@5.0.1.patch - dependencies: '@chakra-ui/react': specifier: ^2.8.2 - version: 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.48)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) + version: 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.57)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/react-use-size': specifier: ^2.1.0 version: 2.1.0(react@18.2.0) @@ -33,22 +28,22 @@ dependencies: version: 5.0.16 '@invoke-ai/ui-library': specifier: ^0.0.21 - version: 0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.2)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0) + version: 0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.2)(@types/react@18.2.57)(i18next@23.9.0)(react-dom@18.2.0)(react@18.2.0) '@mantine/form': specifier: 6.0.21 version: 6.0.21(react@18.2.0) '@nanostores/react': - specifier: ^0.7.1 - version: 0.7.1(nanostores@0.9.5)(react@18.2.0) + specifier: ^0.7.2 + version: 0.7.2(nanostores@0.10.0)(react@18.2.0) '@reduxjs/toolkit': - specifier: 2.0.1 - version: 2.0.1(react-redux@9.1.0)(react@18.2.0) + specifier: 2.2.1 + version: 2.2.1(react-redux@9.1.0)(react@18.2.0) '@roarr/browser-log-writer': specifier: ^1.3.0 version: 1.3.0 chakra-react-select: specifier: ^4.7.6 - version: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + version: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) compare-versions: specifier: ^6.1.0 version: 6.1.0 @@ -56,14 +51,14 @@ dependencies: specifier: ^5.0.3 version: 5.0.3 framer-motion: - specifier: ^10.18.0 - version: 10.18.0(react-dom@18.2.0)(react@18.2.0) + specifier: ^11.0.5 + version: 11.0.5(react-dom@18.2.0)(react@18.2.0) i18next: - specifier: ^23.7.16 - version: 23.7.16 + specifier: ^23.9.0 + version: 23.9.0 i18next-http-backend: - specifier: ^2.4.2 - version: 2.4.2 + specifier: ^2.4.3 + version: 2.4.3 idb-keyval: specifier: ^6.2.1 version: 6.2.1 @@ -71,26 +66,26 @@ dependencies: specifier: ^0.6.0 version: 0.6.0 konva: - specifier: ^9.3.1 - version: 9.3.1 + specifier: ^9.3.3 + version: 9.3.3 lodash-es: specifier: ^4.17.21 version: 4.17.21 nanostores: - specifier: ^0.9.5 - version: 0.9.5 + specifier: ^0.10.0 + version: 0.10.0 new-github-issue-url: specifier: ^1.0.0 version: 1.0.0 overlayscrollbars: - specifier: ^2.4.6 - version: 2.4.6 + specifier: ^2.5.0 + version: 2.5.0 overlayscrollbars-react: - specifier: ^0.5.3 - version: 0.5.3(overlayscrollbars@2.4.6)(react@18.2.0) + specifier: ^0.5.4 + version: 0.5.4(overlayscrollbars@2.5.0)(react@18.2.0) query-string: - specifier: ^8.1.0 - version: 8.1.0 + specifier: ^8.2.0 + version: 8.2.0 react: specifier: ^18.2.0 version: 18.2.0 @@ -107,41 +102,41 @@ dependencies: specifier: ^4.0.12 version: 4.0.12(react@18.2.0) react-hook-form: - specifier: ^7.49.3 - version: 7.49.3(react@18.2.0) + specifier: ^7.50.1 + version: 7.50.1(react@18.2.0) react-hotkeys-hook: - specifier: 4.4.4 - version: 4.4.4(react-dom@18.2.0)(react@18.2.0) + specifier: 4.5.0 + version: 4.5.0(react-dom@18.2.0)(react@18.2.0) react-i18next: - specifier: ^14.0.0 - version: 14.0.0(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0) + specifier: ^14.0.5 + version: 14.0.5(i18next@23.9.0)(react-dom@18.2.0)(react@18.2.0) react-icons: specifier: ^5.0.1 version: 5.0.1(react@18.2.0) react-konva: specifier: ^18.2.10 - version: 18.2.10(konva@9.3.1)(react-dom@18.2.0)(react@18.2.0) + version: 18.2.10(konva@9.3.3)(react-dom@18.2.0)(react@18.2.0) react-redux: specifier: 9.1.0 - version: 9.1.0(@types/react@18.2.48)(react@18.2.0)(redux@5.0.1) + version: 9.1.0(@types/react@18.2.57)(react@18.2.0)(redux@5.0.1) react-resizable-panels: - specifier: ^1.0.9 - version: 1.0.9(react-dom@18.2.0)(react@18.2.0) + specifier: ^2.0.9 + version: 2.0.9(react-dom@18.2.0)(react@18.2.0) react-select: specifier: 5.8.0 - version: 5.8.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + version: 5.8.0(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) react-textarea-autosize: specifier: ^8.5.3 - version: 8.5.3(@types/react@18.2.48)(react@18.2.0) + version: 8.5.3(@types/react@18.2.57)(react@18.2.0) react-use: - specifier: ^17.4.3 - version: 17.4.3(react-dom@18.2.0)(react@18.2.0) + specifier: ^17.5.0 + version: 17.5.0(react-dom@18.2.0)(react@18.2.0) react-virtuoso: - specifier: ^4.6.2 - version: 4.6.2(react-dom@18.2.0)(react@18.2.0) + specifier: ^4.7.0 + version: 4.7.0(react-dom@18.2.0)(react@18.2.0) reactflow: - specifier: ^11.10.2 - version: 11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + specifier: ^11.10.4 + version: 11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) redux-dynamic-middlewares: specifier: ^2.2.0 version: 2.2.0 @@ -158,8 +153,8 @@ dependencies: specifier: ^4.7.4 version: 4.7.4 type-fest: - specifier: ^4.9.0 - version: 4.9.0 + specifier: ^4.10.2 + version: 4.10.2 use-debounce: specifier: ^10.0.0 version: 10.0.0(react@18.2.0) @@ -173,52 +168,49 @@ dependencies: specifier: ^3.22.4 version: 3.22.4 zod-validation-error: - specifier: ^3.0.0 - version: 3.0.0(zod@3.22.4) + specifier: ^3.0.2 + version: 3.0.2(zod@3.22.4) devDependencies: - '@arthurgeron/eslint-plugin-react-usememo': - specifier: ^2.2.3 - version: 2.2.3 '@invoke-ai/eslint-config-react': - specifier: ^0.0.13 - version: 0.0.13(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-config-prettier@9.1.0)(eslint-plugin-import@2.29.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react-refresh@0.4.5)(eslint-plugin-react@7.33.2)(eslint-plugin-simple-import-sort@10.0.0)(eslint-plugin-storybook@0.6.15)(eslint-plugin-unused-imports@3.0.0)(eslint@8.56.0) + specifier: ^0.0.14 + version: 0.0.14(eslint@8.56.0)(prettier@3.2.5)(typescript@5.3.3) '@invoke-ai/prettier-config-react': - specifier: ^0.0.6 - version: 0.0.6(prettier@3.2.4) + specifier: ^0.0.7 + version: 0.0.7(prettier@3.2.5) '@storybook/addon-docs': - specifier: ^7.6.10 - version: 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + specifier: ^7.6.17 + version: 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-essentials': - specifier: ^7.6.10 - version: 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + specifier: ^7.6.17 + version: 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-interactions': - specifier: ^7.6.10 - version: 7.6.10 + specifier: ^7.6.17 + version: 7.6.17 '@storybook/addon-links': - specifier: ^7.6.10 - version: 7.6.10(react@18.2.0) + specifier: ^7.6.17 + version: 7.6.17(react@18.2.0) '@storybook/addon-storysource': - specifier: ^7.6.10 - version: 7.6.10 + specifier: ^7.6.17 + version: 7.6.17 '@storybook/blocks': - specifier: ^7.6.10 - version: 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + specifier: ^7.6.17 + version: 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) '@storybook/manager-api': - specifier: ^7.6.10 - version: 7.6.10(react-dom@18.2.0)(react@18.2.0) + specifier: ^7.6.17 + version: 7.6.17(react-dom@18.2.0)(react@18.2.0) '@storybook/react': - specifier: ^7.6.10 - version: 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + specifier: ^7.6.17 + version: 7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) '@storybook/react-vite': - specifier: ^7.6.10 - version: 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.0.12) + specifier: ^7.6.17 + version: 7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.1.3) '@storybook/test': - specifier: ^7.6.10 - version: 7.6.10(vitest@1.2.2) + specifier: ^7.6.17 + version: 7.6.17(vitest@1.3.1) '@storybook/theming': - specifier: ^7.6.10 - version: 7.6.10(react-dom@18.2.0)(react@18.2.0) + specifier: ^7.6.17 + version: 7.6.17(react-dom@18.2.0)(react@18.2.0) '@types/dateformat': specifier: ^5.0.2 version: 5.0.2 @@ -226,59 +218,32 @@ devDependencies: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^20.11.5 - version: 20.11.5 + specifier: ^20.11.19 + version: 20.11.19 '@types/react': - specifier: ^18.2.48 - version: 18.2.48 + specifier: ^18.2.57 + version: 18.2.57 '@types/react-dom': - specifier: ^18.2.18 - version: 18.2.18 + specifier: ^18.2.19 + version: 18.2.19 '@types/uuid': - specifier: ^9.0.7 - version: 9.0.7 - '@typescript-eslint/eslint-plugin': - specifier: ^6.19.0 - version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/parser': - specifier: ^6.19.0 - version: 6.19.0(eslint@8.56.0)(typescript@5.3.3) + specifier: ^9.0.8 + version: 9.0.8 '@vitejs/plugin-react-swc': - specifier: ^3.5.0 - version: 3.5.0(vite@5.0.12) + specifier: ^3.6.0 + version: 3.6.0(vite@5.1.3) concurrently: specifier: ^8.2.2 version: 8.2.2 eslint: specifier: ^8.56.0 version: 8.56.0 - eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@8.56.0) eslint-plugin-i18next: specifier: ^6.0.3 version: 6.0.3 - eslint-plugin-import: - specifier: ^2.29.1 - version: 2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0) eslint-plugin-path: specifier: ^1.2.4 version: 1.2.4(eslint@8.56.0) - eslint-plugin-react: - specifier: ^7.33.2 - version: 7.33.2(eslint@8.56.0) - eslint-plugin-react-hooks: - specifier: ^4.6.0 - version: 4.6.0(eslint@8.56.0) - eslint-plugin-simple-import-sort: - specifier: ^10.0.0 - version: 10.0.0(eslint@8.56.0) - eslint-plugin-storybook: - specifier: ^0.6.15 - version: 0.6.15(eslint@8.56.0)(typescript@5.3.3) - eslint-plugin-unused-imports: - specifier: ^3.0.0 - version: 3.0.0(@typescript-eslint/eslint-plugin@6.19.0)(eslint@8.56.0) madge: specifier: ^6.1.0 version: 6.1.0(typescript@5.3.3) @@ -286,17 +251,17 @@ devDependencies: specifier: ^12.1.3 version: 12.1.3 openapi-typescript: - specifier: ^6.7.3 - version: 6.7.3 + specifier: ^6.7.4 + version: 6.7.4 prettier: - specifier: ^3.2.4 - version: 3.2.4 + specifier: ^3.2.5 + version: 3.2.5 rollup-plugin-visualizer: specifier: ^5.12.0 version: 5.12.0 storybook: - specifier: ^7.6.10 - version: 7.6.10 + specifier: ^7.6.17 + version: 7.6.17 ts-toolbelt: specifier: ^9.6.0 version: 9.6.0 @@ -307,23 +272,23 @@ devDependencies: specifier: ^5.3.3 version: 5.3.3 vite: - specifier: ^5.0.12 - version: 5.0.12(@types/node@20.11.5) + specifier: ^5.1.3 + version: 5.1.3(@types/node@20.11.19) vite-plugin-css-injected-by-js: - specifier: ^3.3.1 - version: 3.3.1(vite@5.0.12) + specifier: ^3.4.0 + version: 3.4.0(vite@5.1.3) vite-plugin-dts: - specifier: ^3.7.1 - version: 3.7.1(@types/node@20.11.5)(typescript@5.3.3)(vite@5.0.12) + specifier: ^3.7.2 + version: 3.7.2(@types/node@20.11.19)(typescript@5.3.3)(vite@5.1.3) vite-plugin-eslint: specifier: ^1.8.1 - version: 1.8.1(eslint@8.56.0)(vite@5.0.12) + version: 1.8.1(eslint@8.56.0)(vite@5.1.3) vite-tsconfig-paths: specifier: ^4.3.1 - version: 4.3.1(typescript@5.3.3)(vite@5.0.12) + version: 4.3.1(typescript@5.3.3)(vite@5.1.3) vitest: - specifier: ^1.2.2 - version: 1.2.2(@types/node@20.11.5) + specifier: ^1.3.1 + version: 1.3.1(@types/node@20.11.19) packages: @@ -332,8 +297,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /@adobe/css-tools@4.3.2: - resolution: {integrity: sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==} + /@adobe/css-tools@4.3.3: + resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} dev: true /@ampproject/remapping@2.2.1: @@ -341,7 +306,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 dev: true /@ark-ui/anatomy@1.3.0(@internationalized/date@3.5.2): @@ -430,13 +395,6 @@ packages: - '@internationalized/date' dev: false - /@arthurgeron/eslint-plugin-react-usememo@2.2.3: - resolution: {integrity: sha512-YJG+8hULmhHAxztaANswpa9hWNqEOSvbZcbd6R/JQzyNlEZ49Xh97kqZGuJGZ74rrmULckEO1m3Jh5ctqrGA2A==} - dependencies: - minimatch: 9.0.3 - uuid: 9.0.1 - dev: true - /@aw-web-design/x-default-browser@1.4.126: resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==} hasBin: true @@ -456,20 +414,20 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/core@7.23.7: - resolution: {integrity: sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==} + /@babel/core@7.23.9: + resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==} engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.1 '@babel/code-frame': 7.23.5 '@babel/generator': 7.23.6 '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) - '@babel/helpers': 7.23.8 - '@babel/parser': 7.23.6 - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.7 - '@babel/types': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) + '@babel/helpers': 7.23.9 + '@babel/parser': 7.23.9 + '@babel/template': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 convert-source-map: 2.0.0 debug: 4.3.4 gensync: 1.0.0-beta.2 @@ -483,9 +441,9 @@ packages: resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 jsesc: 2.5.2 dev: true @@ -493,14 +451,14 @@ packages: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-compilation-targets@7.23.6: @@ -509,62 +467,47 @@ packages: dependencies: '@babel/compat-data': 7.23.5 '@babel/helper-validator-option': 7.23.5 - browserslist: 4.22.2 + browserslist: 4.23.0 lru-cache: 5.1.1 semver: 6.3.1 dev: true - /@babel/helper-create-class-features-plugin@7.23.7(@babel/core@7.23.7): - resolution: {integrity: sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==} + /@babel/helper-create-class-features-plugin@7.23.10(@babel/core@7.23.9): + resolution: {integrity: sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.9) '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 semver: 6.3.1 dev: true - /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.7): + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.23.9): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-annotate-as-pure': 7.22.5 regexpu-core: 5.3.2 semver: 6.3.1 dev: true - /@babel/helper-define-polyfill-provider@0.4.4(@babel/core@7.23.7): - resolution: {integrity: sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - dependencies: - '@babel/core': 7.23.7 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-define-polyfill-provider@0.5.0(@babel/core@7.23.7): + /@babel/helper-define-polyfill-provider@0.5.0(@babel/core@7.23.9): resolution: {integrity: sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.4 @@ -583,37 +526,37 @@ packages: resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.22.15 - '@babel/types': 7.23.6 + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-member-expression-to-functions@7.23.0: resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-module-imports@7.22.15: resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 - /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.7): + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-module-imports': 7.22.15 '@babel/helper-simple-access': 7.22.5 @@ -625,7 +568,7 @@ packages: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-plugin-utils@7.22.5: @@ -633,25 +576,25 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.7): + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.9): resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-wrap-function': 7.22.20 dev: true - /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.7): + /@babel/helper-replace-supers@7.22.20(@babel/core@7.23.9): resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-member-expression-to-functions': 7.23.0 '@babel/helper-optimise-call-expression': 7.22.5 @@ -661,21 +604,21 @@ packages: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.22.5: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-split-export-declaration@7.22.6: resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@babel/helper-string-parser@7.23.4: @@ -696,17 +639,17 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.22.15 - '@babel/types': 7.23.6 + '@babel/template': 7.23.9 + '@babel/types': 7.23.9 dev: true - /@babel/helpers@7.23.8: - resolution: {integrity: sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==} + /@babel/helpers@7.23.9: + resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.7 - '@babel/types': 7.23.6 + '@babel/template': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 transitivePeerDependencies: - supports-color dev: true @@ -719,966 +662,966 @@ packages: chalk: 2.4.2 js-tokens: 4.0.0 - /@babel/parser@7.23.6: - resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + /@babel/parser@7.23.9: + resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.7): + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.13.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.7) + '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.9) dev: true - /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.7(@babel/core@7.23.7): + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.23.7(@babel/core@7.23.9): resolution: {integrity: sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7): + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.9): resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 dev: true - /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.7): + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.9): resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.7): + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.9): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.7): + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.9): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-flow@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-flow@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.7): + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.9): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.7): + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.9): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.7): + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.9): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.7): + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.9): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.7): + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.9): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.7): + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.9): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.7): + /@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.7): + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.23.9): resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-arrow-functions@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-async-generator-functions@7.23.7(@babel/core@7.23.7): - resolution: {integrity: sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==} + /@babel/plugin-transform-async-generator-functions@7.23.9(@babel/core@7.23.9): + resolution: {integrity: sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-async-to-generator@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-module-imports': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.7) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-block-scoped-functions@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.23.7(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-class-static-block@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.23.7(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-classes@7.23.8(@babel/core@7.23.7): + /@babel/plugin-transform-classes@7.23.8(@babel/core@7.23.9): resolution: {integrity: sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-environment-visitor': 7.22.20 '@babel/helper-function-name': 7.23.0 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.9) '@babel/helper-split-export-declaration': 7.22.6 globals: 11.12.0 dev: true - /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-computed-properties@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/template': 7.22.15 + '@babel/template': 7.23.9 dev: true - /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-duplicate-keys@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-for-of@7.23.6(@babel/core@7.23.7): + /@babel/plugin-transform-for-of@7.23.6(@babel/core@7.23.9): resolution: {integrity: sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-function-name@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-function-name': 7.23.0 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-json-strings@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-literals@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-simple-access': 7.22.5 dev: true - /@babel/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.23.7): - resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==} + /@babel/plugin-transform-modules-systemjs@7.23.9(@babel/core@7.23.9): + resolution: {integrity: sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-identifier': 7.22.20 dev: true - /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.7): + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.23.9): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-new-target@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-numeric-separator@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-object-super@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.7) + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-optional-chaining@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-parameters@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-class-features-plugin': 7.23.7(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.23.7): + /@babel/plugin-transform-private-property-in-object@7.23.4(@babel/core@7.23.9): resolution: {integrity: sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.23.7(@babel/core@7.23.7) + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-property-literals@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-jsx-self@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-react-jsx-self@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-jsx-source@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-react-jsx-source@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 regenerator-transform: 0.15.2 dev: true - /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-spread@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true - /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-typescript@7.23.6(@babel/core@7.23.7): + /@babel/plugin-transform-typescript@7.23.6(@babel/core@7.23.9): resolution: {integrity: sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.23.7(@babel/core@7.23.7) + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.9) dev: true - /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.7): + /@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.9) '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/preset-env@7.23.8(@babel/core@7.23.7): - resolution: {integrity: sha512-lFlpmkApLkEP6woIKprO6DO60RImpatTQKtz4sUcDjVcK8M8mQ4sZsuxaTMNOZf0sqAq/ReYW1ZBHnOQwKpLWA==} + /@babel/preset-env@7.23.9(@babel/core@7.23.9): + resolution: {integrity: sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.7(@babel/core@7.23.7) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.7) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.7) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.7) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.7) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.7) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.7) - '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-async-generator-functions': 7.23.7(@babel/core@7.23.7) - '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-classes': 7.23.8(@babel/core@7.23.7) - '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-for-of': 7.23.6(@babel/core@7.23.7) - '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.7) - '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.7) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.7) - babel-plugin-polyfill-corejs2: 0.4.8(@babel/core@7.23.7) - babel-plugin-polyfill-corejs3: 0.8.7(@babel/core@7.23.7) - babel-plugin-polyfill-regenerator: 0.5.5(@babel/core@7.23.7) - core-js-compat: 3.35.0 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.7(@babel/core@7.23.9) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.23.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.9) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.9) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.9) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.23.9) + '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-async-generator-functions': 7.23.9(@babel/core@7.23.9) + '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-classes': 7.23.8(@babel/core@7.23.9) + '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-for-of': 7.23.6(@babel/core@7.23.9) + '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-systemjs': 7.23.9(@babel/core@7.23.9) + '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.23.9) + '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.23.9) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.23.9) + babel-plugin-polyfill-corejs2: 0.4.8(@babel/core@7.23.9) + babel-plugin-polyfill-corejs3: 0.9.0(@babel/core@7.23.9) + babel-plugin-polyfill-regenerator: 0.5.5(@babel/core@7.23.9) + core-js-compat: 3.36.0 semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true - /@babel/preset-flow@7.23.3(@babel/core@7.23.7): + /@babel/preset-flow@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-7yn6hl8RIv+KNk6iIrGZ+D06VhVY35wLVf23Cz/mMu1zOr7u4MMP4j0nZ9tLf8+4ZFpnib8cFYgB/oYg9hfswA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.23.9) dev: true - /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.7): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.23.9): resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 esutils: 2.0.3 dev: true - /@babel/preset-typescript@7.23.3(@babel/core@7.23.7): + /@babel/preset-typescript@7.23.3(@babel/core@7.23.9): resolution: {integrity: sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.23.7) + '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.23.9) dev: true - /@babel/register@7.23.7(@babel/core@7.23.7): + /@babel/register@7.23.7(@babel/core@7.23.9): resolution: {integrity: sha512-EjJeB6+kvpk+Y5DAkEAmbOBEFkh9OASx0huoEkqYTFxAZHzOAX2Oh5uwAUuL2rUddqfM0SA+KPXV2TbzoZ2kvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -1690,43 +1633,23 @@ packages: resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} dev: true - /@babel/runtime@7.23.6: - resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - - /@babel/runtime@7.23.7: - resolution: {integrity: sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - dev: false - - /@babel/runtime@7.23.8: - resolution: {integrity: sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.14.1 - /@babel/runtime@7.23.9: resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 - dev: false - /@babel/template@7.22.15: - resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + /@babel/template@7.23.9: + resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.23.5 - '@babel/parser': 7.23.6 - '@babel/types': 7.23.6 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 dev: true - /@babel/traverse@7.23.7: - resolution: {integrity: sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==} + /@babel/traverse@7.23.9: + resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} engines: {node: '>=6.9.0'} dependencies: '@babel/code-frame': 7.23.5 @@ -1735,16 +1658,16 @@ packages: '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.23.6 - '@babel/types': 7.23.6 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types@7.23.6: - resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + /@babel/types@7.23.9: + resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-string-parser': 7.23.4 @@ -1774,6 +1697,25 @@ packages: react: 18.2.0 dev: false + /@chakra-ui/accordion@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0): + resolution: {integrity: sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + dependencies: + '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.5)(react@18.2.0) + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /@chakra-ui/alert@2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0): resolution: {integrity: sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw==} peerDependencies: @@ -1928,7 +1870,7 @@ packages: '@emotion/react': '>=10.0.35' react: '>=18' dependencies: - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) react: 18.2.0 dev: false @@ -1969,14 +1911,14 @@ packages: resolution: {integrity: sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==} dev: false - /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.48)(react@18.2.0): + /@chakra-ui/focus-lock@2.1.0(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==} peerDependencies: react: '>=18' dependencies: '@chakra-ui/dom-utils': 2.1.0 react: 18.2.0 - react-focus-lock: 2.11.1(@types/react@18.2.48)(react@18.2.0) + react-focus-lock: 2.11.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -2125,7 +2067,34 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.48)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/menu@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0): + resolution: {integrity: sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + dependencies: + '@chakra-ui/clickable': 2.1.0(react@18.2.0) + '@chakra-ui/descendant': 3.1.0(react@18.2.0) + '@chakra-ui/lazy-utils': 2.0.5 + '@chakra-ui/popper': 3.1.0(react@18.2.0) + '@chakra-ui/react-children-utils': 2.0.6(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-controllable-state': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-outside-click': 2.2.0(react@18.2.0) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.5)(react@18.2.0) + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.57)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} peerDependencies: '@chakra-ui/system': '>=2.0.0' @@ -2134,7 +2103,7 @@ packages: react-dom: '>=18' dependencies: '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.48)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.57)(react@18.2.0) '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/react-context': 2.1.0(react@18.2.0) '@chakra-ui/react-types': 2.0.7(react@18.2.0) @@ -2146,7 +2115,33 @@ packages: framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.7(@types/react@18.2.48)(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.2.57)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@chakra-ui/modal@2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.57)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.57)(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-types': 2.0.7(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.5)(react@18.2.0) + aria-hidden: 1.2.3 + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -2220,6 +2215,29 @@ packages: react: 18.2.0 dev: false + /@chakra-ui/popover@2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0): + resolution: {integrity: sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + dependencies: + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/lazy-utils': 2.0.5 + '@chakra-ui/popper': 3.1.0(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-types': 2.0.7(react@18.2.0) + '@chakra-ui/react-use-animation-state': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-focus-effect': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-focus-on-pointer-down': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /@chakra-ui/popper@3.1.0(react@18.2.0): resolution: {integrity: sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg==} peerDependencies: @@ -2267,8 +2285,8 @@ packages: '@chakra-ui/react-env': 3.1.0(react@18.2.0) '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.48)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.57)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -2484,7 +2502,7 @@ packages: react: 18.2.0 dev: false - /@chakra-ui/react@2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.48)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): + /@chakra-ui/react@2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.57)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} peerDependencies: '@emotion/react': ^11.0.0 @@ -2505,7 +2523,7 @@ packages: '@chakra-ui/counter': 2.1.0(react@18.2.0) '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0) '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.48)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.57)(react@18.2.0) '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/hooks': 2.2.1(react@18.2.0) '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) @@ -2515,7 +2533,7 @@ packages: '@chakra-ui/live-region': 2.1.0(react@18.2.0) '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) - '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.48)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.57)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) @@ -2546,8 +2564,8 @@ packages: '@chakra-ui/transition': 2.1.0(framer-motion@10.18.0)(react@18.2.0) '@chakra-ui/utils': 2.0.15 '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.48)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.57)(react@18.2.0) framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2555,6 +2573,77 @@ packages: - '@types/react' dev: false + /@chakra-ui/react@2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.57)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==} + peerDependencies: + '@emotion/react': ^11.0.0 + '@emotion/styled': ^11.0.0 + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/accordion': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0) + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/avatar': 2.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/breadcrumb': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/button': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/card': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/control-box': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/counter': 2.1.0(react@18.2.0) + '@chakra-ui/css-reset': 2.3.0(@emotion/react@11.11.3)(react@18.2.0) + '@chakra-ui/editable': 3.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/focus-lock': 2.1.0(@types/react@18.2.57)(react@18.2.0) + '@chakra-ui/form-control': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/hooks': 2.2.1(react@18.2.0) + '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/image': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/live-region': 2.1.0(react@18.2.0) + '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0) + '@chakra-ui/modal': 2.3.1(@chakra-ui/system@2.6.2)(@types/react@18.2.57)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/number-input': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/pin-input': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/popover': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0) + '@chakra-ui/popper': 3.1.0(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/progress': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/provider': 2.4.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/radio': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/react-env': 3.1.0(react@18.2.0) + '@chakra-ui/select': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/skeleton': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/skip-nav': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/slider': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/stat': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/stepper': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/styled-system': 2.9.2 + '@chakra-ui/switch': 2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/table': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/tabs': 3.0.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/tag': 3.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/textarea': 2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) + '@chakra-ui/theme-utils': 2.0.21 + '@chakra-ui/toast': 7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/tooltip': 2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/transition': 2.1.0(framer-motion@11.0.5)(react@18.2.0) + '@chakra-ui/utils': 2.0.15 + '@chakra-ui/visually-hidden': 2.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.57)(react@18.2.0) + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + /@chakra-ui/select@2.1.2(@chakra-ui/system@2.6.2)(react@18.2.0): resolution: {integrity: sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==} peerDependencies: @@ -2673,6 +2762,20 @@ packages: react: 18.2.0 dev: false + /@chakra-ui/switch@2.1.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0): + resolution: {integrity: sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + dependencies: + '@chakra-ui/checkbox': 2.3.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /@chakra-ui/system@2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0): resolution: {integrity: sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==} peerDependencies: @@ -2686,8 +2789,8 @@ packages: '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-utils': 2.0.21 '@chakra-ui/utils': 2.0.15 - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.48)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.57)(react@18.2.0) react: 18.2.0 react-fast-compare: 3.2.2 dev: false @@ -2801,6 +2904,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@chakra-ui/toast@7.0.2(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==} + peerDependencies: + '@chakra-ui/system': 2.6.2 + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/alert': 2.2.2(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/close-button': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/react-context': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-timeout': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-update-effect': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/styled-system': 2.9.2 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@chakra-ui/theme': 3.3.1(@chakra-ui/styled-system@2.9.2) + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} peerDependencies: @@ -2823,6 +2949,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@chakra-ui/tooltip@2.3.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==} + peerDependencies: + '@chakra-ui/system': '>=2.0.0' + framer-motion: '>=4.0.0' + react: '>=18' + react-dom: '>=18' + dependencies: + '@chakra-ui/dom-utils': 2.1.0 + '@chakra-ui/popper': 3.1.0(react@18.2.0) + '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/react-types': 2.0.7(react@18.2.0) + '@chakra-ui/react-use-disclosure': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-event-listener': 2.1.0(react@18.2.0) + '@chakra-ui/react-use-merge-refs': 2.1.0(react@18.2.0) + '@chakra-ui/shared-utils': 2.0.5 + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@chakra-ui/transition@2.1.0(framer-motion@10.18.0)(react@18.2.0): resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} peerDependencies: @@ -2834,6 +2982,17 @@ packages: react: 18.2.0 dev: false + /@chakra-ui/transition@2.1.0(framer-motion@11.0.5)(react@18.2.0): + resolution: {integrity: sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==} + peerDependencies: + framer-motion: '>=4.0.0' + react: '>=18' + dependencies: + '@chakra-ui/shared-utils': 2.0.5 + framer-motion: 11.0.5(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + dev: false + /@chakra-ui/utils@2.0.15: resolution: {integrity: sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==} dependencies: @@ -2925,7 +3084,7 @@ packages: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: '@babel/helper-module-imports': 7.22.15 - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 '@emotion/serialize': 1.1.3 @@ -2975,7 +3134,7 @@ packages: resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} dev: false - /@emotion/react@11.11.3(@types/react@18.2.48)(react@18.2.0): + /@emotion/react@11.11.3(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==} peerDependencies: '@types/react': '*' @@ -2984,14 +3143,14 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@emotion/babel-plugin': 11.11.0 '@emotion/cache': 11.11.0 '@emotion/serialize': 1.1.3 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 '@emotion/weak-memoize': 0.3.1 - '@types/react': 18.2.48 + '@types/react': 18.2.57 hoist-non-react-statics: 3.3.2 react: 18.2.0 dev: false @@ -3010,7 +3169,7 @@ packages: resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} dev: false - /@emotion/styled@11.11.0(@emotion/react@11.11.3)(@types/react@18.2.48)(react@18.2.0): + /@emotion/styled@11.11.0(@emotion/react@11.11.3)(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: '@emotion/react': ^11.0.0-rc.0 @@ -3023,11 +3182,11 @@ packages: '@babel/runtime': 7.23.9 '@emotion/babel-plugin': 11.11.0 '@emotion/is-prop-valid': 1.2.1 - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) '@emotion/serialize': 1.1.3 '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) '@emotion/utils': 1.2.1 - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 dev: false @@ -3050,8 +3209,8 @@ packages: resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} dev: false - /@esbuild/aix-ppc64@0.19.11: - resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] @@ -3068,8 +3227,8 @@ packages: dev: true optional: true - /@esbuild/android-arm64@0.19.11: - resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -3086,8 +3245,8 @@ packages: dev: true optional: true - /@esbuild/android-arm@0.19.11: - resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -3104,8 +3263,8 @@ packages: dev: true optional: true - /@esbuild/android-x64@0.19.11: - resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -3122,8 +3281,8 @@ packages: dev: true optional: true - /@esbuild/darwin-arm64@0.19.11: - resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -3140,8 +3299,8 @@ packages: dev: true optional: true - /@esbuild/darwin-x64@0.19.11: - resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -3158,8 +3317,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-arm64@0.19.11: - resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -3176,8 +3335,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-x64@0.19.11: - resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -3194,8 +3353,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm64@0.19.11: - resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -3212,8 +3371,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm@0.19.11: - resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -3230,8 +3389,8 @@ packages: dev: true optional: true - /@esbuild/linux-ia32@0.19.11: - resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -3248,8 +3407,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64@0.19.11: - resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -3266,8 +3425,8 @@ packages: dev: true optional: true - /@esbuild/linux-mips64el@0.19.11: - resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -3284,8 +3443,8 @@ packages: dev: true optional: true - /@esbuild/linux-ppc64@0.19.11: - resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -3302,8 +3461,8 @@ packages: dev: true optional: true - /@esbuild/linux-riscv64@0.19.11: - resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -3320,8 +3479,8 @@ packages: dev: true optional: true - /@esbuild/linux-s390x@0.19.11: - resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -3338,8 +3497,8 @@ packages: dev: true optional: true - /@esbuild/linux-x64@0.19.11: - resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -3356,8 +3515,8 @@ packages: dev: true optional: true - /@esbuild/netbsd-x64@0.19.11: - resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -3374,8 +3533,8 @@ packages: dev: true optional: true - /@esbuild/openbsd-x64@0.19.11: - resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -3392,8 +3551,8 @@ packages: dev: true optional: true - /@esbuild/sunos-x64@0.19.11: - resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -3410,8 +3569,8 @@ packages: dev: true optional: true - /@esbuild/win32-arm64@0.19.11: - resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -3428,8 +3587,8 @@ packages: dev: true optional: true - /@esbuild/win32-ia32@0.19.11: - resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -3446,8 +3605,8 @@ packages: dev: true optional: true - /@esbuild/win32-x64@0.19.11: - resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -3478,7 +3637,7 @@ packages: debug: 4.3.4 espree: 9.6.1 globals: 13.24.0 - ignore: 5.3.0 + ignore: 5.3.1 import-fresh: 3.3.0 js-yaml: 4.1.0 minimatch: 3.1.2 @@ -3501,45 +3660,35 @@ packages: engines: {node: '>=14'} dev: true - /@floating-ui/core@1.5.2: - resolution: {integrity: sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==} - dependencies: - '@floating-ui/utils': 0.1.6 - dev: false - - /@floating-ui/core@1.5.3: - resolution: {integrity: sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==} + /@floating-ui/core@1.6.0: + resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} dependencies: '@floating-ui/utils': 0.2.1 - /@floating-ui/dom@1.5.3: - resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} - dependencies: - '@floating-ui/core': 1.5.2 - '@floating-ui/utils': 0.1.6 - dev: false - /@floating-ui/dom@1.5.4: resolution: {integrity: sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==} dependencies: - '@floating-ui/core': 1.5.3 + '@floating-ui/core': 1.6.0 + '@floating-ui/utils': 0.2.1 + dev: false + + /@floating-ui/dom@1.6.3: + resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} + dependencies: + '@floating-ui/core': 1.6.0 '@floating-ui/utils': 0.2.1 - /@floating-ui/react-dom@2.0.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-IB8aCRFxr8nFkdYZgH+Otd9EVQPJoynxeFRGTB8voPoZMRWo8XjYuCRgpI1btvuKY69XMiLnW+ym7zoBHM90Rw==} + /@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@floating-ui/dom': 1.5.4 + '@floating-ui/dom': 1.6.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@floating-ui/utils@0.1.6: - resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} - dev: false - /@floating-ui/utils@0.2.1: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} @@ -3547,11 +3696,11 @@ packages: resolution: {integrity: sha512-k+BUNqksTL+AN+o+OV7ILeiE9B5M5X+/jA7LWvCwjbV9ovXTqZyKRhA/x7uYv/ml8WQ0XNLBM7cRFIx4jW0/hg==} dev: false - /@humanwhocodes/config-array@0.11.13: - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} dependencies: - '@humanwhocodes/object-schema': 2.0.1 + '@humanwhocodes/object-schema': 2.0.2 debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: @@ -3563,8 +3712,8 @@ packages: engines: {node: '>=12.22'} dev: true - /@humanwhocodes/object-schema@2.0.1: - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + /@humanwhocodes/object-schema@2.0.2: + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} dev: true /@internationalized/date@3.5.2: @@ -3579,43 +3728,41 @@ packages: '@swc/helpers': 0.5.6 dev: false - /@invoke-ai/eslint-config-react@0.0.13(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-config-prettier@9.1.0)(eslint-plugin-import@2.29.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react-refresh@0.4.5)(eslint-plugin-react@7.33.2)(eslint-plugin-simple-import-sort@10.0.0)(eslint-plugin-storybook@0.6.15)(eslint-plugin-unused-imports@3.0.0)(eslint@8.56.0): - resolution: {integrity: sha512-dfo9k+wPHdvpy1z6ABoYXR/Ttzs1FAnbC46ttIxVhZuqDq8K5cLWznivrOfl7f0hJb8Cb8HiuQb4pHDxhHBDqA==} + /@invoke-ai/eslint-config-react@0.0.14(eslint@8.56.0)(prettier@3.2.5)(typescript@5.3.3): + resolution: {integrity: sha512-6ZUY9zgdDhv2WUoLdDKOQdU9ImnH0CBOFtRlOaNOh34IOsNRfn+JA7wqA0PKnkiNrlfPkIQWhn4GRJp68NT5bw==} peerDependencies: - '@typescript-eslint/eslint-plugin': ^6.19.0 - '@typescript-eslint/parser': ^6.19.0 eslint: ^8.56.0 - eslint-config-prettier: ^9.1.0 - eslint-plugin-import: ^2.29.1 - eslint-plugin-react: ^7.33.2 - eslint-plugin-react-hooks: ^4.6.0 - eslint-plugin-react-refresh: ^0.4.5 - eslint-plugin-simple-import-sort: ^10.0.0 - eslint-plugin-storybook: ^0.6.15 - eslint-plugin-unused-imports: ^3.0.0 + prettier: ^3.2.5 + typescript: ^5.3.3 dependencies: - '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 7.0.2(@typescript-eslint/parser@7.0.2)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 7.0.2(eslint@8.56.0)(typescript@5.3.3) eslint: 8.56.0 eslint-config-prettier: 9.1.0(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.0.2)(eslint@8.56.0) eslint-plugin-react: 7.33.2(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0) eslint-plugin-react-refresh: 0.4.5(eslint@8.56.0) - eslint-plugin-simple-import-sort: 10.0.0(eslint@8.56.0) - eslint-plugin-storybook: 0.6.15(eslint@8.56.0)(typescript@5.3.3) - eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@6.19.0)(eslint@8.56.0) + eslint-plugin-simple-import-sort: 12.0.0(eslint@8.56.0) + eslint-plugin-storybook: 0.8.0(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-unused-imports: 3.1.0(@typescript-eslint/eslint-plugin@7.0.2)(eslint@8.56.0) + prettier: 3.2.5 + typescript: 5.3.3 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color dev: true - /@invoke-ai/prettier-config-react@0.0.6(prettier@3.2.4): - resolution: {integrity: sha512-qHE6GAw/Aka/8TLTN9U1U+8pxjaFe5irDv/uSgzqmrBR1rGiVyMp19pEficWRRt+03zYdquiiDjTmoabWQxY0Q==} + /@invoke-ai/prettier-config-react@0.0.7(prettier@3.2.5): + resolution: {integrity: sha512-vQeWzqwih116TBlIJII93L8ictj6uv7PxcSlAGNZrzG2UcaCFMsQqKCsB/qio26uihgv/EtvN6XAF96SnE0TKw==} peerDependencies: - prettier: ^3.2.4 + prettier: ^3.2.5 dependencies: - prettier: 3.2.4 + prettier: 3.2.5 dev: true - /@invoke-ai/ui-library@0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.2)(@types/react@18.2.48)(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0): + /@invoke-ai/ui-library@0.0.21(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.16)(@internationalized/date@3.5.2)(@types/react@18.2.57)(i18next@23.9.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-tCvgkBPDt0gNq+8IcR03e/Mw7R8Mb/SMXTqx3FEIxlTQEo93A/D38dKXeDCzTdx4sQ+sknfB+JLBbHs6sg5hhQ==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 @@ -3627,14 +3774,14 @@ packages: '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/portal': 2.1.0(react-dom@18.2.0)(react@18.2.0) - '@chakra-ui/react': 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.48)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) + '@chakra-ui/react': 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.57)(framer-motion@10.18.0)(react-dom@18.2.0)(react@18.2.0) '@chakra-ui/styled-system': 2.9.2 '@chakra-ui/theme-tools': 2.1.2(@chakra-ui/styled-system@2.9.2) - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) - '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.48)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.3)(@types/react@18.2.57)(react@18.2.0) '@fontsource-variable/inter': 5.0.16 - '@nanostores/react': 0.7.1(nanostores@0.9.5)(react@18.2.0) - chakra-react-select: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@nanostores/react': 0.7.2(nanostores@0.9.5)(react@18.2.0) + chakra-react-select: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) framer-motion: 10.18.0(react-dom@18.2.0)(react@18.2.0) lodash-es: 4.17.21 nanostores: 0.9.5 @@ -3642,9 +3789,9 @@ packages: overlayscrollbars-react: 0.5.4(overlayscrollbars@2.5.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-i18next: 14.0.5(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0) + react-i18next: 14.0.5(i18next@23.9.0)(react-dom@18.2.0)(react@18.2.0) react-icons: 5.0.1(react@18.2.0) - react-select: 5.8.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + react-select: 5.8.0(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) transitivePeerDependencies: - '@chakra-ui/form-control' - '@chakra-ui/icon' @@ -3697,9 +3844,9 @@ packages: resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -3722,7 +3869,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.11.5 + '@types/node': 20.11.19 '@types/yargs': 16.0.9 chalk: 4.1.2 dev: true @@ -3734,12 +3881,12 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.11.5 + '@types/node': 20.11.19 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.3.3)(vite@5.0.12): + /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.3.3)(vite@5.1.3): resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} peerDependencies: typescript: '>= 4.3.x' @@ -3753,7 +3900,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.3.3) typescript: 5.3.3 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) dev: true /@jridgewell/gen-mapping@0.3.3: @@ -3762,11 +3909,11 @@ packages: dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.21 + '@jridgewell/trace-mapping': 0.3.22 dev: true - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} dev: true @@ -3778,10 +3925,10 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - /@jridgewell/trace-mapping@0.3.21: - resolution: {integrity: sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==} + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} dependencies: - '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 dev: true @@ -3804,29 +3951,29 @@ packages: peerDependencies: react: '>=16' dependencies: - '@types/mdx': 2.0.10 - '@types/react': 18.2.48 + '@types/mdx': 2.0.11 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@microsoft/api-extractor-model@7.28.3(@types/node@20.11.5): + /@microsoft/api-extractor-model@7.28.3(@types/node@20.11.19): resolution: {integrity: sha512-wT/kB2oDbdZXITyDh2SQLzaWwTOFbV326fP0pUwNW00WeliARs0qjmXBWmGWardEzp2U3/axkO3Lboqun6vrig==} dependencies: '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.62.0(@types/node@20.11.5) + '@rushstack/node-core-library': 3.62.0(@types/node@20.11.19) transitivePeerDependencies: - '@types/node' dev: true - /@microsoft/api-extractor@7.39.0(@types/node@20.11.5): + /@microsoft/api-extractor@7.39.0(@types/node@20.11.19): resolution: {integrity: sha512-PuXxzadgnvp+wdeZFPonssRAj/EW4Gm4s75TXzPk09h3wJ8RS3x7typf95B4vwZRrPTQBGopdUl+/vHvlPdAcg==} hasBin: true dependencies: - '@microsoft/api-extractor-model': 7.28.3(@types/node@20.11.5) + '@microsoft/api-extractor-model': 7.28.3(@types/node@20.11.19) '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.62.0(@types/node@20.11.5) + '@rushstack/node-core-library': 3.62.0(@types/node@20.11.19) '@rushstack/rig-package': 0.5.1 '@rushstack/ts-command-line': 4.17.1 colors: 1.2.5 @@ -3852,11 +3999,22 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true - /@nanostores/react@0.7.1(nanostores@0.9.5)(react@18.2.0): - resolution: {integrity: sha512-EXQg9N4MdI4eJQz/AZLIx3hxQ6BuBmV4Q55bCd5YCSgEOAW7tGTsIZxpRXxvxLXzflNvHTBvfrDNY38TlSVBkQ==} - engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + /@nanostores/react@0.7.2(nanostores@0.10.0)(react@18.2.0): + resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} + engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: - nanostores: ^0.9.0 + nanostores: ^0.9.0 || ^0.10.0 + react: '>=18.0.0' + dependencies: + nanostores: 0.10.0 + react: 18.2.0 + dev: false + + /@nanostores/react@0.7.2(nanostores@0.9.5)(react@18.2.0): + resolution: {integrity: sha512-e3OhHJFv3NMSFYDgREdlAQqkyBTHJM91s31kOZ4OvZwJKdFk5BLk0MLbh51EOGUz9QGX2aCHfy1RvweSi7fgwA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + nanostores: ^0.9.0 || ^0.10.0 react: '>=18.0.0' dependencies: nanostores: 0.9.5 @@ -3889,7 +4047,7 @@ packages: engines: {node: '>= 8'} dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.16.0 + fastq: 1.17.1 dev: true /@pkgjs/parseargs@0.11.0: @@ -3906,16 +4064,16 @@ packages: /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 dev: true /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 dev: true - /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: '@types/react': '*' @@ -3928,15 +4086,15 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@babel/runtime': 7.23.9 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} peerDependencies: '@types/react': '*' @@ -3949,18 +4107,18 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@babel/runtime': 7.23.9 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: '@types/react': '*' @@ -3969,12 +4127,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-context@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-context@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: '@types/react': '*' @@ -3983,12 +4141,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-direction@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-direction@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: '@types/react': '*' @@ -3997,12 +4155,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} peerDependencies: '@types/react': '*' @@ -4015,19 +4173,19 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: '@types/react': '*' @@ -4036,12 +4194,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} peerDependencies: '@types/react': '*' @@ -4054,17 +4212,17 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@babel/runtime': 7.23.9 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-id@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-id@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} peerDependencies: '@types/react': '*' @@ -4073,13 +4231,13 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} peerDependencies: '@types/react': '*' @@ -4092,24 +4250,24 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@floating-ui/react-dom': 2.0.6(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.48)(react@18.2.0) + '@babel/runtime': 7.23.9 + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.57)(react@18.2.0) '@radix-ui/rect': 1.0.1 - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} peerDependencies: '@types/react': '*' @@ -4122,15 +4280,15 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@babel/runtime': 7.23.9 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: '@types/react': '*' @@ -4143,15 +4301,15 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@babel/runtime': 7.23.9 + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: '@types/react': '*' @@ -4164,23 +4322,23 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-select@1.2.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-select@1.2.2(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} peerDependencies: '@types/react': '*' @@ -4193,35 +4351,35 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@radix-ui/number': 1.0.1 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-id': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 aria-hidden: 1.2.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-remove-scroll: 2.5.5(@types/react@18.2.48)(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.57)(react@18.2.0) dev: true - /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} peerDependencies: '@types/react': '*' @@ -4234,15 +4392,15 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@babel/runtime': 7.23.9 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-slot@1.0.2(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-slot@1.0.2(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: '@types/react': '*' @@ -4251,13 +4409,13 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==} peerDependencies: '@types/react': '*' @@ -4270,21 +4428,21 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==} peerDependencies: '@types/react': '*' @@ -4297,17 +4455,17 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==} peerDependencies: '@types/react': '*' @@ -4320,21 +4478,21 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-direction': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-separator': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: '@types/react': '*' @@ -4343,12 +4501,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} peerDependencies: '@types/react': '*' @@ -4357,13 +4515,13 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} peerDependencies: '@types/react': '*' @@ -4372,13 +4530,13 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} peerDependencies: '@types/react': '*' @@ -4387,12 +4545,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} peerDependencies: '@types/react': '*' @@ -4401,12 +4559,12 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: '@types/react': '*' @@ -4415,13 +4573,13 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@radix-ui/rect': 1.0.1 - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-use-size@1.0.1(@types/react@18.2.48)(react@18.2.0): + /@radix-ui/react-use-size@1.0.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} peerDependencies: '@types/react': '*' @@ -4430,13 +4588,13 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.48)(react@18.2.0) - '@types/react': 18.2.48 + '@babel/runtime': 7.23.9 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.57)(react@18.2.0) + '@types/react': 18.2.57 react: 18.2.0 dev: true - /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} peerDependencies: '@types/react': '*' @@ -4449,10 +4607,10 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.23.8 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@types/react': 18.2.48 - '@types/react-dom': 18.2.18 + '@babel/runtime': 7.23.9 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.57 + '@types/react-dom': 18.2.19 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true @@ -4460,43 +4618,43 @@ packages: /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 dev: true - /@reactflow/background@11.3.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-PhkvoFtO/NXJgFtBvfbPwdR/6/dl25egQlFhKWS3T4aYa7rh80dvf6dF3t6+JXJS4q5ToYJizD2/n8/qylo1yQ==} + /@reactflow/background@11.3.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.7(@types/react@18.2.48)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/controls@11.2.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-mugzVALH/SuKlVKk+JCRm1OXQ+p8e9+k8PCTIaqL+nBl+lPF8KA4uMm8ApsOvhuSAb2A80ezewpyvYHr0qSYVA==} + /@reactflow/controls@11.2.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.7(@types/react@18.2.48)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/core@11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-/cbTxtFpfkIGReSVkcnQhS4Jx4VFY2AhPlJ5n0sbPtnR7OWowF9zodh5Yyzr4j1NOUoBgJ9h+UqGEwwY2dbAlw==} + /@reactflow/core@11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==} peerDependencies: react: '>=17' react-dom: '>=17' @@ -4511,19 +4669,19 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.7(@types/react@18.2.48)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/minimap@11.7.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Pwqw31tJ663cJur6ypqyJU33nPckvTepmz96erdQZoHsfOyLmFj4nXT7afC30DJ48lp0nfNsw+028mlf7f/h4g==} + /@reactflow/minimap@11.7.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 classcat: 5.0.4 @@ -4531,48 +4689,48 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.7(@types/react@18.2.48)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-resizer@2.2.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-BMBstmWNiklHnnAjHu8irkiPQ8/k8nnjzqlTql4acbVhD6Tsdxx/t/saOkELmfQODqGZNiPw9+pHcAHgtE6oNQ==} + /@reactflow/node-resizer@2.2.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.7(@types/react@18.2.48)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-toolbar@1.3.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-75moEQKg23YKA3A2DNSFhq719ZPmby5mpwOD+NO7ZffJ88oMS/2eY8l8qpA3hvb1PTBHDxyKazhJirW+f4t0Wg==} + /@reactflow/node-toolbar@1.3.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.7(@types/react@18.2.48)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reduxjs/toolkit@2.0.1(react-redux@9.1.0)(react@18.2.0): - resolution: {integrity: sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==} + /@reduxjs/toolkit@2.2.1(react-redux@9.1.0)(react@18.2.0): + resolution: {integrity: sha512-8CREoqJovQW/5I4yvvijm/emUiCCmcs4Ev4XPWd4mizSO+dD3g5G6w34QK5AGeNrSH7qM8Fl66j4vuV7dpOdkw==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -4584,10 +4742,10 @@ packages: dependencies: immer: 10.0.3 react: 18.2.0 - react-redux: 9.1.0(@types/react@18.2.48)(react@18.2.0)(redux@5.0.1) + react-redux: 9.1.0(@types/react@18.2.57)(react@18.2.0)(redux@5.0.1) redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.0.1(patch_hash=kvbgwzjyy4x4fnh7znyocvb75q) + reselect: 5.1.0 dev: false /@roarr/browser-log-writer@1.3.0: @@ -4621,111 +4779,111 @@ packages: picomatch: 2.3.1 dev: true - /@rollup/rollup-android-arm-eabi@4.9.4: - resolution: {integrity: sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==} + /@rollup/rollup-android-arm-eabi@4.12.0: + resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.9.4: - resolution: {integrity: sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==} + /@rollup/rollup-android-arm64@4.12.0: + resolution: {integrity: sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.9.4: - resolution: {integrity: sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==} + /@rollup/rollup-darwin-arm64@4.12.0: + resolution: {integrity: sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.9.4: - resolution: {integrity: sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==} + /@rollup/rollup-darwin-x64@4.12.0: + resolution: {integrity: sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.9.4: - resolution: {integrity: sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==} + /@rollup/rollup-linux-arm-gnueabihf@4.12.0: + resolution: {integrity: sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.9.4: - resolution: {integrity: sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==} + /@rollup/rollup-linux-arm64-gnu@4.12.0: + resolution: {integrity: sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.9.4: - resolution: {integrity: sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==} + /@rollup/rollup-linux-arm64-musl@4.12.0: + resolution: {integrity: sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.9.4: - resolution: {integrity: sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==} + /@rollup/rollup-linux-riscv64-gnu@4.12.0: + resolution: {integrity: sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.9.4: - resolution: {integrity: sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==} + /@rollup/rollup-linux-x64-gnu@4.12.0: + resolution: {integrity: sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.9.4: - resolution: {integrity: sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==} + /@rollup/rollup-linux-x64-musl@4.12.0: + resolution: {integrity: sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.9.4: - resolution: {integrity: sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==} + /@rollup/rollup-win32-arm64-msvc@4.12.0: + resolution: {integrity: sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.9.4: - resolution: {integrity: sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==} + /@rollup/rollup-win32-ia32-msvc@4.12.0: + resolution: {integrity: sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.9.4: - resolution: {integrity: sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==} + /@rollup/rollup-win32-x64-msvc@4.12.0: + resolution: {integrity: sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /@rushstack/node-core-library@3.62.0(@types/node@20.11.5): + /@rushstack/node-core-library@3.62.0(@types/node@20.11.19): resolution: {integrity: sha512-88aJn2h8UpSvdwuDXBv1/v1heM6GnBf3RjEy6ZPP7UnzHNCqOHA2Ut+ScYUbXcqIdfew9JlTAe3g+cnX9xQ/Aw==} peerDependencies: '@types/node': '*' @@ -4733,7 +4891,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.19 colors: 1.2.5 fs-extra: 7.0.1 import-lazy: 4.0.0 @@ -4767,29 +4925,29 @@ packages: resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} dev: false - /@storybook/addon-actions@7.6.10: - resolution: {integrity: sha512-pcKmf0H/caGzKDy8cz1adNSjv+KOBWLJ11RzGExrWm+Ad5ACifwlsQPykJ3TQ/21sTd9IXVrE9uuq4LldEnPbg==} + /@storybook/addon-actions@7.6.17: + resolution: {integrity: sha512-TBphs4v6LRfyTpFo/WINF0TkMaE3rrNog7wW5mbz6n0j8o53kDN4o9ZEcygSL5zQX43CAaghQTeDCss7ueG7ZQ==} dependencies: - '@storybook/core-events': 7.6.10 + '@storybook/core-events': 7.6.17 '@storybook/global': 5.0.0 - '@types/uuid': 9.0.7 + '@types/uuid': 9.0.8 dequal: 2.0.3 - polished: 4.2.2 + polished: 4.3.1 uuid: 9.0.1 dev: true - /@storybook/addon-backgrounds@7.6.10: - resolution: {integrity: sha512-kGzsN1QkfyI8Cz7TErEx9OCB3PMzpCFGLd/iy7FreXwbMbeAQ3/9fYgKUsNOYgOhuTz7S09koZUWjS/WJuZGFA==} + /@storybook/addon-backgrounds@7.6.17: + resolution: {integrity: sha512-7dize7x8+37PH77kmt69b0xSaeDqOcZ4fpzW6+hk53hIaCVU26eGs4+j+743Xva31eOgZWNLupUhOpUDc6SqZw==} dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 ts-dedent: 2.2.0 dev: true - /@storybook/addon-controls@7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-LjwCQRMWq1apLtFwDi6U8MI6ITUr+KhxJucZ60tfc58RgB2v8ayozyDAonFEONsx9YSR1dNIJ2Z/e2rWTBJeYA==} + /@storybook/addon-controls@7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zR0aLaUF7FtV/nMRyfniFbCls/e0DAAoXACuOAUAwNAv0lbIS8AyZZiHSmKucCvziUQ6WceeCC7+du3C+9y0rQ==} dependencies: - '@storybook/blocks': 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@storybook/blocks': 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) lodash: 4.17.21 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -4801,27 +4959,27 @@ packages: - supports-color dev: true - /@storybook/addon-docs@7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-GtyQ9bMx1AOOtl6ZS9vwK104HFRK+tqzxddRRxhXkpyeKu3olm9aMgXp35atE/3fJSqyyDm2vFtxxH8mzBA20A==} + /@storybook/addon-docs@7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-FKa4Mdy7nhgvEVZJHpMkHriDzpVHbohn87zv9NCL+Ctjs1iAmzGwxEm0culszyDS1HN2ToVoY0h8CSi2RSSZqA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@jest/transform': 29.7.0 '@mdx-js/react': 2.3.0(react@18.2.0) - '@storybook/blocks': 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.6.10 - '@storybook/components': 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@storybook/csf-plugin': 7.6.10 - '@storybook/csf-tools': 7.6.10 + '@storybook/blocks': 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.6.17 + '@storybook/components': 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@storybook/csf-plugin': 7.6.17 + '@storybook/csf-tools': 7.6.17 '@storybook/global': 5.0.0 '@storybook/mdx2-csf': 1.1.0 - '@storybook/node-logger': 7.6.10 - '@storybook/postinstall': 7.6.10 - '@storybook/preview-api': 7.6.10 - '@storybook/react-dom-shim': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/theming': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.6.10 + '@storybook/node-logger': 7.6.17 + '@storybook/postinstall': 7.6.17 + '@storybook/preview-api': 7.6.17 + '@storybook/react-dom-shim': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.6.17 fs-extra: 11.2.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -4835,25 +4993,25 @@ packages: - supports-color dev: true - /@storybook/addon-essentials@7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-cjbuCCK/3dtUity0Uqi5LwbkgfxqCCE5x5mXZIk9lTMeDz5vB9q6M5nzncVDy8F8przF3NbDLLgxKlt8wjiICg==} + /@storybook/addon-essentials@7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-qlSpamxuYfT2taF953nC9QijGF2pSbg1ewMNpdwLTj16PTZvR/d8NCDMTJujI1bDwM2m18u8Yc43ibh5LEmxCw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/addon-actions': 7.6.10 - '@storybook/addon-backgrounds': 7.6.10 - '@storybook/addon-controls': 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-docs': 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-highlight': 7.6.10 - '@storybook/addon-measure': 7.6.10 - '@storybook/addon-outline': 7.6.10 - '@storybook/addon-toolbars': 7.6.10 - '@storybook/addon-viewport': 7.6.10 - '@storybook/core-common': 7.6.10 - '@storybook/manager-api': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/node-logger': 7.6.10 - '@storybook/preview-api': 7.6.10 + '@storybook/addon-actions': 7.6.17 + '@storybook/addon-backgrounds': 7.6.17 + '@storybook/addon-controls': 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-docs': 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@storybook/addon-highlight': 7.6.17 + '@storybook/addon-measure': 7.6.17 + '@storybook/addon-outline': 7.6.17 + '@storybook/addon-toolbars': 7.6.17 + '@storybook/addon-viewport': 7.6.17 + '@storybook/core-common': 7.6.17 + '@storybook/manager-api': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/node-logger': 7.6.17 + '@storybook/preview-api': 7.6.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) ts-dedent: 2.2.0 @@ -4864,24 +5022,24 @@ packages: - supports-color dev: true - /@storybook/addon-highlight@7.6.10: - resolution: {integrity: sha512-dIuS5QmoT1R+gFOcf6CoBa6D9UR5/wHCfPqPRH8dNNcCLtIGSHWQ4v964mS5OCq1Huj7CghmR15lOUk7SaYwUA==} + /@storybook/addon-highlight@7.6.17: + resolution: {integrity: sha512-R1yBPUUqGn+60aJakn8q+5Zt34E/gU3n3VmgPdryP0LJUdZ5q1/RZShoVDV+yYQ40htMH6oaCv3OyyPzFAGJ6A==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/addon-interactions@7.6.10: - resolution: {integrity: sha512-lEsAdP/PrOZK/KmRbZ/fU4RjEqDP+e/PBlVVVJT2QvHniWK/xxkjCD0axsHU/XuaeQRFhmg0/KR342PC/cIf9A==} + /@storybook/addon-interactions@7.6.17: + resolution: {integrity: sha512-6zlX+RDQ1PlA6fp7C+hun8t7h2RXfCGs5dGrhEenp2lqnR/rYuUJRC0tmKpkZBb8kZVcbSChzkB/JYkBjBCzpQ==} dependencies: '@storybook/global': 5.0.0 - '@storybook/types': 7.6.10 + '@storybook/types': 7.6.17 jest-mock: 27.5.1 - polished: 4.2.2 + polished: 4.3.1 ts-dedent: 2.2.0 dev: true - /@storybook/addon-links@7.6.10(react@18.2.0): - resolution: {integrity: sha512-s/WkSYHpr2pb9p57j6u/xDBg3TKJhBq55YMl0GB5gXgkRPIeuGbPhGJhm2yTGVFLvXgr/aHHnOxb/R/W8PiRhA==} + /@storybook/addon-links@7.6.17(react@18.2.0): + resolution: {integrity: sha512-iFUwKObRn0EKI0zMETsil2p9a/81rCuSMEWECsi+khkCAs1FUnD2cT6Ag5ydcNcBXsdtdfDJdtXQrkw+TSoStQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: @@ -4894,62 +5052,62 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/addon-measure@7.6.10: - resolution: {integrity: sha512-OVfTI56+kc4hLWfZ/YPV3WKj/aA9e4iKXYxZyPdhfX4Z8TgZdD1wv9Z6e8DKS0H5kuybYrHKHaID5ki6t7qz3w==} + /@storybook/addon-measure@7.6.17: + resolution: {integrity: sha512-O5vnHZNkduvZ95jf1UssbOl6ivIxzl5tv+4EpScPYId7w700bxWsJH+QX7ip6KlrCf2o3iUhmPe8bm05ghG2KA==} dependencies: '@storybook/global': 5.0.0 tiny-invariant: 1.3.1 dev: true - /@storybook/addon-outline@7.6.10: - resolution: {integrity: sha512-RVJrEoPArhI6zAIMNl1Gz0zrj84BTfEWYYz0yDWOTVgvN411ugsoIk1hw0671MOneXJ2RcQ9MFIeV/v6AVDQYg==} + /@storybook/addon-outline@7.6.17: + resolution: {integrity: sha512-9o9JXDsYjNaDgz/cY5+jv694+aik/1aiRGGvsCv68e1p/ob0glkGKav4lnJe2VJqD+gCmaARoD8GOJlhoQl8JQ==} dependencies: '@storybook/global': 5.0.0 ts-dedent: 2.2.0 dev: true - /@storybook/addon-storysource@7.6.10: - resolution: {integrity: sha512-ZtMiO26Bqd2oEovEeJ5ulvIL/rsAuHHpjAgBRZd/Byw25DQKY3GTqGtV474Wjm5tzj7HWhfk69fqAv87HnveCw==} + /@storybook/addon-storysource@7.6.17: + resolution: {integrity: sha512-8SZiIuIkRU9NQM3Y2mmE0m+bqtXQefzW8Z9DkPKwTJSJxVBvMZVMHjRiQcPn8ll6zhqQIaQiBj0ahlR8ZqrnqA==} dependencies: - '@storybook/source-loader': 7.6.10 + '@storybook/source-loader': 7.6.17 estraverse: 5.3.0 tiny-invariant: 1.3.1 dev: true - /@storybook/addon-toolbars@7.6.10: - resolution: {integrity: sha512-PaXY/oj9yxF7/H0CNdQKcioincyCkfeHpISZriZbZqhyqsjn3vca7RFEmsB88Q+ou6rMeqyA9st+6e2cx/Ct6A==} + /@storybook/addon-toolbars@7.6.17: + resolution: {integrity: sha512-UMrchbUHiyWrh6WuGnpy34Jqzkx/63B+MSgb3CW7YsQaXz64kE0Rol0TNSznnB+mYXplcqH+ndI4r4kFsmgwDg==} dev: true - /@storybook/addon-viewport@7.6.10: - resolution: {integrity: sha512-+bA6juC/lH4vEhk+w0rXakaG8JgLG4MOYrIudk5vJKQaC6X58LIM9N4kzIS2KSExRhkExXBPrWsnMfCo7uxmKg==} + /@storybook/addon-viewport@7.6.17: + resolution: {integrity: sha512-sA0QCcf4QAMixWvn8uvRYPfkKCSl6JajJaAspoPqXSxHEpK7uwOlpg3kqFU5XJJPXD0X957M+ONgNvBzYqSpEw==} dependencies: memoizerific: 1.11.3 dev: true - /@storybook/blocks@7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-oSIukGC3yuF8pojABC/HLu5tv2axZvf60TaUs8eDg7+NiiKhzYSPoMQxs5uMrKngl+EJDB92ESgWT9vvsfvIPg==} + /@storybook/blocks@7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-PsNVoe0bX1mMn4Kk3nbKZ0ItDZZ0YJnYAFJ6toAbsyBAbgzg1sce88sQinzvbn58/RT9MPKeWMPB45ZS7ggiNg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@storybook/channels': 7.6.10 - '@storybook/client-logger': 7.6.10 - '@storybook/components': 7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.6.10 + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/components': 7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': 7.6.17 '@storybook/csf': 0.1.2 - '@storybook/docs-tools': 7.6.10 + '@storybook/docs-tools': 7.6.17 '@storybook/global': 5.0.0 - '@storybook/manager-api': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.6.10 - '@storybook/theming': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.6.10 + '@storybook/manager-api': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/preview-api': 7.6.17 + '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.6.17 '@types/lodash': 4.14.202 color-convert: 2.0.1 dequal: 2.0.3 lodash: 4.17.21 - markdown-to-jsx: 7.4.0(react@18.2.0) + markdown-to-jsx: 7.4.1(react@18.2.0) memoizerific: 1.11.3 - polished: 4.2.2 + polished: 4.3.1 react: 18.2.0 react-colorful: 5.6.1(react-dom@18.2.0)(react@18.2.0) react-dom: 18.2.0(react@18.2.0) @@ -4964,13 +5122,13 @@ packages: - supports-color dev: true - /@storybook/builder-manager@7.6.10: - resolution: {integrity: sha512-f+YrjZwohGzvfDtH8BHzqM3xW0p4vjjg9u7uzRorqUiNIAAKHpfNrZ/WvwPlPYmrpAHt4xX/nXRJae4rFSygPw==} + /@storybook/builder-manager@7.6.17: + resolution: {integrity: sha512-Sj8hcDYiPCCMfeLzus37czl0zdrAxAz4IyYam2jBjVymrIrcDAFyL1OCZvnq33ft179QYQWhUs9qwzVmlR/ZWg==} dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 7.6.10 - '@storybook/manager': 7.6.10 - '@storybook/node-logger': 7.6.10 + '@storybook/core-common': 7.6.17 + '@storybook/manager': 7.6.17 + '@storybook/node-logger': 7.6.17 '@types/ejs': 3.1.5 '@types/find-cache-dir': 3.2.1 '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.18.20) @@ -4988,8 +5146,8 @@ packages: - supports-color dev: true - /@storybook/builder-vite@7.6.10(typescript@5.3.3)(vite@5.0.12): - resolution: {integrity: sha512-qxe19axiNJVdIKj943e1ucAmADwU42fTGgMSdBzzrvfH3pSOmx2057aIxRzd8YtBRnj327eeqpgCHYIDTunMYQ==} + /@storybook/builder-vite@7.6.17(typescript@5.3.3)(vite@5.1.3): + resolution: {integrity: sha512-2Q32qalI401EsKKr9Hkk8TAOcHEerqwsjCpQgTNJnCu6GgCVKoVUcb99oRbR9Vyg0xh+jb19XiWqqQujFtLYlQ==} peerDependencies: '@preact/preset-vite': '*' typescript: '>= 4.3.x' @@ -5003,64 +5161,64 @@ packages: vite-plugin-glimmerx: optional: true dependencies: - '@storybook/channels': 7.6.10 - '@storybook/client-logger': 7.6.10 - '@storybook/core-common': 7.6.10 - '@storybook/csf-plugin': 7.6.10 - '@storybook/node-logger': 7.6.10 - '@storybook/preview': 7.6.10 - '@storybook/preview-api': 7.6.10 - '@storybook/types': 7.6.10 + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-common': 7.6.17 + '@storybook/csf-plugin': 7.6.17 + '@storybook/node-logger': 7.6.17 + '@storybook/preview': 7.6.17 + '@storybook/preview-api': 7.6.17 + '@storybook/types': 7.6.17 '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 0.9.3 express: 4.18.2 find-cache-dir: 3.3.2 fs-extra: 11.2.0 - magic-string: 0.30.5 + magic-string: 0.30.7 rollup: 3.29.4 typescript: 5.3.3 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) transitivePeerDependencies: - encoding - supports-color dev: true - /@storybook/channels@7.6.10: - resolution: {integrity: sha512-ITCLhFuDBKgxetuKnWwYqMUWlU7zsfH3gEKZltTb+9/2OAWR7ez0iqU7H6bXP1ridm0DCKkt2UMWj2mmr9iQqg==} + /@storybook/channels@7.6.17: + resolution: {integrity: sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==} dependencies: - '@storybook/client-logger': 7.6.10 - '@storybook/core-events': 7.6.10 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 '@storybook/global': 5.0.0 qs: 6.11.2 telejson: 7.2.0 tiny-invariant: 1.3.1 dev: true - /@storybook/cli@7.6.10: - resolution: {integrity: sha512-pK1MEseMm73OMO2OVoSz79QWX8ymxgIGM8IeZTCo9gImiVRChMNDFYcv8yPWkjuyesY8c15CoO48aR7pdA1OjQ==} + /@storybook/cli@7.6.17: + resolution: {integrity: sha512-1sCo+nCqyR+nKfTcEidVu8XzNoECC7Y1l+uW38/r7s2f/TdDorXaIGAVrpjbSaXSoQpx5DxYJVaKCcQuOgqwcA==} hasBin: true dependencies: - '@babel/core': 7.23.7 - '@babel/preset-env': 7.23.8(@babel/core@7.23.7) - '@babel/types': 7.23.6 + '@babel/core': 7.23.9 + '@babel/preset-env': 7.23.9(@babel/core@7.23.9) + '@babel/types': 7.23.9 '@ndelangen/get-tarball': 3.0.9 - '@storybook/codemod': 7.6.10 - '@storybook/core-common': 7.6.10 - '@storybook/core-events': 7.6.10 - '@storybook/core-server': 7.6.10 - '@storybook/csf-tools': 7.6.10 - '@storybook/node-logger': 7.6.10 - '@storybook/telemetry': 7.6.10 - '@storybook/types': 7.6.10 - '@types/semver': 7.5.6 + '@storybook/codemod': 7.6.17 + '@storybook/core-common': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/core-server': 7.6.17 + '@storybook/csf-tools': 7.6.17 + '@storybook/node-logger': 7.6.17 + '@storybook/telemetry': 7.6.17 + '@storybook/types': 7.6.17 + '@types/semver': 7.5.7 '@yarnpkg/fslib': 2.10.3 '@yarnpkg/libzip': 2.3.0 chalk: 4.1.2 commander: 6.2.1 cross-spawn: 7.0.3 detect-indent: 6.1.0 - envinfo: 7.11.0 + envinfo: 7.11.1 execa: 5.1.1 express: 4.18.2 find-up: 5.0.0 @@ -5069,14 +5227,14 @@ packages: get-port: 5.1.1 giget: 1.2.1 globby: 11.1.0 - jscodeshift: 0.15.1(@babel/preset-env@7.23.8) + jscodeshift: 0.15.1(@babel/preset-env@7.23.9) leven: 3.1.0 ora: 5.4.1 prettier: 2.8.8 prompts: 2.4.2 puppeteer-core: 2.1.1 read-pkg-up: 7.0.1 - semver: 7.5.4 + semver: 7.6.0 strip-json-comments: 3.1.1 tempy: 1.0.1 ts-dedent: 2.2.0 @@ -5088,26 +5246,26 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@7.6.10: - resolution: {integrity: sha512-U7bbpu21ntgePMz/mKM18qvCSWCUGCUlYru8mgVlXLCKqFqfTeP887+CsPEQf29aoE3cLgDrxqbRJ1wxX9kL9A==} + /@storybook/client-logger@7.6.17: + resolution: {integrity: sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==} dependencies: '@storybook/global': 5.0.0 dev: true - /@storybook/codemod@7.6.10: - resolution: {integrity: sha512-pzFR0nocBb94vN9QCJLC3C3dP734ZigqyPmd0ZCDj9Xce2ytfHK3v1lKB6TZWzKAZT8zztauECYxrbo4LVuagw==} + /@storybook/codemod@7.6.17: + resolution: {integrity: sha512-JuTmf2u3C4fCnjO7o3dqRgrq3ozNYfWlrRP8xuIdvT7niMap7a396hJtSKqS10FxCgKFcMAOsRgrCalH1dWxUg==} dependencies: - '@babel/core': 7.23.7 - '@babel/preset-env': 7.23.8(@babel/core@7.23.7) - '@babel/types': 7.23.6 + '@babel/core': 7.23.9 + '@babel/preset-env': 7.23.9(@babel/core@7.23.9) + '@babel/types': 7.23.9 '@storybook/csf': 0.1.2 - '@storybook/csf-tools': 7.6.10 - '@storybook/node-logger': 7.6.10 - '@storybook/types': 7.6.10 + '@storybook/csf-tools': 7.6.17 + '@storybook/node-logger': 7.6.17 + '@storybook/types': 7.6.17 '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.3 globby: 11.1.0 - jscodeshift: 0.15.1(@babel/preset-env@7.23.8) + jscodeshift: 0.15.1(@babel/preset-env@7.23.9) lodash: 4.17.21 prettier: 2.8.8 recast: 0.23.4 @@ -5115,19 +5273,19 @@ packages: - supports-color dev: true - /@storybook/components@7.6.10(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-H5hF8pxwtbt0LxV24KMMsPlbYG9Oiui3ObvAQkvGu6q62EYxRPeNSrq3GBI5XEbI33OJY9bT24cVaZx18dXqwQ==} + /@storybook/components@7.6.17(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-lbh7GynMidA+CZcJnstVku6Nhs+YkqjYaZ+mKPugvlVhGVWv0DaaeQFVuZ8cJtUGJ/5FFU4Y+n+gylYUHkGBMA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@radix-ui/react-select': 1.2.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toolbar': 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.6.10 + '@radix-ui/react-select': 1.2.2(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-toolbar': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@storybook/client-logger': 7.6.17 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/theming': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.6.10 + '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.6.17 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5138,21 +5296,21 @@ packages: - '@types/react-dom' dev: true - /@storybook/core-client@7.6.10: - resolution: {integrity: sha512-DjnzSzSNDmZyxyg6TxugzWQwOsW+n/iWVv6sHNEvEd5STr0mjuJjIEELmv58LIr5Lsre5+LEddqHsyuLyt8ubg==} + /@storybook/core-client@7.6.17: + resolution: {integrity: sha512-LuDbADK+DPNAOOCXOlvY09hdGVueXlDetsdOJ/DgYnSa9QSWv9Uv+F8QcEgR3QckZJbPlztKJIVLgP2n/Xkijw==} dependencies: - '@storybook/client-logger': 7.6.10 - '@storybook/preview-api': 7.6.10 + '@storybook/client-logger': 7.6.17 + '@storybook/preview-api': 7.6.17 dev: true - /@storybook/core-common@7.6.10: - resolution: {integrity: sha512-K3YWqjCKMnpvYsWNjOciwTH6zWbuuZzmOiipziZaVJ+sB1XYmH52Y3WGEm07TZI8AYK9DRgwA13dR/7W0nw72Q==} + /@storybook/core-common@7.6.17: + resolution: {integrity: sha512-me2TP3Q9/qzqCLoDHUSsUF+VS1MHxfHbTVF6vAz0D/COTxzsxLpu9TxTbzJoBCxse6XRb6wWI1RgF1mIcjic7g==} dependencies: - '@storybook/core-events': 7.6.10 - '@storybook/node-logger': 7.6.10 - '@storybook/types': 7.6.10 + '@storybook/core-events': 7.6.17 + '@storybook/node-logger': 7.6.17 + '@storybook/types': 7.6.17 '@types/find-cache-dir': 3.2.1 - '@types/node': 18.19.8 + '@types/node': 18.19.17 '@types/node-fetch': 2.6.11 '@types/pretty-hrtime': 1.0.3 chalk: 4.1.2 @@ -5176,34 +5334,34 @@ packages: - supports-color dev: true - /@storybook/core-events@7.6.10: - resolution: {integrity: sha512-yccDH67KoROrdZbRKwxgTswFMAco5nlCyxszCDASCLygGSV2Q2e+YuywrhchQl3U6joiWi3Ps1qWu56NeNafag==} + /@storybook/core-events@7.6.17: + resolution: {integrity: sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==} dependencies: ts-dedent: 2.2.0 dev: true - /@storybook/core-server@7.6.10: - resolution: {integrity: sha512-2icnqJkn3vwq0eJPP0rNaHd7IOvxYf5q4lSVl2AWTxo/Ae19KhokI6j/2vvS2XQJMGQszwshlIwrZUNsj5p0yw==} + /@storybook/core-server@7.6.17: + resolution: {integrity: sha512-KWGhTTaL1Q14FolcoKKZgytlPJUbH6sbJ1Ptj/84EYWFewcnEgVs0Zlnh1VStRZg+Rd1WC1V4yVd/bbDzxrvQA==} dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 7.6.10 - '@storybook/channels': 7.6.10 - '@storybook/core-common': 7.6.10 - '@storybook/core-events': 7.6.10 + '@storybook/builder-manager': 7.6.17 + '@storybook/channels': 7.6.17 + '@storybook/core-common': 7.6.17 + '@storybook/core-events': 7.6.17 '@storybook/csf': 0.1.2 - '@storybook/csf-tools': 7.6.10 + '@storybook/csf-tools': 7.6.17 '@storybook/docs-mdx': 0.1.0 '@storybook/global': 5.0.0 - '@storybook/manager': 7.6.10 - '@storybook/node-logger': 7.6.10 - '@storybook/preview-api': 7.6.10 - '@storybook/telemetry': 7.6.10 - '@storybook/types': 7.6.10 + '@storybook/manager': 7.6.17 + '@storybook/node-logger': 7.6.17 + '@storybook/preview-api': 7.6.17 + '@storybook/telemetry': 7.6.17 + '@storybook/types': 7.6.17 '@types/detect-port': 1.3.5 - '@types/node': 18.19.8 + '@types/node': 18.19.17 '@types/pretty-hrtime': 1.0.3 - '@types/semver': 7.5.6 + '@types/semver': 7.5.7 better-opn: 3.0.2 chalk: 4.1.2 cli-table3: 0.6.3 @@ -5212,13 +5370,13 @@ packages: express: 4.18.2 fs-extra: 11.2.0 globby: 11.1.0 - ip: 2.0.0 + ip: 2.0.1 lodash: 4.17.21 open: 8.4.2 pretty-hrtime: 1.0.3 prompts: 2.4.2 read-pkg-up: 7.0.1 - semver: 7.5.4 + semver: 7.6.0 telejson: 7.2.0 tiny-invariant: 1.3.1 ts-dedent: 2.2.0 @@ -5233,24 +5391,24 @@ packages: - utf-8-validate dev: true - /@storybook/csf-plugin@7.6.10: - resolution: {integrity: sha512-Sc+zZg/BnPH2X28tthNaQBnDiFfO0QmfjVoOx0fGYM9SvY3P5ehzWwp5hMRBim6a/twOTzePADtqYL+t6GMqqg==} + /@storybook/csf-plugin@7.6.17: + resolution: {integrity: sha512-xTHv9BUh3bkDVCvcbmdfVF0/e96BdrEgqPJ3G3RmKbSzWLOkQ2U9yiPfHzT0KJWPhVwj12fjfZp0zunu+pcS6Q==} dependencies: - '@storybook/csf-tools': 7.6.10 - unplugin: 1.6.0 + '@storybook/csf-tools': 7.6.17 + unplugin: 1.7.1 transitivePeerDependencies: - supports-color dev: true - /@storybook/csf-tools@7.6.10: - resolution: {integrity: sha512-TnDNAwIALcN6SA4l00Cb67G02XMOrYU38bIpFJk5VMDX2dvgPjUtJNBuLmEbybGcOt7nPyyFIHzKcY5FCVGoWA==} + /@storybook/csf-tools@7.6.17: + resolution: {integrity: sha512-dAQtam0EBPeTJYcQPLxXgz4L9JFqD+HWbLFG9CmNIhMMjticrB0mpk1EFIS6vPXk/VsVWpBgMLD7dZlD6YMKcQ==} dependencies: '@babel/generator': 7.23.6 - '@babel/parser': 7.23.6 - '@babel/traverse': 7.23.7 - '@babel/types': 7.23.6 + '@babel/parser': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 '@storybook/csf': 0.1.2 - '@storybook/types': 7.6.10 + '@storybook/types': 7.6.17 fs-extra: 11.2.0 recast: 0.23.4 ts-dedent: 2.2.0 @@ -5274,12 +5432,12 @@ packages: resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==} dev: true - /@storybook/docs-tools@7.6.10: - resolution: {integrity: sha512-UgbikducoXzqQHf2TozO0f2rshaeBNnShVbL5Ai4oW7pDymBmrfzdjGbF/milO7yxNKcoIByeoNmu384eBamgQ==} + /@storybook/docs-tools@7.6.17: + resolution: {integrity: sha512-bYrLoj06adqklyLkEwD32C0Ww6t+9ZVvrJHiVT42bIhTRpFiFPAetl1a9KPHtFLnfduh4n2IxIr1jv32ThPDTA==} dependencies: - '@storybook/core-common': 7.6.10 - '@storybook/preview-api': 7.6.10 - '@storybook/types': 7.6.10 + '@storybook/core-common': 7.6.17 + '@storybook/preview-api': 7.6.17 + '@storybook/types': 7.6.17 '@types/doctrine': 0.0.3 assert: 2.1.0 doctrine: 3.0.0 @@ -5293,33 +5451,33 @@ packages: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} dev: true - /@storybook/instrumenter@7.6.10: - resolution: {integrity: sha512-9FYXW1CKXnZ7yYmy2A6U0seqJMe1F7g55J28Vslk3ZLoGATFJ2BR0eoQS+cgfBly6djehjaVeuV3IcUYGnQ/6Q==} + /@storybook/instrumenter@7.6.17: + resolution: {integrity: sha512-zTLIPTt1fvlWgkIVUyQpF327iVE+EiPdpM0Or0aARaNfIikPRBTcjU+6cK96E+Ust2E1qKajEjIuv4i4lLQPng==} dependencies: - '@storybook/channels': 7.6.10 - '@storybook/client-logger': 7.6.10 - '@storybook/core-events': 7.6.10 + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.6.10 + '@storybook/preview-api': 7.6.17 '@vitest/utils': 0.34.7 util: 0.12.5 dev: true - /@storybook/manager-api@7.6.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8eGVpRlpunuFScDtc7nxpPJf/4kJBAAZlNdlhmX09j8M3voX6GpcxabBamSEX5pXZqhwxQCshD4IbqBmjvadlw==} + /@storybook/manager-api@7.6.17(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-IJIV1Yc6yw1dhCY4tReHCfBnUKDqEBnMyHp3mbXpsaHxnxJZrXO45WjRAZIKlQKhl/Ge1CrnznmHRCmYgqmrWg==} dependencies: - '@storybook/channels': 7.6.10 - '@storybook/client-logger': 7.6.10 - '@storybook/core-events': 7.6.10 + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/router': 7.6.10 - '@storybook/theming': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.6.10 + '@storybook/router': 7.6.17 + '@storybook/theming': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.6.17 dequal: 2.0.3 lodash: 4.17.21 memoizerific: 1.11.3 - store2: 2.14.2 + store2: 2.14.3 telejson: 7.2.0 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -5327,31 +5485,31 @@ packages: - react-dom dev: true - /@storybook/manager@7.6.10: - resolution: {integrity: sha512-Co3sLCbNYY6O4iH2ggmRDLCPWLj03JE5s/DOG8OVoXc6vBwTc/Qgiyrsxxp6BHQnPpM0mxL6aKAxE3UjsW/Nog==} + /@storybook/manager@7.6.17: + resolution: {integrity: sha512-A1LDDIqMpwRzq/dqkbbiza0QI04o4ZHCl2a3UMDZUV/+QLc2nsr2DAaLk4CVL4/cIc5zGqmIcaOTvprx2YKVBw==} dev: true /@storybook/mdx2-csf@1.1.0: resolution: {integrity: sha512-TXJJd5RAKakWx4BtpwvSNdgTDkKM6RkXU8GK34S/LhidQ5Pjz3wcnqb0TxEkfhK/ztbP8nKHqXFwLfa2CYkvQw==} dev: true - /@storybook/node-logger@7.6.10: - resolution: {integrity: sha512-ZBuqrv4bjJzKXyfRGFkVIi+z6ekn6rOPoQao4KmsfLNQAUUsEdR8Baw/zMnnU417zw5dSEaZdpuwx75SCQAeOA==} + /@storybook/node-logger@7.6.17: + resolution: {integrity: sha512-w59MQuXhhUNrUVmVkXhMwIg2nvFWjdDczLTwYLorhfsE36CWeUOY5QCZWQy0Qf/h+jz8Uo7Evy64qn18v9C4wA==} dev: true - /@storybook/postinstall@7.6.10: - resolution: {integrity: sha512-SMdXtednPCy3+SRJ7oN1OPN1oVFhj3ih+ChOEX8/kZ5J3nfmV3wLPtsZvFGUCf0KWQEP1xL+1Urv48mzMKcV/w==} + /@storybook/postinstall@7.6.17: + resolution: {integrity: sha512-WaWqB8o9vUc9aaVls+povQSVirf1Xd1LZcVhUKfAocAF3mzYUsnJsVqvnbjRj/F96UFVihOyDt9Zjl/9OvrCvQ==} dev: true - /@storybook/preview-api@7.6.10: - resolution: {integrity: sha512-5A3etoIwZCx05yuv3KSTv1wynN4SR4rrzaIs/CTBp3BC4q1RBL+Or/tClk0IJPXQMlx/4Y134GtNIBbkiDofpw==} + /@storybook/preview-api@7.6.17: + resolution: {integrity: sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==} dependencies: - '@storybook/channels': 7.6.10 - '@storybook/client-logger': 7.6.10 - '@storybook/core-events': 7.6.10 + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 '@storybook/csf': 0.1.2 '@storybook/global': 5.0.0 - '@storybook/types': 7.6.10 + '@storybook/types': 7.6.17 '@types/qs': 6.9.11 dequal: 2.0.3 lodash: 4.17.21 @@ -5362,12 +5520,12 @@ packages: util-deprecate: 1.0.2 dev: true - /@storybook/preview@7.6.10: - resolution: {integrity: sha512-F07BzVXTD3byq+KTWtvsw3pUu3fQbyiBNLFr2CnfU4XSdLKja5lDt8VqDQq70TayVQOf5qfUTzRd4M6pQkjw1w==} + /@storybook/preview@7.6.17: + resolution: {integrity: sha512-LvkMYK/y6alGjwRVNDIKL1lFlbyZ0H0c8iAbcQkiMoaFiujMQyVswMDKlWcj42Upfr/B1igydiruomc+eUt0mw==} dev: true - /@storybook/react-dom-shim@7.6.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-M+N/h6ximacaFdIDjMN2waNoWwApeVYTpFeoDppiFTvdBTXChyIuiPgYX9QSg7gDz92OaA52myGOot4wGvXVzg==} + /@storybook/react-dom-shim@7.6.17(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-32Sa/G+WnvaPiQ1Wvjjw5UM9rr2c4GDohwCcWVv3/LJuiFPqNS6zglAtmnsrlIBnUwRBMLMh/ekCTdqMiUmfDw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5376,24 +5534,24 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/react-vite@7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.0.12): - resolution: {integrity: sha512-YE2+J1wy8nO+c6Nv/hBMu91Edew3K184L1KSnfoZV8vtq2074k1Me/8pfe0QNuq631AncpfCYNb37yBAXQ/80w==} + /@storybook/react-vite@7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.1.3): + resolution: {integrity: sha512-4dIm3CuRl44X1TLzN3WoZh/bChzJF7Ud28li9atj9C8db0bb/y0zl8cahrsRFoR7/LyfqdOVLqaztrnA5SsWfg==} engines: {node: '>=16'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.3.3)(vite@5.0.12) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.3.3)(vite@5.1.3) '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 7.6.10(typescript@5.3.3)(vite@5.0.12) - '@storybook/react': 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) - '@vitejs/plugin-react': 3.1.0(vite@5.0.12) - magic-string: 0.30.5 + '@storybook/builder-vite': 7.6.17(typescript@5.3.3)(vite@5.1.3) + '@storybook/react': 7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) + '@vitejs/plugin-react': 3.1.0(vite@5.1.3) + magic-string: 0.30.7 react: 18.2.0 react-docgen: 7.0.3 react-dom: 18.2.0(react@18.2.0) - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -5403,8 +5561,8 @@ packages: - vite-plugin-glimmerx dev: true - /@storybook/react@7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): - resolution: {integrity: sha512-wwBn1cg2uZWW4peqqBjjU7XGmFq8HdkVUtWwh6dpfgmlY1Aopi+vPgZt7pY9KkWcTOq5+DerMdSfwxukpc3ajQ==} + /@storybook/react@7.6.17(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3): + resolution: {integrity: sha512-lVqzQSU03rRJWYW+gK2gq6mSo3/qtnVICY8B8oP7gc36jVu4ksDIu45bTfukM618ODkUZy0vZe6T4engK3azjA==} engines: {node: '>=16.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -5414,16 +5572,16 @@ packages: typescript: optional: true dependencies: - '@storybook/client-logger': 7.6.10 - '@storybook/core-client': 7.6.10 - '@storybook/docs-tools': 7.6.10 + '@storybook/client-logger': 7.6.17 + '@storybook/core-client': 7.6.17 + '@storybook/docs-tools': 7.6.17 '@storybook/global': 5.0.0 - '@storybook/preview-api': 7.6.10 - '@storybook/react-dom-shim': 7.6.10(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.6.10 + '@storybook/preview-api': 7.6.17 + '@storybook/react-dom-shim': 7.6.17(react-dom@18.2.0)(react@18.2.0) + '@storybook/types': 7.6.17 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 - '@types/node': 18.19.8 + '@types/node': 18.19.17 acorn: 7.4.1 acorn-jsx: 5.3.2(acorn@7.4.1) acorn-walk: 7.2.0 @@ -5443,30 +5601,30 @@ packages: - supports-color dev: true - /@storybook/router@7.6.10: - resolution: {integrity: sha512-G/H4Jn2+y8PDe8Zbq4DVxF/TPn0/goSItdILts39JENucHiuGBCjKjSWGBe1rkwKi1tUbB3yhxJVrLagxFEPpQ==} + /@storybook/router@7.6.17: + resolution: {integrity: sha512-GnyC0j6Wi5hT4qRhSyT8NPtJfGmf82uZw97LQRWeyYu5gWEshUdM7aj40XlNiScd5cZDp0owO1idduVF2k2l2A==} dependencies: - '@storybook/client-logger': 7.6.10 + '@storybook/client-logger': 7.6.17 memoizerific: 1.11.3 qs: 6.11.2 dev: true - /@storybook/source-loader@7.6.10: - resolution: {integrity: sha512-S3nOWyj+sdpsqJqKGIN3DKE1q+Q0KYxEyPlPCawMFazozUH7tOodTIqmHBqJZCSNqdC4M1S/qcL8vpP4PfXhuA==} + /@storybook/source-loader@7.6.17: + resolution: {integrity: sha512-90v1es7dHmHgkGbflPlaRBYcn2+mqdC8OG4QtyYqOUq6xsLsyg+5CX2rupfHbuSLw9r0A3o1ViOII2J/kWtFow==} dependencies: '@storybook/csf': 0.1.2 - '@storybook/types': 7.6.10 + '@storybook/types': 7.6.17 estraverse: 5.3.0 lodash: 4.17.21 prettier: 2.8.8 dev: true - /@storybook/telemetry@7.6.10: - resolution: {integrity: sha512-p3mOSUtIyy2tF1z6pQXxNh1JzYFcAm97nUgkwLzF07GfEdVAPM+ftRSLFbD93zVvLEkmLTlsTiiKaDvOY/lQWg==} + /@storybook/telemetry@7.6.17: + resolution: {integrity: sha512-WOcOAmmengYnGInH98Px44F47DSpLyk20BM+Z/IIQDzfttGOLlxNqBBG1XTEhNRn+AYuk4aZ2JEed2lCjVIxcA==} dependencies: - '@storybook/client-logger': 7.6.10 - '@storybook/core-common': 7.6.10 - '@storybook/csf-tools': 7.6.10 + '@storybook/client-logger': 7.6.17 + '@storybook/core-common': 7.6.17 + '@storybook/csf-tools': 7.6.17 chalk: 4.1.2 detect-package-manager: 2.0.1 fetch-retry: 5.0.6 @@ -5477,15 +5635,15 @@ packages: - supports-color dev: true - /@storybook/test@7.6.10(vitest@1.2.2): - resolution: {integrity: sha512-dn/T+HcWOBlVh3c74BHurp++BaqBoQgNbSIaXlYDpJoZ+DzNIoEQVsWFYm5gCbtKK27iFd4n52RiQI3f6Vblqw==} + /@storybook/test@7.6.17(vitest@1.3.1): + resolution: {integrity: sha512-WGrmUUtKiuq3bzDsN4MUvluGcX120jwczMik1GDTyxS+JBoe7P0t2Y8dDuVs/l3nZd1J7qY4z0RGxMDYqONIOw==} dependencies: - '@storybook/client-logger': 7.6.10 - '@storybook/core-events': 7.6.10 - '@storybook/instrumenter': 7.6.10 - '@storybook/preview-api': 7.6.10 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/instrumenter': 7.6.17 + '@storybook/preview-api': 7.6.17 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.2.0(vitest@1.2.2) + '@testing-library/jest-dom': 6.4.2(vitest@1.3.1) '@testing-library/user-event': 14.3.0(@testing-library/dom@9.3.4) '@types/chai': 4.3.11 '@vitest/expect': 0.34.7 @@ -5494,36 +5652,37 @@ packages: util: 0.12.5 transitivePeerDependencies: - '@jest/globals' + - '@types/bun' - '@types/jest' - jest - vitest dev: true - /@storybook/theming@7.6.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-f5tuy7yV3TOP3fIboSqpgLHy0wKayAw/M8HxX0jVET4Z4fWlFK0BiHJabQ+XEdAfQM97XhPFHB2IPbwsqhCEcQ==} + /@storybook/theming@7.6.17(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) - '@storybook/client-logger': 7.6.10 + '@storybook/client-logger': 7.6.17 '@storybook/global': 5.0.0 memoizerific: 1.11.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/types@7.6.10: - resolution: {integrity: sha512-hcS2HloJblaMpCAj2axgGV+53kgSRYPT0a1PG1IHsZaYQILfHSMmBqM8XzXXYTsgf9250kz3dqFX1l0n3EqMlQ==} + /@storybook/types@7.6.17: + resolution: {integrity: sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==} dependencies: - '@storybook/channels': 7.6.10 + '@storybook/channels': 7.6.17 '@types/babel__core': 7.20.5 '@types/express': 4.17.21 file-system-cache: 2.3.0 dev: true - /@swc/core-darwin-arm64@1.3.101: - resolution: {integrity: sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==} + /@swc/core-darwin-arm64@1.4.2: + resolution: {integrity: sha512-1uSdAn1MRK5C1m/TvLZ2RDvr0zLvochgrZ2xL+lRzugLlCTlSA+Q4TWtrZaOz+vnnFVliCpw7c7qu0JouhgQIw==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] @@ -5531,8 +5690,8 @@ packages: dev: true optional: true - /@swc/core-darwin-x64@1.3.101: - resolution: {integrity: sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==} + /@swc/core-darwin-x64@1.4.2: + resolution: {integrity: sha512-TYD28+dCQKeuxxcy7gLJUCFLqrwDZnHtC2z7cdeGfZpbI2mbfppfTf2wUPzqZk3gEC96zHd4Yr37V3Tvzar+lQ==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -5540,8 +5699,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm-gnueabihf@1.3.101: - resolution: {integrity: sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==} + /@swc/core-linux-arm-gnueabihf@1.4.2: + resolution: {integrity: sha512-Eyqipf7ZPGj0vplKHo8JUOoU1un2sg5PjJMpEesX0k+6HKE2T8pdyeyXODN0YTFqzndSa/J43EEPXm+rHAsLFQ==} engines: {node: '>=10'} cpu: [arm] os: [linux] @@ -5549,8 +5708,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-gnu@1.3.101: - resolution: {integrity: sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==} + /@swc/core-linux-arm64-gnu@1.4.2: + resolution: {integrity: sha512-wZn02DH8VYPv3FC0ub4my52Rttsus/rFw+UUfzdb3tHMHXB66LqN+rR0ssIOZrH6K+VLN6qpTw9VizjyoH0BxA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -5558,8 +5717,8 @@ packages: dev: true optional: true - /@swc/core-linux-arm64-musl@1.3.101: - resolution: {integrity: sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==} + /@swc/core-linux-arm64-musl@1.4.2: + resolution: {integrity: sha512-3G0D5z9hUj9bXNcwmA1eGiFTwe5rWkuL3DsoviTj73TKLpk7u64ND0XjEfO0huVv4vVu9H1jodrKb7nvln/dlw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -5567,8 +5726,8 @@ packages: dev: true optional: true - /@swc/core-linux-x64-gnu@1.3.101: - resolution: {integrity: sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==} + /@swc/core-linux-x64-gnu@1.4.2: + resolution: {integrity: sha512-LFxn9U8cjmYHw3jrdPNqPAkBGglKE3tCZ8rA7hYyp0BFxuo7L2ZcEnPm4RFpmSCCsExFH+LEJWuMGgWERoktvg==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -5576,8 +5735,8 @@ packages: dev: true optional: true - /@swc/core-linux-x64-musl@1.3.101: - resolution: {integrity: sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==} + /@swc/core-linux-x64-musl@1.4.2: + resolution: {integrity: sha512-dp0fAmreeVVYTUcb4u9njTPrYzKnbIH0EhH2qvC9GOYNNREUu2GezSIDgonjOXkHiTCvopG4xU7y56XtXj4VrQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -5585,8 +5744,8 @@ packages: dev: true optional: true - /@swc/core-win32-arm64-msvc@1.3.101: - resolution: {integrity: sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==} + /@swc/core-win32-arm64-msvc@1.4.2: + resolution: {integrity: sha512-HlVIiLMQkzthAdqMslQhDkoXJ5+AOLUSTV6fm6shFKZKqc/9cJvr4S8UveNERL9zUficA36yM3bbfo36McwnvQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] @@ -5594,8 +5753,8 @@ packages: dev: true optional: true - /@swc/core-win32-ia32-msvc@1.3.101: - resolution: {integrity: sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==} + /@swc/core-win32-ia32-msvc@1.4.2: + resolution: {integrity: sha512-WCF8faPGjCl4oIgugkp+kL9nl3nUATlzKXCEGFowMEmVVCFM0GsqlmGdPp1pjZoWc9tpYanoXQDnp5IvlDSLhA==} engines: {node: '>=10'} cpu: [ia32] os: [win32] @@ -5603,8 +5762,8 @@ packages: dev: true optional: true - /@swc/core-win32-x64-msvc@1.3.101: - resolution: {integrity: sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==} + /@swc/core-win32-x64-msvc@1.4.2: + resolution: {integrity: sha512-oV71rwiSpA5xre2C5570BhCsg1HF97SNLsZ/12xv7zayGzqr3yvFALFJN8tHKpqUdCB4FGPjoP3JFdV3i+1wUw==} engines: {node: '>=10'} cpu: [x64] os: [win32] @@ -5612,8 +5771,8 @@ packages: dev: true optional: true - /@swc/core@1.3.101: - resolution: {integrity: sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==} + /@swc/core@1.4.2: + resolution: {integrity: sha512-vWgY07R/eqj1/a0vsRKLI9o9klGZfpLNOVEnrv4nrccxBgYPjcf22IWwAoaBJ+wpA7Q4fVjCUM8lP0m01dpxcg==} engines: {node: '>=10'} requiresBuild: true peerDependencies: @@ -5622,23 +5781,23 @@ packages: '@swc/helpers': optional: true dependencies: - '@swc/counter': 0.1.2 + '@swc/counter': 0.1.3 '@swc/types': 0.1.5 optionalDependencies: - '@swc/core-darwin-arm64': 1.3.101 - '@swc/core-darwin-x64': 1.3.101 - '@swc/core-linux-arm-gnueabihf': 1.3.101 - '@swc/core-linux-arm64-gnu': 1.3.101 - '@swc/core-linux-arm64-musl': 1.3.101 - '@swc/core-linux-x64-gnu': 1.3.101 - '@swc/core-linux-x64-musl': 1.3.101 - '@swc/core-win32-arm64-msvc': 1.3.101 - '@swc/core-win32-ia32-msvc': 1.3.101 - '@swc/core-win32-x64-msvc': 1.3.101 + '@swc/core-darwin-arm64': 1.4.2 + '@swc/core-darwin-x64': 1.4.2 + '@swc/core-linux-arm-gnueabihf': 1.4.2 + '@swc/core-linux-arm64-gnu': 1.4.2 + '@swc/core-linux-arm64-musl': 1.4.2 + '@swc/core-linux-x64-gnu': 1.4.2 + '@swc/core-linux-x64-musl': 1.4.2 + '@swc/core-win32-arm64-msvc': 1.4.2 + '@swc/core-win32-ia32-msvc': 1.4.2 + '@swc/core-win32-x64-msvc': 1.4.2 dev: true - /@swc/counter@0.1.2: - resolution: {integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==} + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} dev: true /@swc/helpers@0.5.6: @@ -5656,7 +5815,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.23.5 - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 '@types/aria-query': 5.0.4 aria-query: 5.1.3 chalk: 4.1.2 @@ -5665,17 +5824,20 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.2.0(vitest@1.2.2): - resolution: {integrity: sha512-+BVQlJ9cmEn5RDMUS8c2+TU6giLvzaHZ8sU/x0Jj7fk+6/46wPdwlgOPcpxS17CjcanBi/3VmGMqVr2rmbUmNw==} + /@testing-library/jest-dom@6.4.2(vitest@1.3.1): + resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: '@jest/globals': '>= 28' + '@types/bun': latest '@types/jest': '>= 28' jest: '>= 28' vitest: '>= 0.32' peerDependenciesMeta: '@jest/globals': optional: true + '@types/bun': + optional: true '@types/jest': optional: true jest: @@ -5683,15 +5845,15 @@ packages: vitest: optional: true dependencies: - '@adobe/css-tools': 4.3.2 - '@babel/runtime': 7.23.8 + '@adobe/css-tools': 4.3.3 + '@babel/runtime': 7.23.9 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.2.2(@types/node@20.11.5) + vitest: 1.3.1(@types/node@20.11.19) dev: true /@testing-library/user-event@14.3.0(@testing-library/dom@9.3.4): @@ -5714,8 +5876,8 @@ packages: /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: - '@babel/parser': 7.23.6 - '@babel/types': 7.23.6 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.5 @@ -5724,27 +5886,27 @@ packages: /@types/babel__generator@7.6.8: resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@types/babel__template@7.4.4: resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} dependencies: - '@babel/parser': 7.23.6 - '@babel/types': 7.23.6 + '@babel/parser': 7.23.9 + '@babel/types': 7.23.9 dev: true /@types/babel__traverse@7.20.5: resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} dependencies: - '@babel/types': 7.23.6 + '@babel/types': 7.23.9 dev: true /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /@types/chai@4.3.11: @@ -5754,13 +5916,13 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /@types/cross-spawn@6.0.6: resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /@types/d3-array@3.2.1: @@ -5791,7 +5953,7 @@ packages: resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} dependencies: '@types/d3-array': 3.2.1 - '@types/geojson': 7946.0.13 + '@types/geojson': 7946.0.14 dev: false /@types/d3-delaunay@6.0.4: @@ -5833,7 +5995,7 @@ packages: /@types/d3-geo@3.1.0: resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} dependencies: - '@types/geojson': 7946.0.13 + '@types/geojson': 7946.0.14 dev: false /@types/d3-hierarchy@3.1.6: @@ -5846,8 +6008,8 @@ packages: '@types/d3-color': 3.1.3 dev: false - /@types/d3-path@3.0.2: - resolution: {integrity: sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==} + /@types/d3-path@3.1.0: + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} dev: false /@types/d3-polygon@3.0.2: @@ -5879,7 +6041,7 @@ packages: /@types/d3-shape@3.1.6: resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} dependencies: - '@types/d3-path': 3.0.2 + '@types/d3-path': 3.1.0 dev: false /@types/d3-time-format@4.0.3: @@ -5927,7 +6089,7 @@ packages: '@types/d3-geo': 3.1.0 '@types/d3-hierarchy': 3.1.6 '@types/d3-interpolate': 3.0.4 - '@types/d3-path': 3.0.2 + '@types/d3-path': 3.1.0 '@types/d3-polygon': 3.0.2 '@types/d3-quadtree': 3.0.6 '@types/d3-random': 3.0.3 @@ -5974,8 +6136,8 @@ packages: resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==} dev: true - /@types/eslint@8.56.0: - resolution: {integrity: sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg==} + /@types/eslint@8.56.2: + resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==} dependencies: '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 @@ -5989,10 +6151,10 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/express-serve-static-core@4.17.41: - resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} + /@types/express-serve-static-core@4.17.43: + resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.19 '@types/qs': 6.9.11 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -6002,7 +6164,7 @@ packages: resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.17.41 + '@types/express-serve-static-core': 4.17.43 '@types/qs': 6.9.11 '@types/serve-static': 1.15.5 dev: true @@ -6011,21 +6173,21 @@ packages: resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} dev: true - /@types/geojson@7946.0.13: - resolution: {integrity: sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==} + /@types/geojson@7946.0.14: + resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} dev: false /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /@types/http-errors@2.0.4: @@ -6075,8 +6237,8 @@ packages: /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} - /@types/mdx@2.0.10: - resolution: {integrity: sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg==} + /@types/mdx@2.0.11: + resolution: {integrity: sha512-HM5bwOaIQJIQbAYfax35HCKxx7a3KrK3nBtIqJgSOitivTD1y3oW9P3rxY9RkXYPUk7y/AjAohfHKmFpGE79zw==} dev: true /@types/mime-types@2.1.4: @@ -6098,18 +6260,18 @@ packages: /@types/node-fetch@2.6.11: resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.19 form-data: 4.0.0 dev: true - /@types/node@18.19.8: - resolution: {integrity: sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==} + /@types/node@18.19.17: + resolution: {integrity: sha512-SzyGKgwPzuWp2SHhlpXKzCX0pIOfcI4V2eF37nNBJOhwlegQ83omtVQ1XxZpDE06V/d6AQvfQdPfnw0tRC//Ng==} dependencies: undici-types: 5.26.5 dev: true - /@types/node@20.11.5: - resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + /@types/node@20.11.19: + resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==} dependencies: undici-types: 5.26.5 dev: true @@ -6137,26 +6299,26 @@ packages: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true - /@types/react-dom@18.2.18: - resolution: {integrity: sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==} + /@types/react-dom@18.2.19: + resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 dev: true /@types/react-reconciler@0.28.8: resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 dev: false /@types/react-transition-group@4.4.10: resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 dev: false - /@types/react@18.2.48: - resolution: {integrity: sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==} + /@types/react@18.2.57: + resolution: {integrity: sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw==} dependencies: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 @@ -6169,15 +6331,15 @@ packages: /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - /@types/semver@7.5.6: - resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + /@types/semver@7.5.7: + resolution: {integrity: sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==} dev: true /@types/send@0.17.4: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /@types/serve-static@1.15.5: @@ -6185,7 +6347,7 @@ packages: dependencies: '@types/http-errors': 2.0.4 '@types/mime': 3.0.4 - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /@types/unist@2.0.10: @@ -6196,8 +6358,8 @@ packages: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} dev: false - /@types/uuid@9.0.7: - resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==} + /@types/uuid@9.0.8: + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} dev: true /@types/yargs-parser@21.0.3: @@ -6216,49 +6378,49 @@ packages: '@types/yargs-parser': 21.0.3 dev: true - /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} + /@typescript-eslint/eslint-plugin@7.0.2(@typescript-eslint/parser@7.0.2)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/scope-manager': 6.19.0 - '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.19.0 + '@typescript-eslint/parser': 7.0.2(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 7.0.2 + '@typescript-eslint/type-utils': 7.0.2(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 7.0.2(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.0.2 debug: 4.3.4 eslint: 8.56.0 graphemer: 1.4.0 - ignore: 5.3.0 + ignore: 5.3.1 natural-compare: 1.4.0 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.3.3) + semver: 7.6.0 + ts-api-utils: 1.2.1(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==} + /@typescript-eslint/parser@7.0.2(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.19.0 - '@typescript-eslint/types': 6.19.0 - '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.19.0 + '@typescript-eslint/scope-manager': 7.0.2 + '@typescript-eslint/types': 7.0.2 + '@typescript-eslint/typescript-estree': 7.0.2(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 7.0.2 debug: 4.3.4 eslint: 8.56.0 typescript: 5.3.3 @@ -6274,29 +6436,29 @@ packages: '@typescript-eslint/visitor-keys': 5.62.0 dev: true - /@typescript-eslint/scope-manager@6.19.0: - resolution: {integrity: sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==} + /@typescript-eslint/scope-manager@7.0.2: + resolution: {integrity: sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.19.0 - '@typescript-eslint/visitor-keys': 6.19.0 + '@typescript-eslint/types': 7.0.2 + '@typescript-eslint/visitor-keys': 7.0.2 dev: true - /@typescript-eslint/type-utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==} + /@typescript-eslint/type-utils@7.0.2(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/typescript-estree': 7.0.2(typescript@5.3.3) + '@typescript-eslint/utils': 7.0.2(eslint@8.56.0)(typescript@5.3.3) debug: 4.3.4 eslint: 8.56.0 - ts-api-utils: 1.0.3(typescript@5.3.3) + ts-api-utils: 1.2.1(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -6312,8 +6474,8 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/types@6.19.0: - resolution: {integrity: sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==} + /@typescript-eslint/types@7.0.2: + resolution: {integrity: sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==} engines: {node: ^16.0.0 || >=18.0.0} dev: true @@ -6331,7 +6493,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.0 tsutils: 3.21.0(typescript@3.9.10) typescript: 3.9.10 transitivePeerDependencies: @@ -6352,7 +6514,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.0 tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: @@ -6373,15 +6535,15 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.0 tsutils: 3.21.0(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.19.0(typescript@5.3.3): - resolution: {integrity: sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==} + /@typescript-eslint/typescript-estree@7.0.2(typescript@5.3.3): + resolution: {integrity: sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -6389,14 +6551,14 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.19.0 - '@typescript-eslint/visitor-keys': 6.19.0 + '@typescript-eslint/types': 7.0.2 + '@typescript-eslint/visitor-keys': 7.0.2 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.3.3) + semver: 7.6.0 + ts-api-utils: 1.2.1(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -6410,32 +6572,32 @@ packages: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) '@types/json-schema': 7.0.15 - '@types/semver': 7.5.6 + '@types/semver': 7.5.7 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.3.3) eslint: 8.56.0 eslint-scope: 5.1.1 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==} + /@typescript-eslint/utils@7.0.2(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint: ^8.56.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) '@types/json-schema': 7.0.15 - '@types/semver': 7.5.6 - '@typescript-eslint/scope-manager': 6.19.0 - '@typescript-eslint/types': 6.19.0 - '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@types/semver': 7.5.7 + '@typescript-eslint/scope-manager': 7.0.2 + '@typescript-eslint/types': 7.0.2 + '@typescript-eslint/typescript-estree': 7.0.2(typescript@5.3.3) eslint: 8.56.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript @@ -6457,11 +6619,11 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@typescript-eslint/visitor-keys@6.19.0: - resolution: {integrity: sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==} + /@typescript-eslint/visitor-keys@7.0.2: + resolution: {integrity: sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/types': 7.0.2 eslint-visitor-keys: 3.4.3 dev: true @@ -6469,29 +6631,29 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.5.0(vite@5.0.12): - resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==} + /@vitejs/plugin-react-swc@3.6.0(vite@5.1.3): + resolution: {integrity: sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==} peerDependencies: vite: ^4 || ^5 dependencies: - '@swc/core': 1.3.101 - vite: 5.0.12(@types/node@20.11.5) + '@swc/core': 1.4.2 + vite: 5.1.3(@types/node@20.11.19) transitivePeerDependencies: - '@swc/helpers' dev: true - /@vitejs/plugin-react@3.1.0(vite@5.0.12): + /@vitejs/plugin-react@3.1.0(vite@5.1.3): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.1.0-beta.0 dependencies: - '@babel/core': 7.23.7 - '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.9) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) transitivePeerDependencies: - supports-color dev: true @@ -6504,26 +6666,26 @@ packages: chai: 4.4.1 dev: true - /@vitest/expect@1.2.2: - resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==} + /@vitest/expect@1.3.1: + resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} dependencies: - '@vitest/spy': 1.2.2 - '@vitest/utils': 1.2.2 + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 chai: 4.4.1 dev: true - /@vitest/runner@1.2.2: - resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==} + /@vitest/runner@1.3.1: + resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==} dependencies: - '@vitest/utils': 1.2.2 + '@vitest/utils': 1.3.1 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.2.2: - resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==} + /@vitest/snapshot@1.3.1: + resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} dependencies: - magic-string: 0.30.5 + magic-string: 0.30.7 pathe: 1.1.2 pretty-format: 29.7.0 dev: true @@ -6531,13 +6693,13 @@ packages: /@vitest/spy@0.34.7: resolution: {integrity: sha512-NMMSzOY2d8L0mcOt4XcliDOS1ISyGlAXuQtERWVOoVHnKwmG+kKhinAiGw3dTtMQWybfa89FG8Ucg9tiC/FhTQ==} dependencies: - tinyspy: 2.2.0 + tinyspy: 2.2.1 dev: true - /@vitest/spy@1.2.2: - resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==} + /@vitest/spy@1.3.1: + resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} dependencies: - tinyspy: 2.2.0 + tinyspy: 2.2.1 dev: true /@vitest/utils@0.34.7: @@ -6548,8 +6710,8 @@ packages: pretty-format: 29.7.0 dev: true - /@vitest/utils@1.2.2: - resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==} + /@vitest/utils@1.3.1: + resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -6576,21 +6738,21 @@ packages: path-browserify: 1.0.1 dev: true - /@vue/compiler-core@3.4.15: - resolution: {integrity: sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==} + /@vue/compiler-core@3.4.19: + resolution: {integrity: sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==} dependencies: - '@babel/parser': 7.23.6 - '@vue/shared': 3.4.15 + '@babel/parser': 7.23.9 + '@vue/shared': 3.4.19 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.0.2 dev: true - /@vue/compiler-dom@3.4.15: - resolution: {integrity: sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==} + /@vue/compiler-dom@3.4.19: + resolution: {integrity: sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==} dependencies: - '@vue/compiler-core': 3.4.15 - '@vue/shared': 3.4.15 + '@vue/compiler-core': 3.4.19 + '@vue/shared': 3.4.19 dev: true /@vue/language-core@1.8.27(typescript@5.3.3): @@ -6603,8 +6765,8 @@ packages: dependencies: '@volar/language-core': 1.11.1 '@volar/source-map': 1.11.1 - '@vue/compiler-dom': 3.4.15 - '@vue/shared': 3.4.15 + '@vue/compiler-dom': 3.4.19 + '@vue/shared': 3.4.19 computeds: 0.0.1 minimatch: 9.0.3 muggle-string: 0.3.1 @@ -6613,8 +6775,8 @@ packages: vue-template-compiler: 2.7.16 dev: true - /@vue/shared@3.4.15: - resolution: {integrity: sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==} + /@vue/shared@3.4.19: + resolution: {integrity: sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==} dev: true /@xobotyi/scrollbar-width@1.9.5: @@ -7224,12 +7386,12 @@ packages: acorn: 7.4.1 dev: true - /acorn-jsx@5.3.2(acorn@8.11.2): + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.11.2 + acorn: 8.11.3 dev: true /acorn-walk@7.2.0: @@ -7248,12 +7410,6 @@ packages: hasBin: true dev: true - /acorn@8.11.2: - resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} @@ -7373,11 +7529,12 @@ packages: dequal: 2.0.3 dev: true - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - is-array-buffer: 3.0.2 + call-bind: 1.0.7 + is-array-buffer: 3.0.4 dev: true /array-flatten@1.1.1: @@ -7388,10 +7545,10 @@ packages: resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 + es-abstract: 1.22.4 + get-intrinsic: 1.2.4 is-string: 1.0.7 dev: true @@ -7400,24 +7557,35 @@ packages: engines: {node: '>=8'} dev: true - /array.prototype.findlastindex@1.2.3: - resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + /array.prototype.filter@1.0.3: + resolution: {integrity: sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 + es-array-method-boxes-properly: 1.0.0 + is-string: 1.0.7 + dev: true + + /array.prototype.findlastindex@1.2.4: + resolution: {integrity: sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.22.4 + es-errors: 1.3.0 es-shim-unscopables: 1.0.2 - get-intrinsic: 1.2.2 dev: true /array.prototype.flat@1.3.2: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 es-shim-unscopables: 1.0.2 dev: true @@ -7425,39 +7593,40 @@ packages: resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 es-shim-unscopables: 1.0.2 dev: true - /array.prototype.tosorted@1.1.2: - resolution: {integrity: sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==} + /array.prototype.tosorted@1.1.3: + resolution: {integrity: sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 + es-errors: 1.3.0 es-shim-unscopables: 1.0.2 - get-intrinsic: 1.2.2 dev: true - /arraybuffer.prototype.slice@1.0.2: - resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} dependencies: - array-buffer-byte-length: 1.0.0 - call-bind: 1.0.5 + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 - is-array-buffer: 3.0.2 + es-abstract: 1.22.4 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.2 dev: true /assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 is-nan: 1.3.2 object-is: 1.1.5 object.assign: 4.1.5 @@ -7512,17 +7681,19 @@ packages: engines: {node: '>=4'} dev: false - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 dev: true - /babel-core@7.0.0-bridge.0(@babel/core@7.23.7): + /babel-core@7.0.0-bridge.0(@babel/core@7.23.9): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.7 + '@babel/core': 7.23.9 dev: true /babel-plugin-istanbul@6.1.1: @@ -7542,43 +7713,43 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 cosmiconfig: 7.1.0 resolve: 1.22.8 dev: false - /babel-plugin-polyfill-corejs2@0.4.8(@babel/core@7.23.7): + /babel-plugin-polyfill-corejs2@0.4.8(@babel/core@7.23.9): resolution: {integrity: sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: '@babel/compat-data': 7.23.5 - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.9) semver: 6.3.1 transitivePeerDependencies: - supports-color dev: true - /babel-plugin-polyfill-corejs3@0.8.7(@babel/core@7.23.7): - resolution: {integrity: sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==} + /babel-plugin-polyfill-corejs3@0.9.0(@babel/core@7.23.9): + resolution: {integrity: sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.4.4(@babel/core@7.23.7) - core-js-compat: 3.35.0 + '@babel/core': 7.23.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.9) + core-js-compat: 3.36.0 transitivePeerDependencies: - supports-color dev: true - /babel-plugin-polyfill-regenerator@0.5.5(@babel/core@7.23.7): + /babel-plugin-polyfill-regenerator@0.5.5(@babel/core@7.23.9): resolution: {integrity: sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.23.9) transitivePeerDependencies: - supports-color dev: true @@ -7677,15 +7848,15 @@ packages: pako: 0.2.9 dev: true - /browserslist@4.22.2: - resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + /browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001579 - electron-to-chromium: 1.4.639 + caniuse-lite: 1.0.30001588 + electron-to-chromium: 1.4.677 node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.22.2) + update-browserslist-db: 1.0.13(browserslist@4.23.0) dev: true /bser@2.1.1: @@ -7724,12 +7895,15 @@ packages: engines: {node: '>=8'} dev: true - /call-bind@1.0.5: - resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.2.2 - set-function-length: 1.1.1 + get-intrinsic: 1.2.4 + set-function-length: 1.2.1 dev: true /callsites@3.1.0: @@ -7741,8 +7915,8 @@ packages: engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001579: - resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} + /caniuse-lite@1.0.30001588: + resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==} dev: true /chai@4.4.1: @@ -7758,7 +7932,7 @@ packages: type-detect: 4.0.8 dev: true - /chakra-react-select@4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /chakra-react-select@4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ZL43hyXPnWf1g/HjsZDecbeJ4F2Q6tTPYJozlKWkrQ7lIX7ORP0aZYwmc5/Wly4UNzMimj2Vuosl6MmIXH+G2g==} peerDependencies: '@chakra-ui/form-control': ^2.0.0 @@ -7776,13 +7950,13 @@ packages: '@chakra-ui/icon': 3.2.0(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/layout': 2.3.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/media-query': 3.3.0(@chakra-ui/system@2.6.2)(react@18.2.0) - '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@10.18.0)(react@18.2.0) + '@chakra-ui/menu': 2.2.1(@chakra-ui/system@2.6.2)(framer-motion@11.0.5)(react@18.2.0) '@chakra-ui/spinner': 2.1.0(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-select: 5.7.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + react-select: 5.7.7(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -7822,8 +7996,8 @@ packages: get-func-name: 2.0.2 dev: true - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} dependencies: anymatch: 3.1.3 @@ -7851,8 +8025,8 @@ packages: engines: {node: '>=8'} dev: true - /citty@0.1.5: - resolution: {integrity: sha512-AS7n5NSc0OQVMV9v6wt3ByujNIrne0/cTjiC2MYqhvao57VNfiuVksTSr2p17nVOhEr2KtqiAkGwHcgMC/qUuQ==} + /citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} dependencies: consola: 3.2.3 dev: true @@ -8071,10 +8245,10 @@ packages: toggle-selection: 1.0.6 dev: false - /core-js-compat@3.35.0: - resolution: {integrity: sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==} + /core-js-compat@3.36.0: + resolution: {integrity: sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==} dependencies: - browserslist: 4.22.2 + browserslist: 4.23.0 dev: true /core-util-is@1.0.3: @@ -8210,7 +8384,7 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} dependencies: - '@babel/runtime': 7.23.6 + '@babel/runtime': 7.23.9 dev: true /dateformat@5.0.3: @@ -8271,12 +8445,12 @@ packages: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} dependencies: - array-buffer-byte-length: 1.0.0 - call-bind: 1.0.5 + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 es-get-iterator: 1.1.3 - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 is-arguments: 1.1.1 - is-array-buffer: 3.0.2 + is-array-buffer: 3.0.4 is-date-object: 1.0.5 is-regex: 1.1.4 is-shared-array-buffer: 1.0.2 @@ -8284,11 +8458,11 @@ packages: object-is: 1.1.5 object-keys: 1.1.1 object.assign: 4.1.5 - regexp.prototype.flags: 1.5.1 - side-channel: 1.0.4 + regexp.prototype.flags: 1.5.2 + side-channel: 1.0.5 which-boxed-primitive: 1.0.2 which-collection: 1.0.1 - which-typed-array: 1.1.13 + which-typed-array: 1.1.14 dev: true /deep-extend@0.6.0: @@ -8314,13 +8488,13 @@ packages: clone: 1.0.4 dev: true - /define-data-property@1.1.1: - resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.2 + es-define-property: 1.0.0 + es-errors: 1.3.0 gopd: 1.0.1 - has-property-descriptors: 1.0.1 /define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} @@ -8331,8 +8505,8 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} dependencies: - define-data-property: 1.1.1 - has-property-descriptors: 1.0.1 + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 object-keys: 1.1.1 /defu@6.1.4: @@ -8481,7 +8655,7 @@ packages: dependencies: debug: 4.3.4 is-url: 1.2.4 - postcss: 8.4.33 + postcss: 8.4.35 postcss-values-parser: 2.0.1 transitivePeerDependencies: - supports-color @@ -8492,8 +8666,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dependencies: is-url: 1.2.4 - postcss: 8.4.33 - postcss-values-parser: 6.0.2(postcss@8.4.33) + postcss: 8.4.35 + postcss-values-parser: 6.0.2(postcss@8.4.35) dev: true /detective-sass@3.0.2: @@ -8611,7 +8785,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.23.7 + '@babel/runtime': 7.23.9 csstype: 3.1.3 dev: false @@ -8620,8 +8794,8 @@ packages: engines: {node: '>=12'} dev: true - /dotenv@16.3.1: - resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} dev: true @@ -8650,8 +8824,8 @@ packages: jake: 10.8.7 dev: true - /electron-to-chromium@1.4.639: - resolution: {integrity: sha512-CkKf3ZUVZchr+zDpAlNLEEy2NJJ9T64ULWaDgy3THXXlPVPkLu3VOs9Bac44nebVtdwl2geSj6AxTtGDOxoXhg==} + /electron-to-chromium@1.4.677: + resolution: {integrity: sha512-erDa3CaDzwJOpyvfKhOiJjBVNnMM0qxHq47RheVVwsSQrgBA9ZSGV9kdaOfZDPXcHzhG7lBxhj6A7KvfLJBd6Q==} dev: true /emoji-regex@8.0.0: @@ -8678,7 +8852,7 @@ packages: dependencies: '@socket.io/component-emitter': 3.1.0 debug: 4.3.4 - engine.io-parser: 5.2.1 + engine.io-parser: 5.2.2 ws: 8.11.0 xmlhttprequest-ssl: 2.0.0 transitivePeerDependencies: @@ -8687,8 +8861,8 @@ packages: - utf-8-validate dev: false - /engine.io-parser@5.2.1: - resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + /engine.io-parser@5.2.2: + resolution: {integrity: sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==} engines: {node: '>=10.0.0'} dev: false @@ -8705,8 +8879,8 @@ packages: engines: {node: '>=0.12'} dev: true - /envinfo@7.11.0: - resolution: {integrity: sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==} + /envinfo@7.11.1: + resolution: {integrity: sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==} engines: {node: '>=4'} hasBin: true dev: true @@ -8722,56 +8896,72 @@ packages: stackframe: 1.3.4 dev: false - /es-abstract@1.22.3: - resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + /es-abstract@1.22.4: + resolution: {integrity: sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==} engines: {node: '>= 0.4'} dependencies: - array-buffer-byte-length: 1.0.0 - arraybuffer.prototype.slice: 1.0.2 - available-typed-arrays: 1.0.5 - call-bind: 1.0.5 - es-set-tostringtag: 2.0.2 + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 es-to-primitive: 1.2.1 function.prototype.name: 1.1.6 - get-intrinsic: 1.2.2 - get-symbol-description: 1.0.0 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 globalthis: 1.0.3 gopd: 1.0.1 - has-property-descriptors: 1.0.1 - has-proto: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 has-symbols: 1.0.3 - hasown: 2.0.0 - internal-slot: 1.0.6 - is-array-buffer: 3.0.2 + hasown: 2.0.1 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 is-callable: 1.2.7 - is-negative-zero: 2.0.2 + is-negative-zero: 2.0.3 is-regex: 1.1.4 is-shared-array-buffer: 1.0.2 is-string: 1.0.7 - is-typed-array: 1.1.12 + is-typed-array: 1.1.13 is-weakref: 1.0.2 object-inspect: 1.13.1 object-keys: 1.1.1 object.assign: 4.1.5 - regexp.prototype.flags: 1.5.1 - safe-array-concat: 1.0.1 - safe-regex-test: 1.0.0 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.0 + safe-regex-test: 1.0.3 string.prototype.trim: 1.2.8 string.prototype.trimend: 1.0.7 string.prototype.trimstart: 1.0.7 - typed-array-buffer: 1.0.0 + typed-array-buffer: 1.0.2 typed-array-byte-length: 1.0.0 - typed-array-byte-offset: 1.0.0 - typed-array-length: 1.0.4 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.5 unbox-primitive: 1.0.2 - which-typed-array: 1.1.13 + which-typed-array: 1.1.14 dev: true + /es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + /es-get-iterator@1.1.3: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 has-symbols: 1.0.3 is-arguments: 1.1.1 is-map: 2.0.2 @@ -8781,42 +8971,44 @@ packages: stop-iteration-iterator: 1.0.0 dev: true - /es-iterator-helpers@1.0.15: - resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} + /es-iterator-helpers@1.0.17: + resolution: {integrity: sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==} + engines: {node: '>= 0.4'} dependencies: asynciterator.prototype: 1.0.0 - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 - es-set-tostringtag: 2.0.2 + es-abstract: 1.22.4 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 function-bind: 1.1.2 - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 globalthis: 1.0.3 - has-property-descriptors: 1.0.1 - has-proto: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 has-symbols: 1.0.3 - internal-slot: 1.0.6 + internal-slot: 1.0.7 iterator.prototype: 1.1.2 - safe-array-concat: 1.0.1 + safe-array-concat: 1.1.0 dev: true /es-module-lexer@0.9.3: resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} dev: true - /es-set-tostringtag@2.0.2: - resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.2 - has-tostringtag: 1.0.0 - hasown: 2.0.0 + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.1 dev: true /es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: - hasown: 2.0.0 + hasown: 2.0.1 dev: true /es-to-primitive@1.2.1: @@ -8873,39 +9065,39 @@ packages: '@esbuild/win32-x64': 0.18.20 dev: true - /esbuild@0.19.11: - resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/aix-ppc64': 0.19.11 - '@esbuild/android-arm': 0.19.11 - '@esbuild/android-arm64': 0.19.11 - '@esbuild/android-x64': 0.19.11 - '@esbuild/darwin-arm64': 0.19.11 - '@esbuild/darwin-x64': 0.19.11 - '@esbuild/freebsd-arm64': 0.19.11 - '@esbuild/freebsd-x64': 0.19.11 - '@esbuild/linux-arm': 0.19.11 - '@esbuild/linux-arm64': 0.19.11 - '@esbuild/linux-ia32': 0.19.11 - '@esbuild/linux-loong64': 0.19.11 - '@esbuild/linux-mips64el': 0.19.11 - '@esbuild/linux-ppc64': 0.19.11 - '@esbuild/linux-riscv64': 0.19.11 - '@esbuild/linux-s390x': 0.19.11 - '@esbuild/linux-x64': 0.19.11 - '@esbuild/netbsd-x64': 0.19.11 - '@esbuild/openbsd-x64': 0.19.11 - '@esbuild/sunos-x64': 0.19.11 - '@esbuild/win32-arm64': 0.19.11 - '@esbuild/win32-ia32': 0.19.11 - '@esbuild/win32-x64': 0.19.11 + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 dev: true - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} dev: true @@ -8952,7 +9144,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@7.0.2)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -8973,7 +9165,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 7.0.2(eslint@8.56.0)(typescript@5.3.3) debug: 3.2.7 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 @@ -8989,7 +9181,7 @@ packages: requireindex: 1.1.0 dev: true - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.0.2)(eslint@8.56.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -8999,22 +9191,22 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 7.0.2(eslint@8.56.0)(typescript@5.3.3) array-includes: 3.1.7 - array.prototype.findlastindex: 1.2.3 + array.prototype.findlastindex: 1.2.4 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) - hasown: 2.0.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.0.2)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + hasown: 2.0.1 is-core-module: 2.13.1 is-glob: 4.0.3 minimatch: 3.1.2 object.fromentries: 2.0.7 - object.groupby: 1.0.1 + object.groupby: 1.0.2 object.values: 1.1.7 semver: 6.3.1 tsconfig-paths: 3.15.0 @@ -9059,9 +9251,9 @@ packages: dependencies: array-includes: 3.1.7 array.prototype.flatmap: 1.3.2 - array.prototype.tosorted: 1.1.2 + array.prototype.tosorted: 1.1.3 doctrine: 2.1.0 - es-iterator-helpers: 1.0.15 + es-iterator-helpers: 1.0.17 eslint: 8.56.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.5 @@ -9076,17 +9268,17 @@ packages: string.prototype.matchall: 4.0.10 dev: true - /eslint-plugin-simple-import-sort@10.0.0(eslint@8.56.0): - resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} + /eslint-plugin-simple-import-sort@12.0.0(eslint@8.56.0): + resolution: {integrity: sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==} peerDependencies: eslint: '>=5.0.0' dependencies: eslint: 8.56.0 dev: true - /eslint-plugin-storybook@0.6.15(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-lAGqVAJGob47Griu29KXYowI4G7KwMoJDOkEip8ujikuDLxU+oWJ1l0WL6F2oDO4QiyUFXvtDkEkISMOPzo+7w==} - engines: {node: 12.x || 14.x || >= 16} + /eslint-plugin-storybook@0.8.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA==} + engines: {node: '>= 18'} peerDependencies: eslint: '>=6' dependencies: @@ -9100,17 +9292,17 @@ packages: - typescript dev: true - /eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.19.0)(eslint@8.56.0): - resolution: {integrity: sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==} + /eslint-plugin-unused-imports@3.1.0(@typescript-eslint/eslint-plugin@7.0.2)(eslint@8.56.0): + resolution: {integrity: sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - '@typescript-eslint/eslint-plugin': ^6.0.0 - eslint: ^8.0.0 + '@typescript-eslint/eslint-plugin': 6 - 7 + eslint: '8' peerDependenciesMeta: '@typescript-eslint/eslint-plugin': optional: true dependencies: - '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 7.0.2(@typescript-eslint/parser@7.0.2)(eslint@8.56.0)(typescript@5.3.3) eslint: 8.56.0 eslint-rule-composer: 0.3.0 dev: true @@ -9155,7 +9347,7 @@ packages: '@eslint-community/regexpp': 4.10.0 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.56.0 - '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/config-array': 0.11.14 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 '@ungap/structured-clone': 1.2.0 @@ -9176,7 +9368,7 @@ packages: glob-parent: 6.0.2 globals: 13.24.0 graphemer: 1.4.0 - ignore: 5.3.0 + ignore: 5.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 @@ -9197,8 +9389,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.11.2 - acorn-jsx: 5.3.2(acorn@8.11.2) + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) eslint-visitor-keys: 3.4.3 dev: true @@ -9378,8 +9570,8 @@ packages: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} dev: false - /fastq@1.16.0: - resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} dependencies: reusify: 1.0.4 dev: true @@ -9525,13 +9717,13 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} dependencies: - flatted: 3.2.9 + flatted: 3.3.0 keyv: 4.5.4 rimraf: 3.0.2 dev: true - /flatted@3.2.9: - resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + /flatted@3.3.0: + resolution: {integrity: sha512-noqGuLw158+DuD9UPRKHpJ2hGxpFyDlYYrfM0mWt4XhT4n0lwzTLh70Tkdyy4kyTmyTT9Bv7bWAJqw7cgkEXDg==} dev: true /flatten@1.0.3: @@ -9539,13 +9731,13 @@ packages: deprecated: flatten is deprecated in favor of utility frameworks such as lodash. dev: true - /flow-parser@0.227.0: - resolution: {integrity: sha512-nOygtGKcX/siZK/lFzpfdHEfOkfGcTW7rNroR1Zsz6T/JxSahPALXVt5qVHq/fgvMJuv096BTKbgxN3PzVBaDA==} + /flow-parser@0.229.0: + resolution: {integrity: sha512-mOYmMuvJwAo/CvnMFEq4SHftq7E5188hYMTTxJyQOXk2nh+sgslRdYMw3wTthH+FMcFaZLtmBPuMu6IwztdoUQ==} engines: {node: '>=0.4.0'} dev: true - /focus-lock@1.3.2: - resolution: {integrity: sha512-kFI92jZVqa8rP4Yer2sLNlUDcOdEFxYum2tIIr4eCH0XF+pOmlg0xiY4tkbDmHJXt3phtbJoWs1L6PgUVk97rA==} + /focus-lock@1.3.3: + resolution: {integrity: sha512-hfXkZha7Xt4RQtrL1HBfspAuIj89Y0fb6GX0dfJilb8S2G/lvL4akPAcHq6xoD2NuZnDMCnZL/zQesMyeu6Psg==} engines: {node: '>=10'} dependencies: tslib: 2.6.2 @@ -9603,6 +9795,24 @@ packages: '@emotion/is-prop-valid': 0.8.8 dev: false + /framer-motion@11.0.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Lb0EYbQcSK/pgyQUJm+KzsQrKrJRX9sFRyzl9hSr9gFG4Mk8yP7BjhuxvRXzblOM/+JxycrJdCDVmOQBsjpYlw==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + dev: false + /framesync@6.1.2: resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} dependencies: @@ -9671,9 +9881,9 @@ packages: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 functions-have-names: 1.2.3 dev: true @@ -9711,13 +9921,15 @@ packages: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true - /get-intrinsic@1.2.2: - resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} dependencies: + es-errors: 1.3.0 function-bind: 1.1.2 - has-proto: 1.0.1 + has-proto: 1.0.3 has-symbols: 1.0.3 - hasown: 2.0.0 + hasown: 2.0.1 /get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} @@ -9752,23 +9964,24 @@ packages: engines: {node: '>=16'} dev: true - /get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 dev: true /giget@1.2.1: resolution: {integrity: sha512-4VG22mopWtIeHwogGSy1FViXVo0YT+m6BrqZfz0JJFwbSsePsCdOzdLIIli5BtMp7Xe8f/o2OmBpQX2NBOC24g==} hasBin: true dependencies: - citty: 0.1.5 + citty: 0.1.6 consola: 3.2.3 defu: 6.1.4 - node-fetch-native: 1.6.1 - nypm: 0.3.4 + node-fetch-native: 1.6.2 + nypm: 0.3.6 ohash: 1.1.3 pathe: 1.1.2 tar: 6.2.0 @@ -9854,7 +10067,7 @@ packages: array-union: 2.1.0 dir-glob: 3.0.1 fast-glob: 3.3.2 - ignore: 5.3.0 + ignore: 5.3.1 merge2: 1.4.1 slash: 3.0.0 dev: true @@ -9874,7 +10087,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -9922,28 +10135,28 @@ packages: engines: {node: '>=8'} dev: true - /has-property-descriptors@1.0.1: - resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} dependencies: - get-intrinsic: 1.2.2 + es-define-property: 1.0.0 - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} engines: {node: '>= 0.4'} /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 dev: true - /hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + /hasown@2.0.1: + resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 @@ -10009,18 +10222,18 @@ packages: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} dev: false - /i18next-http-backend@2.4.2: - resolution: {integrity: sha512-wKrgGcaFQ4EPjfzBTjzMU0rbFTYpa0S5gv9N/d8WBmWS64+IgJb7cHddMvV+tUkse7vUfco3eVs2lB+nJhPo3w==} + /i18next-http-backend@2.4.3: + resolution: {integrity: sha512-jo2M03O6n1/DNb51WSQ8PsQ0xEELzLZRdYUTbf17mLw3rVwnJF9hwNgMXvEFSxxb+N8dT+o0vtigA6s5mGWyPA==} dependencies: cross-fetch: 4.0.0 transitivePeerDependencies: - encoding dev: false - /i18next@23.7.16: - resolution: {integrity: sha512-SrqFkMn9W6Wb43ZJ9qrO6U2U4S80RsFMA7VYFSqp7oc7RllQOYDCdRfsse6A7Cq/V8MnpxKvJCYgM8++27n4Fw==} + /i18next@23.9.0: + resolution: {integrity: sha512-f3MUciKqwzNV//mHG6EtdSlC65+nqH/3zK8sOSWqNV6FVu2tmHhF/rFOp9UF8S4m1odojtuipKaKJrP0Loh60g==} dependencies: - '@babel/runtime': 7.23.7 + '@babel/runtime': 7.23.9 dev: false /iconv-lite@0.4.24: @@ -10038,8 +10251,8 @@ packages: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true - /ignore@5.3.0: - resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} dev: true @@ -10095,13 +10308,13 @@ packages: fast-loops: 1.1.3 dev: false - /internal-slot@1.0.6: - resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.2 - hasown: 2.0.0 - side-channel: 1.0.4 + es-errors: 1.3.0 + hasown: 2.0.1 + side-channel: 1.0.5 dev: true /invariant@2.2.4: @@ -10109,8 +10322,8 @@ packages: dependencies: loose-envify: 1.4.0 - /ip@2.0.0: - resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + /ip@2.0.1: + resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} dev: true /ipaddr.js@1.9.1: @@ -10127,16 +10340,16 @@ packages: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - has-tostringtag: 1.0.0 + call-bind: 1.0.7 + has-tostringtag: 1.0.2 dev: true - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - is-typed-array: 1.1.12 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 dev: true /is-arrayish@0.2.1: @@ -10146,7 +10359,7 @@ packages: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-bigint@1.0.4: @@ -10166,8 +10379,8 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - has-tostringtag: 1.0.0 + call-bind: 1.0.7 + has-tostringtag: 1.0.2 dev: true /is-callable@1.2.7: @@ -10178,13 +10391,13 @@ packages: /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: - hasown: 2.0.0 + hasown: 2.0.1 /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-deflate@1.0.0: @@ -10205,7 +10418,7 @@ packages: /is-finalizationregistry@1.0.2: resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 dev: true /is-fullwidth-code-point@3.0.0: @@ -10217,7 +10430,7 @@ packages: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} engines: {node: '>= 0.4'} dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-glob@4.0.3: @@ -10245,12 +10458,12 @@ packages: resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 dev: true - /is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} dev: true @@ -10258,7 +10471,7 @@ packages: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-number@7.0.0: @@ -10297,8 +10510,8 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - has-tostringtag: 1.0.0 + call-bind: 1.0.7 + has-tostringtag: 1.0.2 dev: true /is-regexp@1.0.0: @@ -10317,7 +10530,7 @@ packages: /is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 dev: true /is-stream@2.0.1: @@ -10334,7 +10547,7 @@ packages: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} dependencies: - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /is-symbol@1.0.4: @@ -10344,11 +10557,11 @@ packages: has-symbols: 1.0.3 dev: true - /is-typed-array@1.1.12: - resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} dependencies: - which-typed-array: 1.1.13 + which-typed-array: 1.1.14 dev: true /is-unicode-supported@0.1.0: @@ -10372,14 +10585,14 @@ packages: /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 dev: true /is-weakset@2.0.2: resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 dev: true /is-wsl@2.2.0: @@ -10415,8 +10628,8 @@ packages: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.23.7 - '@babel/parser': 7.23.6 + '@babel/core': 7.23.9 + '@babel/parser': 7.23.9 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -10428,10 +10641,10 @@ packages: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} dependencies: define-properties: 1.2.1 - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 has-symbols: 1.0.3 - reflect.getprototypeof: 1.0.4 - set-function-name: 2.0.1 + reflect.getprototypeof: 1.0.5 + set-function-name: 2.0.2 dev: true /its-fine@1.1.1(react@18.2.0): @@ -10469,7 +10682,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.11.5 + '@types/node': 20.11.19 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -10487,7 +10700,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.11.5 + '@types/node': 20.11.19 dev: true /jest-regex-util@29.6.3: @@ -10500,7 +10713,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.5 + '@types/node': 20.11.19 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10511,7 +10724,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.11.5 + '@types/node': 20.11.19 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -10528,6 +10741,10 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -10543,7 +10760,7 @@ packages: argparse: 2.0.1 dev: true - /jscodeshift@0.15.1(@babel/preset-env@7.23.8): + /jscodeshift@0.15.1(@babel/preset-env@7.23.9): resolution: {integrity: sha512-hIJfxUy8Rt4HkJn/zZPU9ChKfKZM1342waJ1QC2e2YsPcWhM+3BJ4dcfQCzArTrk1jJeNLB341H+qOcEHRxJZg==} hasBin: true peerDependencies: @@ -10552,20 +10769,20 @@ packages: '@babel/preset-env': optional: true dependencies: - '@babel/core': 7.23.7 - '@babel/parser': 7.23.6 - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.7) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.7) - '@babel/preset-env': 7.23.8(@babel/core@7.23.7) - '@babel/preset-flow': 7.23.3(@babel/core@7.23.7) - '@babel/preset-typescript': 7.23.3(@babel/core@7.23.7) - '@babel/register': 7.23.7(@babel/core@7.23.7) - babel-core: 7.0.0-bridge.0(@babel/core@7.23.7) + '@babel/core': 7.23.9 + '@babel/parser': 7.23.9 + '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.9) + '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.23.9) + '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.23.9) + '@babel/preset-env': 7.23.9(@babel/core@7.23.9) + '@babel/preset-flow': 7.23.3(@babel/core@7.23.9) + '@babel/preset-typescript': 7.23.3(@babel/core@7.23.9) + '@babel/register': 7.23.7(@babel/core@7.23.9) + babel-core: 7.0.0-bridge.0(@babel/core@7.23.9) chalk: 4.1.2 - flow-parser: 0.227.0 + flow-parser: 0.229.0 graceful-fs: 4.2.11 micromatch: 4.0.5 neo-async: 2.6.2 @@ -10679,8 +10896,8 @@ packages: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} dev: true - /konva@9.3.1: - resolution: {integrity: sha512-KXHJVUrYVWFIJUbnlw8QUZDBGC1jx6wwRsGaByPm/2yk78xw7hKquCMNEd9EtVqGz/jUkKFJAWom77TLB+zVOA==} + /konva@9.3.3: + resolution: {integrity: sha512-cg/AHxnfawZ1rKxygCnzx0TZY7hQiQiAKgAHPinEwMn49MVrBkeKLj2d0EaleoFG/0y0XhEKTD0dFZiPPdWlCQ==} dev: false /lazy-universal-dotenv@4.0.0: @@ -10688,7 +10905,7 @@ packages: engines: {node: '>=14.0.0'} dependencies: app-root-dir: 1.0.2 - dotenv: 16.3.1 + dotenv: 16.4.5 dotenv-expand: 10.0.0 dev: true @@ -10799,8 +11016,8 @@ packages: get-func-name: 2.0.2 dev: true - /lru-cache@10.1.0: - resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} dev: true @@ -10852,7 +11069,7 @@ packages: pretty-ms: 7.0.1 rc: 1.2.8 stream-to-array: 2.3.0 - ts-graphviz: 1.8.1 + ts-graphviz: 1.8.2 typescript: 5.3.3 walkdir: 0.4.1 transitivePeerDependencies: @@ -10866,8 +11083,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /magic-string@0.30.5: - resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + /magic-string@0.30.7: + resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -10898,8 +11115,8 @@ packages: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true - /markdown-to-jsx@7.4.0(react@18.2.0): - resolution: {integrity: sha512-zilc+MIkVVXPyTb4iIUTIz9yyqfcWjszGXnwF9K/aiBWcHXFcmdEMTkG01/oQhwSCH7SY1BnG6+ev5BzWmbPrg==} + /markdown-to-jsx@7.4.1(react@18.2.0): + resolution: {integrity: sha512-GbrbkTnHp9u6+HqbPRFJbObi369AgJNXi/sGqq5HRsoZW063xR1XDCaConqq+whfEIAlzB1YPnOgsPc7B7bc/A==} engines: {node: '>= 10'} peerDependencies: react: '>= 0.14.0' @@ -11073,7 +11290,7 @@ packages: acorn: 8.11.3 pathe: 1.1.2 pkg-types: 1.0.3 - ufo: 1.3.2 + ufo: 1.4.0 dev: true /module-definition@3.4.0: @@ -11151,6 +11368,11 @@ packages: hasBin: true dev: true + /nanostores@0.10.0: + resolution: {integrity: sha512-Poy5+9wFXOD0jAstn4kv9n686U2BFw48z/W8lms8cS8lcbRz7BU20JxZ3e/kkKQVfRrkm4yLWCUA6GQINdvJCQ==} + engines: {node: ^18.0.0 || >=20.0.0} + dev: false + /nanostores@0.9.5: resolution: {integrity: sha512-Z+p+g8E7yzaWwOe5gEUB2Ox0rCEeXWYIZWmYvw/ajNYX8DlXdMvMDj8DWfM/subqPAcsf8l8Td4iAwO1DeIIRQ==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} @@ -11191,8 +11413,8 @@ packages: minimatch: 3.1.2 dev: true - /node-fetch-native@1.6.1: - resolution: {integrity: sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==} + /node-fetch-native@1.6.2: + resolution: {integrity: sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==} dev: true /node-fetch@2.7.0: @@ -11218,14 +11440,14 @@ packages: resolution: {integrity: sha512-8Q1hXew6ETzqKRAs3jjLioSxNfT1cx74ooiF8RlAONwVMcfq+UdzLC2eB5qcPldUxaE5w3ytLkrmV1TGddhZTA==} engines: {node: '>=6.0'} dependencies: - '@babel/parser': 7.23.6 + '@babel/parser': 7.23.9 dev: true /node-source-walk@5.0.2: resolution: {integrity: sha512-Y4jr/8SRS5hzEdZ7SGuvZGwfORvNsSsNRwDXx5WisiqzsVfeftDvRgfeqWNgZvWSJbgubTRVRYBzK6UO+ErqjA==} engines: {node: '>=12'} dependencies: - '@babel/parser': 7.23.6 + '@babel/parser': 7.23.9 dev: true /normalize-package-data@2.5.0: @@ -11256,15 +11478,15 @@ packages: path-key: 4.0.0 dev: true - /nypm@0.3.4: - resolution: {integrity: sha512-1JLkp/zHBrkS3pZ692IqOaIKSYHmQXgqfELk6YTOfVBnwealAmPA1q2kKK7PHJAHSMBozerThEFZXP3G6o7Ukg==} + /nypm@0.3.6: + resolution: {integrity: sha512-2CATJh3pd6CyNfU5VZM7qSwFu0ieyabkEdnogE30Obn1czrmOYiZ8DOZLe1yBdLKWoyD3Mcy2maUs+0MR3yVjQ==} engines: {node: ^14.16.0 || >=16.10.0} hasBin: true dependencies: - citty: 0.1.5 + citty: 0.1.6 execa: 8.0.1 pathe: 1.1.2 - ufo: 1.3.2 + ufo: 1.4.0 dev: true /object-assign@4.1.1: @@ -11279,7 +11501,7 @@ packages: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 dev: true @@ -11291,7 +11513,7 @@ packages: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -11301,43 +11523,44 @@ packages: resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 dev: true /object.fromentries@2.0.7: resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 dev: true - /object.groupby@1.0.1: - resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + /object.groupby@1.0.2: + resolution: {integrity: sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==} dependencies: - call-bind: 1.0.5 + array.prototype.filter: 1.0.3 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 + es-abstract: 1.22.4 + es-errors: 1.3.0 dev: true /object.hasown@1.1.3: resolution: {integrity: sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==} dependencies: define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 dev: true /object.values@1.1.7: resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 dev: true /ohash@1.1.3: @@ -11389,15 +11612,15 @@ packages: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} dev: true - /openapi-typescript@6.7.3: - resolution: {integrity: sha512-es3mGcDXV6TKPo6n3aohzHm0qxhLyR39MhF6mkD1FwFGjhxnqMqfSIgM0eCpInZvqatve4CxmXcMZw3jnnsaXw==} + /openapi-typescript@6.7.4: + resolution: {integrity: sha512-EZyeW9Wy7UDCKv0iYmKrq2pVZtquXiD/YHiUClAKqiMi42nodx/EQH11K6fLqjt1IZlJmVokrAsExsBMM2RROQ==} hasBin: true dependencies: ansi-colors: 4.1.3 fast-glob: 3.3.2 js-yaml: 4.1.0 supports-color: 9.4.0 - undici: 5.28.2 + undici: 5.28.3 yargs-parser: 21.1.1 dev: true @@ -11428,16 +11651,6 @@ packages: wcwidth: 1.0.1 dev: true - /overlayscrollbars-react@0.5.3(overlayscrollbars@2.4.6)(react@18.2.0): - resolution: {integrity: sha512-mq9D9tbfSeq0cti1kKMf3B3AzsEGwHcRIDX/K49CvYkHz/tKeU38GiahDkIPKTMEAp6lzKCo4x1eJZA6ZFYOxQ==} - peerDependencies: - overlayscrollbars: ^2.0.0 - react: '>=16.8.0' - dependencies: - overlayscrollbars: 2.4.6 - react: 18.2.0 - dev: false - /overlayscrollbars-react@0.5.4(overlayscrollbars@2.5.0)(react@18.2.0): resolution: {integrity: sha512-FPKx9XnXovTnI4+2JXig5uEaTLSEJ6svOwPzIfBBXTHBRNsz2+WhYUmfM0K/BNYxjgDEwuPm+NQhEoOA0RoG1g==} peerDependencies: @@ -11448,10 +11661,6 @@ packages: react: 18.2.0 dev: false - /overlayscrollbars@2.4.6: - resolution: {integrity: sha512-C7tmhetwMv9frEvIT/RfkAVEgbjRNz/Gh2zE8BVmN+jl35GRaAnz73rlGQCMRoC2arpACAXyMNnJkzHb7GBrcA==} - dev: false - /overlayscrollbars@2.5.0: resolution: {integrity: sha512-CWVC2dwS07XZfLHDm5GmZN1iYggiJ8Vufnvzwt0gwR9Yz1hVckKeTxg7VILZeYVGhDYJHZ1Xc8Xfys5dWZ1qiA==} dev: false @@ -11575,7 +11784,7 @@ packages: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 10.1.0 + lru-cache: 10.2.0 minipass: 7.0.4 dev: true @@ -11660,11 +11869,16 @@ packages: engines: {node: '>=4'} dev: true - /polished@4.2.2: - resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} + /polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} engines: {node: '>=10'} dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 + dev: true + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} dev: true /postcss-values-parser@2.0.1: @@ -11676,7 +11890,7 @@ packages: uniq: 1.0.1 dev: true - /postcss-values-parser@6.0.2(postcss@8.4.33): + /postcss-values-parser@6.0.2(postcss@8.4.35): resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} engines: {node: '>=10'} peerDependencies: @@ -11684,12 +11898,12 @@ packages: dependencies: color-name: 1.1.4 is-url-superb: 4.0.0 - postcss: 8.4.33 + postcss: 8.4.35 quote-unquote: 1.0.0 dev: true - /postcss@8.4.33: - resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} + /postcss@8.4.35: + resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.7 @@ -11751,8 +11965,8 @@ packages: hasBin: true dev: true - /prettier@3.2.4: - resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==} + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} engines: {node: '>=14'} hasBin: true dev: true @@ -11883,18 +12097,18 @@ packages: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} dependencies: - side-channel: 1.0.4 + side-channel: 1.0.5 dev: true /qs@6.11.2: resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} engines: {node: '>=0.6'} dependencies: - side-channel: 1.0.4 + side-channel: 1.0.5 dev: true - /query-string@8.1.0: - resolution: {integrity: sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==} + /query-string@8.2.0: + resolution: {integrity: sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g==} engines: {node: '>=14.16'} dependencies: decode-uri-component: 0.4.1 @@ -11981,9 +12195,9 @@ packages: resolution: {integrity: sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==} engines: {node: '>=16.14.0'} dependencies: - '@babel/core': 7.23.7 - '@babel/traverse': 7.23.7 - '@babel/types': 7.23.6 + '@babel/core': 7.23.9 + '@babel/traverse': 7.23.9 + '@babel/types': 7.23.9 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.5 '@types/doctrine': 0.0.9 @@ -12034,7 +12248,7 @@ packages: peerDependencies: react: '>=16.13.1' dependencies: - '@babel/runtime': 7.23.6 + '@babel/runtime': 7.23.9 react: 18.2.0 dev: false @@ -12042,7 +12256,7 @@ packages: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false - /react-focus-lock@2.11.1(@types/react@18.2.48)(react@18.2.0): + /react-focus-lock@2.11.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-IXLwnTBrLTlKTpASZXqqXJ8oymWrgAlOfuuDYN4XCuN1YJ72dwX198UCaF1QqGUk5C3QOnlMik//n3ufcfe8Ig==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -12052,26 +12266,26 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.9 - '@types/react': 18.2.48 - focus-lock: 1.3.2 + '@types/react': 18.2.57 + focus-lock: 1.3.3 prop-types: 15.8.1 react: 18.2.0 react-clientside-effect: 1.2.6(react@18.2.0) - use-callback-ref: 1.3.1(@types/react@18.2.48)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.48)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.2.57)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.57)(react@18.2.0) dev: false - /react-hook-form@7.49.3(react@18.2.0): - resolution: {integrity: sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==} - engines: {node: '>=18', pnpm: '8'} + /react-hook-form@7.50.1(react@18.2.0): + resolution: {integrity: sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==} + engines: {node: '>=12.22.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 dependencies: react: 18.2.0 dev: false - /react-hotkeys-hook@4.4.4(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-wzZmqb/Obr0ds9Myc1sIFPJ52GA/Eeg/vXBWV0HA1LvHlVAW5Va3KB0q6EZNlNSHQWscWZ2K8+6w0GYSie2o7A==} + /react-hotkeys-hook@4.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==} peerDependencies: react: '>=16.8.1' react-dom: '>=16.8.1' @@ -12080,27 +12294,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-i18next@14.0.0(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OCrS8rHNAmnr8ggGRDxjakzihrMW7HCbsplduTm3EuuQ6fyvWGT41ksZpqbduYoqJurBmEsEVZ1pILSUWkHZng==} - peerDependencies: - i18next: '>= 23.2.3' - react: '>= 16.8.0' - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - dependencies: - '@babel/runtime': 7.23.7 - html-parse-stringify: 3.0.1 - i18next: 23.7.16 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /react-i18next@14.0.5(i18next@23.7.16)(react-dom@18.2.0)(react@18.2.0): + /react-i18next@14.0.5(i18next@23.9.0)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-5+bQSeEtgJrMBABBL5lO7jPdSNAbeAZ+MlFWDw//7FnVacuVu3l9EeWFzBQvZsKy+cihkbThWOAThEdH8YjGEw==} peerDependencies: i18next: '>= 23.2.3' @@ -12115,7 +12309,7 @@ packages: dependencies: '@babel/runtime': 7.23.9 html-parse-stringify: 3.0.1 - i18next: 23.7.16 + i18next: 23.9.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -12143,7 +12337,7 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true - /react-konva@18.2.10(konva@9.3.1)(react-dom@18.2.0)(react@18.2.0): + /react-konva@18.2.10(konva@9.3.3)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==} peerDependencies: konva: ^8.0.1 || ^7.2.5 || ^9.0.0 @@ -12152,7 +12346,7 @@ packages: dependencies: '@types/react-reconciler': 0.28.8 its-fine: 1.1.1(react@18.2.0) - konva: 9.3.1 + konva: 9.3.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-reconciler: 0.29.0(react@18.2.0) @@ -12170,7 +12364,7 @@ packages: scheduler: 0.23.0 dev: false - /react-redux@9.1.0(@types/react@18.2.48)(react@18.2.0)(redux@5.0.1): + /react-redux@9.1.0(@types/react@18.2.57)(react@18.2.0)(redux@5.0.1): resolution: {integrity: sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==} peerDependencies: '@types/react': ^18.2.25 @@ -12185,7 +12379,7 @@ packages: redux: optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 '@types/use-sync-external-store': 0.0.3 react: 18.2.0 redux: 5.0.1 @@ -12197,23 +12391,7 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-remove-scroll-bar@2.3.4(@types/react@18.2.48)(react@18.2.0): - resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - dependencies: - '@types/react': 18.2.48 - react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) - tslib: 2.6.2 - dev: true - - /react-remove-scroll-bar@2.3.5(@types/react@18.2.48)(react@18.2.0): + /react-remove-scroll-bar@2.3.5(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} peerDependencies: @@ -12223,13 +12401,12 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 - react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.57)(react@18.2.0) tslib: 2.6.2 - dev: false - /react-remove-scroll@2.5.5(@types/react@18.2.48)(react@18.2.0): + /react-remove-scroll@2.5.5(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} engines: {node: '>=10'} peerDependencies: @@ -12239,16 +12416,16 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 - react-remove-scroll-bar: 2.3.4(@types/react@18.2.48)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) + react-remove-scroll-bar: 2.3.5(@types/react@18.2.57)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.57)(react@18.2.0) tslib: 2.6.2 - use-callback-ref: 1.3.1(@types/react@18.2.48)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.48)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.2.57)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.57)(react@18.2.0) dev: true - /react-remove-scroll@2.5.7(@types/react@18.2.48)(react@18.2.0): + /react-remove-scroll@2.5.7(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} peerDependencies: @@ -12258,17 +12435,17 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 - react-remove-scroll-bar: 2.3.5(@types/react@18.2.48)(react@18.2.0) - react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) + react-remove-scroll-bar: 2.3.5(@types/react@18.2.57)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.57)(react@18.2.0) tslib: 2.6.2 - use-callback-ref: 1.3.1(@types/react@18.2.48)(react@18.2.0) - use-sidecar: 1.1.2(@types/react@18.2.48)(react@18.2.0) + use-callback-ref: 1.3.1(@types/react@18.2.57)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.57)(react@18.2.0) dev: false - /react-resizable-panels@1.0.9(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-QPfW3L7yetEC6z04G9AYYFz5kBklh8rTWcOsVFImYMNUVhr1Y1r9Qc/20Yks2tA+lXMBWCUz4fkGEvbS7tpBSg==} + /react-resizable-panels@2.0.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ZylBvs7oG7Y/INWw3oYGolqgpFvoPW8MPeg9l1fURDeKpxrmUuCHBUmPj47BdZ11MODImu3kZYXG85rbySab7w==} peerDependencies: react: ^16.14.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 @@ -12277,49 +12454,49 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-select@5.7.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /react-select@5.7.7(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.23.6 + '@babel/runtime': 7.23.9 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) - '@floating-ui/dom': 1.5.3 + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) + '@floating-ui/dom': 1.6.3 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.48)(react@18.2.0) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false - /react-select@5.8.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): + /react-select@5.8.0(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.23.7 + '@babel/runtime': 7.23.9 '@emotion/cache': 11.11.0 - '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) - '@floating-ui/dom': 1.5.3 + '@emotion/react': 11.11.3(@types/react@18.2.57)(react@18.2.0) + '@floating-ui/dom': 1.6.3 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.48)(react@18.2.0) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false - /react-style-singleton@2.2.1(@types/react@18.2.48)(react@18.2.0): + /react-style-singleton@2.2.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: @@ -12329,22 +12506,22 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 tslib: 2.6.2 - /react-textarea-autosize@8.5.3(@types/react@18.2.48)(react@18.2.0): + /react-textarea-autosize@8.5.3(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==} engines: {node: '>=10'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.23.6 + '@babel/runtime': 7.23.9 react: 18.2.0 use-composed-ref: 1.3.0(react@18.2.0) - use-latest: 1.2.1(@types/react@18.2.48)(react@18.2.0) + use-latest: 1.2.1(@types/react@18.2.57)(react@18.2.0) transitivePeerDependencies: - '@types/react' dev: false @@ -12355,7 +12532,7 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.23.7 + '@babel/runtime': 7.23.9 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -12373,8 +12550,8 @@ packages: tslib: 2.6.2 dev: false - /react-use@17.4.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-05Oyuwn4ZccdzLD4ttLbMe8TkobdKpOj7YCFE9VhVpbXrTWZpvCcMyroRw/Banh1RIcQRcM06tfzPpY5D9sTsQ==} + /react-use@17.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} peerDependencies: react: '*' react-dom: '*' @@ -12397,8 +12574,8 @@ packages: tslib: 2.6.2 dev: false - /react-virtuoso@4.6.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-vvlqvzPif+MvBrJ09+hJJrVY0xJK9yran+A+/1iwY78k0YCVKsyoNPqoLxOxzYPggspNBNXqUXEcvckN29OxyQ==} + /react-virtuoso@4.7.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cpgvI1rSOETGDMhqVAVDuH+XHbWO1uIGKv5I6l4CyC71xWYUeGrE5n7sgTZklROB4+Vbv85pcgfWloTlY48HGQ==} engines: {node: '>=10'} peerDependencies: react: '>=16 || >=17 || >= 18' @@ -12414,18 +12591,18 @@ packages: dependencies: loose-envify: 1.4.0 - /reactflow@11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-tqQJfPEiIkXonT3piVYf+F9CvABI5e28t5I6rpaLTnO8YVCAOh1h0f+ziDKz0Bx9Y2B/mFgyz+H7LZeUp/+lhQ==} + /reactflow@11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.3.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.2(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.7(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/background': 11.3.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/controls': 11.2.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/minimap': 11.7.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-resizer': 2.2.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -12523,14 +12700,15 @@ packages: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} dev: false - /reflect.getprototypeof@1.0.4: - resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} + /reflect.getprototypeof@1.0.5: + resolution: {integrity: sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 + es-abstract: 1.22.4 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 globalthis: 1.0.3 which-builtin-type: 1.1.3 dev: true @@ -12552,16 +12730,17 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 dev: true - /regexp.prototype.flags@1.5.1: - resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - set-function-name: 2.0.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 dev: true /regexpu-core@5.3.2: @@ -12630,10 +12809,9 @@ packages: hasBin: true dev: true - /reselect@5.0.1(patch_hash=kvbgwzjyy4x4fnh7znyocvb75q): - resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} + /reselect@5.1.0: + resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} dev: false - patched: true /resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -12757,33 +12935,33 @@ packages: fsevents: 2.3.3 dev: true - /rollup@4.9.4: - resolution: {integrity: sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==} + /rollup@4.12.0: + resolution: {integrity: sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.9.4 - '@rollup/rollup-android-arm64': 4.9.4 - '@rollup/rollup-darwin-arm64': 4.9.4 - '@rollup/rollup-darwin-x64': 4.9.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.9.4 - '@rollup/rollup-linux-arm64-gnu': 4.9.4 - '@rollup/rollup-linux-arm64-musl': 4.9.4 - '@rollup/rollup-linux-riscv64-gnu': 4.9.4 - '@rollup/rollup-linux-x64-gnu': 4.9.4 - '@rollup/rollup-linux-x64-musl': 4.9.4 - '@rollup/rollup-win32-arm64-msvc': 4.9.4 - '@rollup/rollup-win32-ia32-msvc': 4.9.4 - '@rollup/rollup-win32-x64-msvc': 4.9.4 + '@rollup/rollup-android-arm-eabi': 4.12.0 + '@rollup/rollup-android-arm64': 4.12.0 + '@rollup/rollup-darwin-arm64': 4.12.0 + '@rollup/rollup-darwin-x64': 4.12.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.12.0 + '@rollup/rollup-linux-arm64-gnu': 4.12.0 + '@rollup/rollup-linux-arm64-musl': 4.12.0 + '@rollup/rollup-linux-riscv64-gnu': 4.12.0 + '@rollup/rollup-linux-x64-gnu': 4.12.0 + '@rollup/rollup-linux-x64-musl': 4.12.0 + '@rollup/rollup-win32-arm64-msvc': 4.12.0 + '@rollup/rollup-win32-ia32-msvc': 4.12.0 + '@rollup/rollup-win32-x64-msvc': 4.12.0 fsevents: 2.3.3 dev: true /rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} dependencies: - '@babel/runtime': 7.23.8 + '@babel/runtime': 7.23.9 dev: false /run-parallel@1.2.0: @@ -12798,12 +12976,12 @@ packages: tslib: 2.6.2 dev: true - /safe-array-concat@1.0.1: - resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + /safe-array-concat@1.1.0: + resolution: {integrity: sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==} engines: {node: '>=0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 has-symbols: 1.0.3 isarray: 2.0.5 dev: true @@ -12816,11 +12994,12 @@ packages: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: true - /safe-regex-test@1.0.0: - resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.7 + es-errors: 1.3.0 is-regex: 1.1.4 dev: true @@ -12873,6 +13052,14 @@ packages: lru-cache: 6.0.0 dev: true + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -12913,23 +13100,26 @@ packages: - supports-color dev: true - /set-function-length@1.1.1: - resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + /set-function-length@1.2.1: + resolution: {integrity: sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==} engines: {node: '>= 0.4'} dependencies: - define-data-property: 1.1.1 - get-intrinsic: 1.2.2 + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 gopd: 1.0.1 - has-property-descriptors: 1.0.1 + has-property-descriptors: 1.0.2 dev: true - /set-function-name@2.0.1: - resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} dependencies: - define-data-property: 1.1.1 + define-data-property: 1.1.4 + es-errors: 1.3.0 functions-have-names: 1.2.3 - has-property-descriptors: 1.0.1 + has-property-descriptors: 1.0.2 dev: true /set-harmonic-interval@1.0.1: @@ -12964,11 +13154,13 @@ packages: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} dev: true - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + /side-channel@1.0.5: + resolution: {integrity: sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==} + engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 object-inspect: 1.13.1 dev: true @@ -13061,22 +13253,22 @@ packages: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.16 + spdx-license-ids: 3.0.17 dev: true - /spdx-exceptions@2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + /spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} dev: true /spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} dependencies: - spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.16 + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.17 dev: true - /spdx-license-ids@3.0.16: - resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} + /spdx-license-ids@3.0.17: + resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} dev: true /split-on-first@3.0.0: @@ -13130,18 +13322,18 @@ packages: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} dependencies: - internal-slot: 1.0.6 + internal-slot: 1.0.7 dev: true - /store2@2.14.2: - resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} + /store2@2.14.3: + resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} dev: true - /storybook@7.6.10: - resolution: {integrity: sha512-ypFeGhQTUBBfqSUVZYh7wS5ghn3O2wILCiQc4459SeUpvUn+skcqw/TlrwGSoF5EWjDA7gtRrWDxO3mnlPt5Cw==} + /storybook@7.6.17: + resolution: {integrity: sha512-8+EIo91bwmeFWPg1eysrxXlhIYv3OsXrznTr4+4Eq0NikqAoq6oBhtlN5K2RGS2lBVF537eN+9jTCNbR+WrzDA==} hasBin: true dependencies: - '@storybook/cli': 7.6.10 + '@storybook/cli': 7.6.17 transitivePeerDependencies: - bufferutil - encoding @@ -13185,40 +13377,40 @@ packages: /string.prototype.matchall@4.0.10: resolution: {integrity: sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 - get-intrinsic: 1.2.2 + es-abstract: 1.22.4 + get-intrinsic: 1.2.4 has-symbols: 1.0.3 - internal-slot: 1.0.6 - regexp.prototype.flags: 1.5.1 - set-function-name: 2.0.1 - side-channel: 1.0.4 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.5 dev: true /string.prototype.trim@1.2.8: resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 dev: true /string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 dev: true /string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 - es-abstract: 1.22.3 + es-abstract: 1.22.4 dev: true /string_decoder@1.1.1: @@ -13295,10 +13487,10 @@ packages: engines: {node: '>=8'} dev: true - /strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} dependencies: - acorn: 8.11.3 + js-tokens: 8.0.3 dev: true /stylis@4.2.0: @@ -13460,8 +13652,8 @@ packages: engines: {node: '>=14.0.0'} dev: true - /tinyspy@2.2.0: - resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} dev: true @@ -13501,9 +13693,9 @@ packages: hasBin: true dev: true - /ts-api-utils@1.0.3(typescript@5.3.3): - resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} - engines: {node: '>=16.13.0'} + /ts-api-utils@1.2.1(typescript@5.3.3): + resolution: {integrity: sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==} + engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' dependencies: @@ -13523,8 +13715,8 @@ packages: resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} dev: false - /ts-graphviz@1.8.1: - resolution: {integrity: sha512-54/fe5iu0Jb6X0pmDmzsA2UHLfyHjUEUwfHtZcEOR0fZ6Myf+dFoO6eNsyL8CBDMJ9u7WWEewduVaiaXlvjSVw==} + /ts-graphviz@1.8.2: + resolution: {integrity: sha512-5YhbFoHmjxa7pgQLkB07MtGnGJ/yhvjmc9uhsnDBEICME6gkPf83SBwLDQqGDoCa3XzUMWLk1AU2Wn1u1naDtA==} engines: {node: '>=14.16'} dev: true @@ -13536,8 +13728,8 @@ packages: resolution: {integrity: sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==} dev: true - /tsconfck@3.0.1(typescript@5.3.3): - resolution: {integrity: sha512-7ppiBlF3UEddCLeI1JRx5m2Ryq+xk4JrZuq4EuYXykipebaq1dV0Fhgr1hb7CkmHt32QSgOZlcqVLEtHBG4/mg==} + /tsconfck@3.0.2(typescript@5.3.3): + resolution: {integrity: sha512-6lWtFjwuhS3XI4HsX4Zg0izOI3FU/AI9EGVlPEUMDIhvLPMD4wkiof0WCoDgW7qY+Dy198g4d9miAqUHWHFH6Q==} engines: {node: ^18 || >=20} hasBin: true peerDependencies: @@ -13635,8 +13827,8 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - /type-fest@4.9.0: - resolution: {integrity: sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==} + /type-fest@4.10.2: + resolution: {integrity: sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==} engines: {node: '>=16'} dev: false @@ -13648,42 +13840,47 @@ packages: mime-types: 2.1.35 dev: true - /typed-array-buffer@1.0.0: - resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 - get-intrinsic: 1.2.2 - is-typed-array: 1.1.12 + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 dev: true /typed-array-byte-length@1.0.0: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 for-each: 0.3.3 - has-proto: 1.0.1 - is-typed-array: 1.1.12 + has-proto: 1.0.3 + is-typed-array: 1.1.13 dev: true - /typed-array-byte-offset@1.0.0: - resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} engines: {node: '>= 0.4'} dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.5 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 for-each: 0.3.3 - has-proto: 1.0.1 - is-typed-array: 1.1.12 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 dev: true - /typed-array-length@1.0.4: - resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + /typed-array-length@1.0.5: + resolution: {integrity: sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==} + engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 for-each: 0.3.3 - is-typed-array: 1.1.12 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 dev: true /typedarray@0.0.6: @@ -13708,8 +13905,8 @@ packages: hasBin: true dev: true - /ufo@1.3.2: - resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} + /ufo@1.4.0: + resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} dev: true /uglify-js@3.17.4: @@ -13723,7 +13920,7 @@ packages: /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 has-bigints: 1.0.2 has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 @@ -13733,8 +13930,8 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true - /undici@5.28.2: - resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} + /undici@5.28.3: + resolution: {integrity: sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==} engines: {node: '>=14.0'} dependencies: '@fastify/busboy': 2.1.0 @@ -13808,11 +14005,11 @@ packages: engines: {node: '>= 0.8'} dev: true - /unplugin@1.6.0: - resolution: {integrity: sha512-BfJEpWBu3aE/AyHx8VaNE/WgouoQxgH9baAiH82JjX8cqVyi3uJQstqwD5J+SZxIK326SZIhsSZlALXVBCknTQ==} + /unplugin@1.7.1: + resolution: {integrity: sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==} dependencies: acorn: 8.11.3 - chokidar: 3.5.3 + chokidar: 3.6.0 webpack-sources: 3.2.3 webpack-virtual-modules: 0.6.1 dev: true @@ -13822,14 +14019,14 @@ packages: engines: {node: '>=8'} dev: true - /update-browserslist-db@1.0.13(browserslist@4.22.2): + /update-browserslist-db@1.0.13(browserslist@4.23.0): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: - browserslist: 4.22.2 - escalade: 3.1.1 + browserslist: 4.23.0 + escalade: 3.1.2 picocolors: 1.0.0 dev: true @@ -13839,7 +14036,7 @@ packages: punycode: 2.3.1 dev: true - /use-callback-ref@1.3.1(@types/react@18.2.48)(react@18.2.0): + /use-callback-ref@1.3.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==} engines: {node: '>=10'} peerDependencies: @@ -13849,7 +14046,7 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 tslib: 2.6.2 @@ -13880,7 +14077,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.48)(react@18.2.0): + /use-isomorphic-layout-effect@1.1.2(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} peerDependencies: '@types/react': '*' @@ -13889,11 +14086,11 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 dev: false - /use-latest@1.2.1(@types/react@18.2.48)(react@18.2.0): + /use-latest@1.2.1(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==} peerDependencies: '@types/react': '*' @@ -13902,9 +14099,9 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 - use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.48)(react@18.2.0) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.2.57)(react@18.2.0) dev: false /use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0): @@ -13918,7 +14115,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /use-sidecar@1.1.2(@types/react@18.2.48)(react@18.2.0): + /use-sidecar@1.1.2(@types/react@18.2.57)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: @@ -13928,7 +14125,7 @@ packages: '@types/react': optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.6.2 @@ -13951,8 +14148,8 @@ packages: inherits: 2.0.4 is-arguments: 1.1.1 is-generator-function: 1.0.10 - is-typed-array: 1.1.12 - which-typed-array: 1.1.13 + is-typed-array: 1.1.13 + which-typed-array: 1.1.14 dev: true /utils-merge@1.0.1: @@ -13981,8 +14178,8 @@ packages: engines: {node: '>= 0.8'} dev: true - /vite-node@1.2.2(@types/node@20.11.5): - resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==} + /vite-node@1.3.1(@types/node@20.11.19): + resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -13990,7 +14187,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) transitivePeerDependencies: - '@types/node' - less @@ -14002,16 +14199,16 @@ packages: - terser dev: true - /vite-plugin-css-injected-by-js@3.3.1(vite@5.0.12): - resolution: {integrity: sha512-PjM/X45DR3/V1K1fTRs8HtZHEQ55kIfdrn+dzaqNBFrOYO073SeSNCxp4j7gSYhV9NffVHaEnOL4myoko0ePAg==} + /vite-plugin-css-injected-by-js@3.4.0(vite@5.1.3): + resolution: {integrity: sha512-wS5+UYtJXQ/vNornsqTQxOLBVO/UjXU54ZsYMeX0mj2OrbStMQ4GLgvneVDQGPwyGJcm/ntBPawc2lA7xx+Lpg==} peerDependencies: vite: '>2.0.0-0' dependencies: - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) dev: true - /vite-plugin-dts@3.7.1(@types/node@20.11.5)(typescript@5.3.3)(vite@5.0.12): - resolution: {integrity: sha512-VZJckNFpVfRAkmOxhGT5OgTUVWVXxkNQqLpBUuiNGAr9HbtvmvsPLo2JB3Xhn+o/Z9+CT6YZfYa4bX9SGR5hNw==} + /vite-plugin-dts@3.7.2(@types/node@20.11.19)(typescript@5.3.3)(vite@5.1.3): + resolution: {integrity: sha512-kg//1nDA01b8rufJf4TsvYN8LMkdwv0oBYpiQi6nRwpHyue+wTlhrBiqgipdFpMnW1oOYv6ywmzE5B0vg6vSEA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -14020,13 +14217,13 @@ packages: vite: optional: true dependencies: - '@microsoft/api-extractor': 7.39.0(@types/node@20.11.5) + '@microsoft/api-extractor': 7.39.0(@types/node@20.11.19) '@rollup/pluginutils': 5.1.0 '@vue/language-core': 1.8.27(typescript@5.3.3) debug: 4.3.4 kolorist: 1.8.0 typescript: 5.3.3 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) vue-tsc: 1.8.27(typescript@5.3.3) transitivePeerDependencies: - '@types/node' @@ -14034,20 +14231,20 @@ packages: - supports-color dev: true - /vite-plugin-eslint@1.8.1(eslint@8.56.0)(vite@5.0.12): + /vite-plugin-eslint@1.8.1(eslint@8.56.0)(vite@5.1.3): resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: eslint: '>=7' vite: '>=2' dependencies: '@rollup/pluginutils': 4.2.1 - '@types/eslint': 8.56.0 + '@types/eslint': 8.56.2 eslint: 8.56.0 rollup: 2.79.1 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) dev: true - /vite-tsconfig-paths@4.3.1(typescript@5.3.3)(vite@5.0.12): + /vite-tsconfig-paths@4.3.1(typescript@5.3.3)(vite@5.1.3): resolution: {integrity: sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==} peerDependencies: vite: '*' @@ -14057,15 +14254,15 @@ packages: dependencies: debug: 4.3.4 globrex: 0.1.2 - tsconfck: 3.0.1(typescript@5.3.3) - vite: 5.0.12(@types/node@20.11.5) + tsconfck: 3.0.2(typescript@5.3.3) + vite: 5.1.3(@types/node@20.11.19) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.0.12(@types/node@20.11.5): - resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} + /vite@5.1.3(@types/node@20.11.19): + resolution: {integrity: sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -14092,23 +14289,23 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.5 - esbuild: 0.19.11 - postcss: 8.4.33 - rollup: 4.9.4 + '@types/node': 20.11.19 + esbuild: 0.19.12 + postcss: 8.4.35 + rollup: 4.12.0 optionalDependencies: fsevents: 2.3.3 dev: true - /vitest@1.2.2(@types/node@20.11.5): - resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==} + /vitest@1.3.1(@types/node@20.11.19): + resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': ^1.0.0 - '@vitest/ui': ^1.0.0 + '@vitest/browser': 1.3.1 + '@vitest/ui': 1.3.1 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -14125,27 +14322,26 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.5 - '@vitest/expect': 1.2.2 - '@vitest/runner': 1.2.2 - '@vitest/snapshot': 1.2.2 - '@vitest/spy': 1.2.2 - '@vitest/utils': 1.2.2 + '@types/node': 20.11.19 + '@vitest/expect': 1.3.1 + '@vitest/runner': 1.3.1 + '@vitest/snapshot': 1.3.1 + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 acorn-walk: 8.3.2 - cac: 6.7.14 chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 local-pkg: 0.5.0 - magic-string: 0.30.5 + magic-string: 0.30.7 pathe: 1.1.2 picocolors: 1.0.0 std-env: 3.7.0 - strip-literal: 1.3.0 + strip-literal: 2.0.0 tinybench: 2.6.0 tinypool: 0.8.2 - vite: 5.0.12(@types/node@20.11.5) - vite-node: 1.2.2(@types/node@20.11.5) + vite: 5.1.3(@types/node@20.11.19) + vite-node: 1.3.1(@types/node@20.11.19) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -14177,7 +14373,7 @@ packages: dependencies: '@volar/typescript': 1.11.1 '@vue/language-core': 1.8.27(typescript@5.3.3) - semver: 7.5.4 + semver: 7.6.0 typescript: 5.3.3 dev: true @@ -14239,7 +14435,7 @@ packages: engines: {node: '>= 0.4'} dependencies: function.prototype.name: 1.1.6 - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 is-async-function: 2.0.0 is-date-object: 1.0.5 is-finalizationregistry: 1.0.2 @@ -14249,7 +14445,7 @@ packages: isarray: 2.0.5 which-boxed-primitive: 1.0.2 which-collection: 1.0.1 - which-typed-array: 1.1.13 + which-typed-array: 1.1.14 dev: true /which-collection@1.0.1: @@ -14261,15 +14457,15 @@ packages: is-weakset: 2.0.2 dev: true - /which-typed-array@1.1.13: - resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + /which-typed-array@1.1.14: + resolution: {integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==} engines: {node: '>= 0.4'} dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.5 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 for-each: 0.3.3 gopd: 1.0.1 - has-tostringtag: 1.0.0 + has-tostringtag: 1.0.2 dev: true /which@2.0.2: @@ -14409,7 +14605,7 @@ packages: engines: {node: '>=12'} dependencies: cliui: 8.0.1 - escalade: 3.1.1 + escalade: 3.1.2 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 @@ -14446,8 +14642,8 @@ packages: commander: 9.5.0 dev: true - /zod-validation-error@3.0.0(zod@3.22.4): - resolution: {integrity: sha512-x+agsJJG9rvC7axF0xqTEdZhJkLHyIZkdOAWDJSmwGPzxNHMHwtU6w2yDOAAP6yuSfTAUhAMJRBfhVGY64ySEQ==} + /zod-validation-error@3.0.2(zod@3.22.4): + resolution: {integrity: sha512-21xGaDmnU7lJZ4J63n5GXWqi+rTzGy3gDHbuZ1jP6xrK/DEQGyOqs/xW7eH96tIfCOYm+ecCuT0bfajBRKEVUw==} engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.18.0 @@ -14459,12 +14655,12 @@ packages: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false - /zustand@4.4.7(@types/react@18.2.48)(react@18.2.0): - resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} + /zustand@4.5.1(@types/react@18.2.57)(react@18.2.0): + resolution: {integrity: sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==} engines: {node: '>=12.7.0'} peerDependencies: '@types/react': '>=16.8' - immer: '>=9.0' + immer: '>=9.0.6' react: '>=16.8' peerDependenciesMeta: '@types/react': @@ -14474,7 +14670,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.48 + '@types/react': 18.2.57 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts index 61fbd015f8..c8237664c2 100644 --- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/driver.ts @@ -2,15 +2,15 @@ import { StorageError } from 'app/store/enhancers/reduxRemember/errors'; import { $projectId } from 'app/store/nanostores/projectId'; import type { UseStore } from 'idb-keyval'; import { clear, createStore as createIDBKeyValStore, get, set } from 'idb-keyval'; -import { action, atom } from 'nanostores'; +import { atom } from 'nanostores'; import type { Driver } from 'redux-remember'; // Create a custom idb-keyval store (just needed to customize the name) export const $idbKeyValStore = atom(createIDBKeyValStore('invoke', 'invoke-store')); -export const clearIdbKeyValStore = action($idbKeyValStore, 'clear', (store) => { - clear(store.get()); -}); +export const clearIdbKeyValStore = () => { + clear($idbKeyValStore.get()); +}; // Create redux-remember driver, wrapping idb-keyval export const idbKeyValDriver: Driver = { diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 82f382ee10..3b5a10492c 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -1,6 +1,7 @@ +import { createSelectorCreator, lruMemoize } from '@reduxjs/toolkit'; import type { FetchBaseQueryArgs } from '@reduxjs/toolkit/dist/query/fetchBaseQuery'; import type { BaseQueryFn, FetchArgs, FetchBaseQueryError, TagDescription } from '@reduxjs/toolkit/query/react'; -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import { buildCreateApi, coreModule, fetchBaseQuery, reactHooksModule } from '@reduxjs/toolkit/query/react'; import { $authToken } from 'app/store/nanostores/authToken'; import { $baseUrl } from 'app/store/nanostores/baseUrl'; import { $projectId } from 'app/store/nanostores/projectId'; @@ -85,7 +86,14 @@ const dynamicBaseQuery: BaseQueryFn Date: Wed, 21 Feb 2024 11:54:02 -0500 Subject: [PATCH 232/411] Create /search endpoint, update model object structure in scan model page --- invokeai/app/api/routers/model_manager.py | 31 +++++++++++++++++++ .../subpanels/ImportModelsPanel.tsx | 2 +- .../subpanels/ModelManagerPanel/ModelList.tsx | 6 ++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 6b7111dd2c..be4ed75069 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -32,6 +32,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.search import ModelSearch from ..dependencies import ApiDependencies @@ -233,6 +234,36 @@ async def list_tags() -> Set[str]: result: Set[str] = record_store.list_tags() return result +@model_manager_router.get( + "/search", + operation_id="search_for_models", + responses={ + 200: {"description": "Directory searched successfully"}, + 404: {"description": "Invalid directory path"}, + }, + status_code=200, + response_model=List[pathlib.Path], +) +async def search_for_models( + search_path: str = Query(description="Directory path to search for models", default=None), +) -> List[pathlib.Path]: + path = pathlib.Path(search_path) + if not search_path or not path.is_dir(): + raise HTTPException( + status_code=404, + detail=f"The search path '{search_path}' does not exist or is not directory", + ) + + search = ModelSearch() + try: + models_found = list(search.search(path)) + except Exception as e: + raise HTTPException( + status_code=404, + detail=f"An error occurred while searching the directory: {e}", + ) + return models_found + @model_manager_router.get( "/tags/search", diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx index 18fcef9614..960f798e79 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx @@ -20,7 +20,7 @@ const ImportModelsPanel = () => { - diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx index dd74bb0c23..49c9bc6112 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx @@ -139,10 +139,10 @@ const modelsFilter = ( return; } - const matchesFilter = model.model_name.toLowerCase().includes(nameFilter.toLowerCase()); + const matchesFilter = model.name.toLowerCase().includes(nameFilter.toLowerCase()); - const matchesFormat = model_format === undefined || model.model_format === model_format; - const matchesType = model.model_type === model_type; + const matchesFormat = model_format === undefined || model.format === model_format; + const matchesType = model.type === model_type; if (matchesFilter && matchesFormat && matchesType) { filteredModels.push(model); From 9269bdd233d6c02e3bdc253429403991fce88058 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 22 Feb 2024 09:08:18 -0500 Subject: [PATCH 233/411] rename endpoint for scanning --- invokeai/app/api/routers/model_manager.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index be4ed75069..30b78f589d 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -235,23 +235,23 @@ async def list_tags() -> Set[str]: return result @model_manager_router.get( - "/search", - operation_id="search_for_models", + "/scan_folder", + operation_id="scan_for_models", responses={ - 200: {"description": "Directory searched successfully"}, - 404: {"description": "Invalid directory path"}, + 200: {"description": "Directory scanned successfully"}, + 400: {"description": "Invalid directory path"}, }, status_code=200, response_model=List[pathlib.Path], ) -async def search_for_models( - search_path: str = Query(description="Directory path to search for models", default=None), +async def scan_for_models( + scan_path: str = Query(description="Directory path to search for models", default=None), ) -> List[pathlib.Path]: - path = pathlib.Path(search_path) - if not search_path or not path.is_dir(): + path = pathlib.Path(scan_path) + if not scan_path or not path.is_dir(): raise HTTPException( - status_code=404, - detail=f"The search path '{search_path}' does not exist or is not directory", + status_code=400, + detail=f"The search path '{scan_path}' does not exist or is not directory", ) search = ModelSearch() @@ -259,7 +259,7 @@ async def search_for_models( models_found = list(search.search(path)) except Exception as e: raise HTTPException( - status_code=404, + status_code=500, detail=f"An error occurred while searching the directory: {e}", ) return models_found From f7fc20459ad4f21440578335fc5dfecf150b17f8 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Thu, 22 Feb 2024 09:13:50 -0500 Subject: [PATCH 234/411] Run ruff --- invokeai/app/api/routers/model_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 30b78f589d..aee457406a 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -234,6 +234,7 @@ async def list_tags() -> Set[str]: result: Set[str] = record_store.list_tags() return result + @model_manager_router.get( "/scan_folder", operation_id="scan_for_models", From 06cc57d82abedd4ee7165eeb487143ac0e938e43 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Tue, 20 Feb 2024 21:13:19 -0500 Subject: [PATCH 235/411] feat(nodes): added gradient mask node --- invokeai/app/invocations/fields.py | 1 + invokeai/app/invocations/latent.py | 69 +++++++++++++++++-- invokeai/app/invocations/primitives.py | 8 ++- .../stable_diffusion/diffusers_pipeline.py | 13 +++- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 40d403c03d..7f2d2783f2 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -199,6 +199,7 @@ class DenoiseMaskField(BaseModel): mask_name: str = Field(description="The name of the mask image") masked_latents_name: Optional[str] = Field(default=None, description="The name of the masked image latents") + gradient: Optional[bool] = Field(default=False, description="Used for gradient inpainting") class LatentsField(BaseModel): diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index bfe7255b62..97d3c705d4 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -23,7 +23,7 @@ from diffusers.models.attention_processor import ( from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel from diffusers.schedulers import DPMSolverSDEScheduler from diffusers.schedulers import SchedulerMixin as Scheduler -from PIL import Image +from PIL import Image, ImageFilter from pydantic import field_validator from torchvision.transforms.functional import resize as tv_resize @@ -128,7 +128,7 @@ class CreateDenoiseMaskInvocation(BaseInvocation): ui_order=4, ) - def prep_mask_tensor(self, mask_image: Image) -> torch.Tensor: + def prep_mask_tensor(self, mask_image: Image.Image) -> torch.Tensor: if mask_image.mode != "L": mask_image = mask_image.convert("L") mask_tensor: torch.Tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False) @@ -169,6 +169,62 @@ class CreateDenoiseMaskInvocation(BaseInvocation): return DenoiseMaskOutput.build( mask_name=mask_name, masked_latents_name=masked_latents_name, + gradient=False, + ) + + +@invocation( + "create_gradient_mask", + title="Create Gradient Mask", + tags=["mask", "denoise"], + category="latents", + version="1.0.0", +) +class CreateGradientMaskInvocation(BaseInvocation): + """Creates mask for denoising model run.""" + + mask: ImageField = InputField(default=None, description="Image which will be masked", ui_order=1) + edge_radius: int = InputField( + default=16, ge=0, description="How far to blur/expand the edges of the mask", ui_order=2 + ) + coherence_mode: Literal["Gaussian Blur", "Box Blur", "Staged"] = InputField(default="Gaussian Blur", ui_order=3) + minimum_denoise: float = InputField( + default=0.0, ge=0, le=1, description="Minimum denoise level for the coherence region", ui_order=4 + ) + + @torch.no_grad() + def invoke(self, context: InvocationContext) -> DenoiseMaskOutput: + mask_image = context.images.get_pil(self.mask.image_name, mode="L") + if self.coherence_mode == "Box Blur": + blur_mask = mask_image.filter(ImageFilter.BoxBlur(self.edge_radius)) + else: # Gaussian Blur OR Staged + # Gaussian Blur uses standard deviation. 1/2 radius is a good approximation + blur_mask = mask_image.filter(ImageFilter.GaussianBlur(self.edge_radius / 2)) + + mask_tensor: torch.Tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False) + blur_tensor: torch.Tensor = image_resized_to_grid_as_tensor(blur_mask, normalize=False) + + # redistribute blur so that the edges are 0 and blur out to 1 + blur_tensor = (blur_tensor - 0.5) * 2 + + threshold = 1 - self.minimum_denoise + + if self.coherence_mode == "Staged": + # wherever the blur_tensor is masked to any degree, convert it to threshold + blur_tensor = torch.where((blur_tensor < 1), threshold, blur_tensor) + else: + # wherever the blur_tensor is above threshold but less than 1, drop it to threshold + blur_tensor = torch.where((blur_tensor > threshold) & (blur_tensor < 1), threshold, blur_tensor) + + # multiply original mask to force actually masked regions to 0 + blur_tensor = mask_tensor * blur_tensor + + mask_name = context.tensors.save(tensor=blur_tensor.unsqueeze(1)) + + return DenoiseMaskOutput.build( + mask_name=mask_name, + masked_latents_name=None, + gradient=True, ) @@ -606,9 +662,9 @@ class DenoiseLatentsInvocation(BaseInvocation): def prep_inpaint_mask( self, context: InvocationContext, latents: torch.Tensor - ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor]]: + ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], bool]: if self.denoise_mask is None: - return None, None + return None, None, False mask = context.tensors.load(self.denoise_mask.mask_name) mask = tv_resize(mask, latents.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False) @@ -617,7 +673,7 @@ class DenoiseLatentsInvocation(BaseInvocation): else: masked_latents = None - return 1 - mask, masked_latents + return 1 - mask, masked_latents, self.denoise_mask.gradient @torch.no_grad() def invoke(self, context: InvocationContext) -> LatentsOutput: @@ -644,7 +700,7 @@ class DenoiseLatentsInvocation(BaseInvocation): if seed is None: seed = 0 - mask, masked_latents = self.prep_inpaint_mask(context, latents) + mask, masked_latents, gradient_mask = self.prep_inpaint_mask(context, latents) # TODO(ryand): I have hard-coded `do_classifier_free_guidance=True` to mirror the behaviour of ControlNets, # below. Investigate whether this is appropriate. @@ -732,6 +788,7 @@ class DenoiseLatentsInvocation(BaseInvocation): seed=seed, mask=mask, masked_latents=masked_latents, + gradient_mask=gradient_mask, num_inference_steps=num_inference_steps, conditioning_data=conditioning_data, control_data=controlnet_data, diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index 4342213482..c761bb0895 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -299,9 +299,13 @@ class DenoiseMaskOutput(BaseInvocationOutput): denoise_mask: DenoiseMaskField = OutputField(description="Mask for denoise model run") @classmethod - def build(cls, mask_name: str, masked_latents_name: Optional[str] = None) -> "DenoiseMaskOutput": + def build( + cls, mask_name: str, masked_latents_name: Optional[str] = None, gradient: Optional[bool] = False + ) -> "DenoiseMaskOutput": return cls( - denoise_mask=DenoiseMaskField(mask_name=mask_name, masked_latents_name=masked_latents_name), + denoise_mask=DenoiseMaskField( + mask_name=mask_name, masked_latents_name=masked_latents_name, gradient=gradient + ), ) diff --git a/invokeai/backend/stable_diffusion/diffusers_pipeline.py b/invokeai/backend/stable_diffusion/diffusers_pipeline.py index a85e3762dc..fd3ecde47b 100644 --- a/invokeai/backend/stable_diffusion/diffusers_pipeline.py +++ b/invokeai/backend/stable_diffusion/diffusers_pipeline.py @@ -86,6 +86,7 @@ class AddsMaskGuidance: mask_latents: torch.FloatTensor scheduler: SchedulerMixin noise: torch.Tensor + gradient_mask: bool def __call__(self, step_output: Union[BaseOutput, SchedulerOutput], t: torch.Tensor, conditioning) -> BaseOutput: output_class = step_output.__class__ # We'll create a new one with masked data. @@ -121,7 +122,12 @@ class AddsMaskGuidance: # TODO: Do we need to also apply scheduler.scale_model_input? Or is add_noise appropriately scaled already? # mask_latents = self.scheduler.scale_model_input(mask_latents, t) mask_latents = einops.repeat(mask_latents, "b c h w -> (repeat b) c h w", repeat=batch_size) - masked_input = torch.lerp(mask_latents.to(dtype=latents.dtype), latents, mask.to(dtype=latents.dtype)) + if self.gradient_mask: + threshhold = (t.item()) / self.scheduler.config.num_train_timesteps + mask_bool = mask > threshhold # I don't know when mask got inverted, but it did + masked_input = torch.where(mask_bool, latents, mask_latents) + else: + masked_input = torch.lerp(mask_latents.to(dtype=latents.dtype), latents, mask.to(dtype=latents.dtype)) return masked_input @@ -335,6 +341,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline): t2i_adapter_data: Optional[list[T2IAdapterData]] = None, mask: Optional[torch.Tensor] = None, masked_latents: Optional[torch.Tensor] = None, + gradient_mask: Optional[bool] = False, seed: Optional[int] = None, ) -> tuple[torch.Tensor, Optional[AttentionMapSaver]]: if init_timestep.shape[0] == 0: @@ -375,7 +382,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline): self._unet_forward, mask, masked_latents ) else: - additional_guidance.append(AddsMaskGuidance(mask, orig_latents, self.scheduler, noise)) + additional_guidance.append(AddsMaskGuidance(mask, orig_latents, self.scheduler, noise, gradient_mask)) try: latents, attention_map_saver = self.generate_latents_from_embeddings( @@ -392,7 +399,7 @@ class StableDiffusionGeneratorPipeline(StableDiffusionPipeline): self.invokeai_diffuser.model_forward_callback = self._unet_forward # restore unmasked part - if mask is not None: + if mask is not None and not gradient_mask: latents = torch.lerp(orig_latents, latents.to(dtype=orig_latents.dtype), mask.to(dtype=orig_latents.dtype)) return latents, attention_map_saver From 07dde92664dc0e35a4531b7e10e254cb852af041 Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Tue, 20 Feb 2024 21:47:25 -0500 Subject: [PATCH 236/411] chore: typing fix --- invokeai/app/invocations/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py index 7f2d2783f2..712ab415b0 100644 --- a/invokeai/app/invocations/fields.py +++ b/invokeai/app/invocations/fields.py @@ -199,7 +199,7 @@ class DenoiseMaskField(BaseModel): mask_name: str = Field(description="The name of the mask image") masked_latents_name: Optional[str] = Field(default=None, description="The name of the masked image latents") - gradient: Optional[bool] = Field(default=False, description="Used for gradient inpainting") + gradient: bool = Field(default=False, description="Used for gradient inpainting") class LatentsField(BaseModel): From 30a374a70f000611fb5a27902ab40285b36e886c Mon Sep 17 00:00:00 2001 From: dunkeroni Date: Tue, 20 Feb 2024 22:13:01 -0500 Subject: [PATCH 237/411] chore: typing --- invokeai/app/invocations/primitives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index c761bb0895..b80e34dc98 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -300,7 +300,7 @@ class DenoiseMaskOutput(BaseInvocationOutput): @classmethod def build( - cls, mask_name: str, masked_latents_name: Optional[str] = None, gradient: Optional[bool] = False + cls, mask_name: str, masked_latents_name: Optional[str] = None, gradient: bool = False ) -> "DenoiseMaskOutput": return cls( denoise_mask=DenoiseMaskField( From 5bb3aeaccda06bc034ee8f4d7eac2f7a428f19c1 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 21 Feb 2024 10:18:30 -0500 Subject: [PATCH 238/411] remove startup dependency on legacy models.yaml file --- invokeai/app/services/config/config_base.py | 1 + .../app/services/config/config_default.py | 5 +- invokeai/backend/install/check_root.py | 1 - .../backend/install/invokeai_configure.py | 2 +- invokeai/backend/model_manager/merge.py | 4 +- .../training/textual_inversion_training.py | 2 +- invokeai/frontend/install/model_install.py | 2 +- invokeai/frontend/merge/merge_diffusers.py | 2 +- .../frontend/training/textual_inversion.py | 4 +- .../frontend/training/textual_inversion2.py | 454 ------------------ invokeai/frontend/web/public/locales/en.json | 2 +- 11 files changed, 11 insertions(+), 468 deletions(-) mode change 100755 => 100644 invokeai/frontend/training/textual_inversion.py delete mode 100644 invokeai/frontend/training/textual_inversion2.py diff --git a/invokeai/app/services/config/config_base.py b/invokeai/app/services/config/config_base.py index c73aa43809..20dac14937 100644 --- a/invokeai/app/services/config/config_base.py +++ b/invokeai/app/services/config/config_base.py @@ -156,6 +156,7 @@ class InvokeAISettings(BaseSettings): "lora_dir", "embedding_dir", "controlnet_dir", + "conf_path", ] @classmethod diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 2af775372d..01fd5e2179 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -30,7 +30,6 @@ InvokeAI: lora_dir: null embedding_dir: null controlnet_dir: null - conf_path: configs/models.yaml models_dir: models legacy_conf_dir: configs/stable-diffusion db_dir: databases @@ -123,7 +122,6 @@ a Path object: root_path - path to InvokeAI root output_path - path to default outputs directory - model_conf_path - path to models.yaml conf - alias for the above embedding_path - path to the embeddings directory lora_path - path to the LoRA directory @@ -163,7 +161,6 @@ two configs are kept in separate sections of the config file: InvokeAI: Paths: root: /home/lstein/invokeai-main - conf_path: configs/models.yaml legacy_conf_dir: configs/stable-diffusion outdir: outputs ... @@ -237,7 +234,6 @@ class InvokeAIAppConfig(InvokeAISettings): # PATHS root : Optional[Path] = Field(default=None, description='InvokeAI runtime root directory', json_schema_extra=Categories.Paths) autoimport_dir : Path = Field(default=Path('autoimport'), description='Path to a directory of models files to be imported on startup.', json_schema_extra=Categories.Paths) - conf_path : Path = Field(default=Path('configs/models.yaml'), description='Path to models definition file', json_schema_extra=Categories.Paths) models_dir : Path = Field(default=Path('models'), description='Path to the models directory', json_schema_extra=Categories.Paths) convert_cache_dir : Path = Field(default=Path('models/.cache'), description='Path to the converted models cache directory', json_schema_extra=Categories.Paths) legacy_conf_dir : Path = Field(default=Path('configs/stable-diffusion'), description='Path to directory of legacy checkpoint config files', json_schema_extra=Categories.Paths) @@ -301,6 +297,7 @@ class InvokeAIAppConfig(InvokeAISettings): lora_dir : Optional[Path] = Field(default=None, description='Path to a directory of LoRA/LyCORIS models to be imported on startup.', json_schema_extra=Categories.Paths) embedding_dir : Optional[Path] = Field(default=None, description='Path to a directory of Textual Inversion embeddings to be imported on startup.', json_schema_extra=Categories.Paths) controlnet_dir : Optional[Path] = Field(default=None, description='Path to a directory of ControlNet embeddings to be imported on startup.', json_schema_extra=Categories.Paths) + conf_path : Path = Field(default=Path('configs/models.yaml'), description='Path to models definition file', json_schema_extra=Categories.Paths) # this is not referred to in the source code and can be removed entirely #free_gpu_mem : Optional[bool] = Field(default=None, description="If true, purge model from GPU after each generation.", json_schema_extra=Categories.MemoryPerformance) diff --git a/invokeai/backend/install/check_root.py b/invokeai/backend/install/check_root.py index ee264016b4..cbf9976123 100644 --- a/invokeai/backend/install/check_root.py +++ b/invokeai/backend/install/check_root.py @@ -8,7 +8,6 @@ from invokeai.app.services.config import InvokeAIAppConfig def check_invokeai_root(config: InvokeAIAppConfig): try: - assert config.model_conf_path.exists(), f"{config.model_conf_path} not found" assert config.db_path.parent.exists(), f"{config.db_path.parent} not found" assert config.models_path.exists(), f"{config.models_path} not found" if not config.ignore_missing_core_models: diff --git a/invokeai/backend/install/invokeai_configure.py b/invokeai/backend/install/invokeai_configure.py index ac3e583de3..53cca64a1a 100755 --- a/invokeai/backend/install/invokeai_configure.py +++ b/invokeai/backend/install/invokeai_configure.py @@ -939,7 +939,7 @@ def main() -> None: # run this unconditionally in case new directories need to be added initialize_rootdir(config.root_path, opt.yes_to_all) - # this will initialize the models.yaml file if not present + # this will initialize and populate the models tables if not present install_helper = InstallHelper(config, logger) models_to_download = default_user_selections(opt, install_helper) diff --git a/invokeai/backend/model_manager/merge.py b/invokeai/backend/model_manager/merge.py index 7063cb907d..1a3b9cb7de 100644 --- a/invokeai/backend/model_manager/merge.py +++ b/invokeai/backend/model_manager/merge.py @@ -1,7 +1,7 @@ """ invokeai.backend.model_manager.merge exports: merge_diffusion_models() -- combine multiple models by location and return a pipeline object -merge_diffusion_models_and_commit() -- combine multiple models by ModelManager ID and write to models.yaml +merge_diffusion_models_and_commit() -- combine multiple models by ModelManager ID and write to the models tables Copyright (c) 2023 Lincoln Stein and the InvokeAI Development Team """ @@ -101,7 +101,7 @@ class ModelMerger(object): **kwargs: Any, ) -> AnyModelConfig: """ - :param models: up to three models, designated by their InvokeAI models.yaml model name + :param models: up to three models, designated by their registered InvokeAI model name :param merged_model_name: name for new model :param alpha: The interpolation parameter. Ranges from 0 to 1. It affects the ratio in which the checkpoints are merged. A 0.8 alpha would mean that the first model checkpoints would affect the final result far less than an alpha of 0.2 diff --git a/invokeai/backend/training/textual_inversion_training.py b/invokeai/backend/training/textual_inversion_training.py index e31ce959c2..9a38c006a5 100644 --- a/invokeai/backend/training/textual_inversion_training.py +++ b/invokeai/backend/training/textual_inversion_training.py @@ -120,7 +120,7 @@ def parse_args() -> Namespace: "--model", type=str, default="sd-1/main/stable-diffusion-v1-5", - help="Name of the diffusers model to train against, as defined in configs/models.yaml.", + help="Name of the diffusers model to train against.", ) model_group.add_argument( "--revision", diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index 3a4d66ae0a..2f7fd0a1d0 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -455,7 +455,7 @@ class addModelsForm(CyclingForm, npyscreen.FormMultiPage): selections = self.parentApp.install_selections all_models = self.all_models - # Defined models (in INITIAL_CONFIG.yaml or models.yaml) to add/remove + # Defined models (in INITIAL_CONFIG.yaml or invokeai.db) to add/remove ui_sections = [ self.starter_pipelines, self.pipeline_models, diff --git a/invokeai/frontend/merge/merge_diffusers.py b/invokeai/frontend/merge/merge_diffusers.py index 5484040674..ff9acd569e 100644 --- a/invokeai/frontend/merge/merge_diffusers.py +++ b/invokeai/frontend/merge/merge_diffusers.py @@ -435,7 +435,7 @@ def main(): run_cli(args) except widget.NotEnoughSpaceForWidget as e: if str(e).startswith("Height of 1 allocated"): - logger.error("You need to have at least two diffusers models defined in models.yaml in order to merge") + logger.error("You need to have at least two diffusers models in order to merge") else: logger.error("Not enough room for the user interface. Try making this window larger.") sys.exit(-1) diff --git a/invokeai/frontend/training/textual_inversion.py b/invokeai/frontend/training/textual_inversion.py old mode 100755 new mode 100644 index 81b1081bb8..6250b313b0 --- a/invokeai/frontend/training/textual_inversion.py +++ b/invokeai/frontend/training/textual_inversion.py @@ -261,7 +261,7 @@ class textualInversionForm(npyscreen.FormMultiPageAction): def validate_field_values(self) -> bool: bad_fields = [] if self.model.value is None: - bad_fields.append("Model Name must correspond to a known model in models.yaml") + bad_fields.append("Model Name must correspond to a known model in invokeai.db") if not re.match("^[a-zA-Z0-9.-]+$", self.placeholder_token.value): bad_fields.append("Trigger term must only contain alphanumeric characters, the dot and hyphen") if self.train_data_dir.value is None: @@ -442,7 +442,7 @@ def main() -> None: pass except (widget.NotEnoughSpaceForWidget, Exception) as e: if str(e).startswith("Height of 1 allocated"): - logger.error("You need to have at least one diffusers models defined in models.yaml in order to train") + logger.error("You need to have at least one diffusers models defined in invokeai.db in order to train") elif str(e).startswith("addwstr"): logger.error("Not enough window space for the interface. Please make your window larger and try again.") else: diff --git a/invokeai/frontend/training/textual_inversion2.py b/invokeai/frontend/training/textual_inversion2.py deleted file mode 100644 index 81b1081bb8..0000000000 --- a/invokeai/frontend/training/textual_inversion2.py +++ /dev/null @@ -1,454 +0,0 @@ -#!/usr/bin/env python - -""" -This is the frontend to "textual_inversion_training.py". - -Copyright (c) 2023-24 Lincoln Stein and the InvokeAI Development Team -""" - - -import os -import re -import shutil -import sys -import traceback -from argparse import Namespace -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -import npyscreen -from npyscreen import widget -from omegaconf import OmegaConf - -import invokeai.backend.util.logging as logger -from invokeai.app.services.config import InvokeAIAppConfig -from invokeai.backend.install.install_helper import initialize_installer -from invokeai.backend.model_manager import ModelType -from invokeai.backend.training import do_textual_inversion_training, parse_args - -TRAINING_DATA = "text-inversion-training-data" -TRAINING_DIR = "text-inversion-output" -CONF_FILE = "preferences.conf" -config = None - - -class textualInversionForm(npyscreen.FormMultiPageAction): - resolutions = [512, 768, 1024] - lr_schedulers = [ - "linear", - "cosine", - "cosine_with_restarts", - "polynomial", - "constant", - "constant_with_warmup", - ] - precisions = ["no", "fp16", "bf16"] - learnable_properties = ["object", "style"] - - def __init__(self, parentApp: npyscreen.NPSAppManaged, name: str, saved_args: Optional[Dict[str, str]] = None): - self.saved_args = saved_args or {} - super().__init__(parentApp, name) - - def afterEditing(self) -> None: - self.parentApp.setNextForm(None) - - def create(self) -> None: - self.model_names, default = self.get_model_names() - default_initializer_token = "★" - default_placeholder_token = "" - saved_args = self.saved_args - - assert config is not None - - try: - default = self.model_names.index(saved_args["model"]) - except Exception: - pass - - self.add_widget_intelligent( - npyscreen.FixedText, - value="Use ctrl-N and ctrl-P to move to the ext and

revious fields, cursor arrows to make a selection, and space to toggle checkboxes.", - editable=False, - ) - - self.model = self.add_widget_intelligent( - npyscreen.TitleSelectOne, - name="Model Name:", - values=sorted(self.model_names), - value=default, - max_height=len(self.model_names) + 1, - scroll_exit=True, - ) - self.placeholder_token = self.add_widget_intelligent( - npyscreen.TitleText, - name="Trigger Term:", - value="", # saved_args.get('placeholder_token',''), # to restore previous term - scroll_exit=True, - ) - self.placeholder_token.when_value_edited = self.initializer_changed - self.nextrely -= 1 - self.nextrelx += 30 - self.prompt_token = self.add_widget_intelligent( - npyscreen.FixedText, - name="Trigger term for use in prompt", - value="", - editable=False, - scroll_exit=True, - ) - self.nextrelx -= 30 - self.initializer_token = self.add_widget_intelligent( - npyscreen.TitleText, - name="Initializer:", - value=saved_args.get("initializer_token", default_initializer_token), - scroll_exit=True, - ) - self.resume_from_checkpoint = self.add_widget_intelligent( - npyscreen.Checkbox, - name="Resume from last saved checkpoint", - value=False, - scroll_exit=True, - ) - self.learnable_property = self.add_widget_intelligent( - npyscreen.TitleSelectOne, - name="Learnable property:", - values=self.learnable_properties, - value=self.learnable_properties.index(saved_args.get("learnable_property", "object")), - max_height=4, - scroll_exit=True, - ) - self.train_data_dir = self.add_widget_intelligent( - npyscreen.TitleFilename, - name="Data Training Directory:", - select_dir=True, - must_exist=False, - value=str( - saved_args.get( - "train_data_dir", - config.root_dir / TRAINING_DATA / default_placeholder_token, - ) - ), - scroll_exit=True, - ) - self.output_dir = self.add_widget_intelligent( - npyscreen.TitleFilename, - name="Output Destination Directory:", - select_dir=True, - must_exist=False, - value=str( - saved_args.get( - "output_dir", - config.root_dir / TRAINING_DIR / default_placeholder_token, - ) - ), - scroll_exit=True, - ) - self.resolution = self.add_widget_intelligent( - npyscreen.TitleSelectOne, - name="Image resolution (pixels):", - values=self.resolutions, - value=self.resolutions.index(saved_args.get("resolution", 512)), - max_height=4, - scroll_exit=True, - ) - self.center_crop = self.add_widget_intelligent( - npyscreen.Checkbox, - name="Center crop images before resizing to resolution", - value=saved_args.get("center_crop", False), - scroll_exit=True, - ) - self.mixed_precision = self.add_widget_intelligent( - npyscreen.TitleSelectOne, - name="Mixed Precision:", - values=self.precisions, - value=self.precisions.index(saved_args.get("mixed_precision", "fp16")), - max_height=4, - scroll_exit=True, - ) - self.num_train_epochs = self.add_widget_intelligent( - npyscreen.TitleSlider, - name="Number of training epochs:", - out_of=1000, - step=50, - lowest=1, - value=saved_args.get("num_train_epochs", 100), - scroll_exit=True, - ) - self.max_train_steps = self.add_widget_intelligent( - npyscreen.TitleSlider, - name="Max Training Steps:", - out_of=10000, - step=500, - lowest=1, - value=saved_args.get("max_train_steps", 3000), - scroll_exit=True, - ) - self.train_batch_size = self.add_widget_intelligent( - npyscreen.TitleSlider, - name="Batch Size (reduce if you run out of memory):", - out_of=50, - step=1, - lowest=1, - value=saved_args.get("train_batch_size", 8), - scroll_exit=True, - ) - self.gradient_accumulation_steps = self.add_widget_intelligent( - npyscreen.TitleSlider, - name="Gradient Accumulation Steps (may need to decrease this to resume from a checkpoint):", - out_of=10, - step=1, - lowest=1, - value=saved_args.get("gradient_accumulation_steps", 4), - scroll_exit=True, - ) - self.lr_warmup_steps = self.add_widget_intelligent( - npyscreen.TitleSlider, - name="Warmup Steps:", - out_of=100, - step=1, - lowest=0, - value=saved_args.get("lr_warmup_steps", 0), - scroll_exit=True, - ) - self.learning_rate = self.add_widget_intelligent( - npyscreen.TitleText, - name="Learning Rate:", - value=str( - saved_args.get("learning_rate", "5.0e-04"), - ), - scroll_exit=True, - ) - self.scale_lr = self.add_widget_intelligent( - npyscreen.Checkbox, - name="Scale learning rate by number GPUs, steps and batch size", - value=saved_args.get("scale_lr", True), - scroll_exit=True, - ) - self.enable_xformers_memory_efficient_attention = self.add_widget_intelligent( - npyscreen.Checkbox, - name="Use xformers acceleration", - value=saved_args.get("enable_xformers_memory_efficient_attention", False), - scroll_exit=True, - ) - self.lr_scheduler = self.add_widget_intelligent( - npyscreen.TitleSelectOne, - name="Learning rate scheduler:", - values=self.lr_schedulers, - max_height=7, - value=self.lr_schedulers.index(saved_args.get("lr_scheduler", "constant")), - scroll_exit=True, - ) - self.model.editing = True - - def initializer_changed(self) -> None: - placeholder = self.placeholder_token.value - self.prompt_token.value = f"(Trigger by using <{placeholder}> in your prompts)" - self.train_data_dir.value = str(config.root_dir / TRAINING_DATA / placeholder) - self.output_dir.value = str(config.root_dir / TRAINING_DIR / placeholder) - self.resume_from_checkpoint.value = Path(self.output_dir.value).exists() - - def on_ok(self): - if self.validate_field_values(): - self.parentApp.setNextForm(None) - self.editing = False - self.parentApp.ti_arguments = self.marshall_arguments() - npyscreen.notify("Launching textual inversion training. This will take a while...") - else: - self.editing = True - - def ok_cancel(self): - sys.exit(0) - - def validate_field_values(self) -> bool: - bad_fields = [] - if self.model.value is None: - bad_fields.append("Model Name must correspond to a known model in models.yaml") - if not re.match("^[a-zA-Z0-9.-]+$", self.placeholder_token.value): - bad_fields.append("Trigger term must only contain alphanumeric characters, the dot and hyphen") - if self.train_data_dir.value is None: - bad_fields.append("Data Training Directory cannot be empty") - if self.output_dir.value is None: - bad_fields.append("The Output Destination Directory cannot be empty") - if len(bad_fields) > 0: - message = "The following problems were detected and must be corrected:" - for problem in bad_fields: - message += f"\n* {problem}" - npyscreen.notify_confirm(message) - return False - else: - return True - - def get_model_names(self) -> Tuple[List[str], int]: - global config - assert config is not None - installer = initialize_installer(config) - store = installer.record_store - main_models = store.search_by_attr(model_type=ModelType.Main) - model_names = [f"{x.base.value}/{x.type.value}/{x.name}" for x in main_models if x.format == "diffusers"] - default = 0 - return (model_names, default) - - def marshall_arguments(self) -> dict: - args = {} - - # the choices - args.update( - model=self.model_names[self.model.value[0]], - resolution=self.resolutions[self.resolution.value[0]], - lr_scheduler=self.lr_schedulers[self.lr_scheduler.value[0]], - mixed_precision=self.precisions[self.mixed_precision.value[0]], - learnable_property=self.learnable_properties[self.learnable_property.value[0]], - ) - - # all the strings and booleans - for attr in ( - "initializer_token", - "placeholder_token", - "train_data_dir", - "output_dir", - "scale_lr", - "center_crop", - "enable_xformers_memory_efficient_attention", - ): - args[attr] = getattr(self, attr).value - - # all the integers - for attr in ( - "train_batch_size", - "gradient_accumulation_steps", - "num_train_epochs", - "max_train_steps", - "lr_warmup_steps", - ): - args[attr] = int(getattr(self, attr).value) - - # the floats (just one) - args.update(learning_rate=float(self.learning_rate.value)) - - # a special case - if self.resume_from_checkpoint.value and Path(self.output_dir.value).exists(): - args["resume_from_checkpoint"] = "latest" - - return args - - -class MyApplication(npyscreen.NPSAppManaged): - def __init__(self, saved_args: Optional[Dict[str, str]] = None): - super().__init__() - self.ti_arguments = None - self.saved_args = saved_args - - def onStart(self): - npyscreen.setTheme(npyscreen.Themes.DefaultTheme) - self.main = self.addForm( - "MAIN", - textualInversionForm, - name="Textual Inversion Settings", - saved_args=self.saved_args, - ) - - -def copy_to_embeddings_folder(args: Dict[str, str]) -> None: - """ - Copy learned_embeds.bin into the embeddings folder, and offer to - delete the full model and checkpoints. - """ - assert config is not None - source = Path(args["output_dir"], "learned_embeds.bin") - dest_dir_name = args["placeholder_token"].strip("<>") - destination = config.root_dir / "embeddings" / dest_dir_name - os.makedirs(destination, exist_ok=True) - logger.info(f"Training completed. Copying learned_embeds.bin into {str(destination)}") - shutil.copy(source, destination) - if (input("Delete training logs and intermediate checkpoints? [y] ") or "y").startswith(("y", "Y")): - shutil.rmtree(Path(args["output_dir"])) - else: - logger.info(f'Keeping {args["output_dir"]}') - - -def save_args(args: dict) -> None: - """ - Save the current argument values to an omegaconf file - """ - assert config is not None - dest_dir = config.root_dir / TRAINING_DIR - os.makedirs(dest_dir, exist_ok=True) - conf_file = dest_dir / CONF_FILE - conf = OmegaConf.create(args) - OmegaConf.save(config=conf, f=conf_file) - - -def previous_args() -> dict: - """ - Get the previous arguments used. - """ - assert config is not None - conf_file = config.root_dir / TRAINING_DIR / CONF_FILE - try: - conf = OmegaConf.load(conf_file) - conf["placeholder_token"] = conf["placeholder_token"].strip("<>") - except Exception: - conf = None - - return conf - - -def do_front_end() -> None: - global config - saved_args = previous_args() - myapplication = MyApplication(saved_args=saved_args) - myapplication.run() - - if my_args := myapplication.ti_arguments: - os.makedirs(my_args["output_dir"], exist_ok=True) - - # Automatically add angle brackets around the trigger - if not re.match("^<.+>$", my_args["placeholder_token"]): - my_args["placeholder_token"] = f"<{my_args['placeholder_token']}>" - - my_args["only_save_embeds"] = True - save_args(my_args) - - try: - print(my_args) - do_textual_inversion_training(config, **my_args) - copy_to_embeddings_folder(my_args) - except Exception as e: - logger.error("An exception occurred during training. The exception was:") - logger.error(str(e)) - logger.error("DETAILS:") - logger.error(traceback.format_exc()) - - -def main() -> None: - global config - - args: Namespace = parse_args() - config = InvokeAIAppConfig.get_config() - config.parse_args([]) - - # change root if needed - if args.root_dir: - config.root = args.root_dir - - try: - if args.front_end: - do_front_end() - else: - do_textual_inversion_training(config, **vars(args)) - except AssertionError as e: - logger.error(e) - sys.exit(-1) - except KeyboardInterrupt: - pass - except (widget.NotEnoughSpaceForWidget, Exception) as e: - if str(e).startswith("Height of 1 allocated"): - logger.error("You need to have at least one diffusers models defined in models.yaml in order to train") - elif str(e).startswith("addwstr"): - logger.error("Not enough window space for the interface. Please make your window larger and try again.") - else: - logger.error(e) - sys.exit(-1) - - -if __name__ == "__main__": - main() diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 9abf0b80aa..a458563fd5 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -814,7 +814,7 @@ "simpleModelDesc": "Provide a path to a local Diffusers model, local checkpoint / safetensors model a HuggingFace Repo ID, or a checkpoint/diffusers model URL.", "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 manually update your models.yaml file or add models to the InvokeAI root folder 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.", "updateModel": "Update Model", "useCustomConfig": "Use Custom Config", "v1": "v1", From 65dd4f4abc15fa60c0f7b8d9e5cd915a9d418598 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 21 Feb 2024 11:46:23 -0500 Subject: [PATCH 239/411] fix repo-id for the Deliberate v5 model prevent lora and embedding file suffixes from being stripped during installation apply psychedelicious patch to get compel to load proper TI embedding --- .../app/services/model_install/model_install_default.py | 6 +++++- invokeai/configs/INITIAL_MODELS.yaml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 7dee8bfd8c..9b771c5159 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -154,8 +154,12 @@ class ModelInstallService(ModelInstallServiceBase): info: AnyModelConfig = self._probe_model(Path(model_path), config) old_hash = info.current_hash + + if preferred_name := config.get("name"): + preferred_name = Path(preferred_name).with_suffix(model_path.suffix) + dest_path = ( - self.app_config.models_path / info.base.value / info.type.value / (config.get("name") or model_path.name) + self.app_config.models_path / info.base.value / info.type.value / (preferred_name or model_path.name) ) try: new_path = self._copy_model(model_path, dest_path) diff --git a/invokeai/configs/INITIAL_MODELS.yaml b/invokeai/configs/INITIAL_MODELS.yaml index ca2283ab81..811121d1ba 100644 --- a/invokeai/configs/INITIAL_MODELS.yaml +++ b/invokeai/configs/INITIAL_MODELS.yaml @@ -34,7 +34,7 @@ sd-1/main/Analog-Diffusion: recommended: False sd-1/main/Deliberate: description: Versatile model that produces detailed images up to 768px (4.27 GB) - source: XpucT/Deliberate + source: stablediffusionapi/deliberate-v5 recommended: False sd-1/main/Dungeons-and-Diffusion: description: Dungeons & Dragons characters (2.13 GB) From 1cec0bb1793131a5b138a3e0ec84f8623a2e6015 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 21 Feb 2024 17:40:53 -0500 Subject: [PATCH 240/411] use official Deliberate download repo --- invokeai/configs/INITIAL_MODELS.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/configs/INITIAL_MODELS.yaml b/invokeai/configs/INITIAL_MODELS.yaml index 811121d1ba..8ad788fba7 100644 --- a/invokeai/configs/INITIAL_MODELS.yaml +++ b/invokeai/configs/INITIAL_MODELS.yaml @@ -34,7 +34,7 @@ sd-1/main/Analog-Diffusion: recommended: False sd-1/main/Deliberate: description: Versatile model that produces detailed images up to 768px (4.27 GB) - source: stablediffusionapi/deliberate-v5 + source: https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate_v5.safetensors?download=true recommended: False sd-1/main/Dungeons-and-Diffusion: description: Dungeons & Dragons characters (2.13 GB) From cc41e8912cf3e80f2d7adc438fc81f83635dfa59 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 21 Feb 2024 17:15:54 -0500 Subject: [PATCH 241/411] several small model install enhancements - Support extended HF repoid syntax in TUI. This allows installation of subfolders and safetensors files, as in `XpucT/Deliberate::Deliberate_v5.safetensors` - Add `error` and `error_traceback` properties to the install job objects. - Rename the `heuristic_import` route to `heuristic_install`. - Fix the example `config` input in the `heuristic_install` route. --- invokeai/app/api/routers/model_manager.py | 8 ++-- .../model_install/model_install_base.py | 13 ++++-- invokeai/backend/install/install_helper.py | 42 +++---------------- .../model_install/test_model_install.py | 2 +- 4 files changed, 20 insertions(+), 45 deletions(-) diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index aee457406a..f57f5f97b6 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -382,8 +382,8 @@ async def add_model_record( @model_manager_router.post( - "/heuristic_import", - operation_id="heuristic_import_model", + "/heuristic_install", + operation_id="heuristic_install_model", responses={ 201: {"description": "The model imported successfully"}, 415: {"description": "Unrecognized file/folder format"}, @@ -392,12 +392,12 @@ async def add_model_record( }, status_code=201, ) -async def heuristic_import( +async def heuristic_install( source: str, config: Optional[Dict[str, Any]] = Body( description="Dict of fields that override auto-probed values in the model config record, such as name, description and prediction_type ", default=None, - example={"name": "modelT", "description": "antique cars"}, + example={"name": "string", "description": "string"}, ), access_token: Optional[str] = None, ) -> ModelInstallJob: diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py index 080219af75..d1e8e4f8e5 100644 --- a/invokeai/app/services/model_install/model_install_base.py +++ b/invokeai/app/services/model_install/model_install_base.py @@ -177,6 +177,12 @@ class ModelInstallJob(BaseModel): download_parts: Set[DownloadJob] = Field( default_factory=set, description="Download jobs contributing to this install" ) + error: Optional[str] = Field( + default=None, description="On an error condition, this field will contain the text of the exception" + ) + error_traceback: Optional[str] = Field( + default=None, description="On an error condition, this field will contain the exception traceback" + ) # internal flags and transitory settings _install_tmpdir: Optional[Path] = PrivateAttr(default=None) _exception: Optional[Exception] = PrivateAttr(default=None) @@ -184,6 +190,8 @@ class ModelInstallJob(BaseModel): def set_error(self, e: Exception) -> None: """Record the error and traceback from an exception.""" self._exception = e + self.error = str(e) + self.error_traceback = self._format_error(e) self.status = InstallStatus.ERROR def cancel(self) -> None: @@ -195,10 +203,9 @@ class ModelInstallJob(BaseModel): """Class name of the exception that led to status==ERROR.""" return self._exception.__class__.__name__ if self._exception else None - @property - def error(self) -> Optional[str]: + def _format_error(self, exception: Exception) -> str: """Error traceback.""" - return "".join(traceback.format_exception(self._exception)) if self._exception else None + return "".join(traceback.format_exception(exception)) @property def cancelled(self) -> bool: diff --git a/invokeai/backend/install/install_helper.py b/invokeai/backend/install/install_helper.py index 3623b623a9..999dcdd100 100644 --- a/invokeai/backend/install/install_helper.py +++ b/invokeai/backend/install/install_helper.py @@ -1,14 +1,11 @@ """Utility (backend) functions used by model_install.py""" -import re from logging import Logger from pathlib import Path from typing import Any, Dict, List, Optional import omegaconf -from huggingface_hub import HfFolder from pydantic import BaseModel, Field from pydantic.dataclasses import dataclass -from pydantic.networks import AnyHttpUrl from requests import HTTPError from tqdm import tqdm @@ -18,12 +15,8 @@ from invokeai.app.services.download import DownloadQueueService from invokeai.app.services.events.events_base import EventServiceBase from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage from invokeai.app.services.model_install import ( - HFModelSource, - LocalModelSource, ModelInstallService, ModelInstallServiceBase, - ModelSource, - URLModelSource, ) from invokeai.app.services.model_metadata import ModelMetadataStoreSQL from invokeai.app.services.model_records import ModelRecordServiceBase, ModelRecordServiceSQL @@ -31,7 +24,6 @@ from invokeai.app.services.shared.sqlite.sqlite_util import init_db from invokeai.backend.model_manager import ( BaseModelType, InvalidModelConfigException, - ModelRepoVariant, ModelType, ) from invokeai.backend.model_manager.metadata import UnknownMetadataException @@ -226,37 +218,13 @@ class InstallHelper(object): additional_models.append(reverse_source[requirement]) model_list.extend(additional_models) - def _make_install_source(self, model_info: UnifiedModelInfo) -> ModelSource: - assert model_info.source - model_path_id_or_url = model_info.source.strip("\"' ") - model_path = Path(model_path_id_or_url) - - if model_path.exists(): # local file on disk - return LocalModelSource(path=model_path.absolute(), inplace=True) - - # parsing huggingface repo ids - # we're going to do a little trick that allows for extended repo_ids of form "foo/bar:fp16" - variants = "|".join([x.lower() for x in ModelRepoVariant.__members__]) - if match := re.match(f"^([^/]+/[^/]+?)(?::({variants}))?$", model_path_id_or_url): - repo_id = match.group(1) - repo_variant = ModelRepoVariant(match.group(2)) if match.group(2) else None - subfolder = Path(model_info.subfolder) if model_info.subfolder else None - return HFModelSource( - repo_id=repo_id, - access_token=HfFolder.get_token(), - subfolder=subfolder, - variant=repo_variant, - ) - if re.match(r"^(http|https):", model_path_id_or_url): - return URLModelSource(url=AnyHttpUrl(model_path_id_or_url)) - raise ValueError(f"Unsupported model source: {model_path_id_or_url}") - def add_or_delete(self, selections: InstallSelections) -> None: """Add or delete selected models.""" installer = self._installer self._add_required_models(selections.install_models) for model in selections.install_models: - source = self._make_install_source(model) + assert model.source + model_path_id_or_url = model.source.strip("\"' ") config = ( { "description": model.description, @@ -267,12 +235,12 @@ class InstallHelper(object): ) try: - installer.import_model( - source=source, + installer.heuristic_import( + source=model_path_id_or_url, config=config, ) except (UnknownMetadataException, InvalidModelConfigException, HTTPError, OSError) as e: - self._logger.warning(f"{source}: {e}") + self._logger.warning(f"{model.source}: {e}") for model_to_remove in selections.remove_models: parts = model_to_remove.split("/") diff --git a/tests/app/services/model_install/test_model_install.py b/tests/app/services/model_install/test_model_install.py index 55f7e86541..80b106c5cb 100644 --- a/tests/app/services/model_install/test_model_install.py +++ b/tests/app/services/model_install/test_model_install.py @@ -256,4 +256,4 @@ def test_404_download(mm2_installer: ModelInstallServiceBase, mm2_app_config: In assert job.error_type == "HTTPError" assert job.error assert "NOT FOUND" in job.error - assert "Traceback" in job.error + assert job.error_traceback.startswith("Traceback") From 0d9fbe5e043b2abde5cf28a9021b51c9d8ac4459 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:19:08 +1100 Subject: [PATCH 242/411] feat(ui): replace `type-fest` with `utility-types` - The new package has more useful types - Only used `JsonObject` from `type-fest`; added an implementation of that type --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/pnpm-lock.yaml | 16 ++--- invokeai/frontend/web/src/app/store/store.ts | 4 +- invokeai/frontend/web/src/common/types.ts | 7 ++ .../src/features/nodes/util/graph/metadata.ts | 4 +- .../nodes/util/workflow/validateWorkflow.ts | 4 +- .../frontend/web/src/services/api/types.ts | 70 ++++++++++++++----- 7 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 invokeai/frontend/web/src/common/types.ts diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 743cb1e09d..0bf236ee38 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -99,7 +99,6 @@ "roarr": "^7.21.0", "serialize-error": "^11.0.3", "socket.io-client": "^4.7.4", - "type-fest": "^4.10.2", "use-debounce": "^10.0.0", "use-image": "^1.1.1", "uuid": "^9.0.1", @@ -146,6 +145,7 @@ "ts-toolbelt": "^9.6.0", "tsafe": "^1.6.6", "typescript": "^5.3.3", + "utility-types": "^3.11.0", "vite": "^5.1.3", "vite-plugin-css-injected-by-js": "^3.4.0", "vite-plugin-dts": "^3.7.2", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 9e873102e6..f2abdd87bf 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -152,9 +152,6 @@ dependencies: socket.io-client: specifier: ^4.7.4 version: 4.7.4 - type-fest: - specifier: ^4.10.2 - version: 4.10.2 use-debounce: specifier: ^10.0.0 version: 10.0.0(react@18.2.0) @@ -271,6 +268,9 @@ devDependencies: typescript: specifier: ^5.3.3 version: 5.3.3 + utility-types: + specifier: ^3.11.0 + version: 3.11.0 vite: specifier: ^5.1.3 version: 5.1.3(@types/node@20.11.19) @@ -13827,11 +13827,6 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - /type-fest@4.10.2: - resolution: {integrity: sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==} - engines: {node: '>=16'} - dev: false - /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -14152,6 +14147,11 @@ packages: which-typed-array: 1.1.14 dev: true + /utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + dev: true + /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 270662c3d2..16f1632d88 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -3,6 +3,7 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too import { logger } from 'app/logging/logger'; import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver'; import { errorHandler } from 'app/store/enhancers/reduxRemember/errors'; +import type { JSONObject } from 'common/types'; import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice'; import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { @@ -32,7 +33,6 @@ import { rememberEnhancer, rememberReducer } from 'redux-remember'; import { serializeError } from 'serialize-error'; import { api } from 'services/api'; import { authToastMiddleware } from 'services/api/authToastMiddleware'; -import type { JsonObject } from 'type-fest'; import { STORAGE_PREFIX } from './constants'; import { actionSanitizer } from './middleware/devtools/actionSanitizer'; @@ -125,7 +125,7 @@ const unserialize: UnserializeFunction = (data, key) => { { persistedData: parsed, rehydratedData: transformed, - diff: diff(parsed, transformed) as JsonObject, // this is always serializable + diff: diff(parsed, transformed) as JSONObject, // this is always serializable }, `Rehydrated slice "${key}"` ); diff --git a/invokeai/frontend/web/src/common/types.ts b/invokeai/frontend/web/src/common/types.ts new file mode 100644 index 0000000000..29a411788d --- /dev/null +++ b/invokeai/frontend/web/src/common/types.ts @@ -0,0 +1,7 @@ +export type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; + +export interface JSONObject { + [k: string]: JSONValue; +} + +export interface JSONArray extends Array {} diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts b/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts index 781ce57ebc..c48f54d191 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/metadata.ts @@ -1,5 +1,5 @@ +import type { JSONObject } from 'common/types'; import type { CoreMetadataInvocation, NonNullableGraph } from 'services/api/types'; -import type { JsonObject } from 'type-fest'; import { METADATA } from './constants'; @@ -30,7 +30,7 @@ export const addCoreMetadataNode = ( export const upsertMetadata = ( graph: NonNullableGraph, - metadata: Partial | JsonObject + metadata: Partial | JSONObject ): void => { const metadataNode = graph.nodes[METADATA] as CoreMetadataInvocation | undefined; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts index 5096e588b0..b402f2f8af 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts @@ -1,3 +1,4 @@ +import type { JSONObject } from 'common/types'; import { parseify } from 'common/util/serialize'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; @@ -5,14 +6,13 @@ import { isWorkflowInvocationNode } from 'features/nodes/types/workflow'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import { t } from 'i18next'; import { keyBy } from 'lodash-es'; -import type { JsonObject } from 'type-fest'; import { parseAndMigrateWorkflow } from './migrations'; type WorkflowWarning = { message: string; issues?: string[]; - data: JsonObject; + data: JSONObject; }; type ValidateWorkflowResult = { diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index aaa70a2684..d561173337 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -2,7 +2,7 @@ import type { UseToastOptions } from '@invoke-ai/ui-library'; import type { EntityState } from '@reduxjs/toolkit'; import type { components, paths } from 'services/api/schema'; import type { O } from 'ts-toolbelt'; -import type { SetRequired } from 'type-fest'; +import type { Overwrite } from 'utility-types'; export type S = components['schemas']; @@ -61,28 +61,60 @@ export type IPAdapterField = S['IPAdapterField']; // Model Configs // TODO(MM2): Can we make key required in the pydantic model? -type KeyRequired = SetRequired; -export type LoRAConfig = KeyRequired; +export type LoRAModelConfig = S['LoRAConfig']; // TODO(MM2): Can we rename this from Vae -> VAE -export type VAEConfig = KeyRequired | KeyRequired; -export type ControlNetConfig = - | KeyRequired - | KeyRequired; -export type IPAdapterConfig = KeyRequired; +export type VAEModelConfig = S['VaeCheckpointConfig'] | S['VaeDiffusersConfig']; +export type ControlNetModelConfig = S['ControlNetDiffusersConfig'] | S['ControlNetCheckpointConfig']; +export type IPAdapterModelConfig = S['IPAdapterConfig']; // TODO(MM2): Can we rename this to T2IAdapterConfig -export type T2IAdapterConfig = KeyRequired; -export type TextualInversionConfig = KeyRequired; -export type DiffusersModelConfig = KeyRequired; -export type CheckpointModelConfig = KeyRequired; +export type T2IAdapterModelConfig = S['T2IConfig']; +export type TextualInversionModelConfig = S['TextualInversionConfig']; +export type DiffusersModelConfig = S['MainDiffusersConfig']; +export type CheckpointModelConfig = S['MainCheckpointConfig']; export type MainModelConfig = DiffusersModelConfig | CheckpointModelConfig; +export type RefinerMainModelConfig = Overwrite; +export type NonRefinerMainModelConfig = Overwrite; export type AnyModelConfig = - | LoRAConfig - | VAEConfig - | ControlNetConfig - | IPAdapterConfig - | T2IAdapterConfig - | TextualInversionConfig - | MainModelConfig; + | LoRAModelConfig + | VAEModelConfig + | ControlNetModelConfig + | IPAdapterModelConfig + | T2IAdapterModelConfig + | TextualInversionModelConfig + | RefinerMainModelConfig + | NonRefinerMainModelConfig; + +export const isLoRAModelConfig = (config: AnyModelConfig): config is LoRAModelConfig => { + return config.type === 'lora'; +}; + +export const isVAEModelConfig = (config: AnyModelConfig): config is VAEModelConfig => { + return config.type === 'vae'; +}; + +export const isControlNetModelConfig = (config: AnyModelConfig): config is ControlNetModelConfig => { + return config.type === 'controlnet'; +}; + +export const isIPAdapterModelConfig = (config: AnyModelConfig): config is IPAdapterModelConfig => { + return config.type === 'ip_adapter'; +}; + +export const isT2IAdapterModelConfig = (config: AnyModelConfig): config is T2IAdapterModelConfig => { + return config.type === 't2i_adapter'; +}; + +export const isTextualInversionModelConfig = (config: AnyModelConfig): config is TextualInversionModelConfig => { + return config.type === 'embedding'; +}; + +export const isNonRefinerMainModelConfig = (config: AnyModelConfig): config is NonRefinerMainModelConfig => { + return config.type === 'main' && config.base !== 'sdxl-refiner'; +}; + +export const isRefinerMainModelModelConfig = (config: AnyModelConfig): config is RefinerMainModelConfig => { + return config.type === 'main' && config.base === 'sdxl-refiner'; +}; export type MergeModelConfig = S['Body_merge']; export type ImportModelConfig = S['Body_import_model']; From 239ecfaf7905f6349a7a8fccabfebabce72ce243 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:21:37 +1100 Subject: [PATCH 243/411] fix(nodes): make fields on `ModelConfigBase` required The setup of `ModelConfigBase` means autogenerated types have critical fields flagged as nullable (like `key` and `base`). Need to manually flag them as required. --- invokeai/backend/model_manager/config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/invokeai/backend/model_manager/config.py b/invokeai/backend/model_manager/config.py index bc4848b0a5..fb0593a651 100644 --- a/invokeai/backend/model_manager/config.py +++ b/invokeai/backend/model_manager/config.py @@ -138,9 +138,16 @@ class ModelConfigBase(BaseModel): source: Optional[str] = Field(description="model original source (path, URL or repo_id)", default=None) last_modified: Optional[float] = Field(description="timestamp for modification time", default_factory=time.time) + @staticmethod + def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None: + schema["required"].extend( + ["key", "base", "type", "format", "original_hash", "current_hash", "source", "last_modified"] + ) + model_config = ConfigDict( use_enum_values=False, validate_assignment=True, + json_schema_extra=json_schema_extra, ) def update(self, attributes: Dict[str, Any]) -> None: From 79b16596b55cd6ec8503458c8b4a63d3ae965a5c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:21:46 +1100 Subject: [PATCH 244/411] chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 190 +++++++++--------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 2115e79768..3566fdb9e2 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -1420,7 +1420,7 @@ export type components = { * @default clip_vision * @constant */ - type?: "clip_vision"; + type: "clip_vision"; /** * Format * @constant @@ -1431,17 +1431,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -1451,12 +1451,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; }; /** CLIPVisionModelField */ CLIPVisionModelField: { @@ -2538,29 +2538,29 @@ export type components = { * @default controlnet * @constant */ - type?: "controlnet"; + type: "controlnet"; /** * Format * @default checkpoint * @constant */ - format?: "checkpoint"; + format: "checkpoint"; /** * Key * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -2570,12 +2570,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** * Config * @description path to the checkpoint model config file @@ -2604,29 +2604,29 @@ export type components = { * @default controlnet * @constant */ - type?: "controlnet"; + type: "controlnet"; /** * Format * @default diffusers * @constant */ - format?: "diffusers"; + format: "diffusers"; /** * Key * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -2636,12 +2636,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** @default */ repo_variant?: components["schemas"]["ModelRepoVariant"] | null; }; @@ -4214,7 +4214,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["FaceOffInvocation"]; + [key: string]: components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["LatentsCollectionInvocation"]; }; /** * Edges @@ -4251,7 +4251,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["ModelLoaderOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["String2Output"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["StringCollectionOutput"]; + [key: string]: components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["String2Output"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["IdealSizeOutput"]; }; /** * Errors @@ -4422,7 +4422,7 @@ export type components = { * @default ip_adapter * @constant */ - type?: "ip_adapter"; + type: "ip_adapter"; /** * Format * @constant @@ -4433,17 +4433,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -4453,12 +4453,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** Image Encoder Model Id */ image_encoder_model_id: string; }; @@ -6506,7 +6506,7 @@ export type components = { * @default lora * @constant */ - type?: "lora"; + type: "lora"; /** * Format * @enum {string} @@ -6517,17 +6517,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -6537,12 +6537,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; }; /** * LoRAMetadataField @@ -6706,29 +6706,29 @@ export type components = { * @default main * @constant */ - type?: "main"; + type: "main"; /** * Format * @default checkpoint * @constant */ - format?: "checkpoint"; + format: "checkpoint"; /** * Key * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -6738,12 +6738,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** Vae */ vae?: string | null; /** @default normal */ @@ -6788,29 +6788,29 @@ export type components = { * @default main * @constant */ - type?: "main"; + type: "main"; /** * Format * @default diffusers * @constant */ - format?: "diffusers"; + format: "diffusers"; /** * Key * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -6820,12 +6820,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** Vae */ vae?: string | null; /** @default normal */ @@ -7758,13 +7758,13 @@ export type components = { * @default sd-1 * @constant */ - base?: "sd-1"; + base: "sd-1"; /** * Type * @default onnx * @constant */ - type?: "onnx"; + type: "onnx"; /** * Format * @enum {string} @@ -7775,17 +7775,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -7795,12 +7795,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** Vae */ vae?: string | null; /** @default normal */ @@ -7838,13 +7838,13 @@ export type components = { * @default sd-2 * @constant */ - base?: "sd-2"; + base: "sd-2"; /** * Type * @default onnx * @constant */ - type?: "onnx"; + type: "onnx"; /** * Format * @enum {string} @@ -7855,17 +7855,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -7875,12 +7875,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** Vae */ vae?: string | null; /** @default normal */ @@ -7918,13 +7918,13 @@ export type components = { * @default sdxl * @constant */ - base?: "sdxl"; + base: "sdxl"; /** * Type * @default onnx * @constant */ - type?: "onnx"; + type: "onnx"; /** * Format * @enum {string} @@ -7935,17 +7935,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -7955,12 +7955,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; /** Vae */ vae?: string | null; /** @default normal */ @@ -10110,7 +10110,7 @@ export type components = { * @default t2i_adapter * @constant */ - type?: "t2i_adapter"; + type: "t2i_adapter"; /** * Format * @constant @@ -10121,17 +10121,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -10141,12 +10141,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; }; /** TBLR */ TBLR: { @@ -10181,7 +10181,7 @@ export type components = { * @default embedding * @constant */ - type?: "embedding"; + type: "embedding"; /** * Format * @enum {string} @@ -10192,17 +10192,17 @@ export type components = { * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -10212,12 +10212,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; }; /** Tile */ Tile: { @@ -10530,29 +10530,29 @@ export type components = { * @default vae * @constant */ - type?: "vae"; + type: "vae"; /** * Format * @default checkpoint * @constant */ - format?: "checkpoint"; + format: "checkpoint"; /** * Key * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -10562,12 +10562,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; }; /** * VaeDiffusersConfig @@ -10591,29 +10591,29 @@ export type components = { * @default vae * @constant */ - type?: "vae"; + type: "vae"; /** * Format * @default diffusers * @constant */ - format?: "diffusers"; + format: "diffusers"; /** * Key * @description unique key for model * @default */ - key?: string; + key: string; /** * Original Hash * @description original fasthash of model contents */ - original_hash?: string | null; + original_hash: string | null; /** * Current Hash * @description current fasthash of model contents */ - current_hash?: string | null; + current_hash: string | null; /** * Description * @description human readable description of the model @@ -10623,12 +10623,12 @@ export type components = { * Source * @description model original source (path, URL or repo_id) */ - source?: string | null; + source: string | null; /** * Last Modified * @description timestamp for modification time */ - last_modified?: number | null; + last_modified: number | null; }; /** VaeField */ VaeField: { From 3ed2963f43e5b93c8ac0407ad5ba7c9fbb40f524 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:33:20 +1100 Subject: [PATCH 245/411] feat(ui): refactor metadata handling Refactor of metadata recall handling. This is in preparation for a backwards compatibility layer for models. - Create helpers to fetch a model outside react (e.g. not in a hook) - Created helpers to parse model metadata - Renamed a lot of types that were confusing and/or had naming collisions --- .../web/src/app/store/nanostores/store.ts | 22 + .../parameters/ParamControlAdapterModel.tsx | 4 +- .../features/embedding/EmbeddingSelect.tsx | 6 +- .../features/lora/components/LoRASelect.tsx | 6 +- .../web/src/features/lora/store/loraSlice.ts | 9 +- .../ModelManagerPanel/LoRAModelEdit.tsx | 13 +- .../ControlNetModelFieldInputComponent.tsx | 4 +- .../IPAdapterModelFieldInputComponent.tsx | 4 +- .../inputs/LoRAModelFieldInputComponent.tsx | 4 +- .../T2IAdapterModelFieldInputComponent.tsx | 4 +- .../inputs/VAEModelFieldInputComponent.tsx | 4 +- .../VAEModel/ParamVAEModelSelect.tsx | 6 +- .../parameters/hooks/useRecallParameters.ts | 516 ++++-------------- .../parameters/util/modelFetchingHelpers.ts | 113 ++++ .../parameters/util/modelMetadataHelpers.ts | 150 +++++ .../web/src/services/api/endpoints/models.ts | 64 +-- 16 files changed, 443 insertions(+), 486 deletions(-) create mode 100644 invokeai/frontend/web/src/features/parameters/util/modelFetchingHelpers.ts create mode 100644 invokeai/frontend/web/src/features/parameters/util/modelMetadataHelpers.ts diff --git a/invokeai/frontend/web/src/app/store/nanostores/store.ts b/invokeai/frontend/web/src/app/store/nanostores/store.ts index aee0f0e6ef..f4cd001c96 100644 --- a/invokeai/frontend/web/src/app/store/nanostores/store.ts +++ b/invokeai/frontend/web/src/app/store/nanostores/store.ts @@ -8,4 +8,26 @@ declare global { } } +/** + * Raised when the redux store is unable to be retrieved. + */ +export class ReduxStoreNotInitialized extends Error { + /** + * Create ReduxStoreNotInitialized + * @param {String} message + */ + constructor(message = 'Redux store not initialized') { + super(message); + this.name = this.constructor.name; + } +} + export const $store = atom> | undefined>(); + +export const getStore = () => { + const store = $store.get(); + if (!store) { + throw new ReduxStoreNotInitialized(); + } + return store; +}; diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx index 696bf47b2a..75372c350d 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/parameters/ParamControlAdapterModel.tsx @@ -8,7 +8,7 @@ import { useControlAdapterType } from 'features/controlAdapters/hooks/useControl import { controlAdapterModelChanged } from 'features/controlAdapters/store/controlAdaptersSlice'; import { pick } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; -import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'services/api/types'; +import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types'; type ParamControlAdapterModelProps = { id: string; @@ -24,7 +24,7 @@ const ParamControlAdapterModel = ({ id }: ParamControlAdapterModelProps) => { const { data, isLoading } = useControlAdapterModelQuery(controlAdapterType); const _onChange = useCallback( - (model: ControlNetConfig | IPAdapterConfig | T2IAdapterConfig | null) => { + (model: ControlNetModelConfig | IPAdapterModelConfig | T2IAdapterModelConfig | null) => { if (!model) { return; } diff --git a/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx b/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx index fd05edc466..a5ad358fa0 100644 --- a/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx +++ b/invokeai/frontend/web/src/features/embedding/EmbeddingSelect.tsx @@ -7,7 +7,7 @@ import { t } from 'i18next'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; -import type { TextualInversionConfig } from 'services/api/types'; +import type { TextualInversionModelConfig } from 'services/api/types'; const noOptionsMessage = () => t('embedding.noMatchingEmbedding'); @@ -17,7 +17,7 @@ export const EmbeddingSelect = memo(({ onSelect, onClose }: EmbeddingSelectProps const currentBaseModel = useAppSelector((s) => s.generation.model?.base); const getIsDisabled = useCallback( - (embedding: TextualInversionConfig): boolean => { + (embedding: TextualInversionModelConfig): boolean => { const isCompatible = currentBaseModel === embedding.base; const hasMainModel = Boolean(currentBaseModel); return !hasMainModel || !isCompatible; @@ -27,7 +27,7 @@ export const EmbeddingSelect = memo(({ onSelect, onClose }: EmbeddingSelectProps const { data, isLoading } = useGetTextualInversionModelsQuery(); const _onChange = useCallback( - (embedding: TextualInversionConfig | null) => { + (embedding: TextualInversionModelConfig | null) => { if (!embedding) { return; } diff --git a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx index b58751ca5e..e7d40c5eaf 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRASelect.tsx @@ -8,7 +8,7 @@ import { loraAdded, selectLoraSlice } from 'features/lora/store/loraSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; -import type { LoRAConfig } from 'services/api/types'; +import type { LoRAModelConfig } from 'services/api/types'; const selectAddedLoRAs = createMemoizedSelector(selectLoraSlice, (lora) => lora.loras); @@ -19,7 +19,7 @@ const LoRASelect = () => { const addedLoRAs = useAppSelector(selectAddedLoRAs); const currentBaseModel = useAppSelector((s) => s.generation.model?.base); - const getIsDisabled = (lora: LoRAConfig): boolean => { + const getIsDisabled = (lora: LoRAModelConfig): boolean => { const isCompatible = currentBaseModel === lora.base; const isAdded = Boolean(addedLoRAs[lora.key]); const hasMainModel = Boolean(currentBaseModel); @@ -27,7 +27,7 @@ const LoRASelect = () => { }; const _onChange = useCallback( - (lora: LoRAConfig | null) => { + (lora: LoRAModelConfig | null) => { if (!lora) { return; } diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts index dd455e12c3..377406b3e5 100644 --- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts +++ b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts @@ -2,7 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas'; -import type { LoRAConfig } from 'services/api/types'; +import type { LoRAModelConfig } from 'services/api/types'; export type LoRA = ParameterLoRAModel & { weight: number; @@ -28,13 +28,12 @@ export const loraSlice = createSlice({ name: 'lora', initialState: initialLoraState, reducers: { - loraAdded: (state, action: PayloadAction) => { + loraAdded: (state, action: PayloadAction) => { const { key, base } = action.payload; state.loras[key] = { key, base, ...defaultLoRAConfig }; }, - loraRecalled: (state, action: PayloadAction) => { - const { key, base, weight } = action.payload; - state.loras[key] = { key, base, weight, isEnabled: true }; + loraRecalled: (state, action: PayloadAction) => { + state.loras[action.payload.key] = action.payload; }, loraRemoved: (state, action: PayloadAction) => { const key = action.payload; diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx index 75151cd001..1a8f235aaf 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx @@ -8,12 +8,11 @@ import { memo, useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import type { LoRAConfig } from 'services/api/endpoints/models'; import { useUpdateLoRAModelsMutation } from 'services/api/endpoints/models'; -import type { LoRAConfig } from 'services/api/types'; +import type { LoRAModelConfig } from 'services/api/types'; type LoRAModelEditProps = { - model: LoRAConfig; + model: LoRAModelConfig; }; const LoRAModelEdit = (props: LoRAModelEditProps) => { @@ -30,7 +29,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { control, formState: { errors }, reset, - } = useForm({ + } = useForm({ defaultValues: { model_name: model.model_name ? model.model_name : '', base_model: model.base_model, @@ -42,7 +41,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { mode: 'onChange', }); - const onSubmit = useCallback>( + const onSubmit = useCallback>( (values) => { const responseBody = { base_model: model.base_model, @@ -53,7 +52,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { updateLoRAModel(responseBody) .unwrap() .then((payload) => { - reset(payload as LoRAConfig, { keepDefaultValues: true }); + reset(payload as LoRAModelConfig, { keepDefaultValues: true }); dispatch( addToast( makeToast({ @@ -106,7 +105,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { {t('modelManager.description')} - control={control} name="base_model" /> + control={control} name="base_model" /> {t('modelManager.modelLocation')} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx index 1951ec60d3..29a1f93dd5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ControlNetModelFieldInputComponent.tsx @@ -6,7 +6,7 @@ import type { ControlNetModelFieldInputInstance, ControlNetModelFieldInputTempla import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useGetControlNetModelsQuery } from 'services/api/endpoints/models'; -import type { ControlNetConfig } from 'services/api/types'; +import type { ControlNetModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -18,7 +18,7 @@ const ControlNetModelFieldInputComponent = (props: Props) => { const { data, isLoading } = useGetControlNetModelsQuery(); const _onChange = useCallback( - (value: ControlNetConfig | null) => { + (value: ControlNetModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx index 137f751fca..d4f0ae3de1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IPAdapterModelFieldInputComponent.tsx @@ -6,7 +6,7 @@ import type { IPAdapterModelFieldInputInstance, IPAdapterModelFieldInputTemplate import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models'; -import type { IPAdapterConfig } from 'services/api/types'; +import type { IPAdapterModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -18,7 +18,7 @@ const IPAdapterModelFieldInputComponent = ( const { data: ipAdapterModels } = useGetIPAdapterModelsQuery(); const _onChange = useCallback( - (value: IPAdapterConfig | null) => { + (value: IPAdapterModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx index 5f6318de9e..9fd223e694 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/LoRAModelFieldInputComponent.tsx @@ -6,7 +6,7 @@ import type { LoRAModelFieldInputInstance, LoRAModelFieldInputTemplate } from 'f import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useGetLoRAModelsQuery } from 'services/api/endpoints/models'; -import type { LoRAConfig } from 'services/api/types'; +import type { LoRAModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -17,7 +17,7 @@ const LoRAModelFieldInputComponent = (props: Props) => { const dispatch = useAppDispatch(); const { data, isLoading } = useGetLoRAModelsQuery(); const _onChange = useCallback( - (value: LoRAConfig | null) => { + (value: LoRAModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx index 9115f22c14..a38356a0b8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T2IAdapterModelFieldInputComponent.tsx @@ -6,7 +6,7 @@ import type { T2IAdapterModelFieldInputInstance, T2IAdapterModelFieldInputTempla import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useGetT2IAdapterModelsQuery } from 'services/api/endpoints/models'; -import type { T2IAdapterConfig } from 'services/api/types'; +import type { T2IAdapterModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -19,7 +19,7 @@ const T2IAdapterModelFieldInputComponent = ( const { data: t2iAdapterModels } = useGetT2IAdapterModelsQuery(); const _onChange = useCallback( - (value: T2IAdapterConfig | null) => { + (value: T2IAdapterModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx index 87272f48b9..272f7f5b35 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/VAEModelFieldInputComponent.tsx @@ -7,7 +7,7 @@ import type { VAEModelFieldInputInstance, VAEModelFieldInputTemplate } from 'fea import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; -import type { VAEConfig } from 'services/api/types'; +import type { VAEModelConfig } from 'services/api/types'; import type { FieldComponentProps } from './types'; @@ -18,7 +18,7 @@ const VAEModelFieldInputComponent = (props: Props) => { const dispatch = useAppDispatch(); const { data, isLoading } = useGetVaeModelsQuery(); const _onChange = useCallback( - (value: VAEConfig | null) => { + (value: VAEModelConfig | null) => { if (!value) { return; } diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index 4b9f2764bf..4a630fa9ce 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -8,7 +8,7 @@ import { pick } from 'lodash-es'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetVaeModelsQuery } from 'services/api/endpoints/models'; -import type { VAEConfig } from 'services/api/types'; +import type { VAEModelConfig } from 'services/api/types'; const selector = createMemoizedSelector(selectGenerationSlice, (generation) => { const { model, vae } = generation; @@ -21,7 +21,7 @@ const ParamVAEModelSelect = () => { const { model, vae } = useAppSelector(selector); const { data, isLoading } = useGetVaeModelsQuery(); const getIsDisabled = useCallback( - (vae: VAEConfig): boolean => { + (vae: VAEModelConfig): boolean => { const isCompatible = model?.base === vae.base; const hasMainModel = Boolean(model?.base); return !hasMainModel || !isCompatible; @@ -29,7 +29,7 @@ const ParamVAEModelSelect = () => { [model?.base] ); const _onChange = useCallback( - (vae: VAEConfig | null) => { + (vae: VAEModelConfig | null) => { dispatch(vaeSelected(vae ? pick(vae, 'key', 'base') : null)); }, [dispatch] diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts index 0d464cd9b9..0929fc1dc3 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts @@ -1,17 +1,9 @@ import { useAppToaster } from 'app/components/Toaster'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; import { controlAdapterRecalled, controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice'; -import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types'; -import { - initialControlNet, - initialIPAdapter, - initialT2IAdapter, -} from 'features/controlAdapters/util/buildControlAdapter'; import { setHrfEnabled, setHrfMethod, setHrfStrength } from 'features/hrf/store/hrfSlice'; import { loraRecalled, lorasCleared } from 'features/lora/store/loraSlice'; -import type { ModelIdentifier } from 'features/nodes/types/common'; import { isModelIdentifier } from 'features/nodes/types/common'; import type { ControlNetMetadataItem, @@ -56,6 +48,14 @@ import { isParameterStrength, isParameterWidth, } from 'features/parameters/types/parameterSchemas'; +import { + prepareControlNetMetadataItem, + prepareIPAdapterMetadataItem, + prepareLoRAMetadataItem, + prepareMainModelMetadataItem, + prepareT2IAdapterMetadataItem, + prepareVAEMetadataItem, +} from 'features/parameters/util/modelMetadataHelpers'; import { refinerModelChanged, setNegativeStylePromptSDXL, @@ -70,23 +70,7 @@ import { import { isNil } from 'lodash-es'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { ALL_BASE_MODELS } from 'services/api/constants'; -import { - controlNetModelsAdapterSelectors, - ipAdapterModelsAdapterSelectors, - loraModelsAdapterSelectors, - mainModelsAdapterSelectors, - t2iAdapterModelsAdapterSelectors, - useGetControlNetModelsQuery, - useGetIPAdapterModelsQuery, - useGetLoRAModelsQuery, - useGetMainModelsQuery, - useGetT2IAdapterModelsQuery, - useGetVaeModelsQuery, - vaeModelsAdapterSelectors, -} from 'services/api/endpoints/models'; import type { ImageDTO } from 'services/api/types'; -import { v4 as uuidv4 } from 'uuid'; const selectModel = createMemoizedSelector(selectGenerationSlice, (generation) => generation.model); @@ -140,9 +124,6 @@ export const useRecallParameters = () => { [t, toaster] ); - /** - * Recall both prompts with toast - */ const recallBothPrompts = useCallback( (positivePrompt: unknown, negativePrompt: unknown, positiveStylePrompt: unknown, negativeStylePrompt: unknown) => { if ( @@ -175,9 +156,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall positive prompt with toast - */ const recallPositivePrompt = useCallback( (positivePrompt: unknown) => { if (!isParameterPositivePrompt(positivePrompt)) { @@ -190,9 +168,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall negative prompt with toast - */ const recallNegativePrompt = useCallback( (negativePrompt: unknown) => { if (!isParameterNegativePrompt(negativePrompt)) { @@ -205,9 +180,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall SDXL Positive Style Prompt with toast - */ const recallSDXLPositiveStylePrompt = useCallback( (positiveStylePrompt: unknown) => { if (!isParameterPositiveStylePromptSDXL(positiveStylePrompt)) { @@ -220,9 +192,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall SDXL Negative Style Prompt with toast - */ const recallSDXLNegativeStylePrompt = useCallback( (negativeStylePrompt: unknown) => { if (!isParameterNegativeStylePromptSDXL(negativeStylePrompt)) { @@ -235,9 +204,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall seed with toast - */ const recallSeed = useCallback( (seed: unknown) => { if (!isParameterSeed(seed)) { @@ -250,9 +216,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall CFG scale with toast - */ const recallCfgScale = useCallback( (cfgScale: unknown) => { if (!isParameterCFGScale(cfgScale)) { @@ -265,9 +228,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall CFG rescale multiplier with toast - */ const recallCfgRescaleMultiplier = useCallback( (cfgRescaleMultiplier: unknown) => { if (!isParameterCFGRescaleMultiplier(cfgRescaleMultiplier)) { @@ -280,9 +240,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall scheduler with toast - */ const recallScheduler = useCallback( (scheduler: unknown) => { if (!isParameterScheduler(scheduler)) { @@ -295,9 +252,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall steps with toast - */ const recallSteps = useCallback( (steps: unknown) => { if (!isParameterSteps(steps)) { @@ -310,9 +264,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall width with toast - */ const recallWidth = useCallback( (width: unknown) => { if (!isParameterWidth(width)) { @@ -325,9 +276,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall height with toast - */ const recallHeight = useCallback( (height: unknown) => { if (!isParameterHeight(height)) { @@ -340,9 +288,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall width and height with toast - */ const recallWidthAndHeight = useCallback( (width: unknown, height: unknown) => { if (!isParameterWidth(width)) { @@ -360,9 +305,6 @@ export const useRecallParameters = () => { [dispatch, allParameterSetToast, allParameterNotSetToast] ); - /** - * Recall strength with toast - */ const recallStrength = useCallback( (strength: unknown) => { if (!isParameterStrength(strength)) { @@ -375,9 +317,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall high resolution enabled with toast - */ const recallHrfEnabled = useCallback( (hrfEnabled: unknown) => { if (!isParameterHRFEnabled(hrfEnabled)) { @@ -390,9 +329,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall high resolution strength with toast - */ const recallHrfStrength = useCallback( (hrfStrength: unknown) => { if (!isParameterStrength(hrfStrength)) { @@ -405,9 +341,6 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - /** - * Recall high resolution method with toast - */ const recallHrfMethod = useCallback( (hrfMethod: unknown) => { if (!isParameterHRFMethod(hrfMethod)) { @@ -420,358 +353,95 @@ export const useRecallParameters = () => { [dispatch, parameterSetToast, parameterNotSetToast] ); - const { data: mainModels } = useGetMainModelsQuery(ALL_BASE_MODELS); - - const prepareMainModelMetadataItem = useCallback( - (model: ModelIdentifier) => { - const matchingModel = mainModels ? mainModelsAdapterSelectors.selectById(mainModels, model.key) : undefined; - - if (!matchingModel) { - return { model: null, error: 'Model is not installed' }; - } - - return { model: matchingModel, error: null }; - }, - [mainModels] - ); - - /** - * Recall model with toast - */ const recallModel = useCallback( - (model: unknown) => { - if (!isModelIdentifier(model)) { - parameterNotSetToast(); + async (modelMetadataItem: unknown) => { + try { + const model = await prepareMainModelMetadataItem(modelMetadataItem); + dispatch(modelSelected(model)); + parameterSetToast(); + } catch (e) { + parameterNotSetToast((e as unknown as Error).message); return; } - - const result = prepareMainModelMetadataItem(model); - - if (!result.model) { - parameterNotSetToast(result.error); - return; - } - - dispatch(modelSelected(result.model)); - parameterSetToast(); }, - [prepareMainModelMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] + [dispatch, parameterSetToast, parameterNotSetToast] ); - const { data: vaeModels } = useGetVaeModelsQuery(); - - const prepareVAEMetadataItem = useCallback( - (vae: ModelIdentifier, newModel?: ParameterModel) => { - const matchingModel = vaeModels ? vaeModelsAdapterSelectors.selectById(vaeModels, vae.key) : undefined; - if (!matchingModel) { - return { vae: null, error: 'VAE model is not installed' }; - } - const isCompatibleBaseModel = matchingModel?.base === (newModel ?? model)?.base; - - if (!isCompatibleBaseModel) { - return { - vae: null, - error: 'VAE incompatible with currently-selected model', - }; - } - - return { vae: matchingModel, error: null }; - }, - [model, vaeModels] - ); - - /** - * Recall vae model - */ const recallVaeModel = useCallback( - (vae: unknown) => { - if (!isModelIdentifier(vae) && !isNil(vae)) { - parameterNotSetToast(); - return; - } - - if (isNil(vae)) { + async (vaeMetadataItem: unknown) => { + if (isNil(vaeMetadataItem)) { dispatch(vaeSelected(null)); parameterSetToast(); return; } - - const result = prepareVAEMetadataItem(vae); - - if (!result.vae) { - parameterNotSetToast(result.error); + try { + const vae = await prepareVAEMetadataItem(vaeMetadataItem); + dispatch(vaeSelected(vae)); + parameterSetToast(); + } catch (e) { + parameterNotSetToast((e as unknown as Error).message); return; } - - dispatch(vaeSelected(result.vae)); - parameterSetToast(); }, - [prepareVAEMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] - ); - - /** - * Recall LoRA with toast - */ - - const { data: loraModels } = useGetLoRAModelsQuery(undefined); - - const prepareLoRAMetadataItem = useCallback( - (loraMetadataItem: LoRAMetadataItem, newModel?: ParameterModel) => { - if (!isModelIdentifier(loraMetadataItem.lora)) { - return { lora: null, error: 'Invalid LoRA model' }; - } - - const { lora } = loraMetadataItem; - - const matchingLoRA = loraModels ? loraModelsAdapterSelectors.selectById(loraModels, lora.key) : undefined; - - if (!matchingLoRA) { - return { lora: null, error: 'LoRA model is not installed' }; - } - - const isCompatibleBaseModel = matchingLoRA?.base === (newModel ?? model)?.base; - - if (!isCompatibleBaseModel) { - return { - lora: null, - error: 'LoRA incompatible with currently-selected model', - }; - } - - return { lora: matchingLoRA, error: null }; - }, - [loraModels, model] + [dispatch, parameterSetToast, parameterNotSetToast] ); const recallLoRA = useCallback( - (loraMetadataItem: LoRAMetadataItem) => { - const result = prepareLoRAMetadataItem(loraMetadataItem); - - if (!result.lora) { - parameterNotSetToast(result.error); + async (loraMetadataItem: LoRAMetadataItem) => { + try { + const lora = await prepareLoRAMetadataItem(loraMetadataItem, model?.base); + dispatch(loraRecalled(lora)); + parameterSetToast(); + } catch (e) { + parameterNotSetToast((e as unknown as Error).message); return; } - - dispatch(loraRecalled({ ...result.lora, weight: loraMetadataItem.weight })); - - parameterSetToast(); }, - [prepareLoRAMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] - ); - - /** - * Recall ControlNet with toast - */ - - const { data: controlNetModels } = useGetControlNetModelsQuery(undefined); - - const prepareControlNetMetadataItem = useCallback( - (controlnetMetadataItem: ControlNetMetadataItem, newModel?: ParameterModel) => { - if (!isModelIdentifier(controlnetMetadataItem.control_model)) { - return { controlnet: null, error: 'Invalid ControlNet model' }; - } - - const { image, control_model, control_weight, begin_step_percent, end_step_percent, control_mode, resize_mode } = - controlnetMetadataItem; - - const matchingControlNetModel = controlNetModels - ? controlNetModelsAdapterSelectors.selectById(controlNetModels, control_model.key) - : undefined; - - if (!matchingControlNetModel) { - return { controlnet: null, error: 'ControlNet model is not installed' }; - } - - const isCompatibleBaseModel = matchingControlNetModel?.base === (newModel ?? model)?.base; - - if (!isCompatibleBaseModel) { - return { - controlnet: null, - error: 'ControlNet incompatible with currently-selected model', - }; - } - - // We don't save the original image that was processed into a control image, only the processed image - const processorType = 'none'; - const processorNode = CONTROLNET_PROCESSORS.none.default; - - const controlnet: ControlNetConfig = { - type: 'controlnet', - isEnabled: true, - model: matchingControlNetModel, - weight: typeof control_weight === 'number' ? control_weight : initialControlNet.weight, - beginStepPct: begin_step_percent || initialControlNet.beginStepPct, - endStepPct: end_step_percent || initialControlNet.endStepPct, - controlMode: control_mode || initialControlNet.controlMode, - resizeMode: resize_mode || initialControlNet.resizeMode, - controlImage: image?.image_name || null, - processedControlImage: image?.image_name || null, - processorType, - processorNode, - shouldAutoConfig: true, - id: uuidv4(), - }; - - return { controlnet, error: null }; - }, - [controlNetModels, model] + [model?.base, dispatch, parameterSetToast, parameterNotSetToast] ); const recallControlNet = useCallback( - (controlnetMetadataItem: ControlNetMetadataItem) => { - const result = prepareControlNetMetadataItem(controlnetMetadataItem); - - if (!result.controlnet) { - parameterNotSetToast(result.error); + async (controlnetMetadataItem: ControlNetMetadataItem) => { + try { + const controlNetConfig = await prepareControlNetMetadataItem(controlnetMetadataItem, model?.base); + dispatch(controlAdapterRecalled(controlNetConfig)); + parameterSetToast(); + } catch (e) { + parameterNotSetToast((e as unknown as Error).message); return; } - - dispatch(controlAdapterRecalled(result.controlnet)); - - parameterSetToast(); }, - [prepareControlNetMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] - ); - - /** - * Recall T2I Adapter with toast - */ - - const { data: t2iAdapterModels } = useGetT2IAdapterModelsQuery(undefined); - - const prepareT2IAdapterMetadataItem = useCallback( - (t2iAdapterMetadataItem: T2IAdapterMetadataItem, newModel?: ParameterModel) => { - if (!isModelIdentifier(t2iAdapterMetadataItem.t2i_adapter_model)) { - return { controlnet: null, error: 'Invalid ControlNet model' }; - } - - const { image, t2i_adapter_model, weight, begin_step_percent, end_step_percent, resize_mode } = - t2iAdapterMetadataItem; - - const matchingT2IAdapterModel = t2iAdapterModels - ? t2iAdapterModelsAdapterSelectors.selectById(t2iAdapterModels, t2i_adapter_model.key) - : undefined; - - if (!matchingT2IAdapterModel) { - return { controlnet: null, error: 'ControlNet model is not installed' }; - } - - const isCompatibleBaseModel = matchingT2IAdapterModel?.base === (newModel ?? model)?.base; - - if (!isCompatibleBaseModel) { - return { - t2iAdapter: null, - error: 'ControlNet incompatible with currently-selected model', - }; - } - - // We don't save the original image that was processed into a control image, only the processed image - const processorType = 'none'; - const processorNode = CONTROLNET_PROCESSORS.none.default; - - const t2iAdapter: T2IAdapterConfig = { - type: 't2i_adapter', - isEnabled: true, - model: matchingT2IAdapterModel, - weight: typeof weight === 'number' ? weight : initialT2IAdapter.weight, - beginStepPct: begin_step_percent || initialT2IAdapter.beginStepPct, - endStepPct: end_step_percent || initialT2IAdapter.endStepPct, - resizeMode: resize_mode || initialT2IAdapter.resizeMode, - controlImage: image?.image_name || null, - processedControlImage: image?.image_name || null, - processorType, - processorNode, - shouldAutoConfig: true, - id: uuidv4(), - }; - - return { t2iAdapter, error: null }; - }, - [model, t2iAdapterModels] + [model?.base, dispatch, parameterSetToast, parameterNotSetToast] ); const recallT2IAdapter = useCallback( - (t2iAdapterMetadataItem: T2IAdapterMetadataItem) => { - const result = prepareT2IAdapterMetadataItem(t2iAdapterMetadataItem); - - if (!result.t2iAdapter) { - parameterNotSetToast(result.error); + async (t2iAdapterMetadataItem: T2IAdapterMetadataItem) => { + try { + const t2iAdapterConfig = await prepareT2IAdapterMetadataItem(t2iAdapterMetadataItem, model?.base); + dispatch(controlAdapterRecalled(t2iAdapterConfig)); + parameterSetToast(); + } catch (e) { + parameterNotSetToast((e as unknown as Error).message); return; } - - dispatch(controlAdapterRecalled(result.t2iAdapter)); - - parameterSetToast(); }, - [prepareT2IAdapterMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] - ); - - /** - * Recall IP Adapter with toast - */ - - const { data: ipAdapterModels } = useGetIPAdapterModelsQuery(undefined); - - const prepareIPAdapterMetadataItem = useCallback( - (ipAdapterMetadataItem: IPAdapterMetadataItem, newModel?: ParameterModel) => { - if (!isModelIdentifier(ipAdapterMetadataItem?.ip_adapter_model)) { - return { ipAdapter: null, error: 'Invalid IP Adapter model' }; - } - - const { image, ip_adapter_model, weight, begin_step_percent, end_step_percent } = ipAdapterMetadataItem; - - const matchingIPAdapterModel = ipAdapterModels - ? ipAdapterModelsAdapterSelectors.selectById(ipAdapterModels, ip_adapter_model.key) - : undefined; - - if (!matchingIPAdapterModel) { - return { ipAdapter: null, error: 'IP Adapter model is not installed' }; - } - - const isCompatibleBaseModel = matchingIPAdapterModel?.base === (newModel ?? model)?.base; - - if (!isCompatibleBaseModel) { - return { - ipAdapter: null, - error: 'IP Adapter incompatible with currently-selected model', - }; - } - - const ipAdapter: IPAdapterConfig = { - id: uuidv4(), - type: 'ip_adapter', - isEnabled: true, - controlImage: image?.image_name ?? null, - model: matchingIPAdapterModel, - weight: weight ?? initialIPAdapter.weight, - beginStepPct: begin_step_percent ?? initialIPAdapter.beginStepPct, - endStepPct: end_step_percent ?? initialIPAdapter.endStepPct, - }; - - return { ipAdapter, error: null }; - }, - [ipAdapterModels, model] + [model?.base, dispatch, parameterSetToast, parameterNotSetToast] ); const recallIPAdapter = useCallback( - (ipAdapterMetadataItem: IPAdapterMetadataItem) => { - const result = prepareIPAdapterMetadataItem(ipAdapterMetadataItem); - - if (!result.ipAdapter) { - parameterNotSetToast(result.error); + async (ipAdapterMetadataItem: IPAdapterMetadataItem) => { + try { + const ipAdapterConfig = await prepareIPAdapterMetadataItem(ipAdapterMetadataItem, model?.base); + dispatch(controlAdapterRecalled(ipAdapterConfig)); + parameterSetToast(); + } catch (e) { + parameterNotSetToast((e as unknown as Error).message); return; } - - dispatch(controlAdapterRecalled(result.ipAdapter)); - - parameterSetToast(); }, - [prepareIPAdapterMetadataItem, dispatch, parameterSetToast, parameterNotSetToast] + [model?.base, dispatch, parameterSetToast, parameterNotSetToast] ); - /* - * Sets image as initial image with toast - */ const sendToImageToImage = useCallback( (image: ImageDTO) => { dispatch(initialImageSelected(image)); @@ -780,7 +450,7 @@ export const useRecallParameters = () => { ); const recallAllParameters = useCallback( - (metadata: CoreMetadata | undefined) => { + async (metadata: CoreMetadata | undefined) => { if (!metadata) { allParameterNotSetToast(); return; @@ -820,10 +490,12 @@ export const useRecallParameters = () => { let newModel: ParameterModel | undefined = undefined; if (isModelIdentifier(model)) { - const result = prepareMainModelMetadataItem(model); - if (result.model) { - dispatch(modelSelected(result.model)); - newModel = result.model; + try { + const _model = await prepareMainModelMetadataItem(model); + dispatch(modelSelected(_model)); + newModel = _model; + } catch { + return; } } @@ -850,9 +522,11 @@ export const useRecallParameters = () => { if (isNil(vae)) { dispatch(vaeSelected(null)); } else { - const result = prepareVAEMetadataItem(vae, newModel); - if (result.vae) { - dispatch(vaeSelected(result.vae)); + try { + const _vae = await prepareVAEMetadataItem(vae, newModel?.base); + dispatch(vaeSelected(_vae)); + } catch { + return; } } } @@ -926,48 +600,46 @@ export const useRecallParameters = () => { } dispatch(lorasCleared()); - loras?.forEach((lora) => { - const result = prepareLoRAMetadataItem(lora, newModel); - if (result.lora) { - dispatch(loraRecalled({ ...result.lora, weight: lora.weight })); + loras?.forEach(async (loraMetadataItem) => { + try { + const lora = await prepareLoRAMetadataItem(loraMetadataItem, newModel?.base); + dispatch(loraRecalled(lora)); + } catch { + return; } }); dispatch(controlAdaptersReset()); - controlnets?.forEach((controlnet) => { - const result = prepareControlNetMetadataItem(controlnet, newModel); - if (result.controlnet) { - dispatch(controlAdapterRecalled(result.controlnet)); + controlnets?.forEach(async (controlNetMetadataItem) => { + try { + const controlNet = await prepareControlNetMetadataItem(controlNetMetadataItem, newModel?.base); + dispatch(controlAdapterRecalled(controlNet)); + } catch { + return; } }); - ipAdapters?.forEach((ipAdapter) => { - const result = prepareIPAdapterMetadataItem(ipAdapter, newModel); - if (result.ipAdapter) { - dispatch(controlAdapterRecalled(result.ipAdapter)); + ipAdapters?.forEach(async (ipAdapterMetadataItem) => { + try { + const ipAdapter = await prepareIPAdapterMetadataItem(ipAdapterMetadataItem, newModel?.base); + dispatch(controlAdapterRecalled(ipAdapter)); + } catch { + return; } }); - t2iAdapters?.forEach((t2iAdapter) => { - const result = prepareT2IAdapterMetadataItem(t2iAdapter, newModel); - if (result.t2iAdapter) { - dispatch(controlAdapterRecalled(result.t2iAdapter)); + t2iAdapters?.forEach(async (t2iAdapterMetadataItem) => { + try { + const t2iAdapter = await prepareT2IAdapterMetadataItem(t2iAdapterMetadataItem, newModel?.base); + dispatch(controlAdapterRecalled(t2iAdapter)); + } catch { + return; } }); allParameterSetToast(); }, - [ - dispatch, - allParameterSetToast, - allParameterNotSetToast, - prepareMainModelMetadataItem, - prepareVAEMetadataItem, - prepareLoRAMetadataItem, - prepareControlNetMetadataItem, - prepareIPAdapterMetadataItem, - prepareT2IAdapterMetadataItem, - ] + [dispatch, allParameterSetToast, allParameterNotSetToast] ); return { diff --git a/invokeai/frontend/web/src/features/parameters/util/modelFetchingHelpers.ts b/invokeai/frontend/web/src/features/parameters/util/modelFetchingHelpers.ts new file mode 100644 index 0000000000..c7d25fed8b --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/util/modelFetchingHelpers.ts @@ -0,0 +1,113 @@ +import { getStore } from 'app/store/nanostores/store'; +import { isModelIdentifier } from 'features/nodes/types/common'; +import { modelsApi } from 'services/api/endpoints/models'; +import type { AnyModelConfig, BaseModelType } from 'services/api/types'; +import { + isControlNetModelConfig, + isIPAdapterModelConfig, + isLoRAModelConfig, + isNonRefinerMainModelConfig, + isRefinerMainModelModelConfig, + isT2IAdapterModelConfig, + isTextualInversionModelConfig, + isVAEModelConfig, +} from 'services/api/types'; + +/** + * Raised when a model config is unable to be fetched. + */ +export class ModelConfigNotFoundError extends Error { + /** + * Create ModelConfigNotFoundError + * @param {String} message + */ + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +/** + * Raised when a fetched model config is of an unexpected type. + */ +export class InvalidModelConfigError extends Error { + /** + * Create InvalidModelConfigError + * @param {String} message + */ + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export const fetchModelConfig = async (key: string): Promise => { + const { dispatch } = getStore(); + try { + const req = dispatch(modelsApi.endpoints.getModelConfig.initiate(key)); + req.unsubscribe(); + return await req.unwrap(); + } catch { + throw new ModelConfigNotFoundError(`Unable to retrieve model config for key ${key}`); + } +}; + +export const fetchModelConfigWithTypeGuard = async ( + key: string, + typeGuard: (config: AnyModelConfig) => config is T +) => { + const modelConfig = await fetchModelConfig(key); + if (!typeGuard(modelConfig)) { + throw new InvalidModelConfigError(`Invalid model type for key ${key}: ${modelConfig.type}`); + } + return modelConfig; +}; + +export const fetchMainModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isNonRefinerMainModelConfig); +}; + +export const fetchRefinerModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isRefinerMainModelModelConfig); +}; + +export const fetchVAEModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isVAEModelConfig); +}; + +export const fetchLoRAModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isLoRAModelConfig); +}; + +export const fetchControlNetModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isControlNetModelConfig); +}; + +export const fetchIPAdapterModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isIPAdapterModelConfig); +}; + +export const fetchT2IAdapterModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isT2IAdapterModelConfig); +}; + +export const fetchTextualInversionModel = async (key: string) => { + return fetchModelConfigWithTypeGuard(key, isTextualInversionModelConfig); +}; + +export const isBaseCompatible = (sourceBase: BaseModelType, targetBase: BaseModelType) => { + return sourceBase === targetBase; +}; + +export const raiseIfBaseIncompatible = (sourceBase: BaseModelType, targetBase?: BaseModelType, message?: string) => { + if (targetBase && !isBaseCompatible(sourceBase, targetBase)) { + throw new InvalidModelConfigError(message || `Incompatible base models: ${sourceBase} and ${targetBase}`); + } +}; + +export const getModelKey = (modelIdentifier: unknown, message?: string): string => { + if (!isModelIdentifier(modelIdentifier)) { + throw new InvalidModelConfigError(message || `Invalid model identifier: ${modelIdentifier}`); + } + return modelIdentifier.key; +}; diff --git a/invokeai/frontend/web/src/features/parameters/util/modelMetadataHelpers.ts b/invokeai/frontend/web/src/features/parameters/util/modelMetadataHelpers.ts new file mode 100644 index 0000000000..722073366f --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/util/modelMetadataHelpers.ts @@ -0,0 +1,150 @@ +import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; +import type { ControlNetConfig, IPAdapterConfig, T2IAdapterConfig } from 'features/controlAdapters/store/types'; +import { + initialControlNet, + initialIPAdapter, + initialT2IAdapter, +} from 'features/controlAdapters/util/buildControlAdapter'; +import type { LoRA } from 'features/lora/store/loraSlice'; +import type { ModelIdentifierWithBase } from 'features/nodes/types/common'; +import { zModelIdentifierWithBase } from 'features/nodes/types/common'; +import type { + ControlNetMetadataItem, + IPAdapterMetadataItem, + LoRAMetadataItem, + T2IAdapterMetadataItem, +} from 'features/nodes/types/metadata'; +import { + fetchControlNetModel, + fetchIPAdapterModel, + fetchLoRAModel, + fetchMainModel, + fetchRefinerModel, + fetchT2IAdapterModel, + fetchVAEModel, + getModelKey, + raiseIfBaseIncompatible, +} from 'features/parameters/util/modelFetchingHelpers'; +import type { BaseModelType } from 'services/api/types'; +import { v4 as uuidv4 } from 'uuid'; + +export const prepareMainModelMetadataItem = async (model: unknown): Promise => { + const key = getModelKey(model); + const mainModel = await fetchMainModel(key); + return zModelIdentifierWithBase.parse(mainModel); +}; + +export const prepareRefinerMetadataItem = async (model: unknown): Promise => { + const key = getModelKey(model); + const refinerModel = await fetchRefinerModel(key); + return zModelIdentifierWithBase.parse(refinerModel); +}; + +export const prepareVAEMetadataItem = async (vae: unknown, base?: BaseModelType): Promise => { + const key = getModelKey(vae); + const vaeModel = await fetchVAEModel(key); + raiseIfBaseIncompatible(vaeModel.base, base, 'VAE incompatible with currently-selected model'); + return zModelIdentifierWithBase.parse(vaeModel); +}; + +export const prepareLoRAMetadataItem = async ( + loraMetadataItem: LoRAMetadataItem, + base?: BaseModelType +): Promise => { + const key = getModelKey(loraMetadataItem.lora); + const loraModel = await fetchLoRAModel(key); + raiseIfBaseIncompatible(loraModel.base, base, 'LoRA incompatible with currently-selected model'); + return { key: loraModel.key, base: loraModel.base, weight: loraMetadataItem.weight, isEnabled: true }; +}; + +export const prepareControlNetMetadataItem = async ( + controlnetMetadataItem: ControlNetMetadataItem, + base?: BaseModelType +): Promise => { + const key = getModelKey(controlnetMetadataItem.control_model); + const controlNetModel = await fetchControlNetModel(key); + raiseIfBaseIncompatible(controlNetModel.base, base, 'ControlNet incompatible with currently-selected model'); + + const { image, control_weight, begin_step_percent, end_step_percent, control_mode, resize_mode } = + controlnetMetadataItem; + + // We don't save the original image that was processed into a control image, only the processed image + const processorType = 'none'; + const processorNode = CONTROLNET_PROCESSORS.none.default; + + const controlnet: ControlNetConfig = { + type: 'controlnet', + isEnabled: true, + model: zModelIdentifierWithBase.parse(controlNetModel), + weight: typeof control_weight === 'number' ? control_weight : initialControlNet.weight, + beginStepPct: begin_step_percent || initialControlNet.beginStepPct, + endStepPct: end_step_percent || initialControlNet.endStepPct, + controlMode: control_mode || initialControlNet.controlMode, + resizeMode: resize_mode || initialControlNet.resizeMode, + controlImage: image?.image_name || null, + processedControlImage: image?.image_name || null, + processorType, + processorNode, + shouldAutoConfig: true, + id: uuidv4(), + }; + + return controlnet; +}; + +export const prepareT2IAdapterMetadataItem = async ( + t2iAdapterMetadataItem: T2IAdapterMetadataItem, + base?: BaseModelType +): Promise => { + const key = getModelKey(t2iAdapterMetadataItem.t2i_adapter_model); + const t2iAdapterModel = await fetchT2IAdapterModel(key); + raiseIfBaseIncompatible(t2iAdapterModel.base, base, 'T2I Adapter incompatible with currently-selected model'); + + const { image, weight, begin_step_percent, end_step_percent, resize_mode } = t2iAdapterMetadataItem; + + // We don't save the original image that was processed into a control image, only the processed image + const processorType = 'none'; + const processorNode = CONTROLNET_PROCESSORS.none.default; + + const t2iAdapter: T2IAdapterConfig = { + type: 't2i_adapter', + isEnabled: true, + model: zModelIdentifierWithBase.parse(t2iAdapterModel), + weight: typeof weight === 'number' ? weight : initialT2IAdapter.weight, + beginStepPct: begin_step_percent || initialT2IAdapter.beginStepPct, + endStepPct: end_step_percent || initialT2IAdapter.endStepPct, + resizeMode: resize_mode || initialT2IAdapter.resizeMode, + controlImage: image?.image_name || null, + processedControlImage: image?.image_name || null, + processorType, + processorNode, + shouldAutoConfig: true, + id: uuidv4(), + }; + + return t2iAdapter; +}; + +export const prepareIPAdapterMetadataItem = async ( + ipAdapterMetadataItem: IPAdapterMetadataItem, + base?: BaseModelType +): Promise => { + const key = getModelKey(ipAdapterMetadataItem?.ip_adapter_model); + const ipAdapterModel = await fetchIPAdapterModel(key); + raiseIfBaseIncompatible(ipAdapterModel.base, base, 'T2I Adapter incompatible with currently-selected model'); + + const { image, weight, begin_step_percent, end_step_percent } = ipAdapterMetadataItem; + + const ipAdapter: IPAdapterConfig = { + id: uuidv4(), + type: 'ip_adapter', + isEnabled: true, + controlImage: image?.image_name ?? null, + model: zModelIdentifierWithBase.parse(ipAdapterModel), + weight: weight ?? initialIPAdapter.weight, + beginStepPct: begin_step_percent ?? initialIPAdapter.beginStepPct, + endStepPct: end_step_percent ?? initialIPAdapter.endStepPct, + }; + + return ipAdapter; +}; diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 666e0c707d..2bd1a0a246 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -6,16 +6,16 @@ import type { operations, paths } from 'services/api/schema'; import type { AnyModelConfig, BaseModelType, - ControlNetConfig, + ControlNetModelConfig, ImportModelConfig, - IPAdapterConfig, - LoRAConfig, + IPAdapterModelConfig, + LoRAModelConfig, MainModelConfig, MergeModelConfig, ModelType, - T2IAdapterConfig, - TextualInversionConfig, - VAEConfig, + T2IAdapterModelConfig, + TextualInversionModelConfig, + VAEModelConfig, } from 'services/api/types'; import type { ApiTagDescription, tagTypes } from '..'; @@ -30,7 +30,7 @@ type UpdateMainModelArg = { type UpdateLoRAModelArg = { base_model: BaseModelType; model_name: string; - body: LoRAConfig; + body: LoRAModelConfig; }; type UpdateMainModelResponse = @@ -97,27 +97,27 @@ export const mainModelsAdapter = createEntityAdapter({ sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const mainModelsAdapterSelectors = mainModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const loraModelsAdapter = createEntityAdapter({ +export const loraModelsAdapter = createEntityAdapter({ selectId: (entity) => entity.key, sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const loraModelsAdapterSelectors = loraModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const controlNetModelsAdapter = createEntityAdapter({ +export const controlNetModelsAdapter = createEntityAdapter({ selectId: (entity) => entity.key, sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const controlNetModelsAdapterSelectors = controlNetModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const ipAdapterModelsAdapter = createEntityAdapter({ +export const ipAdapterModelsAdapter = createEntityAdapter({ selectId: (entity) => entity.key, sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const ipAdapterModelsAdapterSelectors = ipAdapterModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const t2iAdapterModelsAdapter = createEntityAdapter({ +export const t2iAdapterModelsAdapter = createEntityAdapter({ selectId: (entity) => entity.key, sortComparer: (a, b) => a.name.localeCompare(b.name), }); export const t2iAdapterModelsAdapterSelectors = t2iAdapterModelsAdapter.getSelectors(undefined, getSelectorsOptions); -export const textualInversionModelsAdapter = createEntityAdapter({ +export const textualInversionModelsAdapter = createEntityAdapter({ selectId: (entity) => entity.key, sortComparer: (a, b) => a.name.localeCompare(b.name), }); @@ -125,7 +125,7 @@ export const textualInversionModelsAdapterSelectors = textualInversionModelsAdap undefined, getSelectorsOptions ); -export const vaeModelsAdapter = createEntityAdapter({ +export const vaeModelsAdapter = createEntityAdapter({ selectId: (entity) => entity.key, sortComparer: (a, b) => a.name.localeCompare(b.name), }); @@ -162,6 +162,8 @@ const buildTransformResponse = */ const buildModelsUrl = (path: string = '') => buildV2Url(`models/${path}`); +// TODO(psyche): Ideally we can share the cache between the `getXYZModels` queries and `getModelConfig` query + export const modelsApi = api.injectEndpoints({ endpoints: (build) => ({ getMainModels: build.query, BaseModelType[]>({ @@ -257,10 +259,10 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: ['Model'], }), - getLoRAModels: build.query, void>({ + getLoRAModels: build.query, void>({ query: () => ({ url: buildModelsUrl(), params: { model_type: 'lora' } }), - providesTags: buildProvidesTags('LoRAModel'), - transformResponse: buildTransformResponse(loraModelsAdapter), + providesTags: buildProvidesTags('LoRAModel'), + transformResponse: buildTransformResponse(loraModelsAdapter), }), updateLoRAModels: build.mutation({ query: ({ base_model, model_name, body }) => { @@ -281,30 +283,30 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: [{ type: 'LoRAModel', id: LIST_TAG }], }), - getControlNetModels: build.query, void>({ + getControlNetModels: build.query, void>({ query: () => ({ url: buildModelsUrl(), params: { model_type: 'controlnet' } }), - providesTags: buildProvidesTags('ControlNetModel'), - transformResponse: buildTransformResponse(controlNetModelsAdapter), + providesTags: buildProvidesTags('ControlNetModel'), + transformResponse: buildTransformResponse(controlNetModelsAdapter), }), - getIPAdapterModels: build.query, void>({ + getIPAdapterModels: build.query, void>({ query: () => ({ url: buildModelsUrl(), params: { model_type: 'ip_adapter' } }), - providesTags: buildProvidesTags('IPAdapterModel'), - transformResponse: buildTransformResponse(ipAdapterModelsAdapter), + providesTags: buildProvidesTags('IPAdapterModel'), + transformResponse: buildTransformResponse(ipAdapterModelsAdapter), }), - getT2IAdapterModels: build.query, void>({ + getT2IAdapterModels: build.query, void>({ query: () => ({ url: buildModelsUrl(), params: { model_type: 't2i_adapter' } }), - providesTags: buildProvidesTags('T2IAdapterModel'), - transformResponse: buildTransformResponse(t2iAdapterModelsAdapter), + providesTags: buildProvidesTags('T2IAdapterModel'), + transformResponse: buildTransformResponse(t2iAdapterModelsAdapter), }), - getVaeModels: build.query, void>({ + getVaeModels: build.query, void>({ query: () => ({ url: buildModelsUrl(), params: { model_type: 'vae' } }), - providesTags: buildProvidesTags('VaeModel'), - transformResponse: buildTransformResponse(vaeModelsAdapter), + providesTags: buildProvidesTags('VaeModel'), + transformResponse: buildTransformResponse(vaeModelsAdapter), }), - getTextualInversionModels: build.query, void>({ + getTextualInversionModels: build.query, void>({ query: () => ({ url: buildModelsUrl(), params: { model_type: 'embedding' } }), - providesTags: buildProvidesTags('TextualInversionModel'), - transformResponse: buildTransformResponse(textualInversionModelsAdapter), + providesTags: buildProvidesTags('TextualInversionModel'), + transformResponse: buildTransformResponse(textualInversionModelsAdapter), }), getModelsInFolder: build.query({ query: (arg) => { From abc569c2dd5f63e4bb9d6b8ee369b2526f65dcbd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 22 Feb 2024 22:42:57 +1100 Subject: [PATCH 246/411] fix(ui): roll back utility-types It's `Required` util does not distribute over unions as expected. Also we have `ts-toolbelt` already for some utils. --- invokeai/frontend/web/package.json | 1 - invokeai/frontend/web/pnpm-lock.yaml | 8 -------- invokeai/frontend/web/src/services/api/types.ts | 5 ++--- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 0bf236ee38..48bad31000 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -145,7 +145,6 @@ "ts-toolbelt": "^9.6.0", "tsafe": "^1.6.6", "typescript": "^5.3.3", - "utility-types": "^3.11.0", "vite": "^5.1.3", "vite-plugin-css-injected-by-js": "^3.4.0", "vite-plugin-dts": "^3.7.2", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index f2abdd87bf..d79d482d08 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -268,9 +268,6 @@ devDependencies: typescript: specifier: ^5.3.3 version: 5.3.3 - utility-types: - specifier: ^3.11.0 - version: 3.11.0 vite: specifier: ^5.1.3 version: 5.1.3(@types/node@20.11.19) @@ -14147,11 +14144,6 @@ packages: which-typed-array: 1.1.14 dev: true - /utility-types@3.11.0: - resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} - engines: {node: '>= 4'} - dev: true - /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index d561173337..6f6018d974 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -2,7 +2,6 @@ import type { UseToastOptions } from '@invoke-ai/ui-library'; import type { EntityState } from '@reduxjs/toolkit'; import type { components, paths } from 'services/api/schema'; import type { O } from 'ts-toolbelt'; -import type { Overwrite } from 'utility-types'; export type S = components['schemas']; @@ -72,8 +71,8 @@ export type TextualInversionModelConfig = S['TextualInversionConfig']; export type DiffusersModelConfig = S['MainDiffusersConfig']; export type CheckpointModelConfig = S['MainCheckpointConfig']; export type MainModelConfig = DiffusersModelConfig | CheckpointModelConfig; -export type RefinerMainModelConfig = Overwrite; -export type NonRefinerMainModelConfig = Overwrite; +export type RefinerMainModelConfig = Omit & { base: 'sdxl-refiner' }; +export type NonRefinerMainModelConfig = Omit & { base: 'any' | 'sd-1' | 'sd-2' | 'sdxl' }; export type AnyModelConfig = | LoRAModelConfig | VAEModelConfig From 008716040b8dd2488474028570dceebb6d837e3d Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Thu, 22 Feb 2024 14:27:49 -0500 Subject: [PATCH 247/411] Allow users to run model manager without cuda --- .../app/services/model_install/model_install_default.py | 2 +- .../app/services/model_manager/model_manager_base.py | 5 ++++- .../app/services/model_manager/model_manager_default.py | 9 ++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 9b771c5159..c2718b5b2e 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -543,7 +543,7 @@ class ModelInstallService(ModelInstallServiceBase): self, model_path: Path, config: Optional[Dict[str, Any]] = None, info: Optional[AnyModelConfig] = None ) -> str: info = info or ModelProbe.probe(model_path, config) - key = self._create_key() + key = info.key or self._create_key() model_path = model_path.absolute() if model_path.is_relative_to(self.app_config.models_path): diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index c25aa6fb47..938e14adcb 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -1,5 +1,7 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team +import torch + from abc import ABC, abstractmethod from typing import Optional @@ -32,9 +34,10 @@ class ModelManagerServiceBase(ABC): def build_model_manager( cls, app_config: InvokeAIAppConfig, - db: SqliteDatabase, + model_record_service: ModelRecordServiceBase, download_queue: DownloadQueueServiceBase, events: EventServiceBase, + execution_device: torch.device, ) -> Self: """ Construct the model manager service instance. diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index d029f9e033..2276111586 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -1,6 +1,8 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team """Implementation of ModelManagerServiceBase.""" +import torch + from typing import Optional from typing_extensions import Self @@ -9,6 +11,7 @@ from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.invocation_context import InvocationContextData from invokeai.backend.model_manager import AnyModelConfig, BaseModelType, LoadedModel, ModelType, SubModelType from invokeai.backend.model_manager.load import ModelCache, ModelConvertCache, ModelLoaderRegistry +from invokeai.backend.util.devices import choose_torch_device from invokeai.backend.util.logging import InvokeAILogger from ..config import InvokeAIAppConfig @@ -119,6 +122,7 @@ class ModelManagerService(ModelManagerServiceBase): model_record_service: ModelRecordServiceBase, download_queue: DownloadQueueServiceBase, events: EventServiceBase, + execution_device: torch.device = choose_torch_device(), ) -> Self: """ Construct the model manager service instance. @@ -129,7 +133,10 @@ class ModelManagerService(ModelManagerServiceBase): logger.setLevel(app_config.log_level.upper()) ram_cache = ModelCache( - max_cache_size=app_config.ram_cache_size, max_vram_cache_size=app_config.vram_cache_size, logger=logger + max_cache_size=app_config.ram_cache_size, + max_vram_cache_size=app_config.vram_cache_size, + logger=logger, + execution_device=execution_device, ) convert_cache = ModelConvertCache( cache_path=app_config.models_convert_cache_path, max_size=app_config.convert_cache_size From de9287a3e4c3fdb4d39d5db47f6ee9174574aad5 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Thu, 22 Feb 2024 14:54:48 -0500 Subject: [PATCH 248/411] Run ruff --- invokeai/app/services/model_manager/model_manager_base.py | 4 +--- invokeai/app/services/model_manager/model_manager_default.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py index 938e14adcb..6e886df652 100644 --- a/invokeai/app/services/model_manager/model_manager_base.py +++ b/invokeai/app/services/model_manager/model_manager_base.py @@ -1,10 +1,9 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team -import torch - from abc import ABC, abstractmethod from typing import Optional +import torch from typing_extensions import Self from invokeai.app.services.invoker import Invoker @@ -18,7 +17,6 @@ from ..events.events_base import EventServiceBase from ..model_install import ModelInstallServiceBase from ..model_load import ModelLoadServiceBase from ..model_records import ModelRecordServiceBase -from ..shared.sqlite.sqlite_database import SqliteDatabase class ModelManagerServiceBase(ABC): diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py index 2276111586..7d4b248323 100644 --- a/invokeai/app/services/model_manager/model_manager_default.py +++ b/invokeai/app/services/model_manager/model_manager_default.py @@ -1,10 +1,9 @@ # Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team """Implementation of ModelManagerServiceBase.""" -import torch - from typing import Optional +import torch from typing_extensions import Self from invokeai.app.services.invoker import Invoker From 65b91356d0abd7bab61fe618571e189c1a0aa09c Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Fri, 23 Feb 2024 10:19:20 -0500 Subject: [PATCH 249/411] Remove passing keys in on register --- invokeai/app/services/model_install/model_install_default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index c2718b5b2e..9b771c5159 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -543,7 +543,7 @@ class ModelInstallService(ModelInstallServiceBase): self, model_path: Path, config: Optional[Dict[str, Any]] = None, info: Optional[AnyModelConfig] = None ) -> str: info = info or ModelProbe.probe(model_path, config) - key = info.key or self._create_key() + key = self._create_key() model_path = model_path.absolute() if model_path.is_relative_to(self.app_config.models_path): From c778ab8db4a6809826904ec476921376dccf7a4b Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Fri, 23 Feb 2024 12:57:54 -0500 Subject: [PATCH 250/411] Allow passing in key on register --- .../app/services/model_install/model_install_default.py | 8 +++++--- invokeai/backend/model_manager/probe.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index 9b771c5159..2419fbe5da 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -542,8 +542,10 @@ class ModelInstallService(ModelInstallServiceBase): def _register( self, model_path: Path, config: Optional[Dict[str, Any]] = None, info: Optional[AnyModelConfig] = None ) -> str: - info = info or ModelProbe.probe(model_path, config) key = self._create_key() + if config and not config.get('key', None): + config['key'] = key + info = info or ModelProbe.probe(model_path, config) model_path = model_path.absolute() if model_path.is_relative_to(self.app_config.models_path): @@ -556,8 +558,8 @@ class ModelInstallService(ModelInstallServiceBase): # make config relative to our root legacy_conf = (self.app_config.root_dir / self.app_config.legacy_conf_dir / info.config).resolve() info.config = legacy_conf.relative_to(self.app_config.root_dir).as_posix() - self.record_store.add_model(key, info) - return key + self.record_store.add_model(info.key, info) + return info.key def _next_id(self) -> int: with self._lock: diff --git a/invokeai/backend/model_manager/probe.py b/invokeai/backend/model_manager/probe.py index 7de4289466..c33254ef4e 100644 --- a/invokeai/backend/model_manager/probe.py +++ b/invokeai/backend/model_manager/probe.py @@ -188,7 +188,7 @@ class ModelProbe(object): and fields["prediction_type"] == SchedulerPredictionType.VPrediction ) - model_info = ModelConfigFactory.make_config(fields) + model_info = ModelConfigFactory.make_config(fields, key=fields.get("key", None)) return model_info @classmethod From 55f3c6e7216ae6d5509887ffe6a3ef5b9e7b5f12 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 20 Feb 2024 10:03:10 -0500 Subject: [PATCH 251/411] get old UI working somewhat with new endpoints --- .../subpanels/AddModelsPanel/AddModels.tsx | 6 +- .../AddModelsPanel/SimpleAddModels.tsx | 5 +- .../subpanels/ModelManagerPanel.tsx | 14 +-- .../ModelManagerPanel/CheckpointModelEdit.tsx | 37 +++---- .../ModelManagerPanel/DiffusersModelEdit.tsx | 32 +++--- .../ModelManagerPanel/LoRAModelEdit.tsx | 30 +++--- .../ModelManagerPanel/ModelConvert.tsx | 14 +-- .../subpanels/ModelManagerPanel/ModelList.tsx | 7 +- .../ModelManagerPanel/ModelListItem.tsx | 28 ++--- .../web/src/services/api/endpoints/models.ts | 102 +++++++----------- 10 files changed, 120 insertions(+), 155 deletions(-) diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx index cb50334c99..9b4b95be9e 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx @@ -1,15 +1,18 @@ -import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library'; +import { Button, ButtonGroup, Flex, Text } from '@invoke-ai/ui-library'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import AdvancedAddModels from './AdvancedAddModels'; import SimpleAddModels from './SimpleAddModels'; +import { useGetModelImportsQuery } from '../../../../services/api/endpoints/models'; const AddModels = () => { const { t } = useTranslation(); const [addModelMode, setAddModelMode] = useState<'simple' | 'advanced'>('simple'); const handleAddModelSimple = useCallback(() => setAddModelMode('simple'), []); const handleAddModelAdvanced = useCallback(() => setAddModelMode('advanced'), []); + const { data } = useGetModelImportsQuery({}); + console.log({ data }); return ( @@ -24,6 +27,7 @@ const AddModels = () => { {addModelMode === 'simple' && } {addModelMode === 'advanced' && } + {data?.map((model) => {model.status})} ); }; diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/SimpleAddModels.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/SimpleAddModels.tsx index d7f705aedc..0124d6d570 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/SimpleAddModels.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/SimpleAddModels.tsx @@ -36,11 +36,10 @@ const SimpleAddModels = () => { const handleAddModelSubmit = (values: ExtendedImportModelConfig) => { const importModelResponseBody = { - location: values.location, - prediction_type: values.prediction_type === 'none' ? undefined : values.prediction_type, + config: values.prediction_type === 'none' ? undefined : values.prediction_type, }; - importMainModel({ body: importModelResponseBody }) + importMainModel({ source: values.location, config: importModelResponseBody }) .unwrap() .then((_) => { dispatch( diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx index 15149b339b..dab4e0b872 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx @@ -2,13 +2,13 @@ import { Flex, Text } from '@invoke-ai/ui-library'; import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ALL_BASE_MODELS } from 'services/api/constants'; -import type { DiffusersModelConfig, LoRAConfig, MainModelConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery, useGetMainModelsQuery } from 'services/api/endpoints/models'; import CheckpointModelEdit from './ModelManagerPanel/CheckpointModelEdit'; import DiffusersModelEdit from './ModelManagerPanel/DiffusersModelEdit'; import LoRAModelEdit from './ModelManagerPanel/LoRAModelEdit'; import ModelList from './ModelManagerPanel/ModelList'; +import { DiffusersModelConfig, LoRAConfig, MainModelConfig } from '../../../services/api/types'; const ModelManagerPanel = () => { const [selectedModelId, setSelectedModelId] = useState(); @@ -41,16 +41,16 @@ const ModelEdit = (props: ModelEditProps) => { const { t } = useTranslation(); const { model } = props; - if (model?.model_format === 'checkpoint') { - return ; + if (model?.format === 'checkpoint') { + return ; } - if (model?.model_format === 'diffusers') { - return ; + if (model?.format === 'diffusers') { + return ; } - if (model?.model_type === 'lora') { - return ; + if (model?.type === 'lora') { + return ; } return ( diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx index 43707308e0..0dd8a7add6 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx @@ -21,11 +21,9 @@ import { memo, useCallback, useEffect, useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import type { CheckpointModelConfig } from 'services/api/endpoints/models'; -import { useGetCheckpointConfigsQuery, useUpdateMainModelsMutation } from 'services/api/endpoints/models'; -import type { CheckpointModelConfig } from 'services/api/types'; - +import { useGetCheckpointConfigsQuery, useUpdateModelsMutation } from 'services/api/endpoints/models'; import ModelConvert from './ModelConvert'; +import { CheckpointModelConfig } from '../../../../services/api/types'; type CheckpointModelEditProps = { model: CheckpointModelConfig; @@ -34,7 +32,7 @@ type CheckpointModelEditProps = { const CheckpointModelEdit = (props: CheckpointModelEditProps) => { const { model } = props; - const [updateMainModel, { isLoading }] = useUpdateMainModelsMutation(); + const [updateModel, { isLoading }] = useUpdateModelsMutation(); const { data: availableCheckpointConfigs } = useGetCheckpointConfigsQuery(); const [useCustomConfig, setUseCustomConfig] = useState(false); @@ -56,12 +54,12 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { reset, } = useForm({ defaultValues: { - model_name: model.model_name ? model.model_name : '', - base_model: model.base_model, - model_type: 'main', + name: model.name ? model.name : '', + base: model.base, + type: 'main', path: model.path ? model.path : '', description: model.description ? model.description : '', - model_format: 'checkpoint', + format: 'checkpoint', vae: model.vae ? model.vae : '', config: model.config ? model.config : '', variant: model.variant, @@ -74,11 +72,10 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { const onSubmit = useCallback>( (values) => { const responseBody = { - base_model: model.base_model, - model_name: model.model_name, + key: model.key, body: values, }; - updateMainModel(responseBody) + updateModel(responseBody) .unwrap() .then((payload) => { reset(payload as CheckpointModelConfig, { keepDefaultValues: true }); @@ -103,7 +100,7 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { ); }); }, - [dispatch, model.base_model, model.model_name, reset, t, updateMainModel] + [dispatch, model.key, reset, t, updateModel] ); return ( @@ -111,13 +108,13 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { - {model.model_name} + {model.name} - {MODEL_TYPE_MAP[model.base_model]} {t('modelManager.model')} + {MODEL_TYPE_MAP[model.base]} {t('modelManager.model')} - {![''].includes(model.base_model) ? ( + {![''].includes(model.base) ? ( ) : ( @@ -130,20 +127,20 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => {

- + {t('modelManager.name')} value.trim().length > 3 || 'Must be at least 3 characters', })} /> - {errors.model_name?.message && {errors.model_name?.message}} + {errors.name?.message && {errors.name?.message}} {t('modelManager.description')} - control={control} name="base_model" /> + control={control} name="base" /> control={control} name="variant" /> {t('modelManager.modelLocation')} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx index bf6349234f..5be9a5631c 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx @@ -9,9 +9,8 @@ import { memo, useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import type { DiffusersModelConfig } from 'services/api/endpoints/models'; -import { useUpdateMainModelsMutation } from 'services/api/endpoints/models'; import type { DiffusersModelConfig } from 'services/api/types'; +import { useUpdateModelsMutation } from '../../../../services/api/endpoints/models'; type DiffusersModelEditProps = { model: DiffusersModelConfig; @@ -20,7 +19,7 @@ type DiffusersModelEditProps = { const DiffusersModelEdit = (props: DiffusersModelEditProps) => { const { model } = props; - const [updateMainModel, { isLoading }] = useUpdateMainModelsMutation(); + const [updateModel, { isLoading }] = useUpdateModelsMutation(); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -33,12 +32,12 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { reset, } = useForm({ defaultValues: { - model_name: model.model_name ? model.model_name : '', - base_model: model.base_model, - model_type: 'main', + name: model.name ? model.name : '', + base: model.base, + type: 'main', path: model.path ? model.path : '', description: model.description ? model.description : '', - model_format: 'diffusers', + format: 'diffusers', vae: model.vae ? model.vae : '', variant: model.variant, }, @@ -48,12 +47,11 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { const onSubmit = useCallback>( (values) => { const responseBody = { - base_model: model.base_model, - model_name: model.model_name, + key: model.key, body: values, }; - updateMainModel(responseBody) + updateModel(responseBody) .unwrap() .then((payload) => { reset(payload as DiffusersModelConfig, { keepDefaultValues: true }); @@ -78,37 +76,37 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { ); }); }, - [dispatch, model.base_model, model.model_name, reset, t, updateMainModel] + [dispatch, model.key, reset, t, updateModel] ); return ( - {model.model_name} + {model.name} - {MODEL_TYPE_MAP[model.base_model]} {t('modelManager.model')} + {MODEL_TYPE_MAP[model.base]} {t('modelManager.model')} - + {t('modelManager.name')} value.trim().length > 3 || 'Must be at least 3 characters', })} /> - {errors.model_name?.message && {errors.model_name?.message}} + {errors.name?.message && {errors.name?.message}} {t('modelManager.description')} - control={control} name="base_model" /> + control={control} name="base" /> control={control} name="variant" /> {t('modelManager.modelLocation')} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx index 1a8f235aaf..edb73e8275 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx @@ -8,7 +8,6 @@ import { memo, useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { useUpdateLoRAModelsMutation } from 'services/api/endpoints/models'; import type { LoRAModelConfig } from 'services/api/types'; type LoRAModelEditProps = { @@ -18,7 +17,7 @@ type LoRAModelEditProps = { const LoRAModelEdit = (props: LoRAModelEditProps) => { const { model } = props; - const [updateLoRAModel, { isLoading }] = useUpdateLoRAModelsMutation(); + const [updateModel, { isLoading }] = useUpdateModelsMutation(); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -31,12 +30,12 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { reset, } = useForm({ defaultValues: { - model_name: model.model_name ? model.model_name : '', - base_model: model.base_model, - model_type: 'lora', + name: model.name ? model.name : '', + base: model.base, + type: 'lora', path: model.path ? model.path : '', description: model.description ? model.description : '', - model_format: model.model_format, + format: model.format, }, mode: 'onChange', }); @@ -44,12 +43,11 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { const onSubmit = useCallback>( (values) => { const responseBody = { - base_model: model.base_model, - model_name: model.model_name, + key: model.key, body: values, }; - updateLoRAModel(responseBody) + updateModel(responseBody) .unwrap() .then((payload) => { reset(payload as LoRAModelConfig, { keepDefaultValues: true }); @@ -74,17 +72,17 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { ); }); }, - [dispatch, model.base_model, model.model_name, reset, t, updateLoRAModel] + [dispatch, model.key, reset, t, updateModel] ); return ( - {model.model_name} + {model.name} - {MODEL_TYPE_MAP[model.base_model]} {t('modelManager.model')} ⋅ {LORA_MODEL_FORMAT_MAP[model.model_format]}{' '} + {MODEL_TYPE_MAP[model.base]} {t('modelManager.model')} ⋅ {LORA_MODEL_FORMAT_MAP[model.format]}{' '} {t('common.format')} @@ -92,20 +90,20 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { - + {t('modelManager.name')} value.trim().length > 3 || 'Must be at least 3 characters', })} /> - {errors.model_name?.message && {errors.model_name?.message}} + {errors.name?.message && {errors.name?.message}} {t('modelManager.description')} - control={control} name="base_model" /> + control={control} name="base" /> {t('modelManager.modelLocation')} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelConvert.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelConvert.tsx index 6e34d5039e..9a2746abe6 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelConvert.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelConvert.tsx @@ -54,8 +54,8 @@ const ModelConvert = (props: ModelConvertProps) => { const modelConvertHandler = useCallback(() => { const queryArg = { - base_model: model.base_model, - model_name: model.model_name, + base_model: model.base, + model_name: model.name, convert_dest_directory: saveLocation === 'Custom' ? customSaveLocation : undefined, }; @@ -74,7 +74,7 @@ const ModelConvert = (props: ModelConvertProps) => { dispatch( addToast( makeToast({ - title: `${t('modelManager.convertingModelBegin')}: ${model.model_name}`, + title: `${t('modelManager.convertingModelBegin')}: ${model.name}`, status: 'info', }) ) @@ -86,7 +86,7 @@ const ModelConvert = (props: ModelConvertProps) => { dispatch( addToast( makeToast({ - title: `${t('modelManager.modelConverted')}: ${model.model_name}`, + title: `${t('modelManager.modelConverted')}: ${model.name}`, status: 'success', }) ) @@ -96,13 +96,13 @@ const ModelConvert = (props: ModelConvertProps) => { dispatch( addToast( makeToast({ - title: `${t('modelManager.modelConversionFailed')}: ${model.model_name}`, + title: `${t('modelManager.modelConversionFailed')}: ${model.name}`, status: 'error', }) ) ); }); - }, [convertModel, customSaveLocation, dispatch, model.base_model, model.model_name, saveLocation, t]); + }, [convertModel, customSaveLocation, dispatch, model.base, model.name, saveLocation, t]); return ( <> @@ -116,7 +116,7 @@ const ModelConvert = (props: ModelConvertProps) => { 🧨 {t('modelManager.convertToDiffusers')} { {modelList.map((model) => ( ))} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx index 835499d25a..2014d88961 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -15,8 +15,8 @@ import { makeToast } from 'features/system/util/makeToast'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; -import type { LoRAConfig, MainModelConfig } from 'services/api/endpoints/models'; -import { useDeleteLoRAModelsMutation, useDeleteMainModelsMutation } from 'services/api/endpoints/models'; +import { useDeleteModelsMutation } from 'services/api/endpoints/models'; +import { LoRAConfig, MainModelConfig } from '../../../../services/api/types'; type ModelListItemProps = { model: MainModelConfig | LoRAConfig; @@ -27,29 +27,23 @@ type ModelListItemProps = { const ModelListItem = (props: ModelListItemProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const [deleteMainModel] = useDeleteMainModelsMutation(); - const [deleteLoRAModel] = useDeleteLoRAModelsMutation(); + const [deleteModel] = useDeleteModelsMutation(); const { isOpen, onOpen, onClose } = useDisclosure(); const { model, isSelected, setSelectedModelId } = props; const handleSelectModel = useCallback(() => { - setSelectedModelId(model.id); - }, [model.id, setSelectedModelId]); + setSelectedModelId(model.key); + }, [model.key, setSelectedModelId]); const handleModelDelete = useCallback(() => { - const method = { - main: deleteMainModel, - lora: deleteLoRAModel, - }[model.model_type]; - - method(model) + deleteModel({ key: model.key }) .unwrap() .then((_) => { dispatch( addToast( makeToast({ - title: `${t('modelManager.modelDeleted')}: ${model.model_name}`, + title: `${t('modelManager.modelDeleted')}: ${model.name}`, status: 'success', }) ) @@ -60,7 +54,7 @@ const ModelListItem = (props: ModelListItemProps) => { dispatch( addToast( makeToast({ - title: `${t('modelManager.modelDeleteFailed')}: ${model.model_name}`, + title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`, status: 'error', }) ) @@ -68,7 +62,7 @@ const ModelListItem = (props: ModelListItemProps) => { } }); setSelectedModelId(undefined); - }, [deleteMainModel, deleteLoRAModel, model, setSelectedModelId, dispatch, t]); + }, [deleteModel, model, setSelectedModelId, dispatch, t]); return ( @@ -85,10 +79,10 @@ const ModelListItem = (props: ModelListItemProps) => { > - {MODEL_TYPE_SHORT_MAP[model.base_model as keyof typeof MODEL_TYPE_SHORT_MAP]} + {MODEL_TYPE_SHORT_MAP[model.base as keyof typeof MODEL_TYPE_SHORT_MAP]} - {model.model_name} + {model.name} diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 2bd1a0a246..6897768a1b 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -7,12 +7,10 @@ import type { AnyModelConfig, BaseModelType, ControlNetModelConfig, - ImportModelConfig, IPAdapterModelConfig, LoRAModelConfig, MainModelConfig, MergeModelConfig, - ModelType, T2IAdapterModelConfig, TextualInversionModelConfig, VAEModelConfig, @@ -21,37 +19,21 @@ import type { import type { ApiTagDescription, tagTypes } from '..'; import { api, buildV2Url, LIST_TAG } from '..'; -type UpdateMainModelArg = { - base_model: BaseModelType; - model_name: string; - body: MainModelConfig; +type UpdateModelArg = { + key: NonNullable; + body: NonNullable; }; -type UpdateLoRAModelArg = { - base_model: BaseModelType; - model_name: string; - body: LoRAModelConfig; -}; - -type UpdateMainModelResponse = - 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 ListModelsArg = NonNullable; -type UpdateLoRAModelResponse = UpdateMainModelResponse; - type DeleteMainModelArg = { - base_model: BaseModelType; - model_name: string; - model_type: ModelType; + key: string; }; type DeleteMainModelResponse = void; -type DeleteLoRAModelArg = DeleteMainModelArg; - -type DeleteLoRAModelResponse = void; - type ConvertMainModelArg = { base_model: BaseModelType; model_name: string; @@ -59,36 +41,40 @@ type ConvertMainModelArg = { }; type ConvertMainModelResponse = - paths['/api/v1/models/convert/{base_model}/{model_type}/{model_name}']['put']['responses']['200']['content']['application/json']; + paths['/api/v2/models/convert/{key}']['put']['responses']['200']['content']['application/json']; type MergeMainModelArg = { base_model: BaseModelType; body: MergeModelConfig; }; -type MergeMainModelResponse = - paths['/api/v1/models/merge/{base_model}']['put']['responses']['200']['content']['application/json']; +type MergeMainModelResponse = paths['/api/v2/models/merge']['put']['responses']['200']['content']['application/json']; type ImportMainModelArg = { - body: ImportModelConfig; + source: NonNullable; + access_token?: operations['heuristic_import_model']['parameters']['query']['access_token']; + config: NonNullable; }; type ImportMainModelResponse = - paths['/api/v1/models/import']['post']['responses']['201']['content']['application/json']; + paths['/api/v2/models/import']['post']['responses']['201']['content']['application/json']; + +type ListImportModelsResponse = + paths['/api/v2/models/import']['get']['responses']['200']['content']['application/json']; type AddMainModelArg = { body: MainModelConfig; }; -type AddMainModelResponse = paths['/api/v1/models/add']['post']['responses']['201']['content']['application/json']; +type AddMainModelResponse = paths['/api/v2/models/add']['post']['responses']['201']['content']['application/json']; -type SyncModelsResponse = paths['/api/v1/models/sync']['post']['responses']['201']['content']['application/json']; +type SyncModelsResponse = paths['/api/v2/models/sync']['post']['responses']['201']['content']['application/json']; export type SearchFolderResponse = - paths['/api/v1/models/search']['get']['responses']['200']['content']['application/json']; + paths['/api/v2/models/search']['get']['responses']['200']['content']['application/json']; type CheckpointConfigsResponse = - paths['/api/v1/models/ckpt_confs']['get']['responses']['200']['content']['application/json']; + paths['/api/v2/models/ckpt_confs']['get']['responses']['200']['content']['application/json']; type SearchFolderArg = operations['search_for_models']['parameters']['query']; @@ -179,10 +165,10 @@ export const modelsApi = api.injectEndpoints({ providesTags: buildProvidesTags('MainModel'), transformResponse: buildTransformResponse(mainModelsAdapter), }), - updateMainModels: build.mutation({ - query: ({ base_model, model_name, body }) => { + updateModels: build.mutation({ + query: ({ key, body }) => { return { - url: buildModelsUrl(`${base_model}/main/${model_name}`), + url: buildModelsUrl(`i/${key}`), method: 'PATCH', body: body, }; @@ -190,11 +176,12 @@ export const modelsApi = api.injectEndpoints({ invalidatesTags: ['Model'], }), importMainModels: build.mutation({ - query: ({ body }) => { + query: ({ source, config, access_token }) => { return { - url: buildModelsUrl('import'), + url: buildModelsUrl('heuristic_import'), + params: { source, access_token }, method: 'POST', - body: body, + body: config, }; }, invalidatesTags: ['Model'], @@ -209,10 +196,10 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: ['Model'], }), - deleteMainModels: build.mutation({ - query: ({ base_model, model_name, model_type }) => { + deleteModels: build.mutation({ + query: ({ key }) => { return { - url: buildModelsUrl(`${base_model}/${model_type}/${model_name}`), + url: buildModelsUrl(`i/${key}`), method: 'DELETE', }; }, @@ -264,25 +251,6 @@ export const modelsApi = api.injectEndpoints({ providesTags: buildProvidesTags('LoRAModel'), transformResponse: buildTransformResponse(loraModelsAdapter), }), - updateLoRAModels: build.mutation({ - query: ({ base_model, model_name, body }) => { - return { - url: buildModelsUrl(`${base_model}/lora/${model_name}`), - method: 'PATCH', - body: body, - }; - }, - invalidatesTags: [{ type: 'LoRAModel', id: LIST_TAG }], - }), - deleteLoRAModels: build.mutation({ - query: ({ base_model, model_name }) => { - return { - url: buildModelsUrl(`${base_model}/lora/${model_name}`), - method: 'DELETE', - }; - }, - invalidatesTags: [{ type: 'LoRAModel', id: LIST_TAG }], - }), getControlNetModels: build.query, void>({ query: () => ({ url: buildModelsUrl(), params: { model_type: 'controlnet' } }), providesTags: buildProvidesTags('ControlNetModel'), @@ -316,6 +284,13 @@ export const modelsApi = api.injectEndpoints({ }; }, }), + getModelImports: build.query({ + query: (arg) => { + return { + url: buildModelsUrl(`import`), + }; + }, + }), getCheckpointConfigs: build.query({ query: () => { return { @@ -335,15 +310,14 @@ export const { useGetLoRAModelsQuery, useGetTextualInversionModelsQuery, useGetVaeModelsQuery, - useUpdateMainModelsMutation, - useDeleteMainModelsMutation, + useDeleteModelsMutation, + useUpdateModelsMutation, useImportMainModelsMutation, useAddMainModelsMutation, useConvertMainModelsMutation, useMergeMainModelsMutation, - useDeleteLoRAModelsMutation, - useUpdateLoRAModelsMutation, useSyncModelsMutation, useGetModelsInFolderQuery, useGetCheckpointConfigsQuery, + useGetModelImportsQuery, } = modelsApi; From 9068400433240dc25bee43f951da659f368e64c4 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 20 Feb 2024 10:09:40 -0500 Subject: [PATCH 252/411] workspace for mary and jenn --- .../modelManagerV2/subpanels/ImportModels.tsx | 10 +++ .../modelManagerV2/subpanels/ModelManager.tsx | 44 ++++++++++++ .../ui/components/tabs/ModelManagerTab.tsx | 68 +++---------------- 3 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx new file mode 100644 index 0000000000..2325fcf7dc --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx @@ -0,0 +1,10 @@ +import { Box } from '@invoke-ai/ui-library'; + +//jenn's workspace +export const ImportModels = () => { + return ( + + Import Models + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx new file mode 100644 index 0000000000..59b01de0c0 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx @@ -0,0 +1,44 @@ +import { + Box, + Button, + Flex, + Heading, + IconButton, + Input, + InputGroup, + InputRightElement, + Spacer, +} from '@invoke-ai/ui-library'; +import { t } from 'i18next'; +import { PiXBold } from 'react-icons/pi'; +import { SyncModelsIconButton } from '../../modelManager/components/SyncModels/SyncModelsIconButton'; + +export const ModelManager = () => { + return ( + + + + Model Manager + + + + + + + + + + + + + ( + + } /> + + ) + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx index 124c9b31a5..50db10fb57 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx @@ -1,65 +1,17 @@ -import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; -import ImportModelsPanel from 'features/modelManager/subpanels/ImportModelsPanel'; -import MergeModelsPanel from 'features/modelManager/subpanels/MergeModelsPanel'; -import ModelManagerPanel from 'features/modelManager/subpanels/ModelManagerPanel'; -import ModelManagerSettingsPanel from 'features/modelManager/subpanels/ModelManagerSettingsPanel'; -import type { ReactNode } from 'react'; -import { memo, useMemo } from 'react'; +import { Flex, Box } from '@invoke-ai/ui-library'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; - -type ModelManagerTabName = 'modelManager' | 'importModels' | 'mergeModels' | 'settings'; - -type ModelManagerTabInfo = { - id: ModelManagerTabName; - label: string; - content: ReactNode; -}; +import { ImportModels } from '../../../modelManagerV2/subpanels/ImportModels'; +import { ModelManager } from '../../../modelManagerV2/subpanels/ModelManager'; const ModelManagerTab = () => { - const { t } = useTranslation(); - - const tabs: ModelManagerTabInfo[] = useMemo( - () => [ - { - id: 'modelManager', - label: t('modelManager.modelManager'), - content: , - }, - { - id: 'importModels', - label: t('modelManager.importModels'), - content: , - }, - { - id: 'mergeModels', - label: t('modelManager.mergeModels'), - content: , - }, - { - id: 'settings', - label: t('modelManager.settings'), - content: , - }, - ], - [t] - ); return ( - - - {tabs.map((tab) => ( - - {tab.label} - - ))} - - - {tabs.map((tab) => ( - - {tab.content} - - ))} - - + + + + + + ); }; From c7d462b222f302647fef74110f9bef40a3b57601 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 20 Feb 2024 13:03:28 -0500 Subject: [PATCH 253/411] model list, filtering, searching --- invokeai/frontend/web/src/app/store/store.ts | 2 + .../subpanels/AddModelsPanel/AddModels.tsx | 2 +- .../subpanels/ModelManagerPanel.tsx | 2 +- .../ModelManagerPanel/CheckpointModelEdit.tsx | 3 +- .../ModelManagerPanel/DiffusersModelEdit.tsx | 2 +- .../ModelManagerPanel/LoRAModelEdit.tsx | 1 + .../subpanels/ModelManagerPanel/ModelList.tsx | 2 +- .../ModelManagerPanel/ModelListItem.tsx | 2 +- .../store/modelManagerV2Slice.ts | 54 ++++++ .../modelManagerV2/subpanels/ModelManager.tsx | 32 +--- .../subpanels/ModelManagerPanel/ModelList.tsx | 160 ++++++++++++++++++ .../ModelManagerPanel/ModelListHeader.tsx | 23 +++ .../ModelManagerPanel/ModelListItem.tsx | 115 +++++++++++++ .../ModelManagerPanel/ModelListNavigation.tsx | 52 ++++++ .../ModelManagerPanel/ModelListWrapper.tsx | 25 +++ .../ModelManagerPanel/ModelTypeFilter.tsx | 54 ++++++ .../web/src/features/modelManagerV2/types.ts | 14 ++ .../ui/components/tabs/ModelManagerTab.tsx | 7 +- 18 files changed, 517 insertions(+), 35 deletions(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/types.ts diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 16f1632d88..c63bc02e09 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -16,6 +16,7 @@ import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/galle import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice'; import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice'; import { modelManagerPersistConfig, modelManagerSlice } from 'features/modelManager/store/modelManagerSlice'; +import { modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { nodesPersistConfig, nodesSlice } from 'features/nodes/store/nodesSlice'; import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice'; import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice'; @@ -55,6 +56,7 @@ const allReducers = { [changeBoardModalSlice.name]: changeBoardModalSlice.reducer, [loraSlice.name]: loraSlice.reducer, [modelManagerSlice.name]: modelManagerSlice.reducer, + [modelManagerV2Slice.name]: modelManagerV2Slice.reducer, [sdxlSlice.name]: sdxlSlice.reducer, [queueSlice.name]: queueSlice.reducer, [workflowSlice.name]: workflowSlice.reducer, diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx index 9b4b95be9e..82ccb7f309 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AddModels.tsx @@ -1,10 +1,10 @@ import { Button, ButtonGroup, Flex, Text } from '@invoke-ai/ui-library'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useGetModelImportsQuery } from 'services/api/endpoints/models'; import AdvancedAddModels from './AdvancedAddModels'; import SimpleAddModels from './SimpleAddModels'; -import { useGetModelImportsQuery } from '../../../../services/api/endpoints/models'; const AddModels = () => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx index dab4e0b872..06b5b7db36 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel.tsx @@ -3,12 +3,12 @@ import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ALL_BASE_MODELS } from 'services/api/constants'; import { useGetLoRAModelsQuery, useGetMainModelsQuery } from 'services/api/endpoints/models'; +import type { DiffusersModelConfig, LoRAConfig, MainModelConfig } from 'services/api/types'; import CheckpointModelEdit from './ModelManagerPanel/CheckpointModelEdit'; import DiffusersModelEdit from './ModelManagerPanel/DiffusersModelEdit'; import LoRAModelEdit from './ModelManagerPanel/LoRAModelEdit'; import ModelList from './ModelManagerPanel/ModelList'; -import { DiffusersModelConfig, LoRAConfig, MainModelConfig } from '../../../services/api/types'; const ModelManagerPanel = () => { const [selectedModelId, setSelectedModelId] = useState(); diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx index 0dd8a7add6..c24d660cc6 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/CheckpointModelEdit.tsx @@ -22,8 +22,9 @@ import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useGetCheckpointConfigsQuery, useUpdateModelsMutation } from 'services/api/endpoints/models'; +import type { CheckpointModelConfig } from 'services/api/types'; + import ModelConvert from './ModelConvert'; -import { CheckpointModelConfig } from '../../../../services/api/types'; type CheckpointModelEditProps = { model: CheckpointModelConfig; diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx index 5be9a5631c..b5023f0eff 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx @@ -9,8 +9,8 @@ import { memo, useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { useUpdateModelsMutation } from 'services/api/endpoints/models'; import type { DiffusersModelConfig } from 'services/api/types'; -import { useUpdateModelsMutation } from '../../../../services/api/endpoints/models'; type DiffusersModelEditProps = { model: DiffusersModelConfig; diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx index edb73e8275..81f2c4df29 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx @@ -8,6 +8,7 @@ import { memo, useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { useUpdateModelsMutation } from 'services/api/endpoints/models'; import type { LoRAModelConfig } from 'services/api/types'; type LoRAModelEditProps = { diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx index c546831476..b129b7310d 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelList.tsx @@ -7,9 +7,9 @@ import { useTranslation } from 'react-i18next'; import { ALL_BASE_MODELS } from 'services/api/constants'; // import type { LoRAConfig, MainModelConfig } from 'services/api/endpoints/models'; import { useGetLoRAModelsQuery, useGetMainModelsQuery } from 'services/api/endpoints/models'; +import type { LoRAConfig, MainModelConfig } from 'services/api/types'; import ModelListItem from './ModelListItem'; -import { LoRAConfig, MainModelConfig } from '../../../../services/api/types'; type ModelListProps = { selectedModelId: string | undefined; diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx index 2014d88961..08b9b61aea 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -16,7 +16,7 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiTrashSimpleBold } from 'react-icons/pi'; import { useDeleteModelsMutation } from 'services/api/endpoints/models'; -import { LoRAConfig, MainModelConfig } from '../../../../services/api/types'; +import type { LoRAConfig, MainModelConfig } from 'services/api/types'; type ModelListItemProps = { model: MainModelConfig | LoRAConfig; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts new file mode 100644 index 0000000000..29b071e9b1 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts @@ -0,0 +1,54 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import type { PersistConfig, RootState } from 'app/store/store'; + + +type ModelManagerState = { + _version: 1; + selectedModelKey: string | null; + searchTerm: string; + filteredModelType: string | null; +}; + +export const initialModelManagerState: ModelManagerState = { + _version: 1, + selectedModelKey: null, + filteredModelType: null, + searchTerm: "" +}; + +export const modelManagerV2Slice = createSlice({ + name: 'modelmanagerV2', + initialState: initialModelManagerState, + reducers: { + setSelectedModelKey: (state, action: PayloadAction) => { + state.selectedModelKey = action.payload; + }, + setSearchTerm: (state, action: PayloadAction) => { + state.searchTerm = action.payload; + }, + + setFilteredModelType: (state, action: PayloadAction) => { + state.filteredModelType = action.payload; + }, + }, +}); + +export const { setSelectedModelKey, setSearchTerm, setFilteredModelType } = modelManagerV2Slice.actions; + +export const selectModelManagerSlice = (state: RootState) => state.modelmanager; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export const migrateModelManagerState = (state: any): any => { + if (!('_version' in state)) { + state._version = 1; + } + return state; +}; + +export const modelManagerPersistConfig: PersistConfig = { + name: modelManagerV2Slice.name, + initialState: initialModelManagerState, + migrate: migrateModelManagerState, + persistDenylist: [], +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx index 59b01de0c0..648f98dbb3 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx @@ -1,17 +1,8 @@ -import { - Box, - Button, - Flex, - Heading, - IconButton, - Input, - InputGroup, - InputRightElement, - Spacer, -} from '@invoke-ai/ui-library'; -import { t } from 'i18next'; -import { PiXBold } from 'react-icons/pi'; -import { SyncModelsIconButton } from '../../modelManager/components/SyncModels/SyncModelsIconButton'; +import { Box, Button, Flex, Heading } from '@invoke-ai/ui-library'; +import { SyncModelsIconButton } from 'features/modelManager/components/SyncModels/SyncModelsIconButton'; + +import ModelList from './ModelManagerPanel/ModelList'; +import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation'; export const ModelManager = () => { return ( @@ -27,17 +18,8 @@ export const ModelManager = () => { - - - - - ( - - } /> - - ) - - + + ); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx new file mode 100644 index 0000000000..bf43c01da2 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx @@ -0,0 +1,160 @@ +import { Flex, Spinner, Text } from '@invoke-ai/ui-library'; +import type { EntityState } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { forEach } from 'lodash-es'; +import { memo } from 'react'; +import { ALL_BASE_MODELS } from 'services/api/constants'; +import { + useGetControlNetModelsQuery, + useGetIPAdapterModelsQuery, + useGetLoRAModelsQuery, + useGetMainModelsQuery, + useGetT2IAdapterModelsQuery, + useGetTextualInversionModelsQuery, + useGetVaeModelsQuery, +} from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; + +import { ModelListWrapper } from './ModelListWrapper'; + +const ModelList = () => { + const { searchTerm, filteredModelType } = useAppSelector((s) => s.modelmanagerV2); + + const { filteredMainModels, isLoadingMainModels } = useGetMainModelsQuery(ALL_BASE_MODELS, { + selectFromResult: ({ data, isLoading }) => ({ + filteredMainModels: modelsFilter(data, searchTerm, filteredModelType), + isLoadingMainModels: isLoading, + }), + }); + + const { filteredLoraModels, isLoadingLoraModels } = useGetLoRAModelsQuery(undefined, { + selectFromResult: ({ data, isLoading }) => ({ + filteredLoraModels: modelsFilter(data, searchTerm, filteredModelType), + isLoadingLoraModels: isLoading, + }), + }); + + const { filteredTextualInversionModels, isLoadingTextualInversionModels } = useGetTextualInversionModelsQuery( + undefined, + { + selectFromResult: ({ data, isLoading }) => ({ + filteredTextualInversionModels: modelsFilter(data, searchTerm, filteredModelType), + isLoadingTextualInversionModels: isLoading, + }), + } + ); + + const { filteredControlnetModels, isLoadingControlnetModels } = useGetControlNetModelsQuery(undefined, { + selectFromResult: ({ data, isLoading }) => ({ + filteredControlnetModels: modelsFilter(data, searchTerm, filteredModelType), + isLoadingControlnetModels: isLoading, + }), + }); + + const { filteredT2iAdapterModels, isLoadingT2IAdapterModels } = useGetT2IAdapterModelsQuery(undefined, { + selectFromResult: ({ data, isLoading }) => ({ + filteredT2iAdapterModels: modelsFilter(data, searchTerm, filteredModelType), + isLoadingT2IAdapterModels: isLoading, + }), + }); + + const { filteredIpAdapterModels, isLoadingIpAdapterModels } = useGetIPAdapterModelsQuery(undefined, { + selectFromResult: ({ data, isLoading }) => ({ + filteredIpAdapterModels: modelsFilter(data, searchTerm, filteredModelType), + isLoadingIpAdapterModels: isLoading, + }), + }); + + const { filteredVaeModels, isLoadingVaeModels } = useGetVaeModelsQuery(undefined, { + selectFromResult: ({ data, isLoading }) => ({ + filteredVaeModels: modelsFilter(data, searchTerm, filteredModelType), + isLoadingVaeModels: isLoading, + }), + }); + + return ( + + + {/* Main Model List */} + {isLoadingMainModels && } + {!isLoadingMainModels && filteredMainModels.length > 0 && ( + + )} + {/* LoRAs List */} + {isLoadingLoraModels && } + {!isLoadingLoraModels && filteredLoraModels.length > 0 && ( + + )} + + {/* TI List */} + {isLoadingTextualInversionModels && } + {!isLoadingTextualInversionModels && filteredTextualInversionModels.length > 0 && ( + + )} + + {/* VAE List */} + {isLoadingVaeModels && } + {!isLoadingVaeModels && filteredVaeModels.length > 0 && ( + + )} + + {/* Controlnet List */} + {isLoadingControlnetModels && } + {!isLoadingControlnetModels && filteredControlnetModels.length > 0 && ( + + )} + {/* IP Adapter List */} + {isLoadingIpAdapterModels && } + {!isLoadingIpAdapterModels && filteredIpAdapterModels.length > 0 && ( + + )} + {/* T2I Adapters List */} + {isLoadingT2IAdapterModels && } + {!isLoadingT2IAdapterModels && filteredT2iAdapterModels.length > 0 && ( + + )} + + + ); +}; + +export default memo(ModelList); + +const modelsFilter = ( + data: EntityState | undefined, + nameFilter: string, + filteredModelType: string | null +): T[] => { + const filteredModels: T[] = []; + + forEach(data?.entities, (model) => { + if (!model) { + return; + } + + const matchesFilter = model.name.toLowerCase().includes(nameFilter.toLowerCase()); + const matchesType = filteredModelType ? model.type === filteredModelType : true; + + if (matchesFilter && matchesType) { + filteredModels.push(model); + } + }); + return filteredModels; +}; + +const FetchingModelsLoader = memo(({ loadingMessage }: { loadingMessage?: string }) => { + return ( + + + + {loadingMessage ? loadingMessage : 'Fetching...'} + + + ); +}); + +FetchingModelsLoader.displayName = 'FetchingModelsLoader'; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx new file mode 100644 index 0000000000..874d1c9ac2 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx @@ -0,0 +1,23 @@ +import { Box, Divider, Text } from '@invoke-ai/ui-library'; + +export const ModelListHeader = ({ title }: { title: string }) => { + return ( + + + + + {title} + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx new file mode 100644 index 0000000000..5cc429ebcd --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -0,0 +1,115 @@ +import { + Badge, + Button, + ConfirmationAlertDialog, + Flex, + IconButton, + Text, + Tooltip, + useDisclosure, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleBold } from 'react-icons/pi'; +import { useDeleteModelsMutation } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; + +type ModelListItemProps = { + model: AnyModelConfig; +}; + +const ModelListItem = (props: ModelListItemProps) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const [deleteModel] = useDeleteModelsMutation(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const { model } = props; + + const handleSelectModel = useCallback(() => { + dispatch(setSelectedModelKey(model.key)); + }, [model.key, dispatch]); + + const isSelected = useMemo(() => { + return selectedModelKey === model.key; + }, [selectedModelKey, model.key]); + + const handleModelDelete = useCallback(() => { + deleteModel({ key: model.key }) + .unwrap() + .then((_) => { + dispatch( + addToast( + makeToast({ + title: `${t('modelManager.modelDeleted')}: ${model.name}`, + status: 'success', + }) + ) + ); + }) + .catch((error) => { + if (error) { + dispatch( + addToast( + makeToast({ + title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`, + status: 'error', + }) + ) + ); + } + }); + dispatch(setSelectedModelKey(null)); + }, [deleteModel, model, dispatch, t]); + + return ( + + + + + {MODEL_TYPE_SHORT_MAP[model.base as keyof typeof MODEL_TYPE_SHORT_MAP]} + + + {model.name} + + + + } + aria-label={t('modelManager.deleteConfig')} + colorScheme="error" + /> + + + {t('modelManager.deleteMsg1')} + {t('modelManager.deleteMsg2')} + + + + ); +}; + +export default memo(ModelListItem); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx new file mode 100644 index 0000000000..c0d06af245 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx @@ -0,0 +1,52 @@ +import { Flex, IconButton,Input, InputGroup, InputRightElement, Spacer } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setSearchTerm } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { t } from 'i18next'; +import type { ChangeEventHandler} from 'react'; +import { useCallback } from 'react'; +import { PiXBold } from 'react-icons/pi'; + +import { ModelTypeFilter } from './ModelTypeFilter'; + +export const ModelListNavigation = () => { + const dispatch = useAppDispatch(); + const searchTerm = useAppSelector((s) => s.modelmanagerV2.searchTerm); + + const handleSearch: ChangeEventHandler = useCallback( + (event) => { + dispatch(setSearchTerm(event.target.value)); + }, + [dispatch] + ); + + const clearSearch = useCallback(() => { + dispatch(setSearchTerm('')); + }, [dispatch]); + + return ( + + + + + + + {!!searchTerm?.length && ( + + } + onClick={clearSearch} + /> + + )} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx new file mode 100644 index 0000000000..24460e6453 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx @@ -0,0 +1,25 @@ +import { Flex } from '@invoke-ai/ui-library'; +import type { AnyModelConfig } from 'services/api/types'; + +import { ModelListHeader } from './ModelListHeader'; +import ModelListItem from './ModelListItem'; + +type ModelListWrapperProps = { + title: string; + modelList: AnyModelConfig[]; +}; + +export const ModelListWrapper = (props: ModelListWrapperProps) => { + const { title, modelList } = props; + return ( + + + + + {modelList.map((model) => ( + + ))} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx new file mode 100644 index 0000000000..0134ffc811 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx @@ -0,0 +1,54 @@ +import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setFilteredModelType } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { useCallback } from 'react'; +import { IoFilter } from 'react-icons/io5'; + +export const MODEL_TYPE_LABELS: { [key: string]: string } = { + main: 'Main', + lora: 'LoRA', + embedding: 'Textual Inversion', + controlnet: 'ControlNet', + vae: 'VAE', + t2i_adapter: 'T2I Adapter', + ip_adapter: 'IP Adapter', + clip_vision: 'Clip Vision', + onnx: 'Onnx', +}; + +export const ModelTypeFilter = () => { + const dispatch = useAppDispatch(); + const filteredModelType = useAppSelector((s) => s.modelmanagerV2.filteredModelType); + + const selectModelType = useCallback( + (option: string) => { + dispatch(setFilteredModelType(option)); + }, + [dispatch] + ); + + const clearModelType = useCallback(() => { + dispatch(setFilteredModelType(null)); + }, [dispatch]); + + return ( + + }> + {filteredModelType ? MODEL_TYPE_LABELS[filteredModelType] : 'All Models'} + + + All Models + {Object.keys(MODEL_TYPE_LABELS).map((option) => ( + + {MODEL_TYPE_LABELS[option]} + + ))} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/types.ts b/invokeai/frontend/web/src/features/modelManagerV2/types.ts new file mode 100644 index 0000000000..a209fbb876 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/types.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +export const zBaseModel = z.enum(['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']); +export const zModelType = z.enum([ + 'main', + 'vae', + 'lora', + 'controlnet', + 'embedding', + 'ip_adapter', + 'clip_vision', + 't2i_adapter', + 'onnx', // TODO(psyche): Remove this when removed from backend +]); \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx index 50db10fb57..8117631f22 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx @@ -1,8 +1,7 @@ -import { Flex, Box } from '@invoke-ai/ui-library'; +import { Box,Flex } from '@invoke-ai/ui-library'; +import { ImportModels } from 'features/modelManagerV2/subpanels/ImportModels'; +import { ModelManager } from 'features/modelManagerV2/subpanels/ModelManager'; import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ImportModels } from '../../../modelManagerV2/subpanels/ImportModels'; -import { ModelManager } from '../../../modelManagerV2/subpanels/ModelManager'; const ModelManagerTab = () => { return ( From 87ce74e05d81440bed0ea3577aff6dd78234247d Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Tue, 20 Feb 2024 13:39:27 -0500 Subject: [PATCH 254/411] added import model form and importqueue --- invokeai/frontend/web/public/locales/en.json | 3 + .../modelManagerV2/subpanels/ImportModels.tsx | 83 +++++++++++++++++- .../modelManagerV2/subpanels/ImportQueue.tsx | 85 +++++++++++++++++++ 3 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportQueue.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a458563fd5..406df71e06 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -701,6 +701,7 @@ "availableModels": "Available Models", "baseModel": "Base Model", "cached": "cached", + "cancel": "Cancel", "cannotUseSpaces": "Cannot Use Spaces", "checkpointFolder": "Checkpoint Folder", "checkpointModels": "Checkpoints", @@ -743,6 +744,7 @@ "heightValidationMsg": "Default height of your model.", "ignoreMismatch": "Ignore Mismatches Between Selected Models", "importModels": "Import Models", + "importQueue": "Import Queue", "inpainting": "v1 Inpainting", "interpolationType": "Interpolation Type", "inverseSigmoid": "Inverse Sigmoid", @@ -796,6 +798,7 @@ "pickModelType": "Pick Model Type", "predictionType": "Prediction Type (for Stable Diffusion 2.x Models and occasional Stable Diffusion 1.x Models)", "quickAdd": "Quick Add", + "removeFromQueue": "Remove From Queue", "repo_id": "Repo ID", "repoIDValidationMsg": "Online repository of your model", "safetensorModels": "SafeTensors", diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx index 2325fcf7dc..14ed0c6848 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportModels.tsx @@ -1,10 +1,85 @@ -import { Box } from '@invoke-ai/ui-library'; +import { Button, Box, Flex, FormControl, FormLabel, Heading, Input, Text, Divider } from '@invoke-ai/ui-library'; +import { t } from 'i18next'; +import { CSSProperties } from 'react'; +import { useImportMainModelsMutation } from '../../../services/api/endpoints/models'; +import { useForm } from '@mantine/form'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { ImportQueue } from './ImportQueue'; + +const formStyles: CSSProperties = { + width: '100%', +}; + +type ExtendedImportModelConfig = { + location: string; +}; -//jenn's workspace export const ImportModels = () => { + const dispatch = useAppDispatch(); + + const [importMainModel, { isLoading }] = useImportMainModelsMutation(); + + const addModelForm = useForm({ + initialValues: { + location: '', + }, + }); + + console.log('addModelForm', addModelForm.values.location) + + const handleAddModelSubmit = (values: ExtendedImportModelConfig) => { + importMainModel({ source: values.location, config: undefined }) + .unwrap() + .then((_) => { + dispatch( + addToast( + makeToast({ + title: t('toast.modelAddedSimple'), + status: 'success', + }) + ) + ); + addModelForm.reset(); + }) + .catch((error) => { + if (error) { + dispatch( + addToast( + makeToast({ + title: `${error.data.detail} `, + status: 'error', + }) + ) + ); + } + }); + }; + return ( - - Import Models + + + Add Model + + + handleAddModelSubmit(v))} style={formStyles}> + + + + {t('modelManager.modelLocation')} + + + + + + + + {t('modelManager.importQueue')} + + ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportQueue.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportQueue.tsx new file mode 100644 index 0000000000..4a0407ff1a --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ImportQueue.tsx @@ -0,0 +1,85 @@ +import { + Button, + Box, + Flex, + FormControl, + FormLabel, + Heading, + IconButton, + Input, + InputGroup, + InputRightElement, + Progress, + Text, +} from '@invoke-ai/ui-library'; +import { t } from 'i18next'; +import { useMemo } from 'react'; +import { useGetModelImportsQuery } from '../../../services/api/endpoints/models'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { PiXBold } from 'react-icons/pi'; + +export const ImportQueue = () => { + const dispatch = useAppDispatch(); + + // start with this data then pull from sockets (idk how to do that yet, also might not even use this and just use socket) + const { data } = useGetModelImportsQuery(); + + const progressValues = useMemo(() => { + if (!data) { + return []; + } + const values = []; + for (let i = 0; i < data.length; i++) { + let value; + if (data[i] && data[i]?.bytes && data[i]?.total_bytes) { + value = (data[i]?.bytes / data[i]?.total_bytes) * 100; + } + values.push(value || undefined); + } + return values; + }, [data]); + + return ( + + + {data?.map((model, i) => ( + + + {model.source.repo_id} + + + {model.status} + {model.status === 'completed' ? ( + } + // onClick={handleRemove} + /> + ) : ( + } + // onClick={handleCancel} + colorScheme="error" + /> + )} + + ))} + + + ); +}; From c46eb72d45fe4621bc83c2909da7ed5468c17ac4 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Wed, 21 Feb 2024 09:39:02 -0500 Subject: [PATCH 255/411] single model view --- .../ModelManagerPanel/ModelListItem.tsx | 11 ++ .../modelManagerV2/subpanels/ModelPane.tsx | 13 ++ .../subpanels/ModelPanel/ModelAttrView.tsx | 15 +++ .../subpanels/ModelPanel/ModelView.tsx | 116 ++++++++++++++++++ .../web/src/features/modelManagerV2/types.ts | 14 --- .../ui/components/tabs/ModelManagerTab.tsx | 29 +++-- .../web/src/services/api/endpoints/models.ts | 11 ++ .../frontend/web/src/services/api/schema.ts | 13 +- 8 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelAttrView.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx delete mode 100644 invokeai/frontend/web/src/features/modelManagerV2/types.ts diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx index 5cc429ebcd..d6cb70f4e8 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -3,10 +3,12 @@ import { Button, ConfirmationAlertDialog, Flex, + Icon, IconButton, Text, Tooltip, useDisclosure, + Box, } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; @@ -15,6 +17,7 @@ import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { IoWarning } from 'react-icons/io5'; import { PiTrashSimpleBold } from 'react-icons/pi'; import { useDeleteModelsMutation } from 'services/api/endpoints/models'; import type { AnyModelConfig } from 'services/api/types'; @@ -88,8 +91,16 @@ const ModelListItem = (props: ModelListItemProps) => { {model.name} + {model.format === 'checkpoint' && ( + + + + + + )} + } diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx new file mode 100644 index 0000000000..9756b357b3 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx @@ -0,0 +1,13 @@ +import { Box } from '@invoke-ai/ui-library'; +import { useAppSelector } from '../../../app/store/storeHooks'; +import { ImportModels } from './ImportModels'; +import { ModelView } from './ModelPanel/ModelView'; + +export const ModelPane = () => { + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + return ( + + {selectedModelKey ? : } + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelAttrView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelAttrView.tsx new file mode 100644 index 0000000000..f45bfca993 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelAttrView.tsx @@ -0,0 +1,15 @@ +import { FormControl, FormLabel, Text } from '@invoke-ai/ui-library'; + +interface Props { + label: string; + value: string | null | undefined; +} + +export const ModelAttrView = ({ label, value }: Props) => { + return ( + + {label} + {value || '-'} + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx new file mode 100644 index 0000000000..83e4e5380f --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx @@ -0,0 +1,116 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppSelector } from '../../../../app/store/storeHooks'; +import { useGetModelQuery } from '../../../../services/api/endpoints/models'; +import { Flex, Text, Heading } from '@invoke-ai/ui-library'; +import DataViewer from '../../../gallery/components/ImageMetadataViewer/DataViewer'; +import { useMemo } from 'react'; +import { + CheckpointModelConfig, + ControlNetConfig, + DiffusersModelConfig, + IPAdapterConfig, + LoRAConfig, + T2IAdapterConfig, + TextualInversionConfig, + VAEConfig, +} from '../../../../services/api/types'; +import { ModelAttrView } from './ModelAttrView'; + +export const ModelView = () => { + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const { data, isLoading } = useGetModelQuery(selectedModelKey ?? skipToken); + + const modelConfigData = useMemo(() => { + if (!data) { + return null; + } + const modelFormat = data.config.format; + const modelType = data.config.type; + + if (modelType === 'main') { + if (modelFormat === 'diffusers') { + return data.config as DiffusersModelConfig; + } else if (modelFormat === 'checkpoint') { + return data.config as CheckpointModelConfig; + } + } + + switch (modelType) { + case 'lora': + return data.config as LoRAConfig; + case 'embedding': + return data.config as TextualInversionConfig; + case 't2i_adapter': + return data.config as T2IAdapterConfig; + case 'ip_adapter': + return data.config as IPAdapterConfig; + case 'controlnet': + return data.config as ControlNetConfig; + case 'vae': + return data.config as VAEConfig; + default: + return null; + } + }, [data]); + + if (isLoading) { + return Loading; + } + + if (!modelConfigData) { + return Something went wrong; + } + return ( + + + + {modelConfigData.name} + + {modelConfigData.source && Source: {modelConfigData.source}} + + + + + + + + + + + + + + + {modelConfigData.type === 'main' && ( + <> + + {modelConfigData.format === 'diffusers' && ( + + )} + {modelConfigData.format === 'checkpoint' && ( + + )} + + + + + + + + + + + + + )} + {modelConfigData.type === 'ip_adapter' && ( + + + + )} + + + {!!data?.metadata && } + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/types.ts b/invokeai/frontend/web/src/features/modelManagerV2/types.ts deleted file mode 100644 index a209fbb876..0000000000 --- a/invokeai/frontend/web/src/features/modelManagerV2/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod"; - -export const zBaseModel = z.enum(['any', 'sd-1', 'sd-2', 'sdxl', 'sdxl-refiner']); -export const zModelType = z.enum([ - 'main', - 'vae', - 'lora', - 'controlnet', - 'embedding', - 'ip_adapter', - 'clip_vision', - 't2i_adapter', - 'onnx', // TODO(psyche): Remove this when removed from backend -]); \ No newline at end of file diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx index 8117631f22..9245f5c60d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManagerTab.tsx @@ -1,16 +1,25 @@ -import { Box,Flex } from '@invoke-ai/ui-library'; -import { ImportModels } from 'features/modelManagerV2/subpanels/ImportModels'; -import { ModelManager } from 'features/modelManagerV2/subpanels/ModelManager'; -import { memo } from 'react'; +import { Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Box, Button } from '@invoke-ai/ui-library'; +import ImportModelsPanel from 'features/modelManager/subpanels/ImportModelsPanel'; +import MergeModelsPanel from 'features/modelManager/subpanels/MergeModelsPanel'; +import ModelManagerPanel from 'features/modelManager/subpanels/ModelManagerPanel'; +import ModelManagerSettingsPanel from 'features/modelManager/subpanels/ModelManagerSettingsPanel'; +import type { ReactNode } from 'react'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SyncModelsIconButton } from '../../../modelManager/components/SyncModels/SyncModelsIconButton'; +import { ModelManager } from '../../../modelManagerV2/subpanels/ModelManager'; +import { ModelPane } from '../../../modelManagerV2/subpanels/ModelPane'; + +type ModelManagerTabName = 'modelManager' | 'importModels' | 'mergeModels' | 'settings'; const ModelManagerTab = () => { + const { t } = useTranslation(); + return ( - - - - - - + + + + ); }; diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 6897768a1b..5db2dea00e 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -26,6 +26,10 @@ type UpdateModelArg = { type UpdateModelResponse = paths['/api/v2/models/i/{key}']['patch']['responses']['200']['content']['application/json']; + +type GetModelResponse = + paths['/api/v2/models/i/{key}']['get']['responses']['200']['content']['application/json']; + type ListModelsArg = NonNullable; type DeleteMainModelArg = { @@ -165,6 +169,12 @@ export const modelsApi = api.injectEndpoints({ providesTags: buildProvidesTags('MainModel'), transformResponse: buildTransformResponse(mainModelsAdapter), }), + getModel: build.query({ + query: (key) => { + return buildModelsUrl(`i/${key}`); + }, + providesTags: ['Model'], + }), updateModels: build.mutation({ query: ({ key, body }) => { return { @@ -320,4 +330,5 @@ export const { useGetModelsInFolderQuery, useGetCheckpointConfigsQuery, useGetModelImportsQuery, + useGetModelQuery } = modelsApi; diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 3566fdb9e2..c9690649f8 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -22,7 +22,7 @@ export type paths = { "/api/v2/models/i/{key}": { /** * Get Model Record - * @description Get a model record + * @description Get a model record and metadata */ get: operations["get_model_record"]; /** @@ -4202,6 +4202,13 @@ export type components = { */ type: "freeu"; }; + /** GetModelResponse */ + GetModelResponse: { + /** Config */ + config: (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + /** Metadata */ + metadata: (components["schemas"]["BaseMetadata"] | components["schemas"]["HuggingFaceMetadata"] | components["schemas"]["CivitaiMetadata"]) | null; + }; /** Graph */ Graph: { /** @@ -11169,7 +11176,7 @@ export type operations = { }; /** * Get Model Record - * @description Get a model record + * @description Get a model record and metadata */ get_model_record: { parameters: { @@ -11182,7 +11189,7 @@ export type operations = { /** @description The model configuration was retrieved successfully */ 200: { content: { - "application/json": (components["schemas"]["MainDiffusersConfig"] | components["schemas"]["MainCheckpointConfig"]) | (components["schemas"]["ONNXSD1Config"] | components["schemas"]["ONNXSD2Config"] | components["schemas"]["ONNXSDXLConfig"]) | (components["schemas"]["VaeDiffusersConfig"] | components["schemas"]["VaeCheckpointConfig"]) | (components["schemas"]["ControlNetDiffusersConfig"] | components["schemas"]["ControlNetCheckpointConfig"]) | components["schemas"]["LoRAConfig"] | components["schemas"]["TextualInversionConfig"] | components["schemas"]["IPAdapterConfig"] | components["schemas"]["CLIPVisionDiffusersConfig"] | components["schemas"]["T2IConfig"]; + "application/json": components["schemas"]["GetModelResponse"]; }; }; /** @description Bad request */ From 6b68971f384555d740882cec4229a2f61dc03d04 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Wed, 21 Feb 2024 09:41:50 -0500 Subject: [PATCH 256/411] hook up Add Model button --- .../modelManagerV2/subpanels/ModelManager.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx index 648f98dbb3..900a6a9342 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx @@ -3,8 +3,16 @@ import { SyncModelsIconButton } from 'features/modelManager/components/SyncModel import ModelList from './ModelManagerPanel/ModelList'; import { ModelListNavigation } from './ModelManagerPanel/ModelListNavigation'; +import { useCallback } from 'react'; +import { useAppDispatch } from '../../../app/store/storeHooks'; +import { setSelectedModelKey } from '../store/modelManagerV2Slice'; export const ModelManager = () => { + const dispatch = useAppDispatch(); + const handleClickAddModel = useCallback(() => { + dispatch(setSelectedModelKey(null)); + }, [dispatch]); + return ( @@ -13,7 +21,9 @@ export const ModelManager = () => { - + From 0a69779df90989e7b245c0abdac29080cda967cc Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Wed, 21 Feb 2024 13:56:14 -0500 Subject: [PATCH 257/411] edit view for model, depending on type and valid values --- .../store/modelManagerV2Slice.ts | 8 +- .../modelManagerV2/subpanels/ModelPane.tsx | 4 +- .../ModelPanel/Fields/BaseModelSelect.tsx | 29 +++ .../ModelPanel/Fields/BooleanSelect.tsx | 27 +++ .../ModelPanel/Fields/ModelFormatSelect.tsx | 53 +++++ .../ModelPanel/Fields/ModelTypeSelect.tsx | 33 +++ .../ModelPanel/Fields/ModelVariantSelect.tsx | 27 +++ .../Fields/PredictionTypeSelect.tsx | 28 +++ .../ModelPanel/Fields/RepoVariantSelect.tsx | 30 +++ .../subpanels/ModelPanel/Model.tsx | 8 + .../subpanels/ModelPanel/ModelEdit.tsx | 196 ++++++++++++++++++ .../subpanels/ModelPanel/ModelView.tsx | 138 +++++++----- .../features/parameters/types/constants.ts | 6 +- .../web/src/services/api/endpoints/models.ts | 11 +- .../frontend/web/src/services/api/schema.ts | 17 +- 15 files changed, 538 insertions(+), 77 deletions(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BooleanSelect.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/RepoVariantSelect.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts index 29b071e9b1..83bd0cf8d5 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts @@ -6,6 +6,7 @@ import type { PersistConfig, RootState } from 'app/store/store'; type ModelManagerState = { _version: 1; selectedModelKey: string | null; + selectedModelMode: "edit" | "view", searchTerm: string; filteredModelType: string | null; }; @@ -13,6 +14,7 @@ type ModelManagerState = { export const initialModelManagerState: ModelManagerState = { _version: 1, selectedModelKey: null, + selectedModelMode: "view", filteredModelType: null, searchTerm: "" }; @@ -22,8 +24,12 @@ export const modelManagerV2Slice = createSlice({ initialState: initialModelManagerState, reducers: { setSelectedModelKey: (state, action: PayloadAction) => { + state.selectedModelMode = "view" state.selectedModelKey = action.payload; }, + setSelectedModelMode: (state, action: PayloadAction<"view" | "edit">) => { + state.selectedModelMode = action.payload; + }, setSearchTerm: (state, action: PayloadAction) => { state.searchTerm = action.payload; }, @@ -34,7 +40,7 @@ export const modelManagerV2Slice = createSlice({ }, }); -export const { setSelectedModelKey, setSearchTerm, setFilteredModelType } = modelManagerV2Slice.actions; +export const { setSelectedModelKey, setSearchTerm, setFilteredModelType, setSelectedModelMode } = modelManagerV2Slice.actions; export const selectModelManagerSlice = (state: RootState) => state.modelmanager; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx index 9756b357b3..7658e741d3 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx @@ -1,13 +1,13 @@ import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from '../../../app/store/storeHooks'; import { ImportModels } from './ImportModels'; -import { ModelView } from './ModelPanel/ModelView'; +import { Model } from './ModelPanel/Model'; export const ModelPane = () => { const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); return ( - {selectedModelKey ? : } + {selectedModelKey ? : } ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx new file mode 100644 index 0000000000..da7333c2a8 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx @@ -0,0 +1,29 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'sd-1', label: MODEL_TYPE_MAP['sd-1'] }, + { value: 'sd-2', label: MODEL_TYPE_MAP['sd-2'] }, + { value: 'sdxl', label: MODEL_TYPE_MAP['sdxl'] }, + { value: 'sdxl-refiner', label: MODEL_TYPE_MAP['sdxl-refiner'] }, +]; + +const BaseModelSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(BaseModelSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BooleanSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BooleanSelect.tsx new file mode 100644 index 0000000000..d21ee89531 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BooleanSelect.tsx @@ -0,0 +1,27 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'none', label: '-' }, + { value: true as any, label: 'True' }, + { value: false as any, label: 'False' }, +]; + +const BooleanSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + v?.value === 'none' ? field.onChange(undefined) : field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(BooleanSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx new file mode 100644 index 0000000000..0552789a86 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx @@ -0,0 +1,53 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { LORA_MODEL_FORMAT_MAP, MODEL_TYPE_MAP } from 'features/parameters/types/constants'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'sd-1', label: MODEL_TYPE_MAP['sd-1'] }, + { value: 'sd-2', label: MODEL_TYPE_MAP['sd-2'] }, + { value: 'sdxl', label: MODEL_TYPE_MAP['sdxl'] }, + { value: 'sdxl-refiner', label: MODEL_TYPE_MAP['sdxl-refiner'] }, +]; + +const ModelFormatSelect = (props: UseControllerProps) => { + const { field, formState } = useController(props); + + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); + + const options: ComboboxOption[] = useMemo(() => { + if (formState.defaultValues?.type === 'lora') { + return Object.keys(LORA_MODEL_FORMAT_MAP).map((format) => ({ + value: format, + label: LORA_MODEL_FORMAT_MAP[format], + })) as ComboboxOption[]; + } else if (formState.defaultValues?.type === 'embedding') { + return [ + { value: 'embedding_file', label: 'Embedding File' }, + { value: 'embedding_folder', label: 'Embedding Folder' }, + ]; + } else if (formState.defaultValues?.type === 'ip_adapter') { + return [{ value: 'invokeai', label: 'invokeai' }]; + } else { + return [ + { value: 'diffusers', label: 'Diffusers' }, + { value: 'checkpoint', label: 'Checkpoint' }, + ]; + } + }, [formState.defaultValues?.type]); + + const value = useMemo(() => options.find((o) => o.value === field.value), [options, field.value]); + + return ; +}; + +export default typedMemo(ModelFormatSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx new file mode 100644 index 0000000000..140bfa9fe0 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx @@ -0,0 +1,33 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; +import { MODEL_TYPE_LABELS } from '../../ModelManagerPanel/ModelTypeFilter'; + +const options: ComboboxOption[] = [ + { value: 'main', label: MODEL_TYPE_LABELS['main'] as string }, + { value: 'lora', label: MODEL_TYPE_LABELS['lora'] as string }, + { value: 'embedding', label: MODEL_TYPE_LABELS['embedding'] as string }, + { value: 'vae', label: MODEL_TYPE_LABELS['vae'] as string }, + { value: 'controlnet', label: MODEL_TYPE_LABELS['controlnet'] as string }, + { value: 'ip_adapter', label: MODEL_TYPE_LABELS['ip_adapter'] as string }, + { value: 't2i_adapater', label: MODEL_TYPE_LABELS['t2i_adapter'] as string }, +]; + +const ModelTypeSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(ModelTypeSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx new file mode 100644 index 0000000000..7fb74b0bd9 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx @@ -0,0 +1,27 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig, CheckpointModelConfig, DiffusersModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'normal', label: 'Normal' }, + { value: 'inpaint', label: 'Inpaint' }, + { value: 'depth', label: 'Depth' }, +]; + +const ModelVariantSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(ModelVariantSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx new file mode 100644 index 0000000000..20667ab5bc --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx @@ -0,0 +1,28 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'none', label: '-' }, + { value: 'epsilon', label: 'epsilon' }, + { value: 'v_prediction', label: 'v_prediction' }, + { value: 'sample', label: 'sample' }, +]; + +const PredictionTypeSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + v?.value === 'none' ? field.onChange(undefined) : field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(PredictionTypeSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/RepoVariantSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/RepoVariantSelect.tsx new file mode 100644 index 0000000000..74793be789 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/RepoVariantSelect.tsx @@ -0,0 +1,30 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'none', label: '-' }, + { value: 'fp16', label: 'fp16' }, + { value: 'fp32', label: 'fp32' }, + { value: 'onnx', label: 'onnx' }, + { value: 'openvino', label: 'openvino' }, + { value: 'flax', label: 'flax' }, +]; + +const RepoVariantSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + v?.value === 'none' ? field.onChange(undefined) : field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(RepoVariantSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx new file mode 100644 index 0000000000..8a6f7ddee4 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx @@ -0,0 +1,8 @@ +import { useAppSelector } from '../../../../app/store/storeHooks'; +import { ModelEdit } from './ModelEdit'; +import { ModelView } from './ModelView'; + +export const Model = () => { + const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode); + return selectedModelMode === 'view' ? : ; +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx new file mode 100644 index 0000000000..70d0596cd8 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx @@ -0,0 +1,196 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from '../../../../app/store/storeHooks'; +import { useGetModelQuery } from '../../../../services/api/endpoints/models'; +import { Flex, Text, Heading, Button, Input, FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library'; +import { useCallback, useMemo } from 'react'; +import { + AnyModelConfig, + CheckpointModelConfig, + ControlNetConfig, + DiffusersModelConfig, + IPAdapterConfig, + LoRAConfig, + T2IAdapterConfig, + TextualInversionConfig, + VAEConfig, +} from '../../../../services/api/types'; +import { setSelectedModelMode } from '../../store/modelManagerV2Slice'; +import BaseModelSelect from './Fields/BaseModelSelect'; +import { useForm } from 'react-hook-form'; +import ModelTypeSelect from './Fields/ModelTypeSelect'; +import ModelVariantSelect from './Fields/ModelVariantSelect'; +import RepoVariantSelect from './Fields/RepoVariantSelect'; +import PredictionTypeSelect from './Fields/PredictionTypeSelect'; +import BooleanSelect from './Fields/BooleanSelect'; +import ModelFormatSelect from './Fields/ModelFormatSelect'; + +export const ModelEdit = () => { + const dispatch = useAppDispatch(); + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const { data, isLoading } = useGetModelQuery(selectedModelKey ?? skipToken); + + const modelData = useMemo(() => { + if (!data) { + return null; + } + const modelFormat = data.format; + const modelType = data.type; + + if (modelType === 'main') { + if (modelFormat === 'diffusers') { + return data as DiffusersModelConfig; + } else if (modelFormat === 'checkpoint') { + return data as CheckpointModelConfig; + } + } + + switch (modelType) { + case 'lora': + return data as LoRAConfig; + case 'embedding': + return data as TextualInversionConfig; + case 't2i_adapter': + return data as T2IAdapterConfig; + case 'ip_adapter': + return data as IPAdapterConfig; + case 'controlnet': + return data as ControlNetConfig; + case 'vae': + return data as VAEConfig; + default: + return data as DiffusersModelConfig; + } + }, [data]); + + const { + register, + handleSubmit, + control, + formState: { errors }, + reset, + } = useForm({ + defaultValues: { + ...modelData, + }, + mode: 'onChange', + }); + + const handleClickCancel = useCallback(() => { + dispatch(setSelectedModelMode('view')); + }, [dispatch]); + + if (isLoading) { + return Loading; + } + + if (!modelData) { + return Something went wrong; + } + return ( + + + value.trim().length > 3 || 'Must be at least 3 characters', + })} + size="lg" + /> + + + + + + + + + + Description +

T9IxZ{Ar4C8RW67>9 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 a%$U|fT^f2E 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 o`rXJYN{< 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