From a43c9009610da33448b12e1fce74549f6f462982 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 13 Jul 2023 01:18:32 +1000 Subject: [PATCH] feat(ui): update UI for new metadata - Update for new routes - Update model storage in state to be `MainModelField` type instead of `string`, simplifies a lot of model handling - Update model-related stuff for model `name` --> `model_name` - Update linear graphs to use `MetadataAccumulator` - Update `ImageMetadataViewer` UI - Ensure all `recall` functions work (well, the ones that are active anyways) --- .../middleware/listenerMiddleware/index.ts | 2 + .../listeners/controlNetImageProcessed.ts | 20 +- .../listeners/imageAddedToBoard.ts | 6 +- .../listeners/imageMetadataReceived.ts | 8 +- .../listeners/imageRemovedFromBoard.ts | 6 +- .../listeners/modelSelected.ts | 8 +- .../listeners/modelsLoaded.ts | 42 ++ .../socketio/socketInvocationComplete.ts | 18 +- .../components/ParamEmbeddingPopover.tsx | 4 +- .../components/CurrentImagePreview.tsx | 1 - .../ImageMetadataActions.tsx | 212 ++++++++++ .../ImageMetadataViewer.tsx | 365 ++++-------------- .../ImageMetaDataViewer/MetadataItem.tsx | 77 ++++ .../MetadataJSONViewer.tsx | 70 ++++ .../lora/components/ParamLoraSelect.tsx | 2 +- .../nodes/util/addControlNetToLinearGraph.ts | 94 ----- .../nodes/util/edgeBuilders/buildEdges.ts | 40 -- .../addControlNetToLinearGraph.ts | 100 +++++ .../graphBuilders/addDynamicPromptsToGraph.ts | 47 ++- .../util/graphBuilders/addLoRAsToGraph.ts | 15 +- .../nodes/util/graphBuilders/addVAEToGraph.ts | 13 +- .../buildCanvasImageToImageGraph.ts | 58 ++- .../graphBuilders/buildCanvasInpaintGraph.ts | 4 +- .../buildCanvasTextToImageGraph.ts | 59 ++- .../buildLinearImageToImageGraph.ts | 138 ++++--- .../buildLinearTextToImageGraph.ts | 61 ++- .../nodes/util/graphBuilders/constants.ts | 1 + .../src/features/nodes/util/parseSchema.ts | 10 +- .../parameters/hooks/useRecallParameters.ts | 2 +- .../src/features/parameters/store/actions.ts | 6 +- .../parameters/store/generationSlice.ts | 21 +- .../parameters/store/parameterZodSchemas.ts | 3 +- .../system/components/ModelSelect.tsx | 53 ++- .../features/system/components/VAESelect.tsx | 2 +- .../web/src/services/api/endpoints/images.ts | 23 +- .../web/src/services/api/endpoints/models.ts | 17 +- .../frontend/web/src/services/api/index.ts | 4 +- .../web/src/services/api/thunks/image.ts | 112 +++--- .../frontend/web/src/services/api/types.d.ts | 5 + 39 files changed, 1060 insertions(+), 669 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataActions.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataItem.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataJSONViewer.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/util/edgeBuilders/buildEdges.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts 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 bfaebb805a..23244d9a80 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -51,6 +51,7 @@ import { } from './listeners/imageUrlsReceived'; import { addInitialImageSelectedListener } from './listeners/initialImageSelected'; import { addModelSelectedListener } from './listeners/modelSelected'; +import { addModelsLoadedListener } from './listeners/modelsLoaded'; import { addReceivedOpenAPISchemaListener } from './listeners/receivedOpenAPISchema'; import { addReceivedPageOfImagesFulfilledListener, @@ -224,3 +225,4 @@ addModelSelectedListener(); // app startup addAppStartedListener(); +addModelsLoadedListener(); 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 3e11a5f98b..42387b8078 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 @@ -1,13 +1,13 @@ -import { startAppListening } from '..'; -import { imageMetadataReceived } from 'services/api/thunks/image'; import { log } from 'app/logging/useLogger'; import { controlNetImageProcessed } from 'features/controlNet/store/actions'; -import { Graph } from 'services/api/types'; -import { sessionCreated } from 'services/api/thunks/session'; -import { sessionReadyToInvoke } from 'features/system/store/actions'; -import { socketInvocationComplete } from 'services/events/actions'; -import { isImageOutput } from 'services/api/guards'; import { controlNetProcessedImageChanged } from 'features/controlNet/store/controlNetSlice'; +import { sessionReadyToInvoke } from 'features/system/store/actions'; +import { isImageOutput } from 'services/api/guards'; +import { imageDTOReceived } from 'services/api/thunks/image'; +import { sessionCreated } from 'services/api/thunks/session'; +import { Graph } from 'services/api/types'; +import { socketInvocationComplete } from 'services/events/actions'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'controlNet' }); @@ -63,10 +63,8 @@ export const addControlNetImageProcessedListener = () => { // Wait for the ImageDTO to be received const [imageMetadataReceivedAction] = await take( - ( - action - ): action is ReturnType => - imageMetadataReceived.fulfilled.match(action) && + (action): action is ReturnType => + imageDTOReceived.fulfilled.match(action) && action.payload.image_name === image_name ); const processedControlImage = imageMetadataReceivedAction.payload; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts index 082dfc0efb..e4d8c74bf9 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard.ts @@ -1,7 +1,7 @@ import { log } from 'app/logging/useLogger'; -import { startAppListening } from '..'; -import { imageMetadataReceived } from 'services/api/thunks/image'; import { boardImagesApi } from 'services/api/endpoints/boardImages'; +import { imageDTOReceived } from 'services/api/thunks/image'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'boards' }); @@ -17,7 +17,7 @@ export const addImageAddedToBoardFulfilledListener = () => { ); dispatch( - imageMetadataReceived({ + imageDTOReceived({ image_name, }) ); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts index 19af5b24c3..8a6d069ab0 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts @@ -1,13 +1,13 @@ import { log } from 'app/logging/useLogger'; -import { startAppListening } from '..'; -import { imageMetadataReceived, imageUpdated } from 'services/api/thunks/image'; import { imageUpserted } from 'features/gallery/store/gallerySlice'; +import { imageDTOReceived, imageUpdated } from 'services/api/thunks/image'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'image' }); export const addImageMetadataReceivedFulfilledListener = () => { startAppListening({ - actionCreator: imageMetadataReceived.fulfilled, + actionCreator: imageDTOReceived.fulfilled, effect: (action, { getState, dispatch }) => { const image = action.payload; @@ -40,7 +40,7 @@ export const addImageMetadataReceivedFulfilledListener = () => { export const addImageMetadataReceivedRejectedListener = () => { startAppListening({ - actionCreator: imageMetadataReceived.rejected, + actionCreator: imageDTOReceived.rejected, effect: (action, { getState, dispatch }) => { moduleLog.debug( { data: { image: action.meta.arg } }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts index 5c056474e3..4cf144211c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard.ts @@ -1,7 +1,7 @@ import { log } from 'app/logging/useLogger'; -import { startAppListening } from '..'; -import { imageMetadataReceived } from 'services/api/thunks/image'; import { boardImagesApi } from 'services/api/endpoints/boardImages'; +import { imageDTOReceived } from 'services/api/thunks/image'; +import { startAppListening } from '..'; const moduleLog = log.child({ namespace: 'boards' }); @@ -17,7 +17,7 @@ export const addImageRemovedFromBoardFulfilledListener = () => { ); dispatch( - imageMetadataReceived({ + imageDTOReceived({ image_name, }) ); 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 934581d02a..5ab30570d9 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 @@ -14,7 +14,7 @@ export const addModelSelectedListener = () => { actionCreator: modelSelected, effect: (action, { getState, dispatch }) => { const state = getState(); - const [base_model, type, name] = action.payload.split('/'); + const { base_model, model_name } = action.payload; if (state.generation.model?.base_model !== base_model) { dispatch( @@ -30,11 +30,7 @@ export const addModelSelectedListener = () => { // TODO: controlnet cleared } - const newModel = zMainModel.parse({ - id: action.payload, - base_model, - name, - }); + const newModel = zMainModel.parse(action.payload); dispatch(modelChanged(newModel)); }, 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 new file mode 100644 index 0000000000..ee02028848 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -0,0 +1,42 @@ +import { modelChanged } from 'features/parameters/store/generationSlice'; +import { some } from 'lodash-es'; +import { modelsApi } from 'services/api/endpoints/models'; +import { startAppListening } from '..'; + +export const addModelsLoadedListener = () => { + startAppListening({ + matcher: modelsApi.endpoints.getMainModels.matchFulfilled, + effect: async (action, { getState, dispatch }) => { + // models loaded, we need to ensure the selected model is available and if not, select the first one + + const currentModel = getState().generation.model; + + const isCurrentModelAvailable = some( + action.payload.entities, + (m) => + m?.model_name === currentModel?.model_name && + m?.base_model === currentModel?.base_model + ); + + if (isCurrentModelAvailable) { + return; + } + + const firstModelId = action.payload.ids[0]; + const firstModel = action.payload.entities[firstModelId]; + + if (!firstModel) { + // No models loaded at all + dispatch(modelChanged(null)); + return; + } + + dispatch( + modelChanged({ + base_model: firstModel.base_model, + model_name: firstModel.model_name, + }) + ); + }, + }); +}; 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 3686816d5c..2d091af0b6 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 @@ -1,15 +1,15 @@ -import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; -import { startAppListening } from '../..'; import { log } from 'app/logging/useLogger'; +import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; +import { progressImageSet } from 'features/system/store/systemSlice'; +import { boardImagesApi } from 'services/api/endpoints/boardImages'; +import { isImageOutput } from 'services/api/guards'; +import { imageDTOReceived } from 'services/api/thunks/image'; +import { sessionCanceled } from 'services/api/thunks/session'; import { appSocketInvocationComplete, socketInvocationComplete, } from 'services/events/actions'; -import { imageMetadataReceived } from 'services/api/thunks/image'; -import { sessionCanceled } from 'services/api/thunks/session'; -import { isImageOutput } from 'services/api/guards'; -import { progressImageSet } from 'features/system/store/systemSlice'; -import { boardImagesApi } from 'services/api/endpoints/boardImages'; +import { startAppListening } from '../..'; const moduleLog = log.child({ namespace: 'socketio' }); const nodeDenylist = ['dataURL_image']; @@ -42,13 +42,13 @@ export const addInvocationCompleteEventListener = () => { // Get its metadata dispatch( - imageMetadataReceived({ + imageDTOReceived({ image_name, }) ); const [{ payload: imageDTO }] = await take( - imageMetadataReceived.fulfilled.match + imageDTOReceived.fulfilled.match ); // Handle canvas image diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx index b7de3a8d99..32822125d2 100644 --- a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx @@ -47,8 +47,8 @@ const ParamEmbeddingPopover = (props: Props) => { const disabled = currentMainModel?.base_model !== embedding.base_model; data.push({ - value: embedding.name, - label: embedding.name, + value: embedding.model_name, + label: embedding.model_name, group: MODEL_TYPE_MAP[embedding.base_model], disabled, tooltip: disabled diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 8018beea9a..ef5228434e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -118,7 +118,6 @@ const CurrentImagePreview = () => { width: 'full', height: 'full', borderRadius: 'base', - overflow: 'scroll', }} > diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataActions.tsx new file mode 100644 index 0000000000..35685bde6f --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataActions.tsx @@ -0,0 +1,212 @@ +import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; +import { useCallback } from 'react'; +import { UnsafeImageMetadata } from 'services/api/endpoints/images'; +import MetadataItem from './MetadataItem'; + +type Props = { + metadata?: UnsafeImageMetadata['metadata']; +}; + +const ImageMetadataActions = (props: Props) => { + const { metadata } = props; + + const { + recallBothPrompts, + recallPositivePrompt, + recallNegativePrompt, + recallSeed, + recallInitialImage, + recallCfgScale, + recallModel, + recallScheduler, + recallSteps, + recallWidth, + recallHeight, + recallStrength, + recallAllParameters, + } = useRecallParameters(); + + const handleRecallPositivePrompt = useCallback(() => { + recallPositivePrompt(metadata?.positive_prompt); + }, [metadata?.positive_prompt, recallPositivePrompt]); + + const handleRecallNegativePrompt = useCallback(() => { + recallNegativePrompt(metadata?.negative_prompt); + }, [metadata?.negative_prompt, recallNegativePrompt]); + + const handleRecallSeed = useCallback(() => { + recallSeed(metadata?.seed); + }, [metadata?.seed, recallSeed]); + + const handleRecallModel = useCallback(() => { + recallModel(metadata?.model); + }, [metadata?.model, recallModel]); + + const handleRecallWidth = useCallback(() => { + recallWidth(metadata?.width); + }, [metadata?.width, recallWidth]); + + const handleRecallHeight = useCallback(() => { + recallHeight(metadata?.height); + }, [metadata?.height, recallHeight]); + + const handleRecallScheduler = useCallback(() => { + recallScheduler(metadata?.scheduler); + }, [metadata?.scheduler, recallScheduler]); + + const handleRecallSteps = useCallback(() => { + recallSteps(metadata?.steps); + }, [metadata?.steps, recallSteps]); + + const handleRecallCfgScale = useCallback(() => { + recallCfgScale(metadata?.cfg_scale); + }, [metadata?.cfg_scale, recallCfgScale]); + + const handleRecallStrength = useCallback(() => { + recallStrength(metadata?.strength); + }, [metadata?.strength, recallStrength]); + + if (!metadata || Object.keys(metadata).length === 0) { + return null; + } + + return ( + <> + {metadata.generation_mode && ( + + )} + {metadata.positive_prompt && ( + + )} + {metadata.negative_prompt && ( + + )} + {metadata.seed !== undefined && ( + + )} + {metadata.model !== undefined && ( + + )} + {metadata.width && ( + + )} + {metadata.height && ( + + )} + {/* {metadata.threshold !== undefined && ( + dispatch(setThreshold(Number(metadata.threshold)))} + /> + )} + {metadata.perlin !== undefined && ( + dispatch(setPerlin(Number(metadata.perlin)))} + /> + )} */} + {metadata.scheduler && ( + + )} + {metadata.steps && ( + + )} + {metadata.cfg_scale !== undefined && ( + + )} + {/* {metadata.variations && metadata.variations.length > 0 && ( + + dispatch( + setSeedWeights(seedWeightsToString(metadata.variations)) + ) + } + /> + )} + {metadata.seamless && ( + dispatch(setSeamless(metadata.seamless))} + /> + )} + {metadata.hires_fix && ( + dispatch(setHiresFix(metadata.hires_fix))} + /> + )} */} + + {/* {init_image_path && ( + dispatch(setInitialImage(init_image_path))} + /> + )} */} + {metadata.strength && ( + + )} + {/* {metadata.fit && ( + dispatch(setShouldFitToWidthHeight(metadata.fit))} + /> + )} */} + + ); +}; + +export default ImageMetadataActions; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index e150cea883..8a3078be47 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -1,131 +1,63 @@ import { ExternalLinkIcon } from '@chakra-ui/icons'; import { - Box, - Center, Flex, - IconButton, Link, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, Text, - Tooltip, } from '@chakra-ui/react'; -import { useAppDispatch } from 'app/store/storeHooks'; -import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; -import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; -import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; -import { memo } from 'react'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useTranslation } from 'react-i18next'; -import { FaCopy } from 'react-icons/fa'; -import { IoArrowUndoCircleOutline } from 'react-icons/io5'; +import { skipToken } from '@reduxjs/toolkit/dist/query'; +import { memo, useMemo } from 'react'; +import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; import { ImageDTO } from 'services/api/types'; - -type MetadataItemProps = { - isLink?: boolean; - label: string; - onClick?: () => void; - value: number | string | boolean; - labelPosition?: string; - withCopy?: boolean; -}; - -/** - * Component to display an individual metadata item or parameter. - */ -const MetadataItem = ({ - label, - value, - onClick, - isLink, - labelPosition, - withCopy = false, -}: MetadataItemProps) => { - const { t } = useTranslation(); - - if (!value) { - return null; - } - - return ( - - {onClick && ( - - } - size="xs" - variant="ghost" - fontSize={20} - onClick={onClick} - /> - - )} - {withCopy && ( - - } - size="xs" - variant="ghost" - fontSize={14} - onClick={() => navigator.clipboard.writeText(value.toString())} - /> - - )} - - - {label}: - - {isLink ? ( - - {value.toString()} - - ) : ( - - {value.toString()} - - )} - - - ); -}; +import ImageMetadataActions from './ImageMetadataActions'; +import MetadataJSONViewer from './MetadataJSONViewer'; type ImageMetadataViewerProps = { image: ImageDTO; }; -/** - * Image metadata viewer overlays currently selected image and provides - * access to any of its metadata for use in processing. - */ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { - const dispatch = useAppDispatch(); - const { - recallBothPrompts, - recallPositivePrompt, - recallNegativePrompt, - recallSeed, - recallInitialImage, - recallCfgScale, - recallModel, - recallScheduler, - recallSteps, - recallWidth, - recallHeight, - recallStrength, - recallAllParameters, - } = useRecallParameters(); + // TODO: fix hotkeys + // const dispatch = useAppDispatch(); + // useHotkeys('esc', () => { + // dispatch(setShouldShowImageDetails(false)); + // }); - useHotkeys('esc', () => { - dispatch(setShouldShowImageDetails(false)); - }); + const { data } = useGetImageMetadataQuery(image?.image_name ?? skipToken); + const metadata = data?.metadata; - const sessionId = image?.session_id; + const tabData = useMemo(() => { + const _tabData: { label: string; data: object; copyTooltip: string }[] = []; - const metadata = image?.metadata; + if (data?.metadata) { + _tabData.push({ + label: 'Core Metadata', + data: data?.metadata, + copyTooltip: 'Copy Core Metadata JSON', + }); + } - const { t } = useTranslation(); + if (image) { + _tabData.push({ + label: 'Image Details', + data: image, + copyTooltip: 'Copy Image Details JSON', + }); + } - const metadataJSON = JSON.stringify(image, null, 2); + if (data?.graph) { + _tabData.push({ + label: 'Graph', + data: data?.graph, + copyTooltip: 'Copy Graph JSON', + }); + } + return _tabData; + }, [data?.metadata, data?.graph, image]); return ( { width: 'full', height: 'full', backdropFilter: 'blur(20px)', - bg: 'whiteAlpha.600', + bg: 'baseAlpha.200', _dark: { bg: 'blackAlpha.600', }, - overflow: 'scroll', + borderRadius: 'base', + position: 'absolute', + overflow: 'hidden', }} > @@ -150,179 +84,42 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => { - {metadata && Object.keys(metadata).length > 0 ? ( - <> - {metadata.type && ( - - )} - {sessionId && } - {metadata.positive_conditioning && ( - - recallPositivePrompt(metadata.positive_conditioning) - } - /> - )} - {metadata.negative_conditioning && ( - - recallNegativePrompt(metadata.negative_conditioning) - } - /> - )} - {metadata.seed !== undefined && ( - recallSeed(metadata.seed)} - /> - )} - {metadata.model !== undefined && ( - recallModel(metadata.model)} - /> - )} - {metadata.width && ( - recallWidth(metadata.width)} - /> - )} - {metadata.height && ( - recallHeight(metadata.height)} - /> - )} - {/* {metadata.threshold !== undefined && ( - dispatch(setThreshold(Number(metadata.threshold)))} - /> - )} - {metadata.perlin !== undefined && ( - dispatch(setPerlin(Number(metadata.perlin)))} - /> - )} */} - {metadata.scheduler && ( - recallScheduler(metadata.scheduler)} - /> - )} - {metadata.steps && ( - recallSteps(metadata.steps)} - /> - )} - {metadata.cfg_scale !== undefined && ( - recallCfgScale(metadata.cfg_scale)} - /> - )} - {/* {metadata.variations && metadata.variations.length > 0 && ( - - dispatch( - setSeedWeights(seedWeightsToString(metadata.variations)) - ) - } - /> - )} - {metadata.seamless && ( - dispatch(setSeamless(metadata.seamless))} - /> - )} - {metadata.hires_fix && ( - dispatch(setHiresFix(metadata.hires_fix))} - /> - )} */} - {/* {init_image_path && ( - dispatch(setInitialImage(init_image_path))} - /> - )} */} - {metadata.strength && ( - recallStrength(metadata.strength)} - /> - )} - {/* {metadata.fit && ( - dispatch(setShouldFitToWidthHeight(metadata.fit))} - /> - )} */} - - ) : ( -
- - No metadata available - -
- )} - - - - } - size="xs" - variant="ghost" - fontSize={14} - onClick={() => navigator.clipboard.writeText(metadataJSON)} - /> - - Metadata JSON: - - - -
{metadataJSON}
-
-
-
+ + + + + {tabData.map((tab) => ( + + + {tab.label} + + + ))} + + + + {tabData.map((tab) => ( + + + + ))} + +
); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataItem.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataItem.tsx new file mode 100644 index 0000000000..e311d9260a --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataItem.tsx @@ -0,0 +1,77 @@ +import { ExternalLinkIcon } from '@chakra-ui/icons'; +import { Flex, IconButton, Link, Text, Tooltip } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { FaCopy } from 'react-icons/fa'; +import { IoArrowUndoCircleOutline } from 'react-icons/io5'; + +type MetadataItemProps = { + isLink?: boolean; + label: string; + onClick?: () => void; + value: number | string | boolean; + labelPosition?: string; + withCopy?: boolean; +}; + +/** + * Component to display an individual metadata item or parameter. + */ +const MetadataItem = ({ + label, + value, + onClick, + isLink, + labelPosition, + withCopy = false, +}: MetadataItemProps) => { + const { t } = useTranslation(); + + if (!value) { + return null; + } + + return ( + + {onClick && ( + + } + size="xs" + variant="ghost" + fontSize={20} + onClick={onClick} + /> + + )} + {withCopy && ( + + } + size="xs" + variant="ghost" + fontSize={14} + onClick={() => navigator.clipboard.writeText(value.toString())} + /> + + )} + + + {label}: + + {isLink ? ( + + {value.toString()} + + ) : ( + + {value.toString()} + + )} + + + ); +}; + +export default MetadataItem; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataJSONViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataJSONViewer.tsx new file mode 100644 index 0000000000..9600a535fd --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/MetadataJSONViewer.tsx @@ -0,0 +1,70 @@ +import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react'; +import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import { useMemo } from 'react'; +import { FaCopy } from 'react-icons/fa'; + +type Props = { + copyTooltip: string; + jsonObject: object; +}; + +const MetadataJSONViewer = (props: Props) => { + const { copyTooltip, jsonObject } = props; + const jsonString = useMemo( + () => JSON.stringify(jsonObject, null, 2), + [jsonObject] + ); + + return ( + + + +
{jsonString}
+
+
+ + + } + variant="ghost" + onClick={() => navigator.clipboard.writeText(jsonString)} + /> + + +
+ ); +}; + +export default MetadataJSONViewer; diff --git a/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx b/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx index 7b5aa5946b..ebceeb34db 100644 --- a/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx @@ -45,7 +45,7 @@ const ParamLoraSelect = () => { data.push({ value: id, - label: lora.name, + label: lora.model_name, disabled, group: MODEL_TYPE_MAP[lora.base_model], tooltip: disabled diff --git a/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts deleted file mode 100644 index 5c4d67ebd3..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/addControlNetToLinearGraph.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { RootState } from 'app/store/store'; -import { getValidControlNets } from 'features/controlNet/util/getValidControlNets'; -import { CollectInvocation, ControlNetInvocation } from 'services/api/types'; -import { NonNullableGraph } from '../types/types'; -import { CONTROL_NET_COLLECT } from './graphBuilders/constants'; - -export const addControlNetToLinearGraph = ( - graph: NonNullableGraph, - baseNodeId: string, - state: RootState -): void => { - const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet; - - const validControlNets = getValidControlNets(controlNets); - - if (isControlNetEnabled && Boolean(validControlNets.length)) { - if (validControlNets.length > 1) { - // We have multiple controlnets, add ControlNet collector - const controlNetIterateNode: CollectInvocation = { - id: CONTROL_NET_COLLECT, - type: 'collect', - }; - graph.nodes[controlNetIterateNode.id] = controlNetIterateNode; - graph.edges.push({ - source: { node_id: controlNetIterateNode.id, field: 'collection' }, - destination: { - node_id: baseNodeId, - field: 'control', - }, - }); - } - - validControlNets.forEach((controlNet) => { - const { - controlNetId, - controlImage, - processedControlImage, - beginStepPct, - endStepPct, - controlMode, - model, - processorType, - weight, - } = controlNet; - - const controlNetNode: ControlNetInvocation = { - id: `control_net_${controlNetId}`, - type: 'controlnet', - begin_step_percent: beginStepPct, - end_step_percent: endStepPct, - control_mode: controlMode, - control_model: model as ControlNetInvocation['control_model'], - control_weight: weight, - }; - - if (processedControlImage && processorType !== 'none') { - // We've already processed the image in the app, so we can just use the processed image - controlNetNode.image = { - image_name: processedControlImage, - }; - } else if (controlImage) { - // The control image is preprocessed - controlNetNode.image = { - image_name: controlImage, - }; - } else { - // Skip ControlNets without an unprocessed image - should never happen if everything is working correctly - return; - } - - graph.nodes[controlNetNode.id] = controlNetNode; - - if (validControlNets.length > 1) { - // if we have multiple controlnets, link to the collector - graph.edges.push({ - source: { node_id: controlNetNode.id, field: 'control' }, - destination: { - node_id: CONTROL_NET_COLLECT, - field: 'item', - }, - }); - } else { - // otherwise, link directly to the base node - graph.edges.push({ - source: { node_id: controlNetNode.id, field: 'control' }, - destination: { - node_id: baseNodeId, - field: 'control', - }, - }); - } - }); - } -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/edgeBuilders/buildEdges.ts b/invokeai/frontend/web/src/features/nodes/util/edgeBuilders/buildEdges.ts deleted file mode 100644 index 1c3d2c909e..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/edgeBuilders/buildEdges.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Edge, - ImageToImageInvocation, - InpaintInvocation, - IterateInvocation, - RandomRangeInvocation, - RangeInvocation, - TextToImageInvocation, -} from 'services/api/types'; - -export const buildEdges = ( - baseNode: TextToImageInvocation | ImageToImageInvocation | InpaintInvocation, - rangeNode: RangeInvocation | RandomRangeInvocation, - iterateNode: IterateInvocation -): Edge[] => { - const edges: Edge[] = [ - { - source: { - node_id: rangeNode.id, - field: 'collection', - }, - destination: { - node_id: iterateNode.id, - field: 'collection', - }, - }, - { - source: { - node_id: iterateNode.id, - field: 'item', - }, - destination: { - node_id: baseNode.id, - field: 'seed', - }, - }, - ]; - - return edges; -}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts new file mode 100644 index 0000000000..3291ce389d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addControlNetToLinearGraph.ts @@ -0,0 +1,100 @@ +import { RootState } from 'app/store/store'; +import { getValidControlNets } from 'features/controlNet/util/getValidControlNets'; +import { omit } from 'lodash-es'; +import { + CollectInvocation, + ControlField, + ControlNetInvocation, + MetadataAccumulatorInvocation, +} from 'services/api/types'; +import { NonNullableGraph } from '../../types/types'; +import { CONTROL_NET_COLLECT, METADATA_ACCUMULATOR } from './constants'; + +export const addControlNetToLinearGraph = ( + state: RootState, + graph: NonNullableGraph, + baseNodeId: string +): void => { + const { isEnabled: isControlNetEnabled, controlNets } = state.controlNet; + + const validControlNets = getValidControlNets(controlNets); + + const metadataAccumulator = graph.nodes[ + METADATA_ACCUMULATOR + ] as MetadataAccumulatorInvocation; + + if (isControlNetEnabled && Boolean(validControlNets.length)) { + if (validControlNets.length) { + // We have multiple controlnets, add ControlNet collector + const controlNetIterateNode: CollectInvocation = { + id: CONTROL_NET_COLLECT, + type: 'collect', + }; + graph.nodes[CONTROL_NET_COLLECT] = controlNetIterateNode; + graph.edges.push({ + source: { node_id: CONTROL_NET_COLLECT, field: 'collection' }, + destination: { + node_id: baseNodeId, + field: 'control', + }, + }); + + validControlNets.forEach((controlNet) => { + const { + controlNetId, + controlImage, + processedControlImage, + beginStepPct, + endStepPct, + controlMode, + model, + processorType, + weight, + } = controlNet; + + const controlNetNode: ControlNetInvocation = { + id: `control_net_${controlNetId}`, + type: 'controlnet', + begin_step_percent: beginStepPct, + end_step_percent: endStepPct, + control_mode: controlMode, + control_model: model as ControlNetInvocation['control_model'], + control_weight: weight, + }; + + if (processedControlImage && processorType !== 'none') { + // We've already processed the image in the app, so we can just use the processed image + controlNetNode.image = { + image_name: processedControlImage, + }; + } else if (controlImage) { + // The control image is preprocessed + controlNetNode.image = { + image_name: controlImage, + }; + } else { + // Skip ControlNets without an unprocessed image - should never happen if everything is working correctly + return; + } + + graph.nodes[controlNetNode.id] = controlNetNode; + + // metadata accumulator only needs a control field - not the whole node + // extract what we need and add to the accumulator + const controlField = omit(controlNetNode, [ + 'id', + 'type', + ]) as ControlField; + metadataAccumulator.controlnets.push(controlField); + + graph.edges.push({ + source: { node_id: controlNetNode.id, field: 'control' }, + destination: { + node_id: CONTROL_NET_COLLECT, + field: 'item', + }, + }); + }); + } + } +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts index 264dac2e14..2694cdc812 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addDynamicPromptsToGraph.ts @@ -1,8 +1,10 @@ import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; +import { unset } from 'lodash-es'; import { DynamicPromptInvocation, IterateInvocation, + MetadataAccumulatorInvocation, NoiseInvocation, RandomIntInvocation, RangeOfSizeInvocation, @@ -10,16 +12,16 @@ import { import { DYNAMIC_PROMPT, ITERATE, + METADATA_ACCUMULATOR, NOISE, POSITIVE_CONDITIONING, RANDOM_INT, RANGE_OF_SIZE, } from './constants'; -import { unset } from 'lodash-es'; export const addDynamicPromptsToGraph = ( - graph: NonNullableGraph, - state: RootState + state: RootState, + graph: NonNullableGraph ): void => { const { positivePrompt, iterations, seed, shouldRandomizeSeed } = state.generation; @@ -30,6 +32,10 @@ export const addDynamicPromptsToGraph = ( maxPrompts, } = state.dynamicPrompts; + const metadataAccumulator = graph.nodes[ + METADATA_ACCUMULATOR + ] as MetadataAccumulatorInvocation; + if (isDynamicPromptsEnabled) { // iteration is handled via dynamic prompts unset(graph.nodes[POSITIVE_CONDITIONING], 'prompt'); @@ -74,6 +80,18 @@ export const addDynamicPromptsToGraph = ( } ); + // hook up positive prompt to metadata + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: METADATA_ACCUMULATOR, + field: 'positive_prompt', + }, + }); + if (shouldRandomizeSeed) { // Random int node to generate the starting seed const randomIntNode: RandomIntInvocation = { @@ -88,11 +106,22 @@ export const addDynamicPromptsToGraph = ( source: { node_id: RANDOM_INT, field: 'a' }, destination: { node_id: NOISE, field: 'seed' }, }); + + graph.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { node_id: METADATA_ACCUMULATOR, field: 'seed' }, + }); } else { // User specified seed, so set the start of the range of size to the seed (graph.nodes[NOISE] as NoiseInvocation).seed = seed; + + // hook up seed to metadata + metadataAccumulator.seed = seed; } } else { + // no dynamic prompt - hook up positive prompt + metadataAccumulator.positive_prompt = positivePrompt; + const rangeOfSizeNode: RangeOfSizeInvocation = { id: RANGE_OF_SIZE, type: 'range_of_size', @@ -130,6 +159,18 @@ export const addDynamicPromptsToGraph = ( }, }); + // hook up seed to metadata + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: METADATA_ACCUMULATOR, + field: 'seed', + }, + }); + // handle seed if (shouldRandomizeSeed) { // Random int node to generate the starting seed diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts index 74cc8b1f57..9b8bff2c34 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addLoRAsToGraph.ts @@ -1,19 +1,23 @@ import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; import { forEach, size } from 'lodash-es'; -import { LoraLoaderInvocation } from 'services/api/types'; +import { + LoraLoaderInvocation, + MetadataAccumulatorInvocation, +} from 'services/api/types'; import { modelIdToLoRAModelField } from '../modelIdToLoRAName'; import { CLIP_SKIP, LORA_LOADER, MAIN_MODEL_LOADER, + METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, POSITIVE_CONDITIONING, } from './constants'; export const addLoRAsToGraph = ( - graph: NonNullableGraph, state: RootState, + graph: NonNullableGraph, baseNodeId: string ): void => { /** @@ -26,6 +30,9 @@ export const addLoRAsToGraph = ( const { loras } = state.lora; const loraCount = size(loras); + const metadataAccumulator = graph.nodes[ + METADATA_ACCUMULATOR + ] as MetadataAccumulatorInvocation; if (loraCount > 0) { // Remove MAIN_MODEL_LOADER unet connection to feed it to LoRAs @@ -62,6 +69,10 @@ export const addLoRAsToGraph = ( weight, }; + // add the lora to the metadata accumulator + metadataAccumulator.loras.push({ lora: loraField, weight }); + + // add to graph graph.nodes[currentLoraNodeId] = loraLoaderNode; if (currentLoraIndex === 0) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts index 9de8f6e99d..b3d4416b69 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/addVAEToGraph.ts @@ -1,5 +1,6 @@ import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; +import { MetadataAccumulatorInvocation } from 'services/api/types'; import { modelIdToVAEModelField } from '../modelIdToVAEModelField'; import { IMAGE_TO_IMAGE_GRAPH, @@ -8,18 +9,22 @@ import { INPAINT_GRAPH, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, + METADATA_ACCUMULATOR, TEXT_TO_IMAGE_GRAPH, VAE_LOADER, } from './constants'; export const addVAEToGraph = ( - graph: NonNullableGraph, - state: RootState + state: RootState, + graph: NonNullableGraph ): void => { const { vae } = state.generation; const vae_model = modelIdToVAEModelField(vae?.id || ''); const isAutoVae = !vae; + const metadataAccumulator = graph.nodes[ + METADATA_ACCUMULATOR + ] as MetadataAccumulatorInvocation; if (!isAutoVae) { graph.nodes[VAE_LOADER] = { @@ -67,4 +72,8 @@ export const addVAEToGraph = ( }, }); } + + if (vae) { + metadataAccumulator.vae = vae_model; + } }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts index 197e62aa2d..821df8fe6e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasImageToImageGraph.ts @@ -7,8 +7,7 @@ import { ImageResizeInvocation, ImageToLatentsInvocation, } from 'services/api/types'; -import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; -import { modelIdToMainModelField } from '../modelIdToMainModelField'; +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addVAEToGraph } from './addVAEToGraph'; @@ -19,6 +18,7 @@ import { LATENTS_TO_IMAGE, LATENTS_TO_LATENTS, MAIN_MODEL_LOADER, + METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, @@ -37,7 +37,7 @@ export const buildCanvasImageToImageGraph = ( const { positivePrompt, negativePrompt, - model: currentModel, + model, cfgScale: cfg_scale, scheduler, steps, @@ -50,7 +50,10 @@ export const buildCanvasImageToImageGraph = ( // The bounding box determines width and height, not the width and height params const { width, height } = state.canvas.boundingBoxDimensions; - const model = modelIdToMainModelField(currentModel?.id || ''); + if (!model) { + moduleLog.error('No model found in state'); + throw new Error('No model found in state'); + } const use_cpu = shouldUseNoiseSettings ? shouldUseCpuNoise @@ -275,16 +278,51 @@ export const buildCanvasImageToImageGraph = ( }); } - addLoRAsToGraph(graph, state, LATENTS_TO_LATENTS); + // add metadata accumulator, which is only mostly populated - some fields are added later + graph.nodes[METADATA_ACCUMULATOR] = { + id: METADATA_ACCUMULATOR, + type: 'metadata_accumulator', + generation_mode: 'img2img', + cfg_scale, + height, + width, + positive_prompt: '', // set in addDynamicPromptsToGraph + negative_prompt: negativePrompt, + model, + seed: 0, // set in addDynamicPromptsToGraph + steps, + rand_device: use_cpu ? 'cpu' : 'cuda', + scheduler, + vae: undefined, // option; set in addVAEToGraph + controlnets: [], // populated in addControlNetToLinearGraph + loras: [], // populated in addLoRAsToGraph + clip_skip: clipSkip, + strength, + init_image: initialImage.image_name, + }; - // Add VAE - addVAEToGraph(graph, state); + graph.edges.push({ + source: { + node_id: METADATA_ACCUMULATOR, + field: 'metadata', + }, + destination: { + node_id: LATENTS_TO_IMAGE, + field: 'metadata', + }, + }); - // add dynamic prompts, mutating `graph` - addDynamicPromptsToGraph(graph, state); + // add LoRA support + addLoRAsToGraph(state, graph, LATENTS_TO_LATENTS); + + // optionally add custom VAE + addVAEToGraph(state, graph); + + // add dynamic prompts - also sets up core iteration and seed + addDynamicPromptsToGraph(state, graph); // add controlnet, mutating `graph` - addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state); + addControlNetToLinearGraph(state, graph, LATENTS_TO_LATENTS); return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts index 2bac864015..66d4e7fd53 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasInpaintGraph.ts @@ -212,10 +212,10 @@ export const buildCanvasInpaintGraph = ( ], }; - addLoRAsToGraph(graph, state, INPAINT); + addLoRAsToGraph(state, graph, INPAINT); // Add VAE - addVAEToGraph(graph, state); + addVAEToGraph(state, graph); // handle seed if (shouldRandomizeSeed) { diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts index 918f18a34a..f6fd43b0a5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasTextToImageGraph.ts @@ -1,8 +1,8 @@ +import { log } from 'app/logging/useLogger'; import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; import { initialGenerationState } from 'features/parameters/store/generationSlice'; -import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; -import { modelIdToMainModelField } from '../modelIdToMainModelField'; +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addVAEToGraph } from './addVAEToGraph'; @@ -10,6 +10,7 @@ import { CLIP_SKIP, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, + METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, @@ -17,6 +18,8 @@ import { TEXT_TO_LATENTS, } from './constants'; +const moduleLog = log.child({ namespace: 'nodes' }); + /** * Builds the Canvas tab's Text to Image graph. */ @@ -26,7 +29,7 @@ export const buildCanvasTextToImageGraph = ( const { positivePrompt, negativePrompt, - model: currentModel, + model, cfgScale: cfg_scale, scheduler, steps, @@ -38,7 +41,10 @@ export const buildCanvasTextToImageGraph = ( // The bounding box determines width and height, not the width and height params const { width, height } = state.canvas.boundingBoxDimensions; - const model = modelIdToMainModelField(currentModel?.id || ''); + if (!model) { + moduleLog.error('No model found in state'); + throw new Error('No model found in state'); + } const use_cpu = shouldUseNoiseSettings ? shouldUseCpuNoise @@ -180,16 +186,49 @@ export const buildCanvasTextToImageGraph = ( ], }; - addLoRAsToGraph(graph, state, TEXT_TO_LATENTS); + // add metadata accumulator, which is only mostly populated - some fields are added later + graph.nodes[METADATA_ACCUMULATOR] = { + id: METADATA_ACCUMULATOR, + type: 'metadata_accumulator', + generation_mode: 'txt2img', + cfg_scale, + height, + width, + positive_prompt: '', // set in addDynamicPromptsToGraph + negative_prompt: negativePrompt, + model, + seed: 0, // set in addDynamicPromptsToGraph + steps, + rand_device: use_cpu ? 'cpu' : 'cuda', + scheduler, + vae: undefined, // option; set in addVAEToGraph + controlnets: [], // populated in addControlNetToLinearGraph + loras: [], // populated in addLoRAsToGraph + clip_skip: clipSkip, + }; - // Add VAE - addVAEToGraph(graph, state); + graph.edges.push({ + source: { + node_id: METADATA_ACCUMULATOR, + field: 'metadata', + }, + destination: { + node_id: LATENTS_TO_IMAGE, + field: 'metadata', + }, + }); - // add dynamic prompts, mutating `graph` - addDynamicPromptsToGraph(graph, state); + // add LoRA support + addLoRAsToGraph(state, graph, TEXT_TO_LATENTS); + + // optionally add custom VAE + addVAEToGraph(state, graph); + + // add dynamic prompts - also sets up core iteration and seed + addDynamicPromptsToGraph(state, graph); // add controlnet, mutating `graph` - addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state); + addControlNetToLinearGraph(state, graph, TEXT_TO_LATENTS); return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts index f8b95b4d77..9d200c4574 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearImageToImageGraph.ts @@ -3,25 +3,21 @@ import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; import { initialGenerationState } from 'features/parameters/store/generationSlice'; import { - ImageCollectionInvocation, ImageResizeInvocation, ImageToLatentsInvocation, - IterateInvocation, } from 'services/api/types'; -import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; -import { modelIdToMainModelField } from '../modelIdToMainModelField'; +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addVAEToGraph } from './addVAEToGraph'; import { CLIP_SKIP, - IMAGE_COLLECTION, - IMAGE_COLLECTION_ITERATE, IMAGE_TO_IMAGE_GRAPH, IMAGE_TO_LATENTS, LATENTS_TO_IMAGE, LATENTS_TO_LATENTS, MAIN_MODEL_LOADER, + METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, @@ -39,7 +35,7 @@ export const buildLinearImageToImageGraph = ( const { positivePrompt, negativePrompt, - model: currentModel, + model, cfgScale: cfg_scale, scheduler, steps, @@ -53,14 +49,15 @@ export const buildLinearImageToImageGraph = ( shouldUseNoiseSettings, } = state.generation; - const { - isEnabled: isBatchEnabled, - imageNames: batchImageNames, - asInitialImage, - } = state.batch; + // TODO: add batch functionality + // const { + // isEnabled: isBatchEnabled, + // imageNames: batchImageNames, + // asInitialImage, + // } = state.batch; - const shouldBatch = - isBatchEnabled && batchImageNames.length > 0 && asInitialImage; + // const shouldBatch = + // isBatchEnabled && batchImageNames.length > 0 && asInitialImage; /** * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the @@ -71,12 +68,15 @@ export const buildLinearImageToImageGraph = ( * the `fit` param. These are added to the graph at the end. */ - if (!initialImage && !shouldBatch) { + if (!initialImage) { moduleLog.error('No initial image found in state'); throw new Error('No initial image found in state'); } - const model = modelIdToMainModelField(currentModel?.id || ''); + if (!model) { + moduleLog.error('No model found in state'); + throw new Error('No model found in state'); + } const use_cpu = shouldUseNoiseSettings ? shouldUseCpuNoise @@ -295,51 +295,87 @@ export const buildLinearImageToImageGraph = ( }); } - if (isBatchEnabled && asInitialImage && batchImageNames.length > 0) { - // we are going to connect an iterate up to the init image - delete (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image; + // TODO: add batch functionality + // if (isBatchEnabled && asInitialImage && batchImageNames.length > 0) { + // // we are going to connect an iterate up to the init image + // delete (graph.nodes[IMAGE_TO_LATENTS] as ImageToLatentsInvocation).image; - const imageCollection: ImageCollectionInvocation = { - id: IMAGE_COLLECTION, - type: 'image_collection', - images: batchImageNames.map((image_name) => ({ image_name })), - }; + // const imageCollection: ImageCollectionInvocation = { + // id: IMAGE_COLLECTION, + // type: 'image_collection', + // images: batchImageNames.map((image_name) => ({ image_name })), + // }; - const imageCollectionIterate: IterateInvocation = { - id: IMAGE_COLLECTION_ITERATE, - type: 'iterate', - }; + // const imageCollectionIterate: IterateInvocation = { + // id: IMAGE_COLLECTION_ITERATE, + // type: 'iterate', + // }; - graph.nodes[IMAGE_COLLECTION] = imageCollection; - graph.nodes[IMAGE_COLLECTION_ITERATE] = imageCollectionIterate; + // graph.nodes[IMAGE_COLLECTION] = imageCollection; + // graph.nodes[IMAGE_COLLECTION_ITERATE] = imageCollectionIterate; - graph.edges.push({ - source: { node_id: IMAGE_COLLECTION, field: 'collection' }, - destination: { - node_id: IMAGE_COLLECTION_ITERATE, - field: 'collection', - }, - }); + // graph.edges.push({ + // source: { node_id: IMAGE_COLLECTION, field: 'collection' }, + // destination: { + // node_id: IMAGE_COLLECTION_ITERATE, + // field: 'collection', + // }, + // }); - graph.edges.push({ - source: { node_id: IMAGE_COLLECTION_ITERATE, field: 'item' }, - destination: { - node_id: IMAGE_TO_LATENTS, - field: 'image', - }, - }); - } + // graph.edges.push({ + // source: { node_id: IMAGE_COLLECTION_ITERATE, field: 'item' }, + // destination: { + // node_id: IMAGE_TO_LATENTS, + // field: 'image', + // }, + // }); + // } - addLoRAsToGraph(graph, state, LATENTS_TO_LATENTS); + // add metadata accumulator, which is only mostly populated - some fields are added later + graph.nodes[METADATA_ACCUMULATOR] = { + id: METADATA_ACCUMULATOR, + type: 'metadata_accumulator', + generation_mode: 'img2img', + cfg_scale, + height, + width, + positive_prompt: '', // set in addDynamicPromptsToGraph + negative_prompt: negativePrompt, + model, + seed: 0, // set in addDynamicPromptsToGraph + steps, + rand_device: use_cpu ? 'cpu' : 'cuda', + scheduler, + vae: undefined, // option; set in addVAEToGraph + controlnets: [], // populated in addControlNetToLinearGraph + loras: [], // populated in addLoRAsToGraph + clip_skip: clipSkip, + strength, + init_image: initialImage.imageName, + }; - // Add VAE - addVAEToGraph(graph, state); + graph.edges.push({ + source: { + node_id: METADATA_ACCUMULATOR, + field: 'metadata', + }, + destination: { + node_id: LATENTS_TO_IMAGE, + field: 'metadata', + }, + }); - // add dynamic prompts, mutating `graph` - addDynamicPromptsToGraph(graph, state); + // add LoRA support + addLoRAsToGraph(state, graph, LATENTS_TO_LATENTS); + + // optionally add custom VAE + addVAEToGraph(state, graph); + + // add dynamic prompts - also sets up core iteration and seed + addDynamicPromptsToGraph(state, graph); // add controlnet, mutating `graph` - addControlNetToLinearGraph(graph, LATENTS_TO_LATENTS, state); + addControlNetToLinearGraph(state, graph, LATENTS_TO_LATENTS); return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts index 90ddf09343..98af2a0a2f 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildLinearTextToImageGraph.ts @@ -1,8 +1,8 @@ +import { log } from 'app/logging/useLogger'; import { RootState } from 'app/store/store'; import { NonNullableGraph } from 'features/nodes/types/types'; import { initialGenerationState } from 'features/parameters/store/generationSlice'; -import { addControlNetToLinearGraph } from '../addControlNetToLinearGraph'; -import { modelIdToMainModelField } from '../modelIdToMainModelField'; +import { addControlNetToLinearGraph } from './addControlNetToLinearGraph'; import { addDynamicPromptsToGraph } from './addDynamicPromptsToGraph'; import { addLoRAsToGraph } from './addLoRAsToGraph'; import { addVAEToGraph } from './addVAEToGraph'; @@ -10,6 +10,7 @@ import { CLIP_SKIP, LATENTS_TO_IMAGE, MAIN_MODEL_LOADER, + METADATA_ACCUMULATOR, NEGATIVE_CONDITIONING, NOISE, POSITIVE_CONDITIONING, @@ -17,13 +18,15 @@ import { TEXT_TO_LATENTS, } from './constants'; +const moduleLog = log.child({ namespace: 'nodes' }); + export const buildLinearTextToImageGraph = ( state: RootState ): NonNullableGraph => { const { positivePrompt, negativePrompt, - model: currentModel, + model, cfgScale: cfg_scale, scheduler, steps, @@ -34,12 +37,15 @@ export const buildLinearTextToImageGraph = ( shouldUseNoiseSettings, } = state.generation; - const model = modelIdToMainModelField(currentModel?.id || ''); - const use_cpu = shouldUseNoiseSettings ? shouldUseCpuNoise : initialGenerationState.shouldUseCpuNoise; + if (!model) { + moduleLog.error('No model found in state'); + throw new Error('No model found in state'); + } + /** * The easiest way to build linear graphs is to do it in the node editor, then copy and paste the * full graph here as a template. Then use the parameters from app state and set friendlier node @@ -176,16 +182,49 @@ export const buildLinearTextToImageGraph = ( ], }; - addLoRAsToGraph(graph, state, TEXT_TO_LATENTS); + // add metadata accumulator, which is only mostly populated - some fields are added later + graph.nodes[METADATA_ACCUMULATOR] = { + id: METADATA_ACCUMULATOR, + type: 'metadata_accumulator', + generation_mode: 'txt2img', + cfg_scale, + height, + width, + positive_prompt: '', // set in addDynamicPromptsToGraph + negative_prompt: negativePrompt, + model, + seed: 0, // set in addDynamicPromptsToGraph + steps, + rand_device: use_cpu ? 'cpu' : 'cuda', + scheduler, + vae: undefined, // option; set in addVAEToGraph + controlnets: [], // populated in addControlNetToLinearGraph + loras: [], // populated in addLoRAsToGraph + clip_skip: clipSkip, + }; - // Add Custom VAE Support - addVAEToGraph(graph, state); + graph.edges.push({ + source: { + node_id: METADATA_ACCUMULATOR, + field: 'metadata', + }, + destination: { + node_id: LATENTS_TO_IMAGE, + field: 'metadata', + }, + }); - // add dynamic prompts, mutating `graph` - addDynamicPromptsToGraph(graph, state); + // add LoRA support + addLoRAsToGraph(state, graph, TEXT_TO_LATENTS); + + // optionally add custom VAE + addVAEToGraph(state, graph); + + // add dynamic prompts - also sets up core iteration and seed + addDynamicPromptsToGraph(state, graph); // add controlnet, mutating `graph` - addControlNetToLinearGraph(graph, TEXT_TO_LATENTS, state); + addControlNetToLinearGraph(state, graph, TEXT_TO_LATENTS); return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts index 256a623bba..92ce7715ba 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/constants.ts @@ -19,6 +19,7 @@ export const CONTROL_NET_COLLECT = 'control_net_collect'; export const DYNAMIC_PROMPT = 'dynamic_prompt'; export const IMAGE_COLLECTION = 'image_collection'; export const IMAGE_COLLECTION_ITERATE = 'image_collection_iterate'; +export const METADATA_ACCUMULATOR = 'metadata_accumulator'; // friendly graph ids export const TEXT_TO_IMAGE_GRAPH = 'text_to_image_graph'; diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts index c77fdeca5e..930aab46a3 100644 --- a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -5,17 +5,21 @@ import { InputFieldTemplate, InvocationSchemaObject, InvocationTemplate, - isInvocationSchemaObject, OutputFieldTemplate, + isInvocationSchemaObject, } from '../types/types'; import { buildInputFieldTemplate, buildOutputFieldTemplates, } from './fieldTemplateBuilders'; -const RESERVED_FIELD_NAMES = ['id', 'type', 'is_intermediate']; +const RESERVED_FIELD_NAMES = ['id', 'type', 'is_intermediate', 'core_metadata']; -const invocationDenylist = ['Graph', 'InvocationMeta']; +const invocationDenylist = [ + 'Graph', + 'InvocationMeta', + 'MetadataAccumulatorInvocation', +]; export const parseSchema = (openAPI: OpenAPIV3.Document) => { // filter out non-invocation schemas, plus some tricky invocations for now diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts index 721b44d329..bf09ca3ccb 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts @@ -162,7 +162,7 @@ export const useRecallParameters = () => { parameterNotSetToast(); return; } - dispatch(modelSelected(model?.id || '')); + dispatch(modelSelected(model)); parameterSetToast(); }, [dispatch, parameterSetToast, parameterNotSetToast] diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts index a74a2f633d..7a4f86d681 100644 --- a/invokeai/frontend/web/src/features/parameters/store/actions.ts +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -1,8 +1,10 @@ import { createAction } from '@reduxjs/toolkit'; -import { ImageDTO } from 'services/api/types'; +import { ImageDTO, MainModelField } 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/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 56728f216f..dff277ae7e 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -8,12 +8,11 @@ import { setShouldShowAdvancedOptions, } from 'features/ui/store/uiSlice'; import { clamp } from 'lodash-es'; -import { ImageDTO } from 'services/api/types'; +import { ImageDTO, MainModelField } from 'services/api/types'; import { clipSkipMap } from '../components/Parameters/Advanced/ParamClipSkip'; import { CfgScaleParam, HeightParam, - MainModelParam, NegativePromptParam, PositivePromptParam, SchedulerParam, @@ -54,7 +53,7 @@ export interface GenerationState { shouldUseSymmetry: boolean; horizontalSymmetrySteps: number; verticalSymmetrySteps: number; - model: MainModelParam | null; + model: MainModelField | null; vae: VaeModelParam | null; seamlessXAxis: boolean; seamlessYAxis: boolean; @@ -227,23 +226,17 @@ export const generationSlice = createSlice({ const { image_name, width, height } = action.payload; state.initialImage = { imageName: image_name, width, height }; }, - modelSelected: (state, action: PayloadAction) => { - const [base_model, type, name] = action.payload.split('/'); + modelChanged: (state, action: PayloadAction) => { + if (!action.payload) { + state.model = null; + } - state.model = zMainModel.parse({ - id: action.payload, - base_model, - name, - type, - }); + state.model = zMainModel.parse(action.payload); // Clamp ClipSkip Based On Selected Model const { maxClip } = clipSkipMap[state.model.base_model]; state.clipSkip = clamp(state.clipSkip, 0, maxClip); }, - modelChanged: (state, action: PayloadAction) => { - state.model = action.payload; - }, vaeSelected: (state, action: PayloadAction) => { state.vae = action.payload; }, diff --git a/invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts b/invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts index 074162e5ab..baf560eaff 100644 --- a/invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts +++ b/invokeai/frontend/web/src/features/parameters/store/parameterZodSchemas.ts @@ -135,8 +135,7 @@ export type BaseModelParam = z.infer; * TODO: Make this a dynamically generated enum? */ export const zMainModel = z.object({ - id: z.string(), - name: z.string(), + model_name: z.string(), base_model: zBaseModel, }); diff --git a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx index 6b5aa830d9..bc3da20b06 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx @@ -1,13 +1,16 @@ -import { memo, useCallback, useEffect, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIMantineSelect from 'common/components/IAIMantineSelect'; import { SelectItem } from '@mantine/core'; -import { RootState } from 'app/store/store'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { modelIdToMainModelField } from 'features/nodes/util/modelIdToMainModelField'; import { modelSelected } from 'features/parameters/store/actions'; -import { forEach, isString } from 'lodash-es'; +import { forEach } from 'lodash-es'; import { useGetMainModelsQuery } from 'services/api/endpoints/models'; export const MODEL_TYPE_MAP = { @@ -15,13 +18,17 @@ export const MODEL_TYPE_MAP = { 'sd-2': 'Stable Diffusion 2.x', }; +const selector = createSelector( + stateSelector, + (state) => ({ currentModel: state.generation.model }), + defaultSelectorOptions +); + const ModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const currentModel = useAppSelector( - (state: RootState) => state.generation.model - ); + const { currentModel } = useAppSelector(selector); const { data: mainModels, isLoading } = useGetMainModelsQuery(); @@ -39,7 +46,7 @@ const ModelSelect = () => { data.push({ value: id, - label: model.name, + label: model.model_name, group: MODEL_TYPE_MAP[model.base_model], }); }); @@ -48,7 +55,10 @@ const ModelSelect = () => { }, [mainModels]); const selectedModel = useMemo( - () => mainModels?.entities[currentModel?.id || ''], + () => + mainModels?.entities[ + `${currentModel?.base_model}/main/${currentModel?.model_name}` + ], [mainModels?.entities, currentModel] ); @@ -57,31 +67,13 @@ const ModelSelect = () => { if (!v) { return; } - dispatch(modelSelected(v)); + + const modelField = modelIdToMainModelField(v); + dispatch(modelSelected(modelField)); }, [dispatch] ); - useEffect(() => { - if (isLoading) { - // return early here to avoid resetting model selection before we've loaded the available models - return; - } - - if (selectedModel && mainModels?.ids.includes(selectedModel?.id)) { - // the selected model is an available model, no need to change it - return; - } - - const firstModel = mainModels?.ids[0]; - - if (!isString(firstModel)) { - return; - } - - handleChangeModel(firstModel); - }, [handleChangeModel, isLoading, mainModels?.ids, selectedModel]); - return isLoading ? ( { tooltip={selectedModel?.description} label={t('modelManager.model')} value={selectedModel?.id} - placeholder={data.length > 0 ? 'Select a model' : 'No models detected!'} + placeholder={data.length > 0 ? 'Select a model' : 'No models available'} data={data} error={data.length === 0} + disabled={data.length === 0} onChange={handleChangeModel} /> ); diff --git a/invokeai/frontend/web/src/features/system/components/VAESelect.tsx b/invokeai/frontend/web/src/features/system/components/VAESelect.tsx index 50e82b0699..bed1b72123 100644 --- a/invokeai/frontend/web/src/features/system/components/VAESelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/VAESelect.tsx @@ -50,7 +50,7 @@ const VAESelect = () => { data.push({ value: id, - label: model.name, + label: model.model_name, group: MODEL_TYPE_MAP[model.base_model], disabled, tooltip: disabled diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index 5090fc4fc1..d49ab5f131 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -1,13 +1,22 @@ import { ApiFullTagDescription, api } from '..'; +import { components } from '../schema'; import { ImageDTO } from '../types'; +/** + * This is an unsafe type; the object inside is not guaranteed to be valid. + */ +export type UnsafeImageMetadata = { + metadata: components['schemas']['CoreMetadata']; + graph: NonNullable; +}; + export const imagesApi = api.injectEndpoints({ endpoints: (build) => ({ /** * Image Queries */ getImageDTO: build.query({ - query: (image_name) => ({ url: `images/${image_name}/metadata` }), + query: (image_name) => ({ url: `images/${image_name}` }), providesTags: (result, error, arg) => { const tags: ApiFullTagDescription[] = [{ type: 'Image', id: arg }]; if (result?.board_id) { @@ -17,7 +26,17 @@ export const imagesApi = api.injectEndpoints({ }, keepUnusedDataFor: 86400, // 24 hours }), + getImageMetadata: build.query({ + query: (image_name) => ({ url: `images/${image_name}/metadata` }), + providesTags: (result, error, arg) => { + const tags: ApiFullTagDescription[] = [ + { type: 'ImageMetadata', id: arg }, + ]; + return tags; + }, + keepUnusedDataFor: 86400, // 24 hours + }), }), }); -export const { useGetImageDTOQuery } = imagesApi; +export const { useGetImageDTOQuery, useGetImageMetadataQuery } = imagesApi; diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index a9a914f0f2..ee1a2e7986 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -33,25 +33,28 @@ type AnyModelConfigEntity = | VaeModelConfigEntity; const mainModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.name.localeCompare(b.name), + sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), }); const loraModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.name.localeCompare(b.name), + sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), }); const controlNetModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.name.localeCompare(b.name), + sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), }); const textualInversionModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.name.localeCompare(b.name), + sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), }); const vaeModelsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.name.localeCompare(b.name), + sortComparer: (a, b) => a.model_name.localeCompare(b.model_name), }); -export const getModelId = ({ base_model, type, name }: AnyModelConfig) => - `${base_model}/${type}/${name}`; +export const getModelId = ({ + base_model, + model_type, + model_name, +}: AnyModelConfig) => `${base_model}/${model_type}/${model_name}`; const createModelEntities = ( models: AnyModelConfig[] diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index f91c3b90fd..33eb7c35c6 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -1,3 +1,4 @@ +import { FullTagDescription } from '@reduxjs/toolkit/dist/query/endpointDefinitions'; import { BaseQueryFn, FetchArgs, @@ -5,10 +6,9 @@ import { createApi, fetchBaseQuery, } from '@reduxjs/toolkit/query/react'; -import { FullTagDescription } from '@reduxjs/toolkit/dist/query/endpointDefinitions'; import { $authToken, $baseUrl } from 'services/api/client'; -export const tagTypes = ['Board', 'Image', 'Model']; +export const tagTypes = ['Board', 'Image', 'ImageMetadata', 'Model']; export type ApiFullTagDescription = FullTagDescription< (typeof tagTypes)[number] >; diff --git a/invokeai/frontend/web/src/services/api/thunks/image.ts b/invokeai/frontend/web/src/services/api/thunks/image.ts index 71eedb0327..f20eee9420 100644 --- a/invokeai/frontend/web/src/services/api/thunks/image.ts +++ b/invokeai/frontend/web/src/services/api/thunks/image.ts @@ -1,9 +1,9 @@ -import queryString from 'query-string'; import { createAppAsyncThunk } from 'app/store/storeUtils'; import { selectImagesAll } from 'features/gallery/store/gallerySlice'; import { size } from 'lodash-es'; -import { paths } from 'services/api/schema'; +import queryString from 'query-string'; import { $client } from 'services/api/client'; +import { paths } from 'services/api/schema'; type GetImageUrlsArg = paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path']; @@ -24,7 +24,7 @@ export const imageUrlsReceived = createAppAsyncThunk< GetImageUrlsResponse, GetImageUrlsArg, GetImageUrlsThunkConfig ->('api/imageUrlsReceived', async (arg, { rejectWithValue }) => { +>('thunkApi/imageUrlsReceived', async (arg, { rejectWithValue }) => { const { image_name } = arg; const { get } = $client.get(); const { data, error, response } = await get( @@ -46,10 +46,10 @@ export const imageUrlsReceived = createAppAsyncThunk< }); type GetImageMetadataArg = - paths['/api/v1/images/{image_name}/metadata']['get']['parameters']['path']; + paths['/api/v1/images/{image_name}']['get']['parameters']['path']; type GetImageMetadataResponse = - paths['/api/v1/images/{image_name}/metadata']['get']['responses']['200']['content']['application/json']; + paths['/api/v1/images/{image_name}']['get']['responses']['200']['content']['application/json']; type GetImageMetadataThunkConfig = { rejectValue: { @@ -58,21 +58,18 @@ type GetImageMetadataThunkConfig = { }; }; -export const imageMetadataReceived = createAppAsyncThunk< +export const imageDTOReceived = createAppAsyncThunk< GetImageMetadataResponse, GetImageMetadataArg, GetImageMetadataThunkConfig ->('api/imageMetadataReceived', async (arg, { rejectWithValue }) => { +>('thunkApi/imageMetadataReceived', async (arg, { rejectWithValue }) => { const { image_name } = arg; const { get } = $client.get(); - const { data, error, response } = await get( - '/api/v1/images/{image_name}/metadata', - { - params: { - path: { image_name }, - }, - } - ); + const { data, error, response } = await get('/api/v1/images/{image_name}', { + params: { + path: { image_name }, + }, + }); if (error) { return rejectWithValue({ arg, error }); @@ -148,7 +145,7 @@ export const imageUploaded = createAppAsyncThunk< UploadImageResponse, UploadImageArg, UploadImageThunkConfig ->('api/imageUploaded', async (arg, { rejectWithValue }) => { +>('thunkApi/imageUploaded', async (arg, { rejectWithValue }) => { const { postUploadAction, file, @@ -199,7 +196,7 @@ export const imageDeleted = createAppAsyncThunk< DeleteImageResponse, DeleteImageArg, DeleteImageThunkConfig ->('api/imageDeleted', async (arg, { rejectWithValue }) => { +>('thunkApi/imageDeleted', async (arg, { rejectWithValue }) => { const { image_name } = arg; const { del } = $client.get(); const { data, error, response } = await del('/api/v1/images/{image_name}', { @@ -235,7 +232,7 @@ export const imageUpdated = createAppAsyncThunk< UpdateImageResponse, UpdateImageArg, UpdateImageThunkConfig ->('api/imageUpdated', async (arg, { rejectWithValue }) => { +>('thunkApi/imageUpdated', async (arg, { rejectWithValue }) => { const { image_name, image_category, is_intermediate, session_id } = arg; const { patch } = $client.get(); const { data, error, response } = await patch('/api/v1/images/{image_name}', { @@ -284,46 +281,49 @@ export const receivedPageOfImages = createAppAsyncThunk< ListImagesResponse, ListImagesArg, ListImagesThunkConfig ->('api/receivedPageOfImages', async (arg, { getState, rejectWithValue }) => { - const { get } = $client.get(); +>( + 'thunkApi/receivedPageOfImages', + async (arg, { getState, rejectWithValue }) => { + const { get } = $client.get(); - const state = getState(); - const { categories, selectedBoardId } = state.gallery; + const state = getState(); + const { categories, selectedBoardId } = state.gallery; - const images = selectImagesAll(state).filter((i) => { - const isInCategory = categories.includes(i.image_category); - const isInSelectedBoard = selectedBoardId - ? i.board_id === selectedBoardId - : true; - return isInCategory && isInSelectedBoard; - }); + const images = selectImagesAll(state).filter((i) => { + const isInCategory = categories.includes(i.image_category); + const isInSelectedBoard = selectedBoardId + ? i.board_id === selectedBoardId + : true; + return isInCategory && isInSelectedBoard; + }); - let query: ListImagesArg = {}; + let query: ListImagesArg = {}; - if (size(arg)) { - query = { - ...DEFAULT_IMAGES_LISTED_ARG, - offset: images.length, - ...arg, - }; - } else { - query = { - ...DEFAULT_IMAGES_LISTED_ARG, - categories, - offset: images.length, - }; + if (size(arg)) { + query = { + ...DEFAULT_IMAGES_LISTED_ARG, + offset: images.length, + ...arg, + }; + } else { + query = { + ...DEFAULT_IMAGES_LISTED_ARG, + categories, + offset: images.length, + }; + } + + const { data, error, response } = await get('/api/v1/images/', { + params: { + query, + }, + querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }), + }); + + if (error) { + return rejectWithValue({ arg, error }); + } + + return data; } - - const { data, error, response } = await get('/api/v1/images/', { - params: { - query, - }, - querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }), - }); - - if (error) { - return rejectWithValue({ arg, error }); - } - - return data; -}); +); diff --git a/invokeai/frontend/web/src/services/api/types.d.ts b/invokeai/frontend/web/src/services/api/types.d.ts index ab8214a903..4178b2e196 100644 --- a/invokeai/frontend/web/src/services/api/types.d.ts +++ b/invokeai/frontend/web/src/services/api/types.d.ts @@ -19,6 +19,7 @@ export type ImageChanges = components['schemas']['ImageRecordChanges']; export type ImageCategory = components['schemas']['ImageCategory']; export type ResourceOrigin = components['schemas']['ResourceOrigin']; export type ImageField = components['schemas']['ImageField']; +export type ImageMetadata = components['schemas']['ImageMetadata']; export type OffsetPaginatedResults_BoardDTO_ = components['schemas']['OffsetPaginatedResults_BoardDTO_']; export type OffsetPaginatedResults_ImageDTO_ = @@ -31,6 +32,7 @@ export type MainModelField = components['schemas']['MainModelField']; export type VAEModelField = components['schemas']['VAEModelField']; export type LoRAModelField = components['schemas']['LoRAModelField']; export type ModelsList = components['schemas']['ModelsList']; +export type ControlField = components['schemas']['ControlField']; // Model Configs export type LoRAModelConfig = components['schemas']['LoRAModelConfig']; @@ -107,6 +109,9 @@ export type MainModelLoaderInvocation = TypeReq< export type LoraLoaderInvocation = TypeReq< components['schemas']['LoraLoaderInvocation'] >; +export type MetadataAccumulatorInvocation = TypeReq< + components['schemas']['MetadataAccumulatorInvocation'] +>; // ControlNet Nodes export type ControlNetInvocation = TypeReq<