From 5f498e10bd69247dbccc2a938d369f8fc5633e0f Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 22 Apr 2023 13:10:20 +1000 Subject: [PATCH] Partial migration of UI to nodes API (#3195) * feat(ui): add axios client generator and simple example * fix(ui): update client & nodes test code w/ new Edge type * chore(ui): organize generated files * chore(ui): update .eslintignore, .prettierignore * chore(ui): update openapi.json * feat(backend): fixes for nodes/generator * feat(ui): generate object args for api client * feat(ui): more nodes api prototyping * feat(ui): nodes cancel * chore(ui): regenerate api client * fix(ui): disable OG web server socket connection * fix(ui): fix scrollbar styles typing and prop just noticed the typo, and made the types stronger. * feat(ui): add socketio types * feat(ui): wip nodes - extract api client method arg types instead of manually declaring them - update example to display images - general tidy up * start building out node translations from frontend state and add notes about missing features * use reference to sampler_name * use reference to sampler_name * add optional apiUrl prop * feat(ui): start hooking up dynamic txt2img node generation, create middleware for session invocation * feat(ui): write separate nodes socket layer, txt2img generating and rendering w single node * feat(ui): img2img implementation * feat(ui): get intermediate images working but types are stubbed out * chore(ui): add support for package mode * feat(ui): add nodes mode script * feat(ui): handle random seeds * fix(ui): fix middleware types * feat(ui): add rtk action type guard * feat(ui): disable NodeAPITest This was polluting the network/socket logs. * feat(ui): fix parameters panel border color This commit should be elsewhere but I don't want to break my flow * feat(ui): make thunk types more consistent * feat(ui): add type guards for outputs * feat(ui): load images on socket connect Rudimentary * chore(ui): bump redux-toolkit * docs(ui): update readme * chore(ui): regenerate api client * chore(ui): add typescript as dev dependency I am having trouble with TS versions after vscode updated and now uses TS 5. `madge` has installed 3.9.10 and for whatever reason my vscode wants to use that. Manually specifying 4.9.5 and then setting vscode to use that as the workspace TS fixes the issue. * feat(ui): begin migrating gallery to nodes Along the way, migrate to use RTK `createEntityAdapter` for gallery images, and separate `results` and `uploads` into separate slices. Much cleaner this way. * feat(ui): clean up & comment results slice * fix(ui): separate thunk for initial gallery load so it properly gets index 0 * feat(ui): POST upload working * fix(ui): restore removed type * feat(ui): patch api generation for headers access * chore(ui): regenerate api * feat(ui): wip gallery migration * feat(ui): wip gallery migration * chore(ui): regenerate api * feat(ui): wip refactor socket events * feat(ui): disable panels based on app props * feat(ui): invert logic to be disabled * disable panels when app mounts * feat(ui): add support to disableTabs * docs(ui): organise and update docs * lang(ui): add toast strings * feat(ui): wip events, comments, and general refactoring * feat(ui): add optional token for auth * feat(ui): export StatusIndicator and ModelSelect for header use * feat(ui) working on making socket URL dynamic * feat(ui): dynamic middleware loading * feat(ui): prep for socket jwt * feat(ui): migrate cancelation also updated action names to be event-like instead of declaration-like sorry, i was scattered and this commit has a lot of unrelated stuff in it. * fix(ui): fix img2img type * chore(ui): regenerate api client * feat(ui): improve InvocationCompleteEvent types * feat(ui): increase StatusIndicator font size * fix(ui): fix middleware order for multi-node graphs * feat(ui): add exampleGraphs object w/ iterations example * feat(ui): generate iterations graph * feat(ui): update ModelSelect for nodes API * feat(ui): add hi-res functionality for txt2img generations * feat(ui): "subscribe" to particular nodes feels like a dirty hack but oh well it works * feat(ui): first steps to node editor ui * fix(ui): disable event subscription it is not fully baked just yet * feat(ui): wip node editor * feat(ui): remove extraneous field types * feat(ui): nodes before deleting stuff * feat(ui): cleanup nodes ui stuff * feat(ui): hook up nodes to redux * fix(ui): fix handle * fix(ui): add basic node edges & connection validation * feat(ui): add connection validation styling * feat(ui): increase edge width * feat(ui): it blends * feat(ui): wip model handling and graph topology validation * feat(ui): validation connections w/ graphlib * docs(ui): update nodes doc * feat(ui): wip node editor * chore(ui): rebuild api, update types * add redux-dynamic-middlewares as a dependency * feat(ui): add url host transformation * feat(ui): handle already-connected fields * feat(ui): rewrite SqliteItemStore in sqlalchemy * fix(ui): fix sqlalchemy dynamic model instantiation * feat(ui, nodes): metadata wip * feat(ui, nodes): models * feat(ui, nodes): more metadata wip * feat(ui): wip range/iterate * fix(nodes): fix sqlite typing * feat(ui): export new type for invoke component * tests(nodes): fix test instantiation of ImageField * feat(nodes): fix LoadImageInvocation * feat(nodes): add `title` ui hint * feat(nodes): make ImageField attrs optional * feat(ui): wip nodes etc * feat(nodes): roll back sqlalchemy * fix(nodes): partially address feedback * fix(backend): roll back changes to pngwriter * feat(nodes): wip address metadata feedback * feat(nodes): add seeded rng to RandomRange * feat(nodes): address feedback * feat(nodes): move GET images error handling to DiskImageStorage * feat(nodes): move GET images error handling to DiskImageStorage * fix(nodes): fix image output schema customization * feat(ui): img2img/txt2img -> linear - remove txt2img and img2img tabs - add linear tab - add initial image selection to linear parameters accordion * feat(ui): tidy graph builders * feat(ui): tidy misc * feat(ui): improve invocation union types * feat(ui): wip metadata viewer recall * feat(ui): move fonts to normal deps * feat(nodes): fix broken upload * feat(nodes): add metadata module + tests, thumbnails - `MetadataModule` is stateless and needed in places where the `InvocationContext` is not available, so have not made it a `service` - Handles loading/parsing/building metadata, and creating png info objects - added tests for MetadataModule - Lifted thumbnail stuff to util * fix(nodes): revert change to RandomRangeInvocation * feat(nodes): address feedback - make metadata a service - rip out pydantic validation, implement metadata parsing as simple functions - update tests - address other minor feedback items * fix(nodes): fix other tests * fix(nodes): add metadata service to cli * fix(nodes): fix latents/image field parsing * feat(nodes): customise LatentsField schema * feat(nodes): move metadata parsing to frontend * fix(nodes): fix metadata test --------- Co-authored-by: maryhipp Co-authored-by: Mary Hipp --- invokeai/app/api/dependencies.py | 7 +- invokeai/app/api/models/images.py | 24 +- invokeai/app/api/routers/images.py | 104 +++- invokeai/app/cli_app.py | 7 +- invokeai/app/invocations/baseinvocation.py | 2 +- invokeai/app/invocations/collections.py | 32 +- invokeai/app/invocations/cv.py | 15 +- invokeai/app/invocations/generate.py | 176 +++--- invokeai/app/invocations/image.py | 175 ++++-- invokeai/app/invocations/latent.py | 56 +- invokeai/app/invocations/reconstruct.py | 18 +- invokeai/app/invocations/upscale.py | 17 +- .../util/{get_model.py => choose_model.py} | 11 +- invokeai/app/models/image.py | 15 +- invokeai/app/models/metadata.py | 11 - invokeai/app/services/events.py | 45 +- invokeai/app/services/image_storage.py | 106 +++- invokeai/app/services/invocation_services.py | 4 + invokeai/app/services/metadata.py | 96 ++++ invokeai/app/services/processor.py | 12 +- invokeai/app/services/sqlite.py | 31 +- invokeai/app/util/misc.py | 5 + invokeai/app/util/save_thumbnail.py | 25 - invokeai/app/util/step_callback.py | 62 +- invokeai/app/util/thumbnails.py | 15 + invokeai/frontend/web/.eslintignore | 2 + invokeai/frontend/web/.prettierignore | 4 + invokeai/frontend/web/docs/API_CLIENT.md | 87 +++ invokeai/frontend/web/docs/EVENTS.md | 21 + invokeai/frontend/web/docs/NODE_EDITOR.md | 17 + invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md | 29 + invokeai/frontend/web/{ => docs}/README.md | 10 +- invokeai/frontend/web/index.d.ts | 21 +- invokeai/frontend/web/package.json | 16 +- invokeai/frontend/web/public/locales/en.json | 5 + invokeai/frontend/web/src/app/App.tsx | 30 +- invokeai/frontend/web/src/app/invokeai.d.ts | 22 +- .../src/app/selectors/readinessSelector.ts | 3 +- .../frontend/web/src/app/socketio/actions.ts | 10 +- .../frontend/web/src/app/socketio/emitters.ts | 6 +- .../web/src/app/socketio/listeners.ts | 27 +- .../web/src/app/socketio/middleware.ts | 2 + invokeai/frontend/web/src/app/store.ts | 88 +-- invokeai/frontend/web/src/app/storeUtils.ts | 8 + .../web/src/common/components/IAISlider.tsx | 17 +- .../common/components/ImageToImageOverlay.tsx | 79 +++ .../src/common/components/ImageUploader.tsx | 6 +- .../components/SelectImagePlaceholder.tsx | 12 + .../components/WorkInProgress/NodesWIP.tsx | 185 +++++- .../WorkInProgress/WorkInProgress.tsx | 2 + .../web/src/common/util/_parseMetadataZod.ts | 119 ++++ .../web/src/common/util/getTimestamp.ts | 6 + .../frontend/web/src/common/util/getUrl.ts | 28 + .../web/src/common/util/parseMetadata.ts | 169 ++++++ invokeai/frontend/web/src/component.tsx | 53 +- invokeai/frontend/web/src/exports.tsx | 4 + .../components/IAICanvasIntermediateImage.tsx | 7 +- .../components/IAICanvasObjectRenderer.tsx | 9 +- .../components/IAICanvasStagingArea.tsx | 8 +- .../canvas/store/canvasPersistBlacklist.ts | 14 + .../src/features/canvas/store/canvasSlice.ts | 4 +- .../src/features/canvas/store/canvasTypes.ts | 4 +- .../store/thunks/mergeAndUploadCanvas.ts | 2 +- .../components/CurrentImageButtons.tsx | 133 +++-- .../components/CurrentImageDisplay.tsx | 11 +- .../components/CurrentImagePreview.tsx | 89 ++- .../gallery/components/DeleteImageModal.tsx | 2 +- .../gallery/components/HoverableImage.tsx | 78 +-- .../components/ImageGalleryContent.tsx | 62 +- .../gallery/components/ImageGalleryPanel.tsx | 9 +- .../ImageMetadataViewer.tsx | 299 +++------- .../OLD_ImageMetadataViewer.tsx | 470 +++++++++++++++ .../gallery/hooks/useGetImageByName.ts | 35 ++ .../gallery/store/galleryPersistBlacklist.ts | 17 + .../gallery/store/gallerySelectors.ts | 25 + .../features/gallery/store/gallerySlice.ts | 63 ++- .../gallery/store/resultsPersistBlacklist.ts | 12 + .../features/gallery/store/resultsSlice.ts | 139 +++++ .../gallery/store/thunks/uploadImage.ts | 54 -- .../gallery/store/uploadsPersistBlacklist.ts | 12 + .../features/gallery/store/uploadsSlice.ts | 87 +++ .../lightbox/components/ReactPanZoomImage.tsx | 6 +- .../store/lightboxPersistBlacklist.ts | 10 + .../features/nodes/components/AddNodeMenu.tsx | 63 +++ .../features/nodes/components/FieldHandle.tsx | 69 +++ .../nodes/components/FieldTypeLegend.tsx | 18 + .../src/features/nodes/components/Flow.tsx | 104 ++++ .../nodes/components/InputFieldComponent.tsx | 107 ++++ .../nodes/components/InvocationComponent.tsx | 243 ++++++++ .../features/nodes/components/NodeEditor.tsx | 46 ++ .../components/fields/ArrayInputField.tsx.tsx | 14 + .../fields/BooleanInputFieldComponent.tsx | 31 + .../fields/EnumInputFieldComponent.tsx | 35 ++ .../fields/ImageInputFieldComponent.tsx | 64 +++ .../fields/LatentsInputFieldComponent.tsx | 13 + .../fields/ModelInputFieldComponent.tsx | 57 ++ .../fields/NumberInputFieldComponent.tsx | 41 ++ .../fields/StringInputFieldComponent.tsx | 29 + .../features/nodes/components/fields/types.ts | 10 + .../features/nodes/examples/iterationGraph.ts | 52 ++ .../nodes/hooks/useBuildInvocation.ts | 78 +++ .../nodes/hooks/useInvocationTemplate.ts | 16 + .../nodes/hooks/useIsValidConnection.ts | 93 +++ .../nodes/store/nodesPersistBlacklist.ts | 10 + .../src/features/nodes/store/nodesSlice.ts | 103 ++++ .../selectors/invocationTemplatesSelector.ts | 7 + .../web/src/features/nodes/types/constants.ts | 69 +++ .../src/features/nodes/types/typeGuards.ts | 9 + .../web/src/features/nodes/types/types.ts | 296 ++++++++++ .../nodes/util/fieldTemplateBuilders.ts | 336 +++++++++++ .../features/nodes/util/fieldValueBuilders.ts | 57 ++ .../util/linearGraphBuilder/buildEdges.ts | 39 ++ .../buildImageToImageNode.ts | 99 ++++ .../linearGraphBuilder/buildIterateNode.ts | 13 + .../linearGraphBuilder/buildLinearGraph.ts | 39 ++ .../util/linearGraphBuilder/buildRangeNode.ts | 26 + .../buildTextToImageNode.ts | 42 ++ .../util/nodesGraphBuilder/buildNodesGraph.ts | 77 +++ .../src/features/nodes/util/parseSchema.ts | 120 ++++ .../AccordionItems/InvokeAccordionItem.tsx | 21 +- .../Canvas/InfillAndScalingSettings.tsx | 12 +- .../FaceRestore/CodeformerFidelity.tsx | 4 +- .../FaceRestore/FaceRestoreStrength.tsx | 4 +- .../ImageToImage/ImageFit.tsx | 5 + .../ImageToImage/ImageToImageSettings.tsx | 4 +- .../ImageToImage/ImageToImageStrength.tsx | 4 + .../ImageToImage/ImageToImageToggle.tsx | 24 + .../ImageToImage/InitialImagePreview.tsx | 155 +++++ .../Output/HiresSettings.tsx | 4 +- .../Upscale/UpscaleDenoisingStrength.tsx | 4 +- .../Upscale/UpscaleStrength.tsx | 4 +- .../Variations/VariationAmount.tsx | 4 +- .../components/MainParameters/MainHeight.tsx | 4 +- .../components/MainParameters/MainWidth.tsx | 4 +- .../components/ParametersAccordion.tsx | 111 ++-- .../ProcessButtons/CancelButton.tsx | 139 +++-- .../ProcessButtons/InvokeButton.tsx | 4 +- .../store/generationPersistBlacklist.ts | 10 + .../parameters/store/generationSelectors.ts | 18 + .../parameters/store/generationSlice.ts | 27 +- .../store/postprocessingPersistBlacklist.ts | 10 + .../system/components/ModelSelect.tsx | 41 +- .../system/components/StatusIndicator.tsx | 2 +- .../features/system/hooks/useToastWatcher.ts | 17 +- .../features/system/store/modelSelectors.ts | 5 + .../src/features/system/store/modelSlice.ts | 80 +++ .../system/store/modelsPersistBlacklist.ts | 10 + .../system/store/systemPersistsBlacklist.ts | 26 + .../src/features/system/store/systemSlice.ts | 248 ++++++++ .../ui/components/FloatingGalleryButton.tsx | 4 +- .../FloatingParametersPanelButtons.tsx | 2 +- .../src/features/ui/components/InvokeTabs.tsx | 103 ++-- .../features/ui/components/InvokeWorkarea.tsx | 6 +- .../ui/components/ParametersPanel.tsx | 1 - .../tabs/ImageToImage/ImageToImageContent.tsx | 50 -- .../ImageToImage/ImageToImageParameters.tsx | 81 --- .../ImageToImage/ImageToImageWorkarea.tsx | 11 - .../tabs/ImageToImage/InitImagePreview.tsx | 85 --- .../LinearContent.tsx} | 4 +- .../LinearParameters.tsx} | 42 +- .../components/tabs/Linear/LinearWorkarea.tsx | 11 + .../tabs/TextToImage/TextToImageWorkarea.tsx | 11 - .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 16 +- .../src/features/ui/store/extraReducers.ts | 13 + .../web/src/features/ui/store/tabMap.ts | 9 +- .../features/ui/store/uiPersistBlacklist.ts | 10 + .../web/src/features/ui/store/uiSlice.ts | 30 +- .../web/src/features/ui/store/uiTypes.ts | 6 + .../web/src/services/api/core/ApiError.ts | 24 + .../services/api/core/ApiRequestOptions.ts | 16 + .../web/src/services/api/core/ApiResult.ts | 10 + .../services/api/core/CancelablePromise.ts | 128 +++++ .../web/src/services/api/core/OpenAPI.ts | 31 + .../web/src/services/api/core/request.ts | 351 ++++++++++++ .../frontend/web/src/services/api/index.ts | 133 +++++ .../src/services/api/models/AddInvocation.ts | 23 + .../src/services/api/models/BlurInvocation.ts | 29 + .../services/api/models/Body_upload_image.ts | 8 + .../src/services/api/models/CkptModelInfo.ts | 32 ++ .../services/api/models/CollectInvocation.ts | 23 + .../api/models/CollectInvocationOutput.ts | 15 + .../services/api/models/CreateModelRequest.ts | 18 + .../api/models/CropImageInvocation.ts | 37 ++ .../api/models/CvInpaintInvocation.ts | 25 + .../services/api/models/DiffusersModelInfo.ts | 26 + .../services/api/models/DivideInvocation.ts | 23 + .../web/src/services/api/models/Edge.ts | 17 + .../src/services/api/models/EdgeConnection.ts | 15 + .../web/src/services/api/models/Graph.ts | 49 ++ .../api/models/GraphExecutionState.ts | 58 ++ .../services/api/models/GraphInvocation.ts | 22 + .../api/models/GraphInvocationOutput.ts | 11 + .../api/models/HTTPValidationError.ts | 10 + .../web/src/services/api/models/ImageField.ts | 20 + .../src/services/api/models/ImageOutput.ts | 25 + .../src/services/api/models/ImageResponse.ts | 33 ++ .../api/models/ImageResponseMetadata.ts | 28 + .../api/models/ImageToImageInvocation.ts | 69 +++ .../web/src/services/api/models/ImageType.ts | 8 + .../services/api/models/InpaintInvocation.ts | 77 +++ .../api/models/IntCollectionOutput.ts | 15 + .../web/src/services/api/models/IntOutput.ts | 15 + .../api/models/InverseLerpInvocation.ts | 29 + .../services/api/models/InvokeAIMetadata.ts | 12 + .../services/api/models/IterateInvocation.ts | 24 + .../api/models/IterateInvocationOutput.ts | 15 + .../src/services/api/models/LatentsField.ts | 14 + .../src/services/api/models/LatentsOutput.ts | 17 + .../api/models/LatentsToImageInvocation.ts | 25 + .../api/models/LatentsToLatentsInvocation.ts | 73 +++ .../src/services/api/models/LerpInvocation.ts | 29 + .../api/models/LoadImageInvocation.ts | 25 + .../api/models/MaskFromAlphaInvocation.ts | 25 + .../web/src/services/api/models/MaskOutput.ts | 17 + .../services/api/models/MetadataImageField.ts | 11 + .../api/models/MetadataLatentsField.ts | 8 + .../web/src/services/api/models/ModelsList.ts | 11 + .../services/api/models/MultiplyInvocation.ts | 23 + .../services/api/models/NoiseInvocation.ts | 27 + .../src/services/api/models/NoiseOutput.ts | 17 + .../PaginatedResults_GraphExecutionState_.ts | 32 ++ .../models/PaginatedResults_ImageResponse_.ts | 32 ++ .../services/api/models/ParamIntInvocation.ts | 19 + .../api/models/PasteImageInvocation.ts | 37 ++ .../src/services/api/models/PromptOutput.ts | 15 + .../api/models/RandomRangeInvocation.ts | 31 + .../services/api/models/RangeInvocation.ts | 27 + .../api/models/RestoreFaceInvocation.ts | 25 + .../api/models/ShowImageInvocation.ts | 21 + .../services/api/models/SubtractInvocation.ts | 23 + .../api/models/TextToImageInvocation.ts | 55 ++ .../api/models/TextToLatentsInvocation.ts | 65 +++ .../services/api/models/UpscaleInvocation.ts | 29 + .../web/src/services/api/models/VaeRepo.ts | 19 + .../services/api/models/ValidationError.ts | 10 + .../services/api/schemas/$AddInvocation.ts | 24 + .../services/api/schemas/$BlurInvocation.ts | 30 + .../api/schemas/$Body_upload_image.ts | 12 + .../services/api/schemas/$CkptModelInfo.ts | 37 ++ .../api/schemas/$CollectInvocation.ts | 28 + .../api/schemas/$CollectInvocationOutput.ts | 20 + .../api/schemas/$CreateModelRequest.ts | 22 + .../api/schemas/$CropImageInvocation.ts | 39 ++ .../api/schemas/$CvInpaintInvocation.ts | 30 + .../api/schemas/$DiffusersModelInfo.ts | 29 + .../services/api/schemas/$DivideInvocation.ts | 24 + .../web/src/services/api/schemas/$Edge.ts | 23 + .../services/api/schemas/$EdgeConnection.ts | 17 + .../web/src/services/api/schemas/$Graph.ts | 80 +++ .../api/schemas/$GraphExecutionState.ts | 95 ++++ .../services/api/schemas/$GraphInvocation.ts | 24 + .../api/schemas/$GraphInvocationOutput.ts | 12 + .../api/schemas/$HTTPValidationError.ts | 13 + .../src/services/api/schemas/$ImageField.ts | 21 + .../src/services/api/schemas/$ImageOutput.ts | 30 + .../services/api/schemas/$ImageResponse.ts | 39 ++ .../api/schemas/$ImageResponseMetadata.ts | 30 + .../api/schemas/$ImageToImageInvocation.ts | 75 +++ .../src/services/api/schemas/$ImageType.ts | 6 + .../api/schemas/$InpaintInvocation.ts | 87 +++ .../api/schemas/$IntCollectionOutput.ts | 17 + .../src/services/api/schemas/$IntOutput.ts | 15 + .../api/schemas/$InverseLerpInvocation.ts | 33 ++ .../services/api/schemas/$InvokeAIMetadata.ts | 29 + .../api/schemas/$IterateInvocation.ts | 28 + .../api/schemas/$IterateInvocationOutput.ts | 18 + .../src/services/api/schemas/$LatentsField.ts | 13 + .../services/api/schemas/$LatentsOutput.ts | 18 + .../api/schemas/$LatentsToImageInvocation.ts | 27 + .../schemas/$LatentsToLatentsInvocation.ts | 81 +++ .../services/api/schemas/$LerpInvocation.ts | 33 ++ .../api/schemas/$LoadImageInvocation.ts | 29 + .../api/schemas/$MaskFromAlphaInvocation.ts | 27 + .../src/services/api/schemas/$MaskOutput.ts | 20 + .../api/schemas/$MetadataImageField.ts | 15 + .../api/schemas/$MetadataLatentsField.ts | 11 + .../src/services/api/schemas/$ModelsList.ts | 19 + .../api/schemas/$MultiplyInvocation.ts | 24 + .../services/api/schemas/$NoiseInvocation.ts | 31 + .../src/services/api/schemas/$NoiseOutput.ts | 18 + .../$PaginatedResults_GraphExecutionState_.ts | 35 ++ .../$PaginatedResults_ImageResponse_.ts | 35 ++ .../api/schemas/$ParamIntInvocation.ts | 20 + .../api/schemas/$PasteImageInvocation.ts | 45 ++ .../src/services/api/schemas/$PromptOutput.ts | 17 + .../api/schemas/$RandomRangeInvocation.ts | 33 ++ .../services/api/schemas/$RangeInvocation.ts | 28 + .../api/schemas/$RestoreFaceInvocation.ts | 28 + .../api/schemas/$ShowImageInvocation.ts | 23 + .../api/schemas/$SubtractInvocation.ts | 24 + .../api/schemas/$TextToImageInvocation.ts | 59 ++ .../api/schemas/$TextToLatentsInvocation.ts | 70 +++ .../api/schemas/$UpscaleInvocation.ts | 31 + .../web/src/services/api/schemas/$VaeRepo.ts | 20 + .../services/api/schemas/$ValidationError.ts | 27 + .../services/api/services/ImagesService.ts | 139 +++++ .../services/api/services/ModelsService.ts | 72 +++ .../services/api/services/SessionsService.ts | 381 +++++++++++++ .../web/src/services/events/actions.ts | 50 ++ .../web/src/services/events/middleware.ts | 221 ++++++++ .../frontend/web/src/services/events/types.ts | 109 ++++ .../web/src/services/fixtures/openapi.json | 1 + .../web/src/services/fixtures/request.ts | 351 ++++++++++++ .../web/src/services/thunks/gallery.ts | 30 + .../frontend/web/src/services/thunks/image.ts | 36 ++ .../frontend/web/src/services/thunks/model.ts | 24 + .../web/src/services/thunks/schema.ts | 14 + .../web/src/services/thunks/session.ts | 132 +++++ .../frontend/web/src/services/types/guards.ts | 33 ++ .../services/util/deserializeImageField.ts | 29 + .../services/util/deserializeImageResponse.ts | 29 + .../web/src/services/util/getHeaders.ts | 12 + .../src/services/util/makeGraphOfXImages.ts | 24 + .../web/src/theme/components/progress.ts | 4 +- .../web/src/theme/components/scrollbar.ts | 8 +- .../web/src/theme/components/slider.ts | 7 + invokeai/frontend/web/tests/metadata.ts | 174 ++++++ invokeai/frontend/web/tsconfig.json | 1 + invokeai/frontend/web/vite.config.ts | 17 + invokeai/frontend/web/yarn.lock | 535 +++++++++++++++++- tests/nodes/test_graph_execution_state.py | 1 + tests/nodes/test_invoker.py | 1 + tests/nodes/test_nodes.py | 2 +- tests/nodes/test_png_metadata_service.py | 55 ++ 324 files changed, 13051 insertions(+), 1400 deletions(-) rename invokeai/app/invocations/util/{get_model.py => choose_model.py} (53%) delete mode 100644 invokeai/app/models/metadata.py create mode 100644 invokeai/app/services/metadata.py create mode 100644 invokeai/app/util/misc.py delete mode 100644 invokeai/app/util/save_thumbnail.py create mode 100644 invokeai/app/util/thumbnails.py create mode 100644 invokeai/frontend/web/docs/API_CLIENT.md create mode 100644 invokeai/frontend/web/docs/EVENTS.md create mode 100644 invokeai/frontend/web/docs/NODE_EDITOR.md create mode 100644 invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md rename invokeai/frontend/web/{ => docs}/README.md (88%) create mode 100644 invokeai/frontend/web/src/app/storeUtils.ts create mode 100644 invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx create mode 100644 invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx create mode 100644 invokeai/frontend/web/src/common/util/_parseMetadataZod.ts create mode 100644 invokeai/frontend/web/src/common/util/getTimestamp.ts create mode 100644 invokeai/frontend/web/src/common/util/getUrl.ts create mode 100644 invokeai/frontend/web/src/common/util/parseMetadata.ts create mode 100644 invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx create mode 100644 invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts delete mode 100644 invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts create mode 100644 invokeai/frontend/web/src/features/lightbox/store/lightboxPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/Flow.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/fields/types.ts create mode 100644 invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts create mode 100644 invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/constants.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/typeGuards.ts create mode 100644 invokeai/frontend/web/src/features/nodes/types/types.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts create mode 100644 invokeai/frontend/web/src/features/nodes/util/parseSchema.ts create mode 100644 invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/system/store/modelSelectors.ts create mode 100644 invokeai/frontend/web/src/features/system/store/modelSlice.ts create mode 100644 invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx rename invokeai/frontend/web/src/features/ui/components/tabs/{TextToImage/TextToImageContent.tsx => Linear/LinearContent.tsx} (85%) rename invokeai/frontend/web/src/features/ui/components/tabs/{TextToImage/TextToImageParameters.tsx => Linear/LinearParameters.tsx} (65%) create mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx delete mode 100644 invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx create mode 100644 invokeai/frontend/web/src/features/ui/store/extraReducers.ts create mode 100644 invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts create mode 100644 invokeai/frontend/web/src/services/api/core/ApiError.ts create mode 100644 invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts create mode 100644 invokeai/frontend/web/src/services/api/core/ApiResult.ts create mode 100644 invokeai/frontend/web/src/services/api/core/CancelablePromise.ts create mode 100644 invokeai/frontend/web/src/services/api/core/OpenAPI.ts create mode 100644 invokeai/frontend/web/src/services/api/core/request.ts create mode 100644 invokeai/frontend/web/src/services/api/index.ts create mode 100644 invokeai/frontend/web/src/services/api/models/AddInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/BlurInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/Body_upload_image.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CollectInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/models/DivideInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/Edge.ts create mode 100644 invokeai/frontend/web/src/services/api/models/EdgeConnection.ts create mode 100644 invokeai/frontend/web/src/services/api/models/Graph.ts create mode 100644 invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts create mode 100644 invokeai/frontend/web/src/services/api/models/GraphInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageResponse.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ImageType.ts create mode 100644 invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IntOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IterateInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MaskOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MetadataImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ModelsList.ts create mode 100644 invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/NoiseOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/PromptOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/RangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/models/VaeRepo.ts create mode 100644 invokeai/frontend/web/src/services/api/models/ValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$Edge.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$Graph.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ImageType.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts create mode 100644 invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts create mode 100644 invokeai/frontend/web/src/services/api/services/ImagesService.ts create mode 100644 invokeai/frontend/web/src/services/api/services/ModelsService.ts create mode 100644 invokeai/frontend/web/src/services/api/services/SessionsService.ts create mode 100644 invokeai/frontend/web/src/services/events/actions.ts create mode 100644 invokeai/frontend/web/src/services/events/middleware.ts create mode 100644 invokeai/frontend/web/src/services/events/types.ts create mode 100644 invokeai/frontend/web/src/services/fixtures/openapi.json create mode 100644 invokeai/frontend/web/src/services/fixtures/request.ts create mode 100644 invokeai/frontend/web/src/services/thunks/gallery.ts create mode 100644 invokeai/frontend/web/src/services/thunks/image.ts create mode 100644 invokeai/frontend/web/src/services/thunks/model.ts create mode 100644 invokeai/frontend/web/src/services/thunks/schema.ts create mode 100644 invokeai/frontend/web/src/services/thunks/session.ts create mode 100644 invokeai/frontend/web/src/services/types/guards.ts create mode 100644 invokeai/frontend/web/src/services/util/deserializeImageField.ts create mode 100644 invokeai/frontend/web/src/services/util/deserializeImageResponse.ts create mode 100644 invokeai/frontend/web/src/services/util/getHeaders.ts create mode 100644 invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts create mode 100644 invokeai/frontend/web/tests/metadata.ts create mode 100644 tests/nodes/test_png_metadata_service.py diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py index cd5d8a61b2..f33bfff26e 100644 --- a/invokeai/app/api/dependencies.py +++ b/invokeai/app/api/dependencies.py @@ -3,6 +3,8 @@ import os from argparse import Namespace +from invokeai.app.services.metadata import PngMetadataService, MetadataServiceBase + from ..services.default_graphs import create_system_graphs from ..services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage @@ -60,7 +62,9 @@ class ApiDependencies: latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')) - images = DiskImageStorage(f'{output_folder}/images') + metadata = PngMetadataService() + + images = DiskImageStorage(f'{output_folder}/images', metadata_service=metadata) # TODO: build a file/path manager? db_location = os.path.join(output_folder, "invokeai.db") @@ -70,6 +74,7 @@ class ApiDependencies: events=events, latents=latents, images=images, + metadata=metadata, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" diff --git a/invokeai/app/api/models/images.py b/invokeai/app/api/models/images.py index 5ff0a48a44..53caca63f5 100644 --- a/invokeai/app/api/models/images.py +++ b/invokeai/app/api/models/images.py @@ -1,7 +1,19 @@ +from typing import Optional from pydantic import BaseModel, Field from invokeai.app.models.image import ImageType -from invokeai.app.models.metadata import ImageMetadata +from invokeai.app.services.metadata import InvokeAIMetadata + + +class ImageResponseMetadata(BaseModel): + """An image's metadata. Used only in HTTP responses.""" + + created: int = Field(description="The creation timestamp of the image") + width: int = Field(description="The width of the image in pixels") + height: int = Field(description="The height of the image in pixels") + invokeai: Optional[InvokeAIMetadata] = Field( + description="The image's InvokeAI-specific metadata" + ) class ImageResponse(BaseModel): @@ -11,4 +23,12 @@ class ImageResponse(BaseModel): image_name: str = Field(description="The name of the image") image_url: str = Field(description="The url of the image") thumbnail_url: str = Field(description="The url of the image's thumbnail") - metadata: ImageMetadata = Field(description="The image's metadata") + metadata: ImageResponseMetadata = Field(description="The image's metadata") + + +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/api/routers/images.py b/invokeai/app/api/routers/images.py index bb3aabae6d..14a84a5dd4 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,13 +1,17 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - +import io from datetime import datetime, timezone +import json +import os +from typing import Any import uuid -from fastapi import Path, Query, Request, UploadFile +from fastapi import HTTPException, Path, Query, Request, UploadFile from fastapi.responses import FileResponse, Response from fastapi.routing import APIRouter from PIL import Image -from invokeai.app.api.models.images import ImageResponse +from invokeai.app.api.models.images import ImageResponse, ImageResponseMetadata +from invokeai.app.services.metadata import InvokeAIMetadata from invokeai.app.services.item_storage import PaginatedResults from ...services.image_storage import ImageType @@ -15,70 +19,110 @@ from ..dependencies import ApiDependencies images_router = APIRouter(prefix="/v1/images", tags=["images"]) + @images_router.get("/{image_type}/{image_name}", operation_id="get_image") async def get_image( image_type: ImageType = Path(description="The type of image to get"), image_name: str = Path(description="The name of the image to get"), -): +) -> FileResponse | Response: """Gets a result""" - # TODO: This is not really secure at all. At least make sure only output results are served - filename = ApiDependencies.invoker.services.images.get_path(image_type, image_name) - return FileResponse(filename) -@images_router.get("/{image_type}/thumbnails/{image_name}", operation_id="get_thumbnail") + path = ApiDependencies.invoker.services.images.get_path( + image_type=image_type, image_name=image_name + ) + + if ApiDependencies.invoker.services.images.validate_path(path): + return FileResponse(path) + else: + raise HTTPException(status_code=404) + + +@images_router.get( + "/{image_type}/thumbnails/{image_name}", operation_id="get_thumbnail" +) async def get_thumbnail( image_type: ImageType = Path(description="The type of image to get"), image_name: str = Path(description="The name of the image to get"), -): +) -> FileResponse | Response: """Gets a thumbnail""" - # TODO: This is not really secure at all. At least make sure only output results are served - filename = ApiDependencies.invoker.services.images.get_path(image_type, 'thumbnails/' + image_name) - return FileResponse(filename) + + path = ApiDependencies.invoker.services.images.get_path( + image_type=image_type, image_name=image_name, is_thumbnail=True + ) + + if ApiDependencies.invoker.services.images.validate_path(path): + return FileResponse(path) + else: + raise HTTPException(status_code=404) @images_router.post( "/uploads/", operation_id="upload_image", responses={ - 201: {"description": "The image was uploaded successfully"}, - 404: {"description": "Session not found"}, + 201: { + "description": "The image was uploaded successfully", + "model": ImageResponse, + }, + 415: {"description": "Image upload failed"}, }, + status_code=201, ) -async def upload_image(file: UploadFile, request: Request): +async def upload_image( + file: UploadFile, request: Request, response: Response +) -> ImageResponse: if not file.content_type.startswith("image"): - return Response(status_code=415) + raise HTTPException(status_code=415, detail="Not an image") contents = await file.read() + try: - im = Image.open(contents) + img = Image.open(io.BytesIO(contents)) except: # Error opening the image - return Response(status_code=415) + raise HTTPException(status_code=415, detail="Failed to read image") filename = f"{uuid.uuid4()}_{str(int(datetime.now(timezone.utc).timestamp()))}.png" - ApiDependencies.invoker.services.images.save(ImageType.UPLOAD, filename, im) - return Response( - status_code=201, - headers={ - "Location": request.url_for( - "get_image", image_type=ImageType.UPLOAD.value, image_name=filename - ) - }, + (image_path, thumbnail_path, ctime) = ApiDependencies.invoker.services.images.save( + ImageType.UPLOAD, filename, img ) + invokeai_metadata = ApiDependencies.invoker.services.metadata.get_metadata(img) + + res = ImageResponse( + image_type=ImageType.UPLOAD, + image_name=filename, + image_url=f"api/v1/images/{ImageType.UPLOAD.value}/{filename}", + thumbnail_url=f"api/v1/images/{ImageType.UPLOAD.value}/thumbnails/{os.path.splitext(filename)[0]}.webp", + metadata=ImageResponseMetadata( + created=ctime, + width=img.width, + height=img.height, + invokeai=invokeai_metadata, + ), + ) + + response.status_code = 201 + response.headers["Location"] = request.url_for( + "get_image", image_type=ImageType.UPLOAD.value, image_name=filename + ) + + return res + + @images_router.get( "/", operation_id="list_images", responses={200: {"model": PaginatedResults[ImageResponse]}}, ) async def list_images( - image_type: ImageType = Query(default=ImageType.RESULT, description="The type of images to get"), + image_type: ImageType = Query( + default=ImageType.RESULT, description="The type of images to get" + ), page: int = Query(default=0, description="The page of images to get"), per_page: int = Query(default=10, description="The number of images per page"), ) -> PaginatedResults[ImageResponse]: """Gets a list of images""" - result = ApiDependencies.invoker.services.images.list( - image_type, page, per_page - ) + result = ApiDependencies.invoker.services.images.list(image_type, page, per_page) return result diff --git a/invokeai/app/cli_app.py b/invokeai/app/cli_app.py index 86fd18ca60..9ac156916b 100644 --- a/invokeai/app/cli_app.py +++ b/invokeai/app/cli_app.py @@ -13,6 +13,8 @@ from typing import ( from pydantic import BaseModel from pydantic.fields import Field +from invokeai.app.services.metadata import PngMetadataService + from .services.default_graphs import create_system_graphs from .services.latent_storage import DiskLatentsStorage, ForwardCacheLatentsStorage @@ -200,6 +202,8 @@ def invoke_cli(): events = EventServiceBase() + metadata = PngMetadataService() + output_folder = os.path.abspath( os.path.join(os.path.dirname(__file__), "../../../outputs") ) @@ -211,7 +215,8 @@ def invoke_cli(): model_manager=model_manager, events=events, latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f'{output_folder}/latents')), - images=DiskImageStorage(f'{output_folder}/images'), + images=DiskImageStorage(f'{output_folder}/images', metadata_service=metadata), + metadata=metadata, queue=MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=db_location, table_name="graphs" diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py index 3590129b96..7daaa588b1 100644 --- a/invokeai/app/invocations/baseinvocation.py +++ b/invokeai/app/invocations/baseinvocation.py @@ -95,7 +95,7 @@ class UIConfig(TypedDict, total=False): ], ] tags: List[str] - + title: str class CustomisedSchemaExtra(TypedDict): ui: UIConfig diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py index c68b7449cc..24a89c2cf4 100644 --- a/invokeai/app/invocations/collections.py +++ b/invokeai/app/invocations/collections.py @@ -1,16 +1,17 @@ # Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) -from typing import Literal +from typing import Literal, Optional -import cv2 as cv import numpy as np import numpy.random -from PIL import Image, ImageOps from pydantic import Field -from ..services.image_storage import ImageType -from .baseinvocation import BaseInvocation, InvocationContext, BaseInvocationOutput -from .image import ImageField, ImageOutput +from .baseinvocation import ( + BaseInvocation, + InvocationConfig, + InvocationContext, + BaseInvocationOutput, +) class IntCollectionOutput(BaseInvocationOutput): @@ -33,7 +34,9 @@ class RangeInvocation(BaseInvocation): step: int = Field(default=1, description="The step of the range") def invoke(self, context: InvocationContext) -> IntCollectionOutput: - return IntCollectionOutput(collection=list(range(self.start, self.stop, self.step))) + return IntCollectionOutput( + collection=list(range(self.start, self.stop, self.step)) + ) class RandomRangeInvocation(BaseInvocation): @@ -43,8 +46,19 @@ class RandomRangeInvocation(BaseInvocation): # Inputs low: int = Field(default=0, description="The inclusive low value") - high: int = Field(default=np.iinfo(np.int32).max, description="The exclusive high value") + high: int = Field( + default=np.iinfo(np.int32).max, description="The exclusive high value" + ) size: int = Field(default=1, description="The number of values to generate") + seed: Optional[int] = Field( + ge=0, + le=np.iinfo(np.int32).max, + description="The seed for the RNG", + default_factory=lambda: numpy.random.randint(0, np.iinfo(np.int32).max), + ) def invoke(self, context: InvocationContext) -> IntCollectionOutput: - return IntCollectionOutput(collection=list(numpy.random.randint(self.low, self.high, size=self.size))) + rng = np.random.default_rng(self.seed) + return IntCollectionOutput( + collection=list(rng.integers(low=self.low, high=self.high, size=self.size)) + ) diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py index 52e59b16ac..5a6d703d83 100644 --- a/invokeai/app/invocations/cv.py +++ b/invokeai/app/invocations/cv.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field from invokeai.app.models.image import ImageField, ImageType from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output class CvInvocationConfig(BaseModel): @@ -56,7 +56,14 @@ class CvInpaintInvocation(BaseInvocation, CvInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image_inpainted) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + + context.services.images.save(image_type, image_name, image_inpainted, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image_inpainted, + ) \ No newline at end of file diff --git a/invokeai/app/invocations/generate.py b/invokeai/app/invocations/generate.py index d0eeeae698..df79baa0f3 100644 --- a/invokeai/app/invocations/generate.py +++ b/invokeai/app/invocations/generate.py @@ -9,13 +9,12 @@ from torch import Tensor from pydantic import BaseModel, Field from invokeai.app.models.image import ImageField, ImageType -from invokeai.app.invocations.util.get_model import choose_model +from invokeai.app.invocations.util.choose_model import choose_model from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output from ...backend.generator import Txt2Img, Img2Img, Inpaint, InvokeAIGenerator from ...backend.stable_diffusion import PipelineIntermediateState -from ..models.exceptions import CanceledException -from ..util.step_callback import diffusers_step_callback_adapter +from ..util.step_callback import stable_diffusion_step_callback SAMPLER_NAME_VALUES = Literal[tuple(InvokeAIGenerator.schedulers())] @@ -58,28 +57,31 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): # TODO: pass this an emitter method or something? or a session for dispatching? def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState + self, + context: InvocationContext, + source_node_id: str, + intermediate_state: PipelineIntermediateState, ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def invoke(self, context: InvocationContext) -> ImageOutput: # Handle invalid model parameter model = choose_model(context.services.model_manager, self.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] + outputs = Txt2Img(model).generate( prompt=self.prompt, - step_callback=partial(self.dispatch_progress, context), + step_callback=partial(self.dispatch_progress, context, source_node_id), **self.dict( exclude={"prompt"} ), # Shorthand for passing all of the parameters above manually @@ -95,9 +97,18 @@ class TextToImageInvocation(BaseInvocation, SDImageInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, generate_output.image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save( + image_type, image_name, generate_output.image, metadata + ) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=generate_output.image, ) @@ -117,20 +128,17 @@ class ImageToImageInvocation(TextToImageInvocation): ) def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState - ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) + self, + context: InvocationContext, + source_node_id: str, + intermediate_state: PipelineIntermediateState, + ) -> None: + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def invoke(self, context: InvocationContext) -> ImageOutput: image = ( @@ -145,15 +153,21 @@ class ImageToImageInvocation(TextToImageInvocation): # Handle invalid model parameter model = choose_model(context.services.model_manager, self.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] + outputs = Img2Img(model).generate( - prompt=self.prompt, - init_image=image, - init_mask=mask, - step_callback=partial(self.dispatch_progress, context), - **self.dict( - exclude={"prompt", "image", "mask"} - ), # Shorthand for passing all of the parameters above manually - ) + prompt=self.prompt, + init_image=image, + init_mask=mask, + step_callback=partial(self.dispatch_progress, context, source_node_id), + **self.dict( + exclude={"prompt", "image", "mask"} + ), # Shorthand for passing all of the parameters above manually + ) # Outputs is an infinite iterator that will return a new InvokeAIGeneratorOutput object # each time it is called. We only need the first one. @@ -168,11 +182,19 @@ class ImageToImageInvocation(TextToImageInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, result_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + context.services.images.save(image_type, image_name, result_image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=result_image, + ) + + class InpaintInvocation(ImageToImageInvocation): """Generates an image using inpaint.""" @@ -188,20 +210,17 @@ class InpaintInvocation(ImageToImageInvocation): ) def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState - ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) + self, + context: InvocationContext, + source_node_id: str, + intermediate_state: PipelineIntermediateState, + ) -> None: + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def invoke(self, context: InvocationContext) -> ImageOutput: image = ( @@ -218,17 +237,23 @@ class InpaintInvocation(ImageToImageInvocation): ) # Handle invalid model parameter - model = choose_model(context.services.model_manager, self.model) + model = choose_model(context.services.model_manager, self.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] outputs = Inpaint(model).generate( - prompt=self.prompt, - init_img=image, - init_mask=mask, - step_callback=partial(self.dispatch_progress, context), - **self.dict( - exclude={"prompt", "image", "mask"} - ), # Shorthand for passing all of the parameters above manually - ) + prompt=self.prompt, + init_img=image, + init_mask=mask, + step_callback=partial(self.dispatch_progress, context, source_node_id), + **self.dict( + exclude={"prompt", "image", "mask"} + ), # Shorthand for passing all of the parameters above manually + ) # Outputs is an infinite iterator that will return a new InvokeAIGeneratorOutput object # each time it is called. We only need the first one. @@ -243,7 +268,14 @@ class InpaintInvocation(ImageToImageInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, result_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, result_image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=result_image, ) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index cc5f6b53c7..883ef63f69 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -1,6 +1,5 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from datetime import datetime, timezone from typing import Literal, Optional import numpy @@ -8,8 +7,12 @@ from PIL import Image, ImageFilter, ImageOps from pydantic import BaseModel, Field from ..models.image import ImageField, ImageType -from ..services.invocation_services import InvocationServices -from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext, InvocationConfig +from .baseinvocation import ( + BaseInvocation, + BaseInvocationOutput, + InvocationContext, + InvocationConfig, +) class PILInvocationConfig(BaseModel): @@ -22,50 +25,73 @@ class PILInvocationConfig(BaseModel): }, } + class ImageOutput(BaseInvocationOutput): """Base class for invocations that output an image""" - #fmt: off + + # fmt: off type: Literal["image"] = "image" image: ImageField = Field(default=None, description="The output image") - #fmt: on + width: Optional[int] = Field(default=None, description="The width of the image in pixels") + height: Optional[int] = Field(default=None, description="The height of the image in pixels") + # fmt: on class Config: schema_extra = { - 'required': [ - 'type', - 'image', - ] + "required": ["type", "image", "width", "height", "mode"] } + +def build_image_output( + image_type: ImageType, image_name: str, image: Image.Image +) -> ImageOutput: + """Builds an ImageOutput and its ImageField""" + image_field = ImageField( + image_name=image_name, + image_type=image_type, + ) + return ImageOutput( + image=image_field, + width=image.width, + height=image.height, + mode=image.mode, + ) + + class MaskOutput(BaseInvocationOutput): """Base class for invocations that output a mask""" - #fmt: off + + # fmt: off type: Literal["mask"] = "mask" mask: ImageField = Field(default=None, description="The output mask") - #fmt: on + # fmt: on class Config: schema_extra = { - 'required': [ - 'type', - 'mask', + "required": [ + "type", + "mask", ] } -# TODO: this isn't really necessary anymore + class LoadImageInvocation(BaseInvocation): - """Load an image from a filename and provide it as output.""" - #fmt: off + """Load an image and provide it as output.""" + + # fmt: off type: Literal["load_image"] = "load_image" # Inputs image_type: ImageType = Field(description="The type of the image") image_name: str = Field(description="The name of the image") - #fmt: on - + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: - return ImageOutput( - image=ImageField(image_type=self.image_type, image_name=self.image_name) + image = context.services.images.get(self.image_type, self.image_name) + + return build_image_output( + image_type=self.image_type, + image_name=self.image_name, + image=image, ) @@ -86,16 +112,17 @@ class ShowImageInvocation(BaseInvocation): # TODO: how to handle failure? - return ImageOutput( - image=ImageField( - image_type=self.image.image_type, image_name=self.image.image_name - ) + return build_image_output( + image_type=self.image.image_type, + image_name=self.image.image_name, + image=image, ) class CropImageInvocation(BaseInvocation, PILInvocationConfig): """Crops an image to a specified box. The box can be outside of the image.""" - #fmt: off + + # fmt: off type: Literal["crop"] = "crop" # Inputs @@ -104,7 +131,7 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): y: int = Field(default=0, description="The top y coordinate of the crop rectangle") width: int = Field(default=512, gt=0, description="The width of the crop rectangle") height: int = Field(default=512, gt=0, description="The height of the crop rectangle") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( @@ -120,15 +147,23 @@ class CropImageInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image_crop) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, image_crop, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image_crop, ) class PasteImageInvocation(BaseInvocation, PILInvocationConfig): """Pastes an image into another image.""" - #fmt: off + + # fmt: off type: Literal["paste"] = "paste" # Inputs @@ -137,7 +172,7 @@ class PasteImageInvocation(BaseInvocation, PILInvocationConfig): mask: Optional[ImageField] = Field(default=None, description="The mask to use when pasting") x: int = Field(default=0, description="The left x coordinate at which to paste the image") y: int = Field(default=0, description="The top y coordinate at which to paste the image") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: base_image = context.services.images.get( @@ -170,21 +205,29 @@ class PasteImageInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, new_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, new_image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=new_image, ) class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): """Extracts the alpha channel of an image as a mask.""" - #fmt: off + + # fmt: off type: Literal["tomask"] = "tomask" # Inputs image: ImageField = Field(default=None, description="The image to create the mask from") invert: bool = Field(default=False, description="Whether or not to invert the mask") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> MaskOutput: image = context.services.images.get( @@ -199,22 +242,27 @@ class MaskFromAlphaInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image_mask) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, image_mask, metadata) return MaskOutput(mask=ImageField(image_type=image_type, image_name=image_name)) class BlurInvocation(BaseInvocation, PILInvocationConfig): """Blurs an image""" - #fmt: off + # fmt: off type: Literal["blur"] = "blur" # Inputs image: ImageField = Field(default=None, description="The image to blur") radius: float = Field(default=8.0, ge=0, description="The blur radius") blur_type: Literal["gaussian", "box"] = Field(default="gaussian", description="The type of blur") - #fmt: on - + # fmt: on + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( self.image.image_type, self.image.image_name @@ -231,22 +279,28 @@ class BlurInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, blur_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, blur_image, metadata) + return build_image_output( + image_type=image_type, image_name=image_name, image=blur_image ) class LerpInvocation(BaseInvocation, PILInvocationConfig): """Linear interpolation of all pixels of an image""" - #fmt: off + + # fmt: off type: Literal["lerp"] = "lerp" # Inputs image: ImageField = Field(default=None, description="The image to lerp") min: int = Field(default=0, ge=0, le=255, description="The minimum output value") max: int = Field(default=255, ge=0, le=255, description="The maximum output value") - #fmt: on + # fmt: on def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( @@ -262,23 +316,29 @@ class LerpInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, lerp_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, lerp_image, metadata) + return build_image_output( + image_type=image_type, image_name=image_name, image=lerp_image ) class InverseLerpInvocation(BaseInvocation, PILInvocationConfig): """Inverse linear interpolation of all pixels of an image""" - #fmt: off + + # fmt: off type: Literal["ilerp"] = "ilerp" # Inputs image: ImageField = Field(default=None, description="The image to lerp") min: int = Field(default=0, ge=0, le=255, description="The minimum input value") max: int = Field(default=255, ge=0, le=255, description="The maximum input value") - #fmt: on - + # fmt: on + def invoke(self, context: InvocationContext) -> ImageOutput: image = context.services.images.get( self.image.image_type, self.image.image_name @@ -298,7 +358,12 @@ class InverseLerpInvocation(BaseInvocation, PILInvocationConfig): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, ilerp_image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, ilerp_image, metadata) + return build_image_output( + image_type=image_type, image_name=image_name, image=ilerp_image ) diff --git a/invokeai/app/invocations/latent.py b/invokeai/app/invocations/latent.py index 7593b34142..3d1c925570 100644 --- a/invokeai/app/invocations/latent.py +++ b/invokeai/app/invocations/latent.py @@ -5,9 +5,9 @@ from typing import Literal, Optional from pydantic import BaseModel, Field import torch -from invokeai.app.models.exceptions import CanceledException -from invokeai.app.invocations.util.get_model import choose_model -from invokeai.app.util.step_callback import diffusers_step_callback_adapter +from invokeai.app.invocations.util.choose_model import choose_model + +from invokeai.app.util.step_callback import stable_diffusion_step_callback from ...backend.model_management.model_manager import ModelManager from ...backend.util.devices import choose_torch_device, torch_dtype @@ -19,7 +19,7 @@ from .baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationCont import numpy as np from ..services.image_storage import ImageType from .baseinvocation import BaseInvocation, InvocationContext -from .image import ImageField, ImageOutput +from .image import ImageField, ImageOutput, build_image_output from ...backend.stable_diffusion import PipelineIntermediateState from diffusers.schedulers import SchedulerMixin as Scheduler import diffusers @@ -31,6 +31,8 @@ class LatentsField(BaseModel): latents_name: Optional[str] = Field(default=None, description="The name of the latents") + class Config: + schema_extra = {"required": ["latents_name"]} class LatentsOutput(BaseInvocationOutput): """Base class for invocations that output latents""" @@ -170,21 +172,14 @@ class TextToLatentsInvocation(BaseInvocation): # TODO: pass this an emitter method or something? or a session for dispatching? def dispatch_progress( - self, context: InvocationContext, intermediate_state: PipelineIntermediateState + self, context: InvocationContext, source_node_id: str, intermediate_state: PipelineIntermediateState ) -> None: - if (context.services.queue.is_canceled(context.graph_execution_state_id)): - raise CanceledException - - step = intermediate_state.step - if intermediate_state.predicted_original is not None: - # Some schedulers report not only the noisy latents at the current timestep, - # but also their estimate so far of what the de-noised latents will be. - sample = intermediate_state.predicted_original - else: - sample = intermediate_state.latents - - diffusers_step_callback_adapter(sample, step, steps=self.steps, id=self.id, context=context) - + stable_diffusion_step_callback( + context=context, + intermediate_state=intermediate_state, + node=self.dict(), + source_node_id=source_node_id, + ) def get_model(self, model_manager: ModelManager) -> StableDiffusionGeneratorPipeline: model_info = choose_model(model_manager, self.model) @@ -231,8 +226,12 @@ class TextToLatentsInvocation(BaseInvocation): def invoke(self, context: InvocationContext) -> LatentsOutput: noise = context.services.latents.get(self.noise.latents_name) + # 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, state) + self.dispatch_progress(context, source_node_id, state) model = self.get_model(context.services.model_manager) conditioning_data = self.get_conditioning_data(model) @@ -281,8 +280,12 @@ class LatentsToLatentsInvocation(TextToLatentsInvocation): noise = context.services.latents.get(self.noise.latents_name) latent = context.services.latents.get(self.latents.latents_name) + # 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, state) + self.dispatch_progress(context, source_node_id, state) model = self.get_model(context.services.model_manager) conditioning_data = self.get_conditioning_data(model) @@ -355,7 +358,14 @@ class LatentsToImageInvocation(BaseInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, image) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self + ) + + context.services.images.save(image_type, image_name, image, metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=image ) diff --git a/invokeai/app/invocations/reconstruct.py b/invokeai/app/invocations/reconstruct.py index f6df5a2254..94a7277acd 100644 --- a/invokeai/app/invocations/reconstruct.py +++ b/invokeai/app/invocations/reconstruct.py @@ -1,12 +1,11 @@ -from datetime import datetime, timezone from typing import Literal, Union from pydantic import Field from invokeai.app.models.image import ImageField, ImageType -from ..services.invocation_services import InvocationServices + from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output class RestoreFaceInvocation(BaseInvocation): """Restores faces in an image.""" @@ -44,7 +43,14 @@ class RestoreFaceInvocation(BaseInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, results[0][0]) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + + context.services.images.save(image_type, image_name, results[0][0], metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=results[0][0] + ) \ No newline at end of file diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index 021f3569e8..c4938dfd19 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -1,14 +1,12 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from datetime import datetime, timezone from typing import Literal, Union from pydantic import Field from invokeai.app.models.image import ImageField, ImageType -from ..services.invocation_services import InvocationServices from .baseinvocation import BaseInvocation, InvocationContext, InvocationConfig -from .image import ImageOutput +from .image import ImageOutput, build_image_output class UpscaleInvocation(BaseInvocation): @@ -49,7 +47,14 @@ class UpscaleInvocation(BaseInvocation): image_name = context.services.images.create_name( context.graph_execution_state_id, self.id ) - context.services.images.save(image_type, image_name, results[0][0]) - return ImageOutput( - image=ImageField(image_type=image_type, image_name=image_name) + + metadata = context.services.metadata.build_metadata( + session_id=context.graph_execution_state_id, node=self ) + + context.services.images.save(image_type, image_name, results[0][0], metadata) + return build_image_output( + image_type=image_type, + image_name=image_name, + image=results[0][0] + ) \ No newline at end of file diff --git a/invokeai/app/invocations/util/get_model.py b/invokeai/app/invocations/util/choose_model.py similarity index 53% rename from invokeai/app/invocations/util/get_model.py rename to invokeai/app/invocations/util/choose_model.py index d3484a0b9d..f0f2dc7120 100644 --- a/invokeai/app/invocations/util/get_model.py +++ b/invokeai/app/invocations/util/choose_model.py @@ -1,11 +1,14 @@ -from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.backend.model_management.model_manager import ModelManager def choose_model(model_manager: ModelManager, model_name: str): """Returns the default model if the `model_name` not a valid model, else returns the selected model.""" if model_manager.valid_model(model_name): - return model_manager.get_model(model_name) + model = model_manager.get_model(model_name) else: - print(f"* Warning: '{model_name}' is not a valid model name. Using default model instead.") - return model_manager.get_model() \ No newline at end of file + model = model_manager.get_model() + print( + f"* Warning: '{model_name}' is not a valid model name. Using default model \'{model['model_name']}\' instead." + ) + + return model diff --git a/invokeai/app/models/image.py b/invokeai/app/models/image.py index 9edb16800d..5ef1ab0d35 100644 --- a/invokeai/app/models/image.py +++ b/invokeai/app/models/image.py @@ -9,6 +9,14 @@ class ImageType(str, Enum): UPLOAD = "uploads" +def is_image_type(obj): + try: + ImageType(obj) + except ValueError: + return False + return True + + class ImageField(BaseModel): """An image field used for passing image objects between invocations""" @@ -18,9 +26,4 @@ class ImageField(BaseModel): image_name: Optional[str] = Field(default=None, description="The name of the image") class Config: - schema_extra = { - "required": [ - "image_type", - "image_name", - ] - } + schema_extra = {"required": ["image_type", "image_name"]} diff --git a/invokeai/app/models/metadata.py b/invokeai/app/models/metadata.py deleted file mode 100644 index 2531168272..0000000000 --- a/invokeai/app/models/metadata.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Optional -from pydantic import BaseModel, Field - -class ImageMetadata(BaseModel): - """An image's metadata""" - - timestamp: float = Field(description="The creation timestamp of the image") - width: int = Field(description="The width of the image in pixels") - height: int = Field(description="The height of the image in pixels") - # TODO: figure out metadata - sd_metadata: Optional[dict] = Field(default={}, description="The image's SD-specific metadata") diff --git a/invokeai/app/services/events.py b/invokeai/app/services/events.py index c8eb7671d0..5f26c42c17 100644 --- a/invokeai/app/services/events.py +++ b/invokeai/app/services/events.py @@ -1,10 +1,9 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -from typing import Any, Dict, TypedDict +from typing import Any +from invokeai.app.api.models.images import ProgressImage +from invokeai.app.util.misc import get_timestamp -ProgressImage = TypedDict( - "ProgressImage", {"dataURL": str, "width": int, "height": int} -) class EventServiceBase: session_event: str = "session_event" @@ -14,7 +13,8 @@ class EventServiceBase: def dispatch(self, event_name: str, payload: Any) -> None: pass - def __emit_session_event(self, event_name: str, payload: Dict) -> None: + def __emit_session_event(self, event_name: str, payload: dict) -> None: + payload["timestamp"] = get_timestamp() self.dispatch( event_name=EventServiceBase.session_event, payload=dict(event=event_name, data=payload), @@ -25,7 +25,8 @@ class EventServiceBase: def emit_generator_progress( self, graph_execution_state_id: str, - invocation_id: str, + node: dict, + source_node_id: str, progress_image: ProgressImage | None, step: int, total_steps: int, @@ -35,48 +36,60 @@ class EventServiceBase: event_name="generator_progress", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, - progress_image=progress_image, + node=node, + source_node_id=source_node_id, + progress_image=progress_image.dict() if progress_image is not None else None, step=step, total_steps=total_steps, ), ) def emit_invocation_complete( - self, graph_execution_state_id: str, invocation_id: str, result: Dict + self, + graph_execution_state_id: str, + result: dict, + node: dict, + source_node_id: str, ) -> None: """Emitted when an invocation has completed""" self.__emit_session_event( event_name="invocation_complete", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, + node=node, + source_node_id=source_node_id, result=result, ), ) def emit_invocation_error( - self, graph_execution_state_id: str, invocation_id: str, error: str + self, + graph_execution_state_id: str, + node: dict, + source_node_id: str, + error: str, ) -> None: """Emitted when an invocation has completed""" self.__emit_session_event( event_name="invocation_error", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, + node=node, + source_node_id=source_node_id, error=error, ), ) def emit_invocation_started( - self, graph_execution_state_id: str, invocation_id: str + self, graph_execution_state_id: str, node: dict, source_node_id: str ) -> None: """Emitted when an invocation has started""" self.__emit_session_event( event_name="invocation_started", payload=dict( graph_execution_state_id=graph_execution_state_id, - invocation_id=invocation_id, + node=node, + source_node_id=source_node_id, ), ) @@ -84,5 +97,7 @@ class EventServiceBase: """Emitted when a session has completed all invocations""" self.__emit_session_event( event_name="graph_execution_state_complete", - payload=dict(graph_execution_state_id=graph_execution_state_id), + payload=dict( + graph_execution_state_id=graph_execution_state_id, + ), ) diff --git a/invokeai/app/services/image_storage.py b/invokeai/app/services/image_storage.py index 80d72efca8..335425fe66 100644 --- a/invokeai/app/services/image_storage.py +++ b/invokeai/app/services/image_storage.py @@ -1,24 +1,24 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) -import datetime import os from glob import glob from abc import ABC, abstractmethod -from enum import Enum from pathlib import Path from queue import Queue -from typing import Callable, Dict, List +from typing import Dict, List, Tuple from PIL.Image import Image import PIL.Image as PILImage -from pydantic import BaseModel -from invokeai.app.api.models.images import ImageResponse -from invokeai.app.models.image import ImageField, ImageType -from invokeai.app.models.metadata import ImageMetadata +from invokeai.app.api.models.images import ImageResponse, ImageResponseMetadata +from invokeai.app.models.image import ImageType +from invokeai.app.services.metadata import ( + InvokeAIMetadata, + MetadataServiceBase, + build_invokeai_metadata_pnginfo, +) from invokeai.app.services.item_storage import PaginatedResults -from invokeai.app.util.save_thumbnail import save_thumbnail - -from invokeai.backend.image_util import PngWriter +from invokeai.app.util.misc import get_timestamp +from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail class ImageStorageBase(ABC): @@ -26,12 +26,14 @@ class ImageStorageBase(ABC): @abstractmethod def get(self, image_type: ImageType, image_name: str) -> Image: + """Retrieves an image as PIL Image.""" pass @abstractmethod def list( self, image_type: ImageType, page: int = 0, per_page: int = 10 ) -> PaginatedResults[ImageResponse]: + """Gets a paginated list of images.""" pass # TODO: make this a bit more flexible for e.g. cloud storage @@ -39,35 +41,51 @@ class ImageStorageBase(ABC): def get_path( self, image_type: ImageType, image_name: str, is_thumbnail: bool = False ) -> str: + """Gets the path to an image or its thumbnail.""" + pass + + # TODO: make this a bit more flexible for e.g. cloud storage + @abstractmethod + def validate_path(self, path: str) -> bool: + """Validates an image path.""" pass @abstractmethod - def save(self, image_type: ImageType, image_name: str, image: Image) -> None: + def save( + self, + image_type: ImageType, + image_name: str, + image: Image, + metadata: InvokeAIMetadata | None = None, + ) -> Tuple[str, str, int]: + """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image path, thumbnail path, and created timestamp.""" pass @abstractmethod def delete(self, image_type: ImageType, image_name: str) -> None: + """Deletes an image and its thumbnail (if one exists).""" pass def create_name(self, context_id: str, node_id: str) -> str: - return f"{context_id}_{node_id}_{str(int(datetime.datetime.now(datetime.timezone.utc).timestamp()))}.png" + """Creates a unique contextual image filename.""" + return f"{context_id}_{node_id}_{str(get_timestamp())}.png" class DiskImageStorage(ImageStorageBase): """Stores images on disk""" __output_folder: str - __pngWriter: PngWriter __cache_ids: Queue # TODO: this is an incredibly naive cache __cache: Dict[str, Image] __max_cache_size: int + __metadata_service: MetadataServiceBase - def __init__(self, output_folder: str): + def __init__(self, output_folder: str, metadata_service: MetadataServiceBase): self.__output_folder = output_folder - self.__pngWriter = PngWriter(output_folder) self.__cache = dict() self.__cache_ids = Queue() self.__max_cache_size = 10 # TODO: get this from config + self.__metadata_service = metadata_service Path(output_folder).mkdir(parents=True, exist_ok=True) @@ -100,6 +118,9 @@ class DiskImageStorage(ImageStorageBase): for path in page_of_image_paths: filename = os.path.basename(path) img = PILImage.open(path) + + invokeai_metadata = self.__metadata_service.get_metadata(img) + page_of_images.append( ImageResponse( image_type=image_type.value, @@ -107,11 +128,12 @@ class DiskImageStorage(ImageStorageBase): # TODO: DiskImageStorage should not be building URLs...? image_url=f"api/v1/images/{image_type.value}/{filename}", thumbnail_url=f"api/v1/images/{image_type.value}/thumbnails/{os.path.splitext(filename)[0]}.webp", - # TODO: Creation of this object should happen elsewhere, just making it fit here so it works - metadata=ImageMetadata( - timestamp=os.path.getctime(path), + # TODO: Creation of this object should happen elsewhere (?), just making it fit here so it works + metadata=ImageResponseMetadata( + created=int(os.path.getctime(path)), width=img.width, height=img.height, + invokeai=invokeai_metadata, ), ) ) @@ -142,26 +164,50 @@ class DiskImageStorage(ImageStorageBase): def get_path( self, image_type: ImageType, image_name: str, is_thumbnail: bool = False ) -> str: + # strip out any relative path shenanigans + basename = os.path.basename(image_name) + if is_thumbnail: path = os.path.join( - self.__output_folder, image_type, "thumbnails", image_name + self.__output_folder, image_type, "thumbnails", basename ) else: - path = os.path.join(self.__output_folder, image_type, image_name) + path = os.path.join(self.__output_folder, image_type, basename) + return path - def save(self, image_type: ImageType, image_name: str, image: Image) -> None: - image_subpath = os.path.join(image_type, image_name) - self.__pngWriter.save_image_and_prompt_to_png( - image, "", image_subpath, None - ) # TODO: just pass full path to png writer - save_thumbnail( - image=image, - filename=image_name, - path=os.path.join(self.__output_folder, image_type, "thumbnails"), - ) + def validate_path(self, path: str) -> bool: + try: + os.stat(path) + return True + except Exception: + return False + + def save( + self, + image_type: ImageType, + image_name: str, + image: Image, + metadata: InvokeAIMetadata | None = None, + ) -> Tuple[str, str, int]: image_path = self.get_path(image_type, image_name) + + # TODO: Reading the image and then saving it strips the metadata... + if metadata: + pnginfo = build_invokeai_metadata_pnginfo(metadata=metadata) + image.save(image_path, "PNG", pnginfo=pnginfo) + else: + image.save(image_path) # this saved image has an empty info + + thumbnail_name = get_thumbnail_name(image_name) + thumbnail_path = self.get_path(image_type, thumbnail_name, is_thumbnail=True) + thumbnail_image = make_thumbnail(image) + thumbnail_image.save(thumbnail_path) + self.__set_cache(image_path, image) + self.__set_cache(thumbnail_path, thumbnail_image) + + return (image_path, thumbnail_path, int(os.path.getctime(image_path))) def delete(self, image_type: ImageType, image_name: str) -> None: image_path = self.get_path(image_type, image_name) diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py index c3c6bbce7e..1ff42f063d 100644 --- a/invokeai/app/services/invocation_services.py +++ b/invokeai/app/services/invocation_services.py @@ -1,4 +1,5 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +from invokeai.app.services.metadata import MetadataServiceBase from invokeai.backend import ModelManager from .events import EventServiceBase @@ -14,6 +15,7 @@ class InvocationServices: events: EventServiceBase latents: LatentsStorageBase images: ImageStorageBase + metadata: MetadataServiceBase queue: InvocationQueueABC model_manager: ModelManager restoration: RestorationServices @@ -29,6 +31,7 @@ class InvocationServices: events: EventServiceBase, latents: LatentsStorageBase, images: ImageStorageBase, + metadata: MetadataServiceBase, queue: InvocationQueueABC, graph_library: ItemStorageABC["LibraryGraph"], graph_execution_manager: ItemStorageABC["GraphExecutionState"], @@ -39,6 +42,7 @@ class InvocationServices: self.events = events self.latents = latents self.images = images + self.metadata = metadata self.queue = queue self.graph_library = graph_library self.graph_execution_manager = graph_execution_manager diff --git a/invokeai/app/services/metadata.py b/invokeai/app/services/metadata.py new file mode 100644 index 0000000000..2c8bb0d26b --- /dev/null +++ b/invokeai/app/services/metadata.py @@ -0,0 +1,96 @@ +import json +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional, TypedDict +from PIL import Image, PngImagePlugin +from pydantic import BaseModel + +from invokeai.app.models.image import ImageType, is_image_type + + +class MetadataImageField(TypedDict): + """Pydantic-less ImageField, used for metadata parsing.""" + + image_type: ImageType + image_name: str + + +class MetadataLatentsField(TypedDict): + """Pydantic-less LatentsField, used for metadata parsing.""" + + latents_name: str + + +# TODO: This is a placeholder for `InvocationsUnion` pending resolution of circular imports +NodeMetadata = Dict[ + str, str | int | float | bool | MetadataImageField | MetadataLatentsField +] + + +class InvokeAIMetadata(TypedDict, total=False): + """InvokeAI-specific metadata format.""" + + session_id: Optional[str] + node: Optional[NodeMetadata] + + +def build_invokeai_metadata_pnginfo( + metadata: InvokeAIMetadata | None, +) -> PngImagePlugin.PngInfo: + """Builds a PngInfo object with key `"invokeai"` and value `metadata`""" + pnginfo = PngImagePlugin.PngInfo() + + if metadata is not None: + pnginfo.add_text("invokeai", json.dumps(metadata)) + + return pnginfo + + +class MetadataServiceBase(ABC): + @abstractmethod + def get_metadata(self, image: Image.Image) -> InvokeAIMetadata | None: + """Gets the InvokeAI metadata from a PIL Image, skipping invalid values""" + pass + + @abstractmethod + def build_metadata( + self, session_id: str, node: BaseModel + ) -> InvokeAIMetadata | None: + """Builds an InvokeAIMetadata object""" + pass + + +class PngMetadataService(MetadataServiceBase): + """Handles loading and building metadata for images.""" + + # TODO: Use `InvocationsUnion` to **validate** metadata as representing a fully-functioning node + def _load_metadata(self, image: Image.Image) -> dict | None: + """Loads a specific info entry from a PIL Image.""" + + try: + info = image.info.get("invokeai") + + if type(info) is not str: + return None + + loaded_metadata = json.loads(info) + + if type(loaded_metadata) is not dict: + return None + + if len(loaded_metadata.items()) == 0: + return None + + return loaded_metadata + except: + return None + + def get_metadata(self, image: Image.Image) -> dict | None: + """Retrieves an image's metadata as a dict""" + loaded_metadata = self._load_metadata(image) + + return loaded_metadata + + def build_metadata(self, session_id: str, node: BaseModel) -> InvokeAIMetadata: + metadata = InvokeAIMetadata(session_id=session_id, node=node.dict()) + + return metadata diff --git a/invokeai/app/services/processor.py b/invokeai/app/services/processor.py index 0125d7eb62..c622906750 100644 --- a/invokeai/app/services/processor.py +++ b/invokeai/app/services/processor.py @@ -43,10 +43,14 @@ class DefaultInvocationProcessor(InvocationProcessorABC): queue_item.invocation_id ) + # 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( graph_execution_state_id=graph_execution_state.id, - invocation_id=invocation.id, + node=invocation.dict(), + source_node_id=source_node_id ) # Invoke @@ -75,7 +79,8 @@ class DefaultInvocationProcessor(InvocationProcessorABC): # Send complete event self.__invoker.services.events.emit_invocation_complete( graph_execution_state_id=graph_execution_state.id, - invocation_id=invocation.id, + node=invocation.dict(), + source_node_id=source_node_id, result=outputs.dict(), ) @@ -99,7 +104,8 @@ class DefaultInvocationProcessor(InvocationProcessorABC): # Send error event self.__invoker.services.events.emit_invocation_error( graph_execution_state_id=graph_execution_state.id, - invocation_id=invocation.id, + node=invocation.dict(), + source_node_id=source_node_id, error=error, ) diff --git a/invokeai/app/services/sqlite.py b/invokeai/app/services/sqlite.py index e06ca8c1ac..fd089014bb 100644 --- a/invokeai/app/services/sqlite.py +++ b/invokeai/app/services/sqlite.py @@ -35,7 +35,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._create_table() def _create_table(self): - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""CREATE TABLE IF NOT EXISTS {self._table_name} ( item TEXT, @@ -44,27 +45,34 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._cursor.execute( f"""CREATE UNIQUE INDEX IF NOT EXISTS {self._table_name}_id ON {self._table_name}(id);""" ) - self._conn.commit() + finally: + self._lock.release() def _parse_item(self, item: str) -> T: item_type = get_args(self.__orig_class__)[0] return parse_raw_as(item_type, item) def set(self, item: T): - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""INSERT OR REPLACE INTO {self._table_name} (item) VALUES (?);""", (item.json(),), ) self._conn.commit() + finally: + self._lock.release() self._on_changed(item) def get(self, id: str) -> Union[T, None]: - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""SELECT item FROM {self._table_name} WHERE id = ?;""", (str(id),) ) result = self._cursor.fetchone() + finally: + self._lock.release() if not result: return None @@ -72,15 +80,19 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): return self._parse_item(result[0]) def delete(self, id: str): - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""DELETE FROM {self._table_name} WHERE id = ?;""", (str(id),) ) self._conn.commit() + finally: + self._lock.release() self._on_deleted(id) def list(self, page: int = 0, per_page: int = 10) -> PaginatedResults[T]: - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""SELECT item FROM {self._table_name} LIMIT ? OFFSET ?;""", (per_page, page * per_page), @@ -91,6 +103,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): self._cursor.execute(f"""SELECT count(*) FROM {self._table_name};""") count = self._cursor.fetchone()[0] + finally: + self._lock.release() pageCount = int(count / per_page) + 1 @@ -101,7 +115,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): def search( self, query: str, page: int = 0, per_page: int = 10 ) -> PaginatedResults[T]: - with self._lock: + try: + self._lock.acquire() self._cursor.execute( f"""SELECT item FROM {self._table_name} WHERE item LIKE ? LIMIT ? OFFSET ?;""", (f"%{query}%", per_page, page * per_page), @@ -115,6 +130,8 @@ class SqliteItemStorage(ItemStorageABC, Generic[T]): (f"%{query}%",), ) count = self._cursor.fetchone()[0] + finally: + self._lock.release() pageCount = int(count / per_page) + 1 diff --git a/invokeai/app/util/misc.py b/invokeai/app/util/misc.py new file mode 100644 index 0000000000..b2b57bd086 --- /dev/null +++ b/invokeai/app/util/misc.py @@ -0,0 +1,5 @@ +import datetime + + +def get_timestamp(): + return int(datetime.datetime.now(datetime.timezone.utc).timestamp()) diff --git a/invokeai/app/util/save_thumbnail.py b/invokeai/app/util/save_thumbnail.py deleted file mode 100644 index 86fdbe7ef6..0000000000 --- a/invokeai/app/util/save_thumbnail.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -from PIL import Image - - -def save_thumbnail( - image: Image.Image, - filename: str, - path: str, - size: int = 256, -) -> str: - """ - Saves a thumbnail of an image, returning its path. - """ - base_filename = os.path.splitext(filename)[0] - thumbnail_path = os.path.join(path, base_filename + ".webp") - - if os.path.exists(thumbnail_path): - return thumbnail_path - - image_copy = image.copy() - image_copy.thumbnail(size=(size, size)) - - image_copy.save(thumbnail_path, "WEBP") - - return thumbnail_path diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py index 466f78ddb0..963e770406 100644 --- a/invokeai/app/util/step_callback.py +++ b/invokeai/app/util/step_callback.py @@ -1,16 +1,41 @@ -import torch +from invokeai.app.api.models.images import ProgressImage +from invokeai.app.models.exceptions import CanceledException from ..invocations.baseinvocation import InvocationContext from ...backend.util.util import image_to_dataURL from ...backend.generator.base import Generator from ...backend.stable_diffusion import PipelineIntermediateState -def fast_latents_step_callback( - sample: torch.Tensor, - step: int, - steps: int, - id: str, + +def stable_diffusion_step_callback( context: InvocationContext, + intermediate_state: PipelineIntermediateState, + node: dict, + source_node_id: str, ): + if context.services.queue.is_canceled(context.graph_execution_state_id): + raise CanceledException + + # Some schedulers report not only the noisy latents at the current timestep, + # but also their estimate so far of what the de-noised latents will be. Use + # that estimate if it is available. + if intermediate_state.predicted_original is not None: + sample = intermediate_state.predicted_original + else: + sample = intermediate_state.latents + + # TODO: This does not seem to be needed any more? + # # txt2img provides a Tensor in the step_callback + # # img2img provides a PipelineIntermediateState + # if isinstance(sample, PipelineIntermediateState): + # # this was an img2img + # print('img2img') + # latents = sample.latents + # step = sample.step + # else: + # print('txt2img') + # latents = sample + # step = intermediate_state.step + # TODO: only output a preview image when requested image = Generator.sample_to_lowres_estimated_image(sample) @@ -21,23 +46,10 @@ def fast_latents_step_callback( dataURL = image_to_dataURL(image, image_format="JPEG") context.services.events.emit_generator_progress( - context.graph_execution_state_id, - id, - {"width": width, "height": height, "dataURL": dataURL}, - step, - steps, + graph_execution_state_id=context.graph_execution_state_id, + node=node, + source_node_id=source_node_id, + progress_image=ProgressImage(width=width, height=height, dataURL=dataURL), + step=intermediate_state.step, + total_steps=node["steps"], ) - - -def diffusers_step_callback_adapter(*cb_args, **kwargs): - """ - txt2img gives us a Tensor in the step_callbak, while img2img gives us a PipelineIntermediateState. - This adapter grabs the needed data and passes it along to the callback function. - """ - if isinstance(cb_args[0], PipelineIntermediateState): - progress_state: PipelineIntermediateState = cb_args[0] - return fast_latents_step_callback( - progress_state.latents, progress_state.step, **kwargs - ) - else: - return fast_latents_step_callback(*cb_args, **kwargs) diff --git a/invokeai/app/util/thumbnails.py b/invokeai/app/util/thumbnails.py new file mode 100644 index 0000000000..42a6fe9962 --- /dev/null +++ b/invokeai/app/util/thumbnails.py @@ -0,0 +1,15 @@ +import os +from PIL import Image + + +def get_thumbnail_name(image_name: str) -> str: + """Formats given an image name, returns the appropriate thumbnail image name""" + thumbnail_name = os.path.splitext(image_name)[0] + ".webp" + return thumbnail_name + + +def make_thumbnail(image: Image.Image, size: int = 256) -> Image.Image: + """Makes a thumbnail from a PIL Image""" + thumbnail = image.copy() + thumbnail.thumbnail(size=(size, size)) + return thumbnail diff --git a/invokeai/frontend/web/.eslintignore b/invokeai/frontend/web/.eslintignore index 99d8bab48c..b351fc6a96 100644 --- a/invokeai/frontend/web/.eslintignore +++ b/invokeai/frontend/web/.eslintignore @@ -6,3 +6,5 @@ stats.html index.html .yarn/ *.scss +src/services/api/ +src/services/fixtures/* diff --git a/invokeai/frontend/web/.prettierignore b/invokeai/frontend/web/.prettierignore index 905f177fde..b351fc6a96 100644 --- a/invokeai/frontend/web/.prettierignore +++ b/invokeai/frontend/web/.prettierignore @@ -3,4 +3,8 @@ dist/ node_modules/ patches/ stats.html +index.html .yarn/ +*.scss +src/services/api/ +src/services/fixtures/* diff --git a/invokeai/frontend/web/docs/API_CLIENT.md b/invokeai/frontend/web/docs/API_CLIENT.md new file mode 100644 index 0000000000..51f3a6510c --- /dev/null +++ b/invokeai/frontend/web/docs/API_CLIENT.md @@ -0,0 +1,87 @@ +# Generated axios API client + +- [Generated axios API client](#generated-axios-api-client) + - [Generation](#generation) + - [Generate the API client from the nodes web server](#generate-the-api-client-from-the-nodes-web-server) + - [Generate the API client from JSON](#generate-the-api-client-from-json) + - [Getting the JSON from the nodes web server](#getting-the-json-from-the-nodes-web-server) + - [Getting the JSON with a python script](#getting-the-json-with-a-python-script) + - [Generate the API client](#generate-the-api-client) + - [The generated client](#the-generated-client) + - [API client customisation](#api-client-customisation) + +This API client is generated by an [openapi code generator](https://github.com/ferdikoomen/openapi-typescript-codegen). + +All files in `invokeai/frontend/web/src/services/api/` are made by the generator. + +## Generation + +The axios client may be generated by from the OpenAPI schema from the nodes web server, or from JSON. + +### Generate the API client from the nodes web server + +We need to start the nodes web server, which serves the OpenAPI schema to the generator. + +1. Start the nodes web server. + +```bash +# from the repo root +python scripts/invoke-new.py --web +``` + +2. Generate the API client. + +```bash +# from invokeai/frontend/web/ +yarn api:web +``` + +### Generate the API client from JSON + +The JSON can be acquired from the nodes web server, or with a python script. + +#### Getting the JSON from the nodes web server + +Start the nodes web server as described above, then download the file. + +```bash +# from invokeai/frontend/web/ +curl http://localhost:9090/openapi.json -o openapi.json +``` + +#### Getting the JSON with a python script + +Run this python script from the repo root, so it can access the nodes server modules. + +The script will output `openapi.json` in the repo root. Then we need to move it to `invokeai/frontend/web/`. + +```bash +# from the repo root +python invokeai/app/util/generate_openapi_json.py +mv invokeai/app/util/openapi.json invokeai/frontend/web/services/fixtures/ +``` + +#### Generate the API client + +Now we can generate the API client from the JSON. + +```bash +# from invokeai/frontend/web/ +yarn api:file +``` + +## The generated client + +The client will be written to `invokeai/frontend/web/services/api/`: + +- `axios` client +- TS types +- An easily parseable schema, which we can use to generate UI + +## API client customisation + +The generator has a default `request.ts` file that implements a base `axios` client. The generated client uses this base client. + +One shortcoming of this is base client is it does not provide response headers unless the response body is empty. To fix this, we provide our own lightly-patched `request.ts`. + +To access the headers, call `getHeaders(response)` on any response from the generated api client. This function is exported from `invokeai/frontend/web/src/services/util/getHeaders.ts`. diff --git a/invokeai/frontend/web/docs/EVENTS.md b/invokeai/frontend/web/docs/EVENTS.md new file mode 100644 index 0000000000..24f2497a20 --- /dev/null +++ b/invokeai/frontend/web/docs/EVENTS.md @@ -0,0 +1,21 @@ +# Events + +Events via `socket.io` + +## `actions.ts` + +Redux actions for all socket events. Payloads all include a timestamp, and optionally some other data. + +Any reducer (or middleware) can respond to the actions. + +## `middleware.ts` + +Redux middleware for events. + +Handles dispatching the event actions. Only put logic here if it can't really go anywhere else. + +For example, on connect we want to load images to the gallery if it's not populated. This requires dispatching a thunk, so we need to directly dispatch this in the middleware. + +## `types.ts` + +Hand-written types for the socket events. Cannot generate these from the server, but fortunately they are few and simple. diff --git a/invokeai/frontend/web/docs/NODE_EDITOR.md b/invokeai/frontend/web/docs/NODE_EDITOR.md new file mode 100644 index 0000000000..0b4fbcbc81 --- /dev/null +++ b/invokeai/frontend/web/docs/NODE_EDITOR.md @@ -0,0 +1,17 @@ +# Node Editor Design + +WIP + +nodes + +everything in `src/features/nodes/` + +have a look at `state.nodes.invocation` + +- on socket connect, if no schema saved, fetch `localhost:9090/openapi.json`, save JSON to `state.nodes.schema` +- on fulfilled schema fetch, `parseSchema()` the schema. this outputs a `Record` which is saved to `state.nodes.invocations` - `Invocation` is like a template for the node +- when you add a node, the the `Invocation` template is passed to `InvocationComponent.tsx` to build the UI component for that node +- inputs/outputs have field types - and each field type gets an `FieldComponent` which includes a dispatcher to write state changes to redux `nodesSlice` +- `reactflow` sends changes to nodes/edges to redux +- to invoke, `buildNodesGraph()` state, then send this +- changed onClick Invoke button actions to build the schema, then when schema builds it dispatches the actual network request to create the session - see `session.ts` diff --git a/invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md b/invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md new file mode 100644 index 0000000000..90d85bb540 --- /dev/null +++ b/invokeai/frontend/web/docs/PACKAGE_SCRIPTS.md @@ -0,0 +1,29 @@ +# Package Scripts + +WIP walkthrough of `package.json` scripts. + +## `theme` & `theme:watch` + +These run the Chakra CLI to generate types for the theme, or watch for code change and re-generate the types. + +The CLI essentially monkeypatches Chakra's files in `node_modules`. + +## `postinstall` + +The `postinstall` script patches a few packages and runs the Chakra CLI to generate types for the theme. + +### Patch `@chakra-ui/cli` + +See: + +### Patch `redux-persist` + +We want to persist the canvas state to `localStorage` but many canvas operations change data very quickly, so we need to debounce the writes to `localStorage`. + +`redux-persist` is unfortunately unmaintained. The repo's current code is nonfunctional, but the last release's code depends on a package that was removed from `npm` for being malware, so we cannot just fork it. + +So, we have to patch it directly. Perhaps a better way would be to write a debounced storage adapter, but I couldn't figure out how to do that. + +### Patch `redux-deep-persist` + +This package makes blacklisting and whitelisting persist configs very simple, but we have to patch it to match `redux-persist` for the types to work. diff --git a/invokeai/frontend/web/README.md b/invokeai/frontend/web/docs/README.md similarity index 88% rename from invokeai/frontend/web/README.md rename to invokeai/frontend/web/docs/README.md index ef8c503550..787725cdda 100644 --- a/invokeai/frontend/web/README.md +++ b/invokeai/frontend/web/docs/README.md @@ -1,10 +1,16 @@ # InvokeAI Web UI +- [InvokeAI Web UI](#invokeai-web-ui) + - [Stack](#stack) + - [Contributing](#contributing) + - [Dev Environment](#dev-environment) + - [Production builds](#production-builds) + The UI is a fairly straightforward Typescript React app. The only really fancy stuff is the Unified Canvas. Code in `invokeai/frontend/web/` if you want to have a look. -## Details +## Stack State management is Redux via [Redux Toolkit](https://github.com/reduxjs/redux-toolkit). Communication with server is a mix of HTTP and [socket.io](https://github.com/socketio/socket.io-client) (with a custom redux middleware to help). @@ -32,7 +38,7 @@ Start everything in dev mode: 1. Start the dev server: `yarn dev` 2. Start the InvokeAI UI per usual: `invokeai --web` -3. Point your browser to the dev server address e.g. `http://localhost:5173/` +3. Point your browser to the dev server address e.g. ### Production builds diff --git a/invokeai/frontend/web/index.d.ts b/invokeai/frontend/web/index.d.ts index a3ab75d17c..af21f29231 100644 --- a/invokeai/frontend/web/index.d.ts +++ b/invokeai/frontend/web/index.d.ts @@ -1,6 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { IAIPopoverProps } from '../web/src/common/components/IAIPopover'; import { IAIIconButtonProps } from '../web/src/common/components/IAIIconButton'; +import { InvokeTabName } from 'features/ui/store/tabMap'; export {}; @@ -64,9 +65,25 @@ declare module '@invoke-ai/invoke-ai-ui' { declare class SettingsModal extends React.Component { public constructor(props: SettingsModalProps); } + + declare class StatusIndicator extends React.Component { + public constructor(props: StatusIndicatorProps); + } + + declare class ModelSelect extends React.Component { + public constructor(props: ModelSelectProps); + } } -declare function Invoke(props: PropsWithChildren): JSX.Element; +interface InvokeProps extends PropsWithChildren { + apiUrl?: string; + disabledPanels?: string[]; + disabledTabs?: InvokeTabName[]; + token?: string; + shouldTransformUrls?: boolean; +} + +declare function Invoke(props: InvokeProps): JSX.Element; export { ThemeChanger, @@ -74,5 +91,7 @@ export { IAIPopover, IAIIconButton, SettingsModal, + StatusIndicator, + ModelSelect, }; export = Invoke; diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index c47948746d..cecba05d6a 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -5,7 +5,10 @@ "scripts": { "prepare": "cd ../../../ && husky install invokeai/frontend/web/.husky", "dev": "concurrently \"vite dev\" \"yarn run theme:watch\"", + "dev:nodes": "concurrently \"vite dev --mode nodes\" \"yarn run theme:watch\"", "build": "yarn run lint && vite build", + "api:web": "openapi -i http://localhost:9090/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --exportSchemas true --indent 2 --request src/services/fixtures/request.ts", + "api:file": "openapi -i src/services/fixtures/openapi.json -o src/services/api --client axios --useOptions --useUnionTypes --exportSchemas true --indent 2 --request src/services/fixtures/request.ts", "preview": "vite preview", "lint:madge": "madge --circular src/main.tsx", "lint:eslint": "eslint --max-warnings=0 .", @@ -41,9 +44,11 @@ "@chakra-ui/react": "^2.5.1", "@chakra-ui/styled-system": "^2.6.1", "@chakra-ui/theme-tools": "^2.0.16", + "@dagrejs/graphlib": "^2.1.12", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", - "@reduxjs/toolkit": "^1.9.2", + "@fontsource/inter": "^4.5.15", + "@reduxjs/toolkit": "^1.9.3", "chakra-ui-contextmenu": "^1.0.5", "dateformat": "^5.0.3", "formik": "^2.2.9", @@ -67,15 +72,17 @@ "react-redux": "^8.0.5", "react-transition-group": "^4.4.5", "react-zoom-pan-pinch": "^2.6.1", + "reactflow": "^11.7.0", "redux-deep-persist": "^1.0.7", + "redux-dynamic-middlewares": "^2.2.0", "redux-persist": "^6.0.0", "socket.io-client": "^4.6.0", "use-image": "^1.1.0", "uuid": "^9.0.0" }, "devDependencies": { - "@fontsource/inter": "^4.5.15", "@types/dateformat": "^5.0.0", + "@types/lodash": "^4.14.194", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/react-transition-group": "^4.4.5", @@ -83,6 +90,7 @@ "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", "@vitejs/plugin-react-swc": "^3.2.0", + "axios": "^1.3.4", "babel-plugin-transform-imports": "^2.0.0", "concurrently": "^7.6.0", "eslint": "^8.34.0", @@ -90,13 +98,17 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "form-data": "^4.0.0", "husky": "^8.0.3", "lint-staged": "^13.1.2", "madge": "^6.0.0", + "openapi-types": "^12.1.0", + "openapi-typescript-codegen": "^0.23.0", "postinstall-postinstall": "^2.1.0", "prettier": "^2.8.4", "rollup-plugin-visualizer": "^5.9.0", "terser": "^5.16.4", + "typescript": "4.9.5", "vite": "^4.1.2", "vite-plugin-eslint": "^1.8.1", "vite-tsconfig-paths": "^4.0.5", diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a99f54d741..0bb6b49f8a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -52,6 +52,7 @@ "txt2img": "Text To Image", "img2img": "Image To Image", "unifiedCanvas": "Unified Canvas", + "linear": "Linear", "nodes": "Nodes", "postprocessing": "Post Processing", "nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.", @@ -524,6 +525,10 @@ "resetComplete": "Web UI has been reset. Refresh the page to reload." }, "toast": { + "serverError": "Server Error", + "disconnected": "Disconnected from Server", + "connected": "Connected to Server", + "canceled": "Processing Canceled", "tempFoldersEmptied": "Temp Folder Emptied", "uploadFailed": "Upload failed", "uploadFailedMultipleImagesDesc": "Multiple images pasted, may only upload one image at a time", diff --git a/invokeai/frontend/web/src/app/App.tsx b/invokeai/frontend/web/src/app/App.tsx index 40c15b38c0..1e86c5f222 100644 --- a/invokeai/frontend/web/src/app/App.tsx +++ b/invokeai/frontend/web/src/app/App.tsx @@ -13,16 +13,42 @@ import { Box, Flex, Grid, Portal, useColorMode } from '@chakra-ui/react'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel'; import Lightbox from 'features/lightbox/components/Lightbox'; -import { useAppSelector } from './storeHooks'; +import { useAppDispatch, useAppSelector } from './storeHooks'; import { PropsWithChildren, useEffect } from 'react'; +import { setDisabledPanels, setDisabledTabs } from 'features/ui/store/uiSlice'; +import { InvokeTabName } from 'features/ui/store/tabMap'; +import { shouldTransformUrlsChanged } from 'features/system/store/systemSlice'; keepGUIAlive(); -const App = (props: PropsWithChildren) => { +interface Props extends PropsWithChildren { + options: { + disabledPanels: string[]; + disabledTabs: InvokeTabName[]; + shouldTransformUrls?: boolean; + }; +} + +const App = (props: Props) => { useToastWatcher(); const currentTheme = useAppSelector((state) => state.ui.currentTheme); const { setColorMode } = useColorMode(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setDisabledPanels(props.options.disabledPanels)); + }, [dispatch, props.options.disabledPanels]); + + useEffect(() => { + dispatch(setDisabledTabs(props.options.disabledTabs)); + }, [dispatch, props.options.disabledTabs]); + + useEffect(() => { + dispatch( + shouldTransformUrlsChanged(Boolean(props.options.shouldTransformUrls)) + ); + }, [dispatch, props.options.shouldTransformUrls]); useEffect(() => { setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark'); diff --git a/invokeai/frontend/web/src/app/invokeai.d.ts b/invokeai/frontend/web/src/app/invokeai.d.ts index e01e414d03..f98ca73675 100644 --- a/invokeai/frontend/web/src/app/invokeai.d.ts +++ b/invokeai/frontend/web/src/app/invokeai.d.ts @@ -14,6 +14,8 @@ import { InvokeTabName } from 'features/ui/store/tabMap'; import { IRect } from 'konva/lib/types'; +import { ImageMetadata, ImageType } from 'services/api'; +import { AnyInvocation } from 'services/events/types'; /** * TODO: @@ -113,7 +115,7 @@ export declare type Metadata = SystemGenerationMetadata & { }; // An Image has a UUID, url, modified timestamp, width, height and maybe metadata -export declare type Image = { +export declare type _Image = { uuid: string; url: string; thumbnail: string; @@ -124,11 +126,23 @@ export declare type Image = { category: GalleryCategory; isBase64?: boolean; dreamPrompt?: 'string'; + name?: string; +}; + +/** + * ResultImage + */ +export declare type Image = { + name: string; + type: ImageType; + url: string; + thumbnail: string; + metadata: ImageMetadata; }; // GalleryImages is an array of Image. export declare type GalleryImages = { - images: Array; + images: Array<_Image>; }; /** @@ -275,7 +289,7 @@ export declare type SystemStatusResponse = SystemStatus; export declare type SystemConfigResponse = SystemConfig; -export declare type ImageResultResponse = Omit & { +export declare type ImageResultResponse = Omit<_Image, 'uuid'> & { boundingBox?: IRect; generationMode: InvokeTabName; }; @@ -296,7 +310,7 @@ export declare type ErrorResponse = { }; export declare type GalleryImagesResponse = { - images: Array>; + images: Array>; areMoreImagesAvailable: boolean; category: GalleryCategory; }; diff --git a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts index cc85c3ca6c..82672756c8 100644 --- a/invokeai/frontend/web/src/app/selectors/readinessSelector.ts +++ b/invokeai/frontend/web/src/app/selectors/readinessSelector.ts @@ -20,6 +20,7 @@ export const readinessSelector = createSelector( seedWeights, initialImage, seed, + isImageToImageEnabled, } = generation; const { isProcessing, isConnected } = system; @@ -33,7 +34,7 @@ export const readinessSelector = createSelector( reasonsWhyNotReady.push('Missing prompt'); } - if (activeTabName === 'img2img' && !initialImage) { + if (isImageToImageEnabled && !initialImage) { isReady = false; reasonsWhyNotReady.push('No initial image selected'); } diff --git a/invokeai/frontend/web/src/app/socketio/actions.ts b/invokeai/frontend/web/src/app/socketio/actions.ts index 57758d1914..4907595c75 100644 --- a/invokeai/frontend/web/src/app/socketio/actions.ts +++ b/invokeai/frontend/web/src/app/socketio/actions.ts @@ -13,9 +13,13 @@ import { InvokeTabName } from 'features/ui/store/tabMap'; export const generateImage = createAction( 'socketio/generateImage' ); -export const runESRGAN = createAction('socketio/runESRGAN'); -export const runFacetool = createAction('socketio/runFacetool'); -export const deleteImage = createAction('socketio/deleteImage'); +export const runESRGAN = createAction('socketio/runESRGAN'); +export const runFacetool = createAction( + 'socketio/runFacetool' +); +export const deleteImage = createAction( + 'socketio/deleteImage' +); export const requestImages = createAction( 'socketio/requestImages' ); diff --git a/invokeai/frontend/web/src/app/socketio/emitters.ts b/invokeai/frontend/web/src/app/socketio/emitters.ts index 2aa1e03552..cd25319aee 100644 --- a/invokeai/frontend/web/src/app/socketio/emitters.ts +++ b/invokeai/frontend/web/src/app/socketio/emitters.ts @@ -91,7 +91,7 @@ const makeSocketIOEmitters = ( }) ); }, - emitRunESRGAN: (imageToProcess: InvokeAI.Image) => { + emitRunESRGAN: (imageToProcess: InvokeAI._Image) => { dispatch(setIsProcessing(true)); const { @@ -119,7 +119,7 @@ const makeSocketIOEmitters = ( }) ); }, - emitRunFacetool: (imageToProcess: InvokeAI.Image) => { + emitRunFacetool: (imageToProcess: InvokeAI._Image) => { dispatch(setIsProcessing(true)); const { @@ -150,7 +150,7 @@ const makeSocketIOEmitters = ( }) ); }, - emitDeleteImage: (imageToDelete: InvokeAI.Image) => { + emitDeleteImage: (imageToDelete: InvokeAI._Image) => { const { url, uuid, category, thumbnail } = imageToDelete; dispatch(removeImage(imageToDelete)); socketio.emit('deleteImage', url, thumbnail, uuid, category); diff --git a/invokeai/frontend/web/src/app/socketio/listeners.ts b/invokeai/frontend/web/src/app/socketio/listeners.ts index 08de671260..dc6c35d862 100644 --- a/invokeai/frontend/web/src/app/socketio/listeners.ts +++ b/invokeai/frontend/web/src/app/socketio/listeners.ts @@ -34,8 +34,9 @@ import type { RootState } from 'app/store'; import { addImageToStagingArea } from 'features/canvas/store/canvasSlice'; import { clearInitialImage, + initialImageSelected, setInfillMethod, - setInitialImage, + // setInitialImage, setMaskPath, } from 'features/parameters/store/generationSlice'; import { tabMap } from 'features/ui/store/tabMap'; @@ -142,15 +143,17 @@ const makeSocketIOListeners = ( } } - if (shouldLoopback) { - const activeTabName = tabMap[activeTab]; - switch (activeTabName) { - case 'img2img': { - dispatch(setInitialImage(newImage)); - break; - } - } - } + // TODO: fix + // if (shouldLoopback) { + // const activeTabName = tabMap[activeTab]; + // switch (activeTabName) { + // case 'img2img': { + // dispatch(initialImageSelected(newImage.uuid)); + // // dispatch(setInitialImage(newImage)); + // break; + // } + // } + // } dispatch(clearIntermediateImage()); @@ -262,7 +265,7 @@ const makeSocketIOListeners = ( */ // Generate a UUID for each image - const preparedImages = images.map((image): InvokeAI.Image => { + const preparedImages = images.map((image): InvokeAI._Image => { return { uuid: uuidv4(), ...image, @@ -334,7 +337,7 @@ const makeSocketIOListeners = ( if ( initialImage === url || - (initialImage as InvokeAI.Image)?.url === url + (initialImage as InvokeAI._Image)?.url === url ) { dispatch(clearInitialImage()); } diff --git a/invokeai/frontend/web/src/app/socketio/middleware.ts b/invokeai/frontend/web/src/app/socketio/middleware.ts index a28e4edc80..46dafc7656 100644 --- a/invokeai/frontend/web/src/app/socketio/middleware.ts +++ b/invokeai/frontend/web/src/app/socketio/middleware.ts @@ -29,6 +29,8 @@ export const socketioMiddleware = () => { path: `${window.location.pathname}socket.io`, }); + socketio.disconnect(); + let areListenersSet = false; const middleware: Middleware = (store) => (next) => (action) => { diff --git a/invokeai/frontend/web/src/app/store.ts b/invokeai/frontend/web/src/app/store.ts index 29dbff3fba..3e046d8ed9 100644 --- a/invokeai/frontend/web/src/app/store.ts +++ b/invokeai/frontend/web/src/app/store.ts @@ -2,18 +2,32 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web - +import dynamicMiddlewares from 'redux-dynamic-middlewares'; import { getPersistConfig } from 'redux-deep-persist'; import canvasReducer from 'features/canvas/store/canvasSlice'; import galleryReducer from 'features/gallery/store/gallerySlice'; +import resultsReducer from 'features/gallery/store/resultsSlice'; +import uploadsReducer from 'features/gallery/store/uploadsSlice'; import lightboxReducer from 'features/lightbox/store/lightboxSlice'; import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import systemReducer from 'features/system/store/systemSlice'; import uiReducer from 'features/ui/store/uiSlice'; +import modelsReducer from 'features/system/store/modelSlice'; +import nodesReducer from 'features/nodes/store/nodesSlice'; import { socketioMiddleware } from './socketio/middleware'; +import { socketMiddleware } from 'services/events/middleware'; +import { canvasBlacklist } from 'features/canvas/store/canvasPersistBlacklist'; +import { galleryBlacklist } from 'features/gallery/store/galleryPersistBlacklist'; +import { generationBlacklist } from 'features/parameters/store/generationPersistBlacklist'; +import { lightboxBlacklist } from 'features/lightbox/store/lightboxPersistBlacklist'; +import { modelsBlacklist } from 'features/system/store/modelsPersistBlacklist'; +import { nodesBlacklist } from 'features/nodes/store/nodesPersistBlacklist'; +import { postprocessingBlacklist } from 'features/parameters/store/postprocessingPersistBlacklist'; +import { systemBlacklist } from 'features/system/store/systemPersistsBlacklist'; +import { uiBlacklist } from 'features/ui/store/uiPersistBlacklist'; /** * redux-persist provides an easy and reliable way to persist state across reloads. @@ -29,49 +43,18 @@ import { socketioMiddleware } from './socketio/middleware'; * The necesssary nested persistors with blacklists are configured below. */ -const canvasBlacklist = [ - 'cursorPosition', - 'isCanvasInitialized', - 'doesCanvasNeedScaling', -].map((blacklistItem) => `canvas.${blacklistItem}`); - -const systemBlacklist = [ - 'currentIteration', - 'currentStatus', - 'currentStep', - 'isCancelable', - 'isConnected', - 'isESRGANAvailable', - 'isGFPGANAvailable', - 'isProcessing', - 'socketId', - 'totalIterations', - 'totalSteps', - 'openModel', - 'cancelOptions.cancelAfter', -].map((blacklistItem) => `system.${blacklistItem}`); - -const galleryBlacklist = [ - 'categories', - 'currentCategory', - 'currentImage', - 'currentImageUuid', - 'shouldAutoSwitchToNewImages', - 'intermediateImage', -].map((blacklistItem) => `gallery.${blacklistItem}`); - -const lightboxBlacklist = ['isLightboxOpen'].map( - (blacklistItem) => `lightbox.${blacklistItem}` -); - const rootReducer = combineReducers({ - generation: generationReducer, - postprocessing: postprocessingReducer, - gallery: galleryReducer, - system: systemReducer, canvas: canvasReducer, - ui: uiReducer, + gallery: galleryReducer, + generation: generationReducer, lightbox: lightboxReducer, + models: modelsReducer, + nodes: nodesReducer, + postprocessing: postprocessingReducer, + results: resultsReducer, + system: systemReducer, + ui: uiReducer, + uploads: uploadsReducer, }); const rootPersistConfig = getPersistConfig({ @@ -80,23 +63,40 @@ const rootPersistConfig = getPersistConfig({ rootReducer, blacklist: [ ...canvasBlacklist, - ...systemBlacklist, ...galleryBlacklist, + ...generationBlacklist, ...lightboxBlacklist, + ...modelsBlacklist, + ...nodesBlacklist, + ...postprocessingBlacklist, + // ...resultsBlacklist, + 'results', + ...systemBlacklist, + ...uiBlacklist, + // ...uploadsBlacklist, + 'uploads', ], debounce: 300, }); const persistedReducer = persistReducer(rootPersistConfig, rootReducer); -// Continue with store setup +// TODO: rip the old middleware out when nodes is complete +export function buildMiddleware() { + if (import.meta.env.MODE === 'nodes' || import.meta.env.MODE === 'package') { + return socketMiddleware(); + } else { + return socketioMiddleware(); + } +} + export const store = configureStore({ reducer: persistedReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ immutableCheck: false, serializableCheck: false, - }).concat(socketioMiddleware()), + }).concat(dynamicMiddlewares), devTools: { // Uncommenting these very rapidly called actions makes the redux dev tools output much more readable actionsDenylist: [ diff --git a/invokeai/frontend/web/src/app/storeUtils.ts b/invokeai/frontend/web/src/app/storeUtils.ts new file mode 100644 index 0000000000..851c0ba09d --- /dev/null +++ b/invokeai/frontend/web/src/app/storeUtils.ts @@ -0,0 +1,8 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { AppDispatch, RootState } from './store'; + +// https://redux-toolkit.js.org/usage/usage-with-typescript#defining-a-pre-typed-createasyncthunk +export const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: RootState; + dispatch: AppDispatch; +}>(); diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index 03742c0100..189ef4f5ad 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -44,12 +44,10 @@ export type IAIFullSliderProps = { inputReadOnly?: boolean; withReset?: boolean; handleReset?: () => void; - isResetDisabled?: boolean; - isSliderDisabled?: boolean; - isInputDisabled?: boolean; tooltipSuffix?: string; hideTooltip?: boolean; isCompact?: boolean; + isDisabled?: boolean; sliderFormControlProps?: FormControlProps; sliderFormLabelProps?: FormLabelProps; sliderMarkProps?: Omit; @@ -80,10 +78,8 @@ const IAISlider = (props: IAIFullSliderProps) => { withReset = false, hideTooltip = false, isCompact = false, + isDisabled = false, handleReset, - isResetDisabled, - isSliderDisabled, - isInputDisabled, sliderFormControlProps, sliderFormLabelProps, sliderMarkProps, @@ -149,6 +145,7 @@ const IAISlider = (props: IAIFullSliderProps) => { } : {} } + isDisabled={isDisabled} {...sliderFormControlProps} > @@ -166,15 +163,13 @@ const IAISlider = (props: IAIFullSliderProps) => { onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} focusThumbOnChange={false} - isDisabled={isSliderDisabled} - // width={width} + isDisabled={isDisabled} {...rest} > {withSliderMarks && ( <> { { value={localInputValue} onChange={handleInputChange} onBlur={handleInputBlur} - isDisabled={isInputDisabled} {...sliderNumberInputProps} > { aria-label={t('accessibility.reset')} tooltip="Reset" icon={} + isDisabled={isDisabled} onClick={handleResetDisable} - isDisabled={isResetDisabled} {...sliderIAIIconButtonProps} /> )} diff --git a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx new file mode 100644 index 0000000000..c006cf0c6b --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx @@ -0,0 +1,79 @@ +import { Badge, Box, ButtonGroup, Flex } from '@chakra-ui/react'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { useCallback } from 'react'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { FaUndo, FaUpload } from 'react-icons/fa'; +import { useTranslation } from 'react-i18next'; +import { Image } from 'app/invokeai'; + +type ImageToImageOverlayProps = { + setIsLoaded: (isLoaded: boolean) => void; + image: Image; +}; + +const ImageToImageOverlay = ({ + setIsLoaded, + image, +}: ImageToImageOverlayProps) => { + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const handleResetInitialImage = useCallback(() => { + dispatch(clearInitialImage()); + setIsLoaded(false); + }, [dispatch, setIsLoaded]); + + return ( + + + } + aria-label={t('accessibility.reset')} + onClick={handleResetInitialImage} + /> + } + aria-label={t('common.upload')} + /> + + + + {image.metadata?.width} × {image.metadata?.height} + + + + ); +}; + +export default ImageToImageOverlay; diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx index c4f4dca9df..beaa5f02d2 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx @@ -2,7 +2,6 @@ import { Box, useToast } from '@chakra-ui/react'; import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import useImageUploader from 'common/hooks/useImageUploader'; -import { uploadImage } from 'features/gallery/store/thunks/uploadImage'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { ResourceKey } from 'i18next'; import { @@ -15,6 +14,7 @@ import { } from 'react'; import { FileRejection, useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; +import { imageUploaded } from 'services/thunks/image'; import ImageUploadOverlay from './ImageUploadOverlay'; type ImageUploaderProps = { @@ -49,7 +49,7 @@ const ImageUploader = (props: ImageUploaderProps) => { const fileAcceptedCallback = useCallback( async (file: File) => { - dispatch(uploadImage({ imageFile: file })); + dispatch(imageUploaded({ formData: { file } })); }, [dispatch] ); @@ -124,7 +124,7 @@ const ImageUploader = (props: ImageUploaderProps) => { return; } - dispatch(uploadImage({ imageFile: file })); + dispatch(imageUploaded({ formData: { file } })); }; document.addEventListener('paste', pasteImageListener); return () => { diff --git a/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx new file mode 100644 index 0000000000..c52cf75f9f --- /dev/null +++ b/invokeai/frontend/web/src/common/components/SelectImagePlaceholder.tsx @@ -0,0 +1,12 @@ +import { Flex, Icon } from '@chakra-ui/react'; +import { FaImage } from 'react-icons/fa'; + +const SelectImagePlaceholder = () => { + return ( + + + + ); +}; + +export default SelectImagePlaceholder; diff --git a/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx b/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx index c86aa767dd..6129670d06 100644 --- a/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx +++ b/invokeai/frontend/web/src/common/components/WorkInProgress/NodesWIP.tsx @@ -1,27 +1,160 @@ -import { Flex, Heading, Text, VStack } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; -import WorkInProgress from './WorkInProgress'; +// import WorkInProgress from './WorkInProgress'; +// import ReactFlow, { +// applyEdgeChanges, +// applyNodeChanges, +// Background, +// Controls, +// Edge, +// Handle, +// Node, +// NodeTypes, +// OnEdgesChange, +// OnNodesChange, +// Position, +// } from 'reactflow'; -export default function NodesWIP() { - const { t } = useTranslation(); - return ( - - - {t('common.nodes')} - - {t('common.nodesDesc')} - - - - ); -} +// import 'reactflow/dist/style.css'; +// import { +// Fragment, +// FunctionComponent, +// ReactNode, +// useCallback, +// useMemo, +// useState, +// } from 'react'; +// import { OpenAPIV3 } from 'openapi-types'; +// import { filter, map, reduce } from 'lodash'; +// import { +// Box, +// Flex, +// FormControl, +// FormLabel, +// Input, +// Select, +// Switch, +// Text, +// NumberInput, +// NumberInputField, +// NumberInputStepper, +// NumberIncrementStepper, +// NumberDecrementStepper, +// Tooltip, +// chakra, +// Badge, +// Heading, +// VStack, +// HStack, +// Menu, +// MenuButton, +// MenuList, +// MenuItem, +// MenuItemOption, +// MenuGroup, +// MenuOptionGroup, +// MenuDivider, +// IconButton, +// } from '@chakra-ui/react'; +// import { FaPlus } from 'react-icons/fa'; +// import { +// FIELD_NAMES as FIELD_NAMES, +// FIELDS, +// INVOCATION_NAMES as INVOCATION_NAMES, +// INVOCATIONS, +// } from 'features/nodeEditor/constants'; + +// console.log('invocations', INVOCATIONS); + +// const nodeTypes = reduce( +// INVOCATIONS, +// (acc, val, key) => { +// acc[key] = val.component; +// return acc; +// }, +// {} as NodeTypes +// ); + +// console.log('nodeTypes', nodeTypes); + +// // make initial nodes one of every node for now +// let n = 0; +// const initialNodes = map(INVOCATIONS, (i) => ({ +// id: i.type, +// type: i.title, +// position: { x: (n += 20), y: (n += 20) }, +// data: {}, +// })); + +// console.log('initialNodes', initialNodes); + +// export default function NodesWIP() { +// const [nodes, setNodes] = useState([]); +// const [edges, setEdges] = useState([]); + +// const onNodesChange: OnNodesChange = useCallback( +// (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), +// [] +// ); + +// const onEdgesChange: OnEdgesChange = useCallback( +// (changes) => setEdges((eds: Edge[]) => applyEdgeChanges(changes, eds)), +// [] +// ); + +// return ( +// +// +// +// +// +// +// {FIELD_NAMES.map((field) => ( +// +// {field} +// +// ))} +// +// +// } +// sx={{ position: 'absolute', top: 2, left: 2 }} +// /> +// +// {INVOCATION_NAMES.map((name) => { +// const invocation = INVOCATIONS[name]; +// return ( +// +// {invocation.title} +// +// ); +// })} +// +// +// +// ); +// } + +export default {}; diff --git a/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx b/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx index deb9110d56..385796d53b 100644 --- a/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx +++ b/invokeai/frontend/web/src/common/components/WorkInProgress/WorkInProgress.tsx @@ -14,6 +14,8 @@ const WorkInProgress = (props: WorkInProgressProps) => { width: '100%', height: '100%', bg: 'base.850', + borderRadius: 'base', + position: 'relative', }} > {children} diff --git a/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts b/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts new file mode 100644 index 0000000000..584399233f --- /dev/null +++ b/invokeai/frontend/web/src/common/util/_parseMetadataZod.ts @@ -0,0 +1,119 @@ +/** + * PARTIAL ZOD IMPLEMENTATION + * + * doesn't work well bc like most validators, zod is not built to skip invalid values. + * it mostly works but just seems clearer and simpler to manually parse for now. + * + * in the future it would be really nice if we could use zod for some things: + * - zodios (axios + zod): https://github.com/ecyrbe/zodios + * - openapi to zodios: https://github.com/astahmer/openapi-zod-client + */ + +// import { z } from 'zod'; + +// const zMetadataStringField = z.string(); +// export type MetadataStringField = z.infer; + +// const zMetadataIntegerField = z.number().int(); +// export type MetadataIntegerField = z.infer; + +// const zMetadataFloatField = z.number(); +// export type MetadataFloatField = z.infer; + +// const zMetadataBooleanField = z.boolean(); +// export type MetadataBooleanField = z.infer; + +// const zMetadataImageField = z.object({ +// image_type: z.union([ +// z.literal('results'), +// z.literal('uploads'), +// z.literal('intermediates'), +// ]), +// image_name: z.string().min(1), +// }); +// export type MetadataImageField = z.infer; + +// const zMetadataLatentsField = z.object({ +// latents_name: z.string().min(1), +// }); +// export type MetadataLatentsField = z.infer; + +// /** +// * zod Schema for any node field. Use a `transform()` to manually parse, skipping invalid values. +// */ +// const zAnyMetadataField = z.any().transform((val, ctx) => { +// // Grab the field name from the path +// const fieldName = String(ctx.path[ctx.path.length - 1]); + +// // `id` and `type` must be strings if they exist +// if (['id', 'type'].includes(fieldName)) { +// const reservedStringPropertyResult = zMetadataStringField.safeParse(val); +// if (reservedStringPropertyResult.success) { +// return reservedStringPropertyResult.data; +// } + +// return; +// } + +// // Parse the rest of the fields, only returning the data if the parsing is successful + +// const stringFieldResult = zMetadataStringField.safeParse(val); +// if (stringFieldResult.success) { +// return stringFieldResult.data; +// } + +// const integerFieldResult = zMetadataIntegerField.safeParse(val); +// if (integerFieldResult.success) { +// return integerFieldResult.data; +// } + +// const floatFieldResult = zMetadataFloatField.safeParse(val); +// if (floatFieldResult.success) { +// return floatFieldResult.data; +// } + +// const booleanFieldResult = zMetadataBooleanField.safeParse(val); +// if (booleanFieldResult.success) { +// return booleanFieldResult.data; +// } + +// const imageFieldResult = zMetadataImageField.safeParse(val); +// if (imageFieldResult.success) { +// return imageFieldResult.data; +// } + +// const latentsFieldResult = zMetadataImageField.safeParse(val); +// if (latentsFieldResult.success) { +// return latentsFieldResult.data; +// } +// }); + +// /** +// * The node metadata schema. +// */ +// const zNodeMetadata = z.object({ +// session_id: z.string().min(1).optional(), +// node: z.record(z.string().min(1), zAnyMetadataField).optional(), +// }); + +// export type NodeMetadata = z.infer; + +// const zMetadata = z.object({ +// invokeai: zNodeMetadata.optional(), +// 'sd-metadata': z.record(z.string().min(1), z.any()).optional(), +// }); +// export type Metadata = z.infer; + +// export const parseMetadata = ( +// metadata: Record +// ): Metadata | undefined => { +// const result = zMetadata.safeParse(metadata); +// if (!result.success) { +// console.log(result.error.issues); +// return; +// } + +// return result.data; +// }; + +export default {}; diff --git a/invokeai/frontend/web/src/common/util/getTimestamp.ts b/invokeai/frontend/web/src/common/util/getTimestamp.ts new file mode 100644 index 0000000000..570283fa8f --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getTimestamp.ts @@ -0,0 +1,6 @@ +import dateFormat from 'dateformat'; + +/** + * Get a `now` timestamp with 1s precision, formatted as ISO datetime. + */ +export const getTimestamp = () => dateFormat(new Date(), 'isoDateTime'); diff --git a/invokeai/frontend/web/src/common/util/getUrl.ts b/invokeai/frontend/web/src/common/util/getUrl.ts new file mode 100644 index 0000000000..325d220e6b --- /dev/null +++ b/invokeai/frontend/web/src/common/util/getUrl.ts @@ -0,0 +1,28 @@ +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { OpenAPI } from 'services/api'; + +export const getUrlAlt = (url: string, shouldTransformUrls: boolean) => { + if (OpenAPI.BASE && shouldTransformUrls) { + return [OpenAPI.BASE, url].join('/'); + } + + return url; +}; + +export const useGetUrl = () => { + const shouldTransformUrls = useAppSelector( + (state: RootState) => state.system.shouldTransformUrls + ); + + return { + shouldTransformUrls, + getUrl: (url?: string) => { + if (OpenAPI.BASE && shouldTransformUrls) { + return [OpenAPI.BASE, url].join('/'); + } + + return url; + }, + }; +}; diff --git a/invokeai/frontend/web/src/common/util/parseMetadata.ts b/invokeai/frontend/web/src/common/util/parseMetadata.ts new file mode 100644 index 0000000000..433aa9b2a1 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/parseMetadata.ts @@ -0,0 +1,169 @@ +import { forEach, size } from 'lodash'; +import { ImageField, LatentsField } from 'services/api'; + +const OBJECT_TYPESTRING = '[object Object]'; +const STRING_TYPESTRING = '[object String]'; +const NUMBER_TYPESTRING = '[object Number]'; +const BOOLEAN_TYPESTRING = '[object Boolean]'; +const ARRAY_TYPESTRING = '[object Array]'; + +const isObject = (obj: unknown): obj is Record => + Object.prototype.toString.call(obj) === OBJECT_TYPESTRING; + +const isString = (obj: unknown): obj is string => + Object.prototype.toString.call(obj) === STRING_TYPESTRING; + +const isNumber = (obj: unknown): obj is number => + Object.prototype.toString.call(obj) === NUMBER_TYPESTRING; + +const isBoolean = (obj: unknown): obj is boolean => + Object.prototype.toString.call(obj) === BOOLEAN_TYPESTRING; + +const isArray = (obj: unknown): obj is Array => + Object.prototype.toString.call(obj) === ARRAY_TYPESTRING; + +const parseImageField = (imageField: unknown): ImageField | undefined => { + // Must be an object + if (!isObject(imageField)) { + return; + } + + // An ImageField must have both `image_name` and `image_type` + if (!('image_name' in imageField && 'image_type' in imageField)) { + return; + } + + // An ImageField's `image_type` must be one of the allowed values + if ( + !['results', 'uploads', 'intermediates'].includes(imageField.image_type) + ) { + return; + } + + // An ImageField's `image_name` must be a string + if (typeof imageField.image_name !== 'string') { + return; + } + + // Build a valid ImageField + return { + image_type: imageField.image_type, + image_name: imageField.image_name, + }; +}; + +const parseLatentsField = (latentsField: unknown): LatentsField | undefined => { + // Must be an object + if (!isObject(latentsField)) { + return; + } + + // A LatentsField must have a `latents_name` + if (!('latents_name' in latentsField)) { + return; + } + + // A LatentsField's `latents_name` must be a string + if (typeof latentsField.latents_name !== 'string') { + return; + } + + // Build a valid LatentsField + return { + latents_name: latentsField.latents_name, + }; +}; + +type NodeMetadata = { + [key: string]: string | number | boolean | ImageField | LatentsField; +}; + +type InvokeAIMetadata = { + session_id?: string; + node?: NodeMetadata; +}; + +export const parseNodeMetadata = ( + nodeMetadata: Record +): NodeMetadata | undefined => { + if (!isObject(nodeMetadata)) { + return; + } + + const parsed: NodeMetadata = {}; + + forEach(nodeMetadata, (nodeItem, nodeKey) => { + // `id` and `type` must be strings if they are present + if (['id', 'type'].includes(nodeKey)) { + if (isString(nodeItem)) { + parsed[nodeKey] = nodeItem; + } + return; + } + + // the only valid object types are ImageField and LatentsField + if (isObject(nodeItem)) { + if ('image_name' in nodeItem || 'image_type' in nodeItem) { + const imageField = parseImageField(nodeItem); + if (imageField) { + parsed[nodeKey] = imageField; + } + return; + } + + if ('latents_name' in nodeItem) { + const latentsField = parseLatentsField(nodeItem); + if (latentsField) { + parsed[nodeKey] = latentsField; + } + return; + } + } + + // otherwise we accept any string, number or boolean + if (isString(nodeItem) || isNumber(nodeItem) || isBoolean(nodeItem)) { + parsed[nodeKey] = nodeItem; + return; + } + }); + + if (size(parsed) === 0) { + return; + } + + return parsed; +}; + +export const parseInvokeAIMetadata = ( + metadata: Record | undefined +): InvokeAIMetadata | undefined => { + if (metadata === undefined) { + return; + } + + if (!isObject(metadata)) { + return; + } + + const parsed: InvokeAIMetadata = {}; + + forEach(metadata, (item, key) => { + if (key === 'session_id' && isString(item)) { + parsed['session_id'] = item; + } + + if (key === 'node' && isObject(item)) { + const nodeMetadata = parseNodeMetadata(item); + + if (nodeMetadata) { + parsed['node'] = nodeMetadata; + } + } + }); + + if (size(parsed) === 0) { + return; + } + + return parsed; +}; diff --git a/invokeai/frontend/web/src/component.tsx b/invokeai/frontend/web/src/component.tsx index 3b6d16855e..01c3513a78 100644 --- a/invokeai/frontend/web/src/component.tsx +++ b/invokeai/frontend/web/src/component.tsx @@ -1,8 +1,10 @@ -import React, { lazy, PropsWithChildren } from 'react'; +import React, { lazy, PropsWithChildren, useEffect, useState } from 'react'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; -import { store } from './app/store'; +import { buildMiddleware, store } from './app/store'; import { persistor } from './persistor'; +import { OpenAPI } from 'services/api'; +import { InvokeTabName } from 'features/ui/store/tabMap'; import '@fontsource/inter/100.css'; import '@fontsource/inter/200.css'; import '@fontsource/inter/300.css'; @@ -17,18 +19,61 @@ import Loading from './Loading'; // Localization import './i18n'; +import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; const App = lazy(() => import('./app/App')); const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider')); -export default function Component(props: PropsWithChildren) { +interface Props extends PropsWithChildren { + apiUrl?: string; + disabledPanels?: string[]; + disabledTabs?: InvokeTabName[]; + token?: string; + shouldTransformUrls?: boolean; +} + +export default function Component({ + apiUrl, + disabledPanels = [], + disabledTabs = [], + token, + children, + shouldTransformUrls, +}: Props) { + useEffect(() => { + // configure API client token + if (token) { + OpenAPI.TOKEN = token; + } + + // configure API client base url + if (apiUrl) { + OpenAPI.BASE = apiUrl; + } + + // reset dynamically added middlewares + resetMiddlewares(); + + // TODO: at this point, after resetting the middleware, we really ought to clean up the socket + // stuff by calling `dispatch(socketReset())`. but we cannot dispatch from here as we are + // outside the provider. it's not needed until there is the possibility that we will change + // the `apiUrl`/`token` dynamically. + + // rebuild socket middleware with token and apiUrl + addMiddleware(buildMiddleware()); + }, [apiUrl, token]); + return ( } persistor={persistor}> }> - {props.children} + + {children} + diff --git a/invokeai/frontend/web/src/exports.tsx b/invokeai/frontend/web/src/exports.tsx index 35c6bb5022..ffab33ead7 100644 --- a/invokeai/frontend/web/src/exports.tsx +++ b/invokeai/frontend/web/src/exports.tsx @@ -5,6 +5,8 @@ import ThemeChanger from './features/system/components/ThemeChanger'; import IAIPopover from './common/components/IAIPopover'; import IAIIconButton from './common/components/IAIIconButton'; import SettingsModal from './features/system/components/SettingsModal/SettingsModal'; +import StatusIndicator from './features/system/components/StatusIndicator'; +import ModelSelect from 'features/system/components/ModelSelect'; export default Component; export { @@ -13,4 +15,6 @@ export { IAIPopover, IAIIconButton, SettingsModal, + StatusIndicator, + ModelSelect, }; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx index 6e47b19bc7..cb7ab5fee8 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasIntermediateImage.tsx @@ -1,6 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import { GalleryState } from 'features/gallery/store/gallerySlice'; import { ImageConfig } from 'konva/lib/shapes/Image'; import { isEqual } from 'lodash'; @@ -25,7 +26,7 @@ type Props = Omit; const IAICanvasIntermediateImage = (props: Props) => { const { ...rest } = props; const intermediateImage = useAppSelector(selector); - + const { getUrl } = useGetUrl(); const [loadedImageElement, setLoadedImageElement] = useState(null); @@ -36,8 +37,8 @@ const IAICanvasIntermediateImage = (props: Props) => { tempImage.onload = () => { setLoadedImageElement(tempImage); }; - tempImage.src = intermediateImage.url; - }, [intermediateImage]); + tempImage.src = getUrl(intermediateImage.url); + }, [intermediateImage, getUrl]); if (!intermediateImage?.boundingBox) return null; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx index 3ee493c7c0..1d2852eef6 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx @@ -1,5 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { rgbaColorToString } from 'features/canvas/util/colorToString'; import { isEqual } from 'lodash'; @@ -32,6 +33,7 @@ const selector = createSelector( const IAICanvasObjectRenderer = () => { const { objects } = useAppSelector(selector); + const { getUrl } = useGetUrl(); if (!objects) return null; @@ -40,7 +42,12 @@ const IAICanvasObjectRenderer = () => { {objects.map((obj, i) => { if (isCanvasBaseImage(obj)) { return ( - + ); } else if (isCanvasBaseLine(obj)) { const line = ( diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx index 5ccb072942..1a84aa88bb 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx @@ -1,5 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import { canvasSelector } from 'features/canvas/store/canvasSelectors'; import { GroupConfig } from 'konva/lib/Group'; import { isEqual } from 'lodash'; @@ -53,11 +54,16 @@ const IAICanvasStagingArea = (props: Props) => { width, height, } = useAppSelector(selector); + const { getUrl } = useGetUrl(); return ( {shouldShowStagingImage && currentStagingAreaImage && ( - + )} {shouldShowStagingOutline && ( diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts b/invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts new file mode 100644 index 0000000000..67754cfc91 --- /dev/null +++ b/invokeai/frontend/web/src/features/canvas/store/canvasPersistBlacklist.ts @@ -0,0 +1,14 @@ +import { CanvasState } from './canvasTypes'; + +/** + * Canvas slice persist blacklist + */ +const itemsToBlacklist: (keyof CanvasState)[] = [ + 'cursorPosition', + 'isCanvasInitialized', + 'doesCanvasNeedScaling', +]; + +export const canvasBlacklist = itemsToBlacklist.map( + (blacklistItem) => `canvas.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 3e564af907..34688ef659 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -156,7 +156,7 @@ export const canvasSlice = createSlice({ setCursorPosition: (state, action: PayloadAction) => { state.cursorPosition = action.payload; }, - setInitialCanvasImage: (state, action: PayloadAction) => { + setInitialCanvasImage: (state, action: PayloadAction) => { const image = action.payload; const { stageDimensions } = state; @@ -291,7 +291,7 @@ export const canvasSlice = createSlice({ state, action: PayloadAction<{ boundingBox: IRect; - image: InvokeAI.Image; + image: InvokeAI._Image; }> ) => { const { boundingBox, image } = action.payload; diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts index 984f0d4f6b..95cf573c3b 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasTypes.ts @@ -37,7 +37,7 @@ export type CanvasImage = { y: number; width: number; height: number; - image: InvokeAI.Image; + image: InvokeAI._Image; }; export type CanvasMaskLine = { @@ -125,7 +125,7 @@ export interface CanvasState { cursorPosition: Vector2d | null; doesCanvasNeedScaling: boolean; futureLayerStates: CanvasLayerState[]; - intermediateImage?: InvokeAI.Image; + intermediateImage?: InvokeAI._Image; isCanvasInitialized: boolean; isDrawing: boolean; isMaskEnabled: boolean; diff --git a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts index 58e3af1523..a1a7bd3989 100644 --- a/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts +++ b/invokeai/frontend/web/src/features/canvas/store/thunks/mergeAndUploadCanvas.ts @@ -105,7 +105,7 @@ export const mergeAndUploadCanvas = const { url, width, height } = image; - const newImage: InvokeAI.Image = { + const newImage: InvokeAI._Image = { uuid: uuidv4(), category: shouldSaveToGallery ? 'result' : 'user', ...image, diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 18457d0cb3..fb6d861d8b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -14,8 +14,9 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; import { + initialImageSelected, setAllParameters, - setInitialImage, + // setInitialImage, setSeed, } from 'features/parameters/store/generationSlice'; import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors'; @@ -48,11 +49,15 @@ import { FaShareAlt, FaTrash, } from 'react-icons/fa'; -import { gallerySelector } from '../store/gallerySelectors'; +import { + gallerySelector, + selectedImageSelector, +} from '../store/gallerySelectors'; import DeleteImageModal from './DeleteImageModal'; import { useCallback } from 'react'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import { useGetUrl } from 'common/util/getUrl'; const currentImageButtonsSelector = createSelector( [ @@ -62,6 +67,7 @@ const currentImageButtonsSelector = createSelector( uiSelector, lightboxSelector, activeTabNameSelector, + selectedImageSelector, ], ( system: SystemState, @@ -69,7 +75,8 @@ const currentImageButtonsSelector = createSelector( postprocessing, ui, lightbox, - activeTabName + activeTabName, + selectedImage ) => { const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } = system; @@ -95,6 +102,7 @@ const currentImageButtonsSelector = createSelector( activeTabName, isLightboxOpen, shouldHidePreview, + selectedImage, }; }, { @@ -121,27 +129,33 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { facetoolStrength, shouldDisableToolbarButtons, shouldShowImageDetails, - currentImage, + // currentImage, isLightboxOpen, activeTabName, shouldHidePreview, + selectedImage, } = useAppSelector(currentImageButtonsSelector); + const { getUrl, shouldTransformUrls } = useGetUrl(); const toast = useToast(); const { t } = useTranslation(); const setBothPrompts = useSetBothPrompts(); const handleClickUseAsInitialImage = () => { - if (!currentImage) return; + if (!selectedImage) return; if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); - dispatch(setInitialImage(currentImage)); - dispatch(setActiveTab('img2img')); + dispatch(initialImageSelected(selectedImage.name)); + // dispatch(setInitialImage(currentImage)); + + // dispatch(setActiveTab('img2img')); }; const handleCopyImage = async () => { - if (!currentImage) return; + if (!selectedImage) return; - const blob = await fetch(currentImage.url).then((res) => res.blob()); + const blob = await fetch(getUrl(selectedImage.url)).then((res) => + res.blob() + ); const data = [new ClipboardItem({ [blob.type]: blob })]; await navigator.clipboard.write(data); @@ -155,24 +169,26 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }; const handleCopyImageLink = () => { - navigator.clipboard - .writeText( - currentImage ? window.location.toString() + currentImage.url : '' - ) - .then(() => { - toast({ - title: t('toast.imageLinkCopied'), - status: 'success', - duration: 2500, - isClosable: true, - }); + const url = selectedImage + ? shouldTransformUrls + ? getUrl(selectedImage.url) + : window.location.toString() + selectedImage.url + : ''; + + navigator.clipboard.writeText(url).then(() => { + toast({ + title: t('toast.imageLinkCopied'), + status: 'success', + duration: 2500, + isClosable: true, }); + }); }; useHotkeys( 'shift+i', () => { - if (currentImage) { + if (selectedImage) { handleClickUseAsInitialImage(); toast({ title: t('toast.sentToImageToImage'), @@ -190,7 +206,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handlePreviewVisibility = () => { @@ -198,20 +214,23 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }; const handleClickUseAllParameters = () => { - if (!currentImage) return; - currentImage.metadata && dispatch(setAllParameters(currentImage.metadata)); - if (currentImage.metadata?.image.type === 'img2img') { - dispatch(setActiveTab('img2img')); - } else if (currentImage.metadata?.image.type === 'txt2img') { - dispatch(setActiveTab('txt2img')); - } + if (!selectedImage) return; + // selectedImage.metadata && + // dispatch(setAllParameters(selectedImage.metadata)); + // if (selectedImage.metadata?.image.type === 'img2img') { + // dispatch(setActiveTab('img2img')); + // } else if (selectedImage.metadata?.image.type === 'txt2img') { + // dispatch(setActiveTab('txt2img')); + // } }; useHotkeys( 'a', () => { if ( - ['txt2img', 'img2img'].includes(currentImage?.metadata?.image?.type) + ['txt2img', 'img2img'].includes( + selectedImage?.metadata?.sd_metadata?.type + ) ) { handleClickUseAllParameters(); toast({ @@ -230,18 +249,18 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handleClickUseSeed = () => { - currentImage?.metadata && - dispatch(setSeed(currentImage.metadata.image.seed)); + selectedImage?.metadata && + dispatch(setSeed(selectedImage.metadata.sd_metadata.seed)); }; useHotkeys( 's', () => { - if (currentImage?.metadata?.image?.seed) { + if (selectedImage?.metadata?.sd_metadata?.seed) { handleClickUseSeed(); toast({ title: t('toast.seedSet'), @@ -259,19 +278,19 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handleClickUsePrompt = useCallback(() => { - if (currentImage?.metadata?.image?.prompt) { - setBothPrompts(currentImage?.metadata?.image?.prompt); + if (selectedImage?.metadata?.sd_metadata?.prompt) { + setBothPrompts(selectedImage?.metadata?.sd_metadata?.prompt); } - }, [currentImage?.metadata?.image?.prompt, setBothPrompts]); + }, [selectedImage?.metadata?.sd_metadata?.prompt, setBothPrompts]); useHotkeys( 'p', () => { - if (currentImage?.metadata?.image?.prompt) { + if (selectedImage?.metadata?.sd_metadata?.prompt) { handleClickUsePrompt(); toast({ title: t('toast.promptSet'), @@ -289,11 +308,11 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage] + [selectedImage] ); const handleClickUpscale = () => { - currentImage && dispatch(runESRGAN(currentImage)); + // selectedImage && dispatch(runESRGAN(selectedImage)); }; useHotkeys( @@ -317,7 +336,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { } }, [ - currentImage, + selectedImage, isESRGANAvailable, shouldDisableToolbarButtons, isConnected, @@ -327,7 +346,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { ); const handleClickFixFaces = () => { - currentImage && dispatch(runFacetool(currentImage)); + // selectedImage && dispatch(runFacetool(selectedImage)); }; useHotkeys( @@ -351,7 +370,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { } }, [ - currentImage, + selectedImage, isGFPGANAvailable, shouldDisableToolbarButtons, isConnected, @@ -364,10 +383,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { dispatch(setShouldShowImageDetails(!shouldShowImageDetails)); const handleSendToCanvas = () => { - if (!currentImage) return; + if (!selectedImage) return; if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); - dispatch(setInitialCanvasImage(currentImage)); + // dispatch(setInitialCanvasImage(selectedImage)); dispatch(requestCanvasRescale()); if (activeTabName !== 'unifiedCanvas') { @@ -385,7 +404,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { useHotkeys( 'i', () => { - if (currentImage) { + if (selectedImage) { handleClickShowImageDetails(); } else { toast({ @@ -396,7 +415,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [currentImage, shouldShowImageDetails] + [selectedImage, shouldShowImageDetails] ); const handleLightBox = () => { @@ -457,7 +476,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {t('parameters.copyImageToLink')} - + } size="sm" w="100%"> {t('parameters.downloadImage')} @@ -501,7 +520,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.usePrompt')} (P)`} aria-label={`${t('parameters.usePrompt')} (P)`} - isDisabled={!currentImage?.metadata?.image?.prompt} + isDisabled={!selectedImage?.metadata?.sd_metadata?.prompt} onClick={handleClickUsePrompt} /> @@ -509,7 +528,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { icon={} tooltip={`${t('parameters.useSeed')} (S)`} aria-label={`${t('parameters.useSeed')} (S)`} - isDisabled={!currentImage?.metadata?.image?.seed} + isDisabled={!selectedImage?.metadata?.sd_metadata?.seed} onClick={handleClickUseSeed} /> @@ -519,7 +538,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { aria-label={`${t('parameters.useAll')} (A)`} isDisabled={ !['txt2img', 'img2img'].includes( - currentImage?.metadata?.image?.type + selectedImage?.metadata?.sd_metadata?.type ) } onClick={handleClickUseAllParameters} @@ -545,7 +564,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { { { /> - + {/* } tooltip={`${t('parameters.deleteImage')} (Del)`} aria-label={`${t('parameters.deleteImage')} (Del)`} - isDisabled={!currentImage || !isConnected || isProcessing} + isDisabled={!selectedImage || !isConnected || isProcessing} colorScheme="error" /> - + */} ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx index 6c46e14391..2f249d77f8 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx @@ -4,17 +4,20 @@ import { useAppSelector } from 'app/storeHooks'; import { isEqual } from 'lodash'; import { MdPhoto } from 'react-icons/md'; -import { gallerySelector } from '../store/gallerySelectors'; +import { + gallerySelector, + selectedImageSelector, +} from '../store/gallerySelectors'; import CurrentImageButtons from './CurrentImageButtons'; import CurrentImagePreview from './CurrentImagePreview'; export const currentImageDisplaySelector = createSelector( - [gallerySelector], - (gallery) => { + [gallerySelector, selectedImageSelector], + (gallery, selectedImage) => { const { currentImage, intermediateImage } = gallery; return { - hasAnImageToDisplay: currentImage || intermediateImage, + hasAnImageToDisplay: selectedImage || intermediateImage, }; }, { diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 7b5ed2c181..0f049d3368 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -1,28 +1,48 @@ import { Box, Flex, Image } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/storeHooks'; -import { GalleryState } from 'features/gallery/store/gallerySlice'; +import { useGetUrl } from 'common/util/getUrl'; +import { systemSelector } from 'features/system/store/systemSelectors'; import { uiSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash'; +import { ReactEventHandler } from 'react'; import { APP_METADATA_HEIGHT } from 'theme/util/constants'; -import { gallerySelector } from '../store/gallerySelectors'; +import { selectedImageSelector } from '../store/gallerySelectors'; import CurrentImageFallback from './CurrentImageFallback'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import NextPrevImageButtons from './NextPrevImageButtons'; import CurrentImageHidden from './CurrentImageHidden'; export const imagesSelector = createSelector( - [gallerySelector, uiSelector], - (gallery: GalleryState, ui) => { - const { currentImage, intermediateImage } = gallery; + [uiSelector, selectedImageSelector, systemSelector], + (ui, selectedImage, system) => { const { shouldShowImageDetails, shouldHidePreview } = ui; + const { progressImage } = system; + + // TODO: Clean this up, this is really gross + const imageToDisplay = progressImage + ? { + url: progressImage.dataURL, + width: progressImage.width, + height: progressImage.height, + isProgressImage: true, + image: progressImage, + } + : selectedImage + ? { + url: selectedImage.url, + width: selectedImage.metadata.width, + height: selectedImage.metadata.height, + isProgressImage: false, + image: selectedImage, + } + : null; return { - imageToDisplay: intermediateImage ? intermediateImage : currentImage, - isIntermediate: Boolean(intermediateImage), shouldShowImageDetails, shouldHidePreview, + imageToDisplay, }; }, { @@ -33,12 +53,9 @@ export const imagesSelector = createSelector( ); export default function CurrentImagePreview() { - const { - shouldShowImageDetails, - imageToDisplay, - isIntermediate, - shouldHidePreview, - } = useAppSelector(imagesSelector); + const { shouldShowImageDetails, imageToDisplay, shouldHidePreview } = + useAppSelector(imagesSelector); + const { getUrl } = useGetUrl(); return ( {imageToDisplay && ( - ) : !isIntermediate ? ( + ) : !imageToDisplay.isProgressImage ? ( ) : undefined } @@ -68,27 +91,31 @@ export default function CurrentImagePreview() { maxHeight: '100%', height: 'auto', position: 'absolute', - imageRendering: isIntermediate ? 'pixelated' : 'initial', + imageRendering: imageToDisplay.isProgressImage + ? 'pixelated' + : 'initial', borderRadius: 'base', }} /> )} {!shouldShowImageDetails && } - {shouldShowImageDetails && imageToDisplay && ( - - - - )} + {shouldShowImageDetails && + imageToDisplay && + 'metadata' in imageToDisplay.image && ( + + + + )} ); } diff --git a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx index 734dc3b682..a1276df6d9 100644 --- a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx @@ -52,7 +52,7 @@ interface DeleteImageModalProps { /** * The image to delete. */ - image?: InvokeAI.Image; + image?: InvokeAI._Image; } /** diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 0d034ed976..5973227b0b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -9,11 +9,14 @@ import { useToast, } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import { setCurrentImage } from 'features/gallery/store/gallerySlice'; import { + imageSelected, + setCurrentImage, +} from 'features/gallery/store/gallerySlice'; +import { + initialImageSelected, setAllImageToImageParameters, setAllParameters, - setInitialImage, setSeed, } from 'features/parameters/store/generationSlice'; import { DragEvent, memo, useState } from 'react'; @@ -31,6 +34,7 @@ import { useTranslation } from 'react-i18next'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import IAIIconButton from 'common/components/IAIIconButton'; +import { useGetUrl } from 'common/util/getUrl'; interface HoverableImageProps { image: InvokeAI.Image; @@ -40,7 +44,7 @@ interface HoverableImageProps { const memoEqualityCheck = ( prev: HoverableImageProps, next: HoverableImageProps -) => prev.image.uuid === next.image.uuid && prev.isSelected === next.isSelected; +) => prev.image.name === next.image.name && prev.isSelected === next.isSelected; /** * Gallery image component with delete/use all/use seed buttons on hover. @@ -55,7 +59,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { shouldUseSingleGalleryColumn, } = useAppSelector(hoverableImageSelector); const { image, isSelected } = props; - const { url, thumbnail, uuid, metadata } = image; + const { url, thumbnail, name, metadata } = image; + const { getUrl } = useGetUrl(); const [isHovered, setIsHovered] = useState(false); @@ -69,10 +74,9 @@ const HoverableImage = memo((props: HoverableImageProps) => { const handleMouseOut = () => setIsHovered(false); const handleUsePrompt = () => { - if (image.metadata?.image?.prompt) { - setBothPrompts(image.metadata?.image?.prompt); + if (image.metadata?.sd_metadata?.prompt) { + setBothPrompts(image.metadata?.sd_metadata?.prompt); } - toast({ title: t('toast.promptSet'), status: 'success', @@ -82,7 +86,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleUseSeed = () => { - image.metadata && dispatch(setSeed(image.metadata.image.seed)); + image.metadata.sd_metadata && + dispatch(setSeed(image.metadata.sd_metadata.image.seed)); toast({ title: t('toast.seedSet'), status: 'success', @@ -92,20 +97,11 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleSendToImageToImage = () => { - dispatch(setInitialImage(image)); - if (activeTabName !== 'img2img') { - dispatch(setActiveTab('img2img')); - } - toast({ - title: t('toast.sentToImageToImage'), - status: 'success', - duration: 2500, - isClosable: true, - }); + dispatch(initialImageSelected(image.name)); }; const handleSendToCanvas = () => { - dispatch(setInitialCanvasImage(image)); + // dispatch(setInitialCanvasImage(image)); dispatch(resizeAndScaleCanvas()); @@ -122,7 +118,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleUseAllParameters = () => { - metadata && dispatch(setAllParameters(metadata)); + metadata.sd_metadata && dispatch(setAllParameters(metadata.sd_metadata)); toast({ title: t('toast.parametersSet'), status: 'success', @@ -132,11 +128,13 @@ const HoverableImage = memo((props: HoverableImageProps) => { }; const handleUseInitialImage = async () => { - if (metadata?.image?.init_image_path) { - const response = await fetch(metadata.image.init_image_path); + if (metadata.sd_metadata?.image?.init_image_path) { + const response = await fetch( + metadata.sd_metadata?.image?.init_image_path + ); if (response.ok) { dispatch(setActiveTab('img2img')); - dispatch(setAllImageToImageParameters(metadata)); + dispatch(setAllImageToImageParameters(metadata?.sd_metadata)); toast({ title: t('toast.initialImageSet'), status: 'success', @@ -155,16 +153,20 @@ const HoverableImage = memo((props: HoverableImageProps) => { }); }; - const handleSelectImage = () => dispatch(setCurrentImage(image)); + const handleSelectImage = () => { + dispatch(imageSelected(image.name)); + }; const handleDragStart = (e: DragEvent) => { - e.dataTransfer.setData('invokeai/imageUuid', uuid); + console.log('drag started'); + e.dataTransfer.setData('invokeai/imageName', image.name); + e.dataTransfer.setData('invokeai/imageType', image.type); e.dataTransfer.effectAllowed = 'move'; }; const handleLightBox = () => { - dispatch(setCurrentImage(image)); - dispatch(setIsLightboxOpen(true)); + // dispatch(setCurrentImage(image)); + // dispatch(setIsLightboxOpen(true)); }; return ( @@ -177,28 +179,30 @@ const HoverableImage = memo((props: HoverableImageProps) => { {t('parameters.usePrompt')} {t('parameters.useSeed')} {t('parameters.useAll')} {t('parameters.useInitImg')} @@ -209,9 +213,9 @@ const HoverableImage = memo((props: HoverableImageProps) => { {t('parameters.sendToUnifiedCanvas')} - + {/*

{t('parameters.deleteImage')}

-
+
*/}
)} @@ -219,7 +223,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { {(ref) => ( { shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit } rounded="md" - src={thumbnail || url} + src={getUrl(thumbnail || url)} loading="lazy" sx={{ position: 'absolute', @@ -290,7 +294,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { insetInlineEnd: 1, }} > - + {/* } @@ -298,7 +302,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { fontSize={14} isDisabled={!mayDeleteImage} /> - + */} )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 8ddf862c47..31a2c4b055 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Flex, Grid, Icon, Text } from '@chakra-ui/react'; +import { ButtonGroup, Flex, Grid, Icon, Image, Text } from '@chakra-ui/react'; import { requestImages } from 'app/socketio/actions'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import IAIButton from 'common/components/IAIButton'; @@ -25,9 +25,44 @@ import HoverableImage from './HoverableImage'; import Scrollable from 'features/ui/components/common/Scrollable'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import { + resultsAdapter, + selectResultsAll, + selectResultsTotal, +} from '../store/resultsSlice'; +import { + receivedResultImagesPage, + receivedUploadImagesPage, +} from 'services/thunks/gallery'; +import { selectUploadsAll, uploadsAdapter } from '../store/uploadsSlice'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; +const gallerySelector = createSelector( + [ + (state: RootState) => state.uploads, + (state: RootState) => state.results, + (state: RootState) => state.gallery, + ], + (uploads, results, gallery) => { + const { currentCategory } = gallery; + + return currentCategory === 'result' + ? { + images: resultsAdapter.getSelectors().selectAll(results), + isLoading: results.isLoading, + areMoreImagesAvailable: results.page < results.pages - 1, + } + : { + images: uploadsAdapter.getSelectors().selectAll(uploads), + isLoading: uploads.isLoading, + areMoreImagesAvailable: uploads.page < uploads.pages - 1, + }; + } +); + const ImageGalleryContent = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -35,7 +70,7 @@ const ImageGalleryContent = () => { const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true); const { - images, + // images, currentCategory, currentImageUuid, shouldPinGallery, @@ -43,12 +78,24 @@ const ImageGalleryContent = () => { galleryGridTemplateColumns, galleryImageObjectFit, shouldAutoSwitchToNewImages, - areMoreImagesAvailable, + // areMoreImagesAvailable, shouldUseSingleGalleryColumn, } = useAppSelector(imageGallerySelector); + const { images, areMoreImagesAvailable, isLoading } = + useAppSelector(gallerySelector); + + // const handleClickLoadMore = () => { + // dispatch(requestImages(currentCategory)); + // }; const handleClickLoadMore = () => { - dispatch(requestImages(currentCategory)); + if (currentCategory === 'result') { + dispatch(receivedResultImagesPage()); + } + + if (currentCategory === 'user') { + dispatch(receivedUploadImagesPage()); + } }; const handleChangeGalleryImageMinimumWidth = (v: number) => { @@ -203,11 +250,11 @@ const ImageGalleryContent = () => { style={{ gridTemplateColumns: galleryGridTemplateColumns }} > {images.map((image) => { - const { uuid } = image; - const isSelected = currentImageUuid === uuid; + const { name } = image; + const isSelected = currentImageUuid === name; return ( @@ -217,6 +264,7 @@ const ImageGalleryContent = () => { {areMoreImagesAvailable diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx index da97dea9bf..1d43cac476 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx @@ -31,12 +31,13 @@ const GALLERY_TAB_WIDTHS: Record< InvokeTabName, { galleryMinWidth: number; galleryMaxWidth: number } > = { - txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + linear: { galleryMinWidth: 200, galleryMaxWidth: 500 }, unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 }, nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - training: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + // training: { galleryMinWidth: 200, galleryMaxWidth: 500 }, }; const galleryPanelSelector = createSelector( 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 130c716f6b..1909dc56a7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -11,6 +11,7 @@ import { } from '@chakra-ui/react'; import * as InvokeAI from 'app/invokeai'; import { useAppDispatch } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; import promptToString from 'common/util/promptToString'; import { seedWeightsToString } from 'common/util/seedWeightPairs'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; @@ -18,7 +19,7 @@ import { setCfgScale, setHeight, setImg2imgStrength, - setInitialImage, + // setInitialImage, setMaskPath, setPerlin, setSampler, @@ -120,7 +121,7 @@ type ImageMetadataViewerProps = { const memoEqualityCheck = ( prev: ImageMetadataViewerProps, next: ImageMetadataViewerProps -) => prev.image.uuid === next.image.uuid; +) => prev.image.name === next.image.name; // TODO: Show more interesting information in this component. @@ -137,34 +138,13 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { dispatch(setShouldShowImageDetails(false)); }); - const metadata = image?.metadata?.image || {}; - const dreamPrompt = image?.dreamPrompt; - - const { - cfg_scale, - fit, - height, - hires_fix, - init_image_path, - mask_image_path, - orig_path, - perlin, - postprocessing, - prompt, - sampler, - seamless, - seed, - steps, - strength, - threshold, - type, - variations, - width, - } = metadata; + const sessionId = image.metadata.invokeai?.session_id; + const node = image.metadata.invokeai?.node as Record; const { t } = useTranslation(); + const { getUrl } = useGetUrl(); - const metadataJSON = JSON.stringify(image.metadata, null, 2); + const metadataJSON = JSON.stringify(image, null, 2); return ( { > File: - + {image.url.length > 64 ? image.url.substring(0, 64).concat('...') : image.url} - {Object.keys(metadata).length > 0 ? ( + {node && Object.keys(node).length > 0 ? ( <> - {type && } - {image.metadata?.model_weights && ( - + {node.type && ( + )} - {['esrgan', 'gfpgan'].includes(type) && ( - - )} - {prompt && ( + {node.model && } + {node.prompt && ( setBothPrompts(prompt)} + onClick={() => setBothPrompts(node.prompt)} /> )} - {seed !== undefined && ( + {node.seed !== undefined && ( dispatch(setSeed(seed))} + value={node.seed} + onClick={() => dispatch(setSeed(Number(node.seed)))} /> )} - {threshold !== undefined && ( + {node.threshold !== undefined && ( dispatch(setThreshold(threshold))} + value={node.threshold} + onClick={() => dispatch(setThreshold(Number(node.threshold)))} /> )} - {perlin !== undefined && ( + {node.perlin !== undefined && ( dispatch(setPerlin(perlin))} + value={node.perlin} + onClick={() => dispatch(setPerlin(Number(node.perlin)))} /> )} - {sampler && ( + {node.scheduler && ( dispatch(setSampler(sampler))} + value={node.scheduler} + onClick={() => dispatch(setSampler(node.scheduler))} /> )} - {steps && ( + {node.steps && ( dispatch(setSteps(steps))} + value={node.steps} + onClick={() => dispatch(setSteps(Number(node.steps)))} /> )} - {cfg_scale !== undefined && ( + {node.cfg_scale !== undefined && ( dispatch(setCfgScale(cfg_scale))} + value={node.cfg_scale} + onClick={() => dispatch(setCfgScale(Number(node.cfg_scale)))} /> )} - {variations && variations.length > 0 && ( + {node.variations && node.variations.length > 0 && ( - dispatch(setSeedWeights(seedWeightsToString(variations))) + dispatch(setSeedWeights(seedWeightsToString(node.variations))) } /> )} - {seamless && ( + {node.seamless && ( dispatch(setSeamless(seamless))} + value={node.seamless} + onClick={() => dispatch(setSeamless(node.seamless))} /> )} - {hires_fix && ( + {node.hires_fix && ( dispatch(setHiresFix(hires_fix))} + value={node.hires_fix} + onClick={() => dispatch(setHiresFix(node.hires_fix))} /> )} - {width && ( + {node.width && ( dispatch(setWidth(width))} + value={node.width} + onClick={() => dispatch(setWidth(Number(node.width)))} /> )} - {height && ( + {node.height && ( dispatch(setHeight(height))} + value={node.height} + onClick={() => dispatch(setHeight(Number(node.height)))} /> )} - {init_image_path && ( + {/* {init_image_path && ( dispatch(setInitialImage(init_image_path))} /> - )} - {mask_image_path && ( - dispatch(setMaskPath(mask_image_path))} - /> - )} - {type === 'img2img' && strength && ( + )} */} + {node.strength && ( dispatch(setImg2imgStrength(strength))} + value={node.strength} + onClick={() => + dispatch(setImg2imgStrength(Number(node.strength))) + } /> )} - {fit && ( + {node.fit && ( dispatch(setShouldFitToWidthHeight(fit))} + value={node.fit} + onClick={() => dispatch(setShouldFitToWidthHeight(node.fit))} /> )} - {postprocessing && postprocessing.length > 0 && ( - <> - Postprocessing - {postprocessing.map( - ( - postprocess: InvokeAI.PostProcessedImageMetadata, - i: number - ) => { - if (postprocess.type === 'esrgan') { - const { scale, strength, denoise_str } = postprocess; - return ( - - {`${i + 1}: Upscale (ESRGAN)`} - dispatch(setUpscalingLevel(scale))} - /> - - dispatch(setUpscalingStrength(strength)) - } - /> - {denoise_str !== undefined && ( - - dispatch(setUpscalingDenoising(denoise_str)) - } - /> - )} - - ); - } else if (postprocess.type === 'gfpgan') { - const { strength } = postprocess; - return ( - - {`${ - i + 1 - }: Face restoration (GFPGAN)`} - - { - dispatch(setFacetoolStrength(strength)); - dispatch(setFacetoolType('gfpgan')); - }} - /> - - ); - } else if (postprocess.type === 'codeformer') { - const { strength, fidelity } = postprocess; - return ( - - {`${ - i + 1 - }: Face restoration (Codeformer)`} - - { - dispatch(setFacetoolStrength(strength)); - dispatch(setFacetoolType('codeformer')); - }} - /> - {fidelity && ( - { - dispatch(setCodeformerFidelity(fidelity)); - dispatch(setFacetoolType('codeformer')); - }} - /> - )} - - ); - } - } - )} - - )} - {dreamPrompt && ( - - )} - - - - } - size="xs" - variant="ghost" - fontSize={14} - onClick={() => navigator.clipboard.writeText(metadataJSON)} - /> - - Metadata JSON: - - -
{metadataJSON}
-
-
) : (
@@ -447,6 +299,37 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
)} + + + + } + size="xs" + variant="ghost" + fontSize={14} + onClick={() => navigator.clipboard.writeText(metadataJSON)} + /> + + Metadata JSON: + + +
{metadataJSON}
+
+
); }, memoEqualityCheck); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx new file mode 100644 index 0000000000..3339140a52 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/OLD_ImageMetadataViewer.tsx @@ -0,0 +1,470 @@ +import { ExternalLinkIcon } from '@chakra-ui/icons'; +import { + Box, + Center, + Flex, + Heading, + IconButton, + Link, + Text, + Tooltip, +} from '@chakra-ui/react'; +import * as InvokeAI from 'app/invokeai'; +import { useAppDispatch } from 'app/storeHooks'; +import { useGetUrl } from 'common/util/getUrl'; +import promptToString from 'common/util/promptToString'; +import { seedWeightsToString } from 'common/util/seedWeightPairs'; +import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; +import { + setCfgScale, + setHeight, + setImg2imgStrength, + // setInitialImage, + setMaskPath, + setPerlin, + setSampler, + setSeamless, + setSeed, + setSeedWeights, + setShouldFitToWidthHeight, + setSteps, + setThreshold, + setWidth, +} from 'features/parameters/store/generationSlice'; +import { + setCodeformerFidelity, + setFacetoolStrength, + setFacetoolType, + setHiresFix, + setUpscalingDenoising, + setUpscalingLevel, + setUpscalingStrength, +} from 'features/parameters/store/postprocessingSlice'; +import { setShouldShowImageDetails } from 'features/ui/store/uiSlice'; +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 * as png from '@stevebel/png'; + +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(); + + 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()} + + )} + + + ); +}; + +type ImageMetadataViewerProps = { + image: InvokeAI.Image; +}; + +// TODO: I don't know if this is needed. +const memoEqualityCheck = ( + prev: ImageMetadataViewerProps, + next: ImageMetadataViewerProps +) => prev.image.name === next.image.name; + +// TODO: Show more interesting information in this component. + +/** + * Image metadata viewer overlays currently selected image and provides + * access to any of its metadata for use in processing. + */ +const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { + const dispatch = useAppDispatch(); + + const setBothPrompts = useSetBothPrompts(); + + useHotkeys('esc', () => { + dispatch(setShouldShowImageDetails(false)); + }); + + const metadata = image?.metadata.sd_metadata || {}; + const dreamPrompt = image?.metadata.sd_metadata?.dreamPrompt; + + const { + cfg_scale, + fit, + height, + hires_fix, + init_image_path, + mask_image_path, + orig_path, + perlin, + postprocessing, + prompt, + sampler, + seamless, + seed, + steps, + strength, + threshold, + type, + variations, + width, + model_weights, + } = metadata; + + const { t } = useTranslation(); + const { getUrl } = useGetUrl(); + + const metadataJSON = JSON.stringify(image, null, 2); + + // fetch(getUrl(image.url)) + // .then((r) => r.arrayBuffer()) + // .then((buffer) => { + // const { text } = png.decode(buffer); + // const metadata = text?.['sd-metadata'] + // ? JSON.parse(text['sd-metadata'] ?? {}) + // : {}; + // console.log(metadata); + // }); + + return ( + + + File: + + {image.url.length > 64 + ? image.url.substring(0, 64).concat('...') + : image.url} + + + + + + + } + size="xs" + variant="ghost" + fontSize={14} + onClick={() => navigator.clipboard.writeText(metadataJSON)} + /> + + Metadata JSON: + + +
{metadataJSON}
+
+
+ {Object.keys(metadata).length > 0 ? ( + <> + {type && } + {model_weights && ( + + )} + {['esrgan', 'gfpgan'].includes(type) && ( + + )} + {prompt && ( + setBothPrompts(prompt)} + /> + )} + {seed !== undefined && ( + dispatch(setSeed(seed))} + /> + )} + {threshold !== undefined && ( + dispatch(setThreshold(threshold))} + /> + )} + {perlin !== undefined && ( + dispatch(setPerlin(perlin))} + /> + )} + {sampler && ( + dispatch(setSampler(sampler))} + /> + )} + {steps && ( + dispatch(setSteps(steps))} + /> + )} + {cfg_scale !== undefined && ( + dispatch(setCfgScale(cfg_scale))} + /> + )} + {variations && variations.length > 0 && ( + + dispatch(setSeedWeights(seedWeightsToString(variations))) + } + /> + )} + {seamless && ( + dispatch(setSeamless(seamless))} + /> + )} + {hires_fix && ( + dispatch(setHiresFix(hires_fix))} + /> + )} + {width && ( + dispatch(setWidth(width))} + /> + )} + {height && ( + dispatch(setHeight(height))} + /> + )} + {/* {init_image_path && ( + dispatch(setInitialImage(init_image_path))} + /> + )} */} + {mask_image_path && ( + dispatch(setMaskPath(mask_image_path))} + /> + )} + {type === 'img2img' && strength && ( + dispatch(setImg2imgStrength(strength))} + /> + )} + {fit && ( + dispatch(setShouldFitToWidthHeight(fit))} + /> + )} + {postprocessing && postprocessing.length > 0 && ( + <> + Postprocessing + {postprocessing.map( + ( + postprocess: InvokeAI.PostProcessedImageMetadata, + i: number + ) => { + if (postprocess.type === 'esrgan') { + const { scale, strength, denoise_str } = postprocess; + return ( + + {`${i + 1}: Upscale (ESRGAN)`} + dispatch(setUpscalingLevel(scale))} + /> + + dispatch(setUpscalingStrength(strength)) + } + /> + {denoise_str !== undefined && ( + + dispatch(setUpscalingDenoising(denoise_str)) + } + /> + )} + + ); + } else if (postprocess.type === 'gfpgan') { + const { strength } = postprocess; + return ( + + {`${ + i + 1 + }: Face restoration (GFPGAN)`} + + { + dispatch(setFacetoolStrength(strength)); + dispatch(setFacetoolType('gfpgan')); + }} + /> + + ); + } else if (postprocess.type === 'codeformer') { + const { strength, fidelity } = postprocess; + return ( + + {`${ + i + 1 + }: Face restoration (Codeformer)`} + + { + dispatch(setFacetoolStrength(strength)); + dispatch(setFacetoolType('codeformer')); + }} + /> + {fidelity && ( + { + dispatch(setCodeformerFidelity(fidelity)); + dispatch(setFacetoolType('codeformer')); + }} + /> + )} + + ); + } + } + )} + + )} + {dreamPrompt && ( + + )} + + ) : ( +
+ + No metadata available + +
+ )} +
+ ); +}, memoEqualityCheck); + +ImageMetadataViewer.displayName = 'ImageMetadataViewer'; + +export default ImageMetadataViewer; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts new file mode 100644 index 0000000000..b662cf02c1 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts @@ -0,0 +1,35 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/storeHooks'; +import { ImageType } from 'services/api'; +import { selectResultsEntities } from '../store/resultsSlice'; +import { selectUploadsEntities } from '../store/uploadsSlice'; + +const useGetImageByNameSelector = createSelector( + [selectResultsEntities, selectUploadsEntities], + (allResults, allUploads) => { + return { allResults, allUploads }; + } +); + +const useGetImageByNameAndType = () => { + const { allResults, allUploads } = useAppSelector(useGetImageByNameSelector); + + return (name: string, type: ImageType) => { + if (type === 'results') { + const resultImagesResult = allResults[name]; + + if (resultImagesResult) { + return resultImagesResult; + } + } + + if (type === 'uploads') { + const userImagesResult = allUploads[name]; + if (userImagesResult) { + return userImagesResult; + } + } + }; +}; + +export default useGetImageByNameAndType; diff --git a/invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts b/invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts new file mode 100644 index 0000000000..37f3f48746 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/galleryPersistBlacklist.ts @@ -0,0 +1,17 @@ +import { GalleryState } from './gallerySlice'; + +/** + * Gallery slice persist blacklist + */ +const itemsToBlacklist: (keyof GalleryState)[] = [ + 'categories', + 'currentCategory', + 'currentImage', + 'currentImageUuid', + 'shouldAutoSwitchToNewImages', + 'intermediateImage', +]; + +export const galleryBlacklist = itemsToBlacklist.map( + (blacklistItem) => `gallery.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 61223692fa..e9ef5ea88d 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -7,6 +7,16 @@ import { uiSelector, } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash'; +import { + selectResultsAll, + selectResultsById, + selectResultsEntities, +} from './resultsSlice'; +import { + selectUploadsAll, + selectUploadsById, + selectUploadsEntities, +} from './uploadsSlice'; export const gallerySelector = (state: RootState) => state.gallery; @@ -75,3 +85,18 @@ export const hoverableImageSelector = createSelector( }, } ); + +export const selectedImageSelector = createSelector( + [gallerySelector, selectResultsEntities, selectUploadsEntities], + (gallery, allResults, allUploads) => { + const selectedImageName = gallery.selectedImageName; + + if (selectedImageName in allResults) { + return allResults[selectedImageName]; + } + + if (selectedImageName in allUploads) { + return allUploads[selectedImageName]; + } + } +); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index dbb173c74a..ab5ac8e466 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,14 +1,17 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/invokeai'; +import { invocationComplete } from 'services/events/actions'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { IRect } from 'konva/lib/types'; import { clamp } from 'lodash'; +import { isImageOutput } from 'services/types/guards'; +import { imageUploaded } from 'services/thunks/image'; export type GalleryCategory = 'user' | 'result'; export type AddImagesPayload = { - images: Array; + images: Array; areMoreImagesAvailable: boolean; category: GalleryCategory; }; @@ -16,16 +19,33 @@ export type AddImagesPayload = { type GalleryImageObjectFitType = 'contain' | 'cover'; export type Gallery = { - images: InvokeAI.Image[]; + images: InvokeAI._Image[]; latest_mtime?: number; earliest_mtime?: number; areMoreImagesAvailable: boolean; }; export interface GalleryState { - currentImage?: InvokeAI.Image; + /** + * The selected image's unique name + * Use `selectedImageSelector` to access the image + */ + selectedImageName: string; + /** + * The currently selected image + * @deprecated See `state.gallery.selectedImageName` + */ + currentImage?: InvokeAI._Image; + /** + * The currently selected image's uuid. + * @deprecated See `state.gallery.selectedImageName`, use `selectedImageSelector` to access the image + */ currentImageUuid: string; - intermediateImage?: InvokeAI.Image & { + /** + * The current progress image + * @deprecated See `state.system.progressImage` + */ + intermediateImage?: InvokeAI._Image & { boundingBox?: IRect; generationMode?: InvokeTabName; }; @@ -42,6 +62,7 @@ export interface GalleryState { } const initialState: GalleryState = { + selectedImageName: '', currentImageUuid: '', galleryImageMinimumWidth: 64, galleryImageObjectFit: 'cover', @@ -69,7 +90,10 @@ export const gallerySlice = createSlice({ name: 'gallery', initialState, reducers: { - setCurrentImage: (state, action: PayloadAction) => { + imageSelected: (state, action: PayloadAction) => { + state.selectedImageName = action.payload; + }, + setCurrentImage: (state, action: PayloadAction) => { state.currentImage = action.payload; state.currentImageUuid = action.payload.uuid; }, @@ -124,7 +148,7 @@ export const gallerySlice = createSlice({ addImage: ( state, action: PayloadAction<{ - image: InvokeAI.Image; + image: InvokeAI._Image; category: GalleryCategory; }> ) => { @@ -150,7 +174,10 @@ export const gallerySlice = createSlice({ setIntermediateImage: ( state, action: PayloadAction< - InvokeAI.Image & { boundingBox?: IRect; generationMode?: InvokeTabName } + InvokeAI._Image & { + boundingBox?: IRect; + generationMode?: InvokeTabName; + } > ) => { state.intermediateImage = action.payload; @@ -252,9 +279,31 @@ export const gallerySlice = createSlice({ state.shouldUseSingleGalleryColumn = action.payload; }, }, + extraReducers(builder) { + /** + * Invocation Complete + */ + builder.addCase(invocationComplete, (state, action) => { + const { data } = action.payload; + if (isImageOutput(data.result)) { + state.selectedImageName = data.result.image.image_name; + state.intermediateImage = undefined; + } + }); + + /** + * Upload Image - FULFILLED + */ + builder.addCase(imageUploaded.fulfilled, (state, action) => { + const { location } = action.payload; + const imageName = location.split('/').pop() || ''; + state.selectedImageName = imageName; + }); + }, }); export const { + imageSelected, addImage, clearIntermediateImage, removeImage, diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts b/invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts new file mode 100644 index 0000000000..bd246865fb --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/resultsPersistBlacklist.ts @@ -0,0 +1,12 @@ +import { ResultsState } from './resultsSlice'; + +/** + * Results slice persist blacklist + * + * Currently blacklisting results slice entirely, see persist config in store.ts + */ +const itemsToBlacklist: (keyof ResultsState)[] = []; + +export const resultsBlacklist = itemsToBlacklist.map( + (blacklistItem) => `results.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts new file mode 100644 index 0000000000..bb789a4a5f --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -0,0 +1,139 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { Image } from 'app/invokeai'; +import { invocationComplete } from 'services/events/actions'; + +import { RootState } from 'app/store'; +import { + receivedResultImagesPage, + IMAGES_PER_PAGE, +} from 'services/thunks/gallery'; +import { isImageOutput } from 'services/types/guards'; +import { + buildImageUrls, + extractTimestampFromImageName, +} from 'services/util/deserializeImageField'; +import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; + +// use `createEntityAdapter` to create a slice for results images +// https://redux-toolkit.js.org/api/createEntityAdapter#overview + +// the "Entity" is InvokeAI.ResultImage, while the "entities" are instances of that type +export const resultsAdapter = createEntityAdapter({ + // Provide a callback to get a stable, unique identifier for each entity. This defaults to + // `(item) => item.id`, but for our result images, the `name` is the unique identifier. + selectId: (image) => image.name, + // Order all images by their time (in descending order) + sortComparer: (a, b) => b.metadata.created - a.metadata.created, +}); + +// This type is intersected with the Entity type to create the shape of the state +type AdditionalResultsState = { + // these are a bit misleading; they refer to sessions, not results, but we don't have a route + // to list all images directly at this time... + page: number; // current page we are on + pages: number; // the total number of pages available + isLoading: boolean; // whether we are loading more images or not, mostly a placeholder + nextPage: number; // the next page to request +}; + +export const initialResultsState = + resultsAdapter.getInitialState({ + // provide the additional initial state + page: 0, + pages: 0, + isLoading: false, + nextPage: 0, + }); + +export type ResultsState = typeof initialResultsState; + +const resultsSlice = createSlice({ + name: 'results', + initialState: initialResultsState, + reducers: { + // the adapter provides some helper reducers; see the docs for all of them + // can use them as helper functions within a reducer, or use the function itself as a reducer + + // here we just use the function itself as the reducer. we'll call this on `invocation_complete` + // to add a single result + resultAdded: resultsAdapter.upsertOne, + }, + extraReducers: (builder) => { + // here we can respond to a fulfilled call of the `getNextResultsPage` thunk + // because we pass in the fulfilled thunk action creator, everything is typed + + /** + * Received Result Images Page - PENDING + */ + builder.addCase(receivedResultImagesPage.pending, (state) => { + state.isLoading = true; + }); + + /** + * Received Result Images Page - FULFILLED + */ + builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => { + const { items, page, pages } = action.payload; + + const resultImages = items.map((image) => + deserializeImageResponse(image) + ); + + // use the adapter reducer to append all the results to state + resultsAdapter.addMany(state, resultImages); + + state.page = page; + state.pages = pages; + state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1; + state.isLoading = false; + }); + + /** + * Invocation Complete + */ + builder.addCase(invocationComplete, (state, action) => { + const { data } = action.payload; + const { result, node, graph_execution_state_id } = data; + + if (isImageOutput(result)) { + const name = result.image.image_name; + const type = result.image.image_type; + const { url, thumbnail } = buildImageUrls(type, name); + + const timestamp = extractTimestampFromImageName(name); + + const image: Image = { + name, + type, + url, + thumbnail, + metadata: { + created: timestamp, + width: result.width, // TODO: add tese dimensions + height: result.height, + invokeai: { + session_id: graph_execution_state_id, + ...(node ? { node } : {}), + }, + }, + }; + + resultsAdapter.addOne(state, image); + } + }); + }, +}); + +// Create a set of memoized selectors based on the location of this entity state +// to be used as selectors in a `useAppSelector()` call +export const { + selectAll: selectResultsAll, + selectById: selectResultsById, + selectEntities: selectResultsEntities, + selectIds: selectResultsIds, + selectTotal: selectResultsTotal, +} = resultsAdapter.getSelectors((state) => state.results); + +export const { resultAdded } = resultsSlice.actions; + +export default resultsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts b/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts deleted file mode 100644 index 7f28928987..0000000000 --- a/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { AnyAction, ThunkAction } from '@reduxjs/toolkit'; -import * as InvokeAI from 'app/invokeai'; -import { RootState } from 'app/store'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { setInitialImage } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { v4 as uuidv4 } from 'uuid'; -import { addImage } from '../gallerySlice'; - -type UploadImageConfig = { - imageFile: File; -}; - -export const uploadImage = - ( - config: UploadImageConfig - ): ThunkAction => - async (dispatch, getState) => { - const { imageFile } = config; - - const state = getState() as RootState; - - const activeTabName = activeTabNameSelector(state); - - const formData = new FormData(); - - formData.append('file', imageFile, imageFile.name); - formData.append( - 'data', - JSON.stringify({ - kind: 'init', - }) - ); - - const response = await fetch(`${window.location.origin}/upload`, { - method: 'POST', - body: formData, - }); - - const image = (await response.json()) as InvokeAI.ImageUploadResponse; - const newImage: InvokeAI.Image = { - uuid: uuidv4(), - category: 'user', - ...image, - }; - - dispatch(addImage({ image: newImage, category: 'user' })); - - if (activeTabName === 'unifiedCanvas') { - dispatch(setInitialCanvasImage(newImage)); - } else if (activeTabName === 'img2img') { - dispatch(setInitialImage(newImage)); - } - }; diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts new file mode 100644 index 0000000000..4159d37184 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistBlacklist.ts @@ -0,0 +1,12 @@ +import { UploadsState } from './uploadsSlice'; + +/** + * Uploads slice persist blacklist + * + * Currently blacklisting uploads slice entirely, see persist config in store.ts + */ +const itemsToBlacklist: (keyof UploadsState)[] = []; + +export const uploadsBlacklist = itemsToBlacklist.map( + (blacklistItem) => `uploads.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts new file mode 100644 index 0000000000..224d4c2335 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -0,0 +1,87 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { Image } from 'app/invokeai'; + +import { RootState } from 'app/store'; +import { + receivedUploadImagesPage, + IMAGES_PER_PAGE, +} from 'services/thunks/gallery'; +import { imageUploaded } from 'services/thunks/image'; +import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; + +export const uploadsAdapter = createEntityAdapter({ + selectId: (image) => image.name, + sortComparer: (a, b) => b.metadata.created - a.metadata.created, +}); + +type AdditionalUploadsState = { + page: number; + pages: number; + isLoading: boolean; + nextPage: number; +}; + +const initialUploadsState = + uploadsAdapter.getInitialState({ + page: 0, + pages: 0, + nextPage: 0, + isLoading: false, + }); + +export type UploadsState = typeof initialUploadsState; + +const uploadsSlice = createSlice({ + name: 'uploads', + initialState: initialUploadsState, + reducers: { + uploadAdded: uploadsAdapter.addOne, + }, + extraReducers: (builder) => { + /** + * Received Upload Images Page - PENDING + */ + builder.addCase(receivedUploadImagesPage.pending, (state) => { + state.isLoading = true; + }); + + /** + * Received Upload Images Page - FULFILLED + */ + builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => { + const { items, page, pages } = action.payload; + + const images = items.map((image) => deserializeImageResponse(image)); + + uploadsAdapter.addMany(state, images); + + state.page = page; + state.pages = pages; + state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1; + state.isLoading = false; + }); + + /** + * Upload Image - FULFILLED + */ + builder.addCase(imageUploaded.fulfilled, (state, action) => { + const { location, response } = action.payload; + + const uploadedImage = deserializeImageResponse(response); + + uploadsAdapter.addOne(state, uploadedImage); + }); + }, +}); + +export const { + selectAll: selectUploadsAll, + selectById: selectUploadsById, + selectEntities: selectUploadsEntities, + selectIds: selectUploadsIds, + selectTotal: selectUploadsTotal, +} = uploadsAdapter.getSelectors((state) => state.uploads); + +export const { uploadAdded } = uploadsSlice.actions; + +export default uploadsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx index 0658fd1756..660b07d75f 100644 --- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx +++ b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch'; import * as InvokeAI from 'app/invokeai'; +import { useGetUrl } from 'common/util/getUrl'; type ReactPanZoomProps = { - image: InvokeAI.Image; + image: InvokeAI._Image; styleClass?: string; alt?: string; ref?: React.Ref; @@ -22,6 +23,7 @@ export default function ReactPanZoomImage({ scaleY, }: ReactPanZoomProps) { const { centerView } = useTransformContext(); + const { getUrl } = useGetUrl(); return ( `lightbox.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx new file mode 100644 index 0000000000..ee6db90ec1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/AddNodeMenu.tsx @@ -0,0 +1,63 @@ +import { v4 as uuidv4 } from 'uuid'; + +import 'reactflow/dist/style.css'; +import { useCallback } from 'react'; +import { + Tooltip, + Menu, + MenuButton, + MenuList, + MenuItem, + IconButton, +} from '@chakra-ui/react'; +import { FaPlus } from 'react-icons/fa'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { nodeAdded } from '../store/nodesSlice'; +import { cloneDeep, map } from 'lodash'; +import { RootState } from 'app/store'; +import { useBuildInvocation } from '../hooks/useBuildInvocation'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/hooks/useToastWatcher'; + +export const AddNodeMenu = () => { + const dispatch = useAppDispatch(); + + const invocationTemplates = useAppSelector( + (state: RootState) => state.nodes.invocationTemplates + ); + + const buildInvocation = useBuildInvocation(); + + const addNode = useCallback( + (nodeType: string) => { + const invocation = buildInvocation(nodeType); + + if (!invocation) { + const toast = makeToast({ + status: 'error', + title: `Unknown Invocation type ${nodeType}`, + }); + dispatch(addToast(toast)); + return; + } + + dispatch(nodeAdded(invocation)); + }, + [dispatch, buildInvocation] + ); + + return ( + + } /> + + {map(invocationTemplates, ({ title, description, type }, key) => { + return ( + + addNode(type)}>{title} + + ); + })} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx new file mode 100644 index 0000000000..cc5b430382 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/FieldHandle.tsx @@ -0,0 +1,69 @@ +import { Tooltip } from '@chakra-ui/react'; +import { CSSProperties, useMemo } from 'react'; +import { + Handle, + Position, + Connection, + HandleType, + useReactFlow, +} from 'reactflow'; +import { FIELDS, HANDLE_TOOLTIP_OPEN_DELAY } from '../types/constants'; +// import { useConnectionEventStyles } from '../hooks/useConnectionEventStyles'; +import { InputFieldTemplate, OutputFieldTemplate } from '../types/types'; + +const handleBaseStyles: CSSProperties = { + position: 'absolute', + width: '1rem', + height: '1rem', + borderWidth: 0, +}; + +const inputHandleStyles: CSSProperties = { + left: '-1.7rem', +}; + +const outputHandleStyles: CSSProperties = { + right: '-1.7rem', +}; + +const requiredConnectionStyles: CSSProperties = { + boxShadow: '0 0 0.5rem 0.5rem var(--invokeai-colors-error-400)', +}; + +type FieldHandleProps = { + nodeId: string; + field: InputFieldTemplate | OutputFieldTemplate; + isValidConnection: (connection: Connection) => boolean; + handleType: HandleType; + styles?: CSSProperties; +}; + +export const FieldHandle = (props: FieldHandleProps) => { + const { nodeId, field, isValidConnection, handleType, styles } = props; + const { name, title, type, description } = field; + + return ( + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx new file mode 100644 index 0000000000..a420376016 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx @@ -0,0 +1,18 @@ +import 'reactflow/dist/style.css'; +import { Tooltip, Badge, HStack } from '@chakra-ui/react'; +import { map } from 'lodash'; +import { FIELDS } from '../types/constants'; + +export const FieldTypeLegend = () => { + return ( + + {map(FIELDS, ({ title, description, color }, key) => ( + + + {title} + + + ))} + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx new file mode 100644 index 0000000000..ad21e92b6d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -0,0 +1,104 @@ +import { + Background, + Controls, + MiniMap, + OnConnect, + OnEdgesChange, + OnNodesChange, + ReactFlow, + ConnectionLineType, + OnConnectStart, + OnConnectEnd, + Panel, +} from 'reactflow'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { + connectionEnded, + connectionMade, + connectionStarted, + edgesChanged, + nodesChanged, +} from '../store/nodesSlice'; +import { useCallback } from 'react'; +import { InvocationComponent } from './InvocationComponent'; +import { AddNodeMenu } from './AddNodeMenu'; +import { FieldTypeLegend } from './FieldTypeLegend'; +import { Button } from '@chakra-ui/react'; +import { nodesGraphBuilt } from 'services/thunks/session'; + +const nodeTypes = { invocation: InvocationComponent }; + +export const Flow = () => { + const dispatch = useAppDispatch(); + const nodes = useAppSelector((state: RootState) => state.nodes.nodes); + const edges = useAppSelector((state: RootState) => state.nodes.edges); + + const onNodesChange: OnNodesChange = useCallback( + (changes) => { + dispatch(nodesChanged(changes)); + }, + [dispatch] + ); + + const onEdgesChange: OnEdgesChange = useCallback( + (changes) => { + dispatch(edgesChanged(changes)); + }, + [dispatch] + ); + + const onConnectStart: OnConnectStart = useCallback( + (event, params) => { + dispatch(connectionStarted(params)); + }, + [dispatch] + ); + + const onConnect: OnConnect = useCallback( + (connection) => { + dispatch(connectionMade(connection)); + }, + [dispatch] + ); + + const onConnectEnd: OnConnectEnd = useCallback( + (event) => { + dispatch(connectionEnded()); + }, + [dispatch] + ); + + const handleInvoke = useCallback(() => { + dispatch(nodesGraphBuilt()); + }, [dispatch]); + + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx new file mode 100644 index 0000000000..58ca432ffc --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/InputFieldComponent.tsx @@ -0,0 +1,107 @@ +import { Box } from '@chakra-ui/react'; +import { InputFieldTemplate, InputFieldValue } from '../types/types'; +import { ArrayInputFieldComponent } from './fields/ArrayInputField.tsx'; +import { BooleanInputFieldComponent } from './fields/BooleanInputFieldComponent'; +import { EnumInputFieldComponent } from './fields/EnumInputFieldComponent'; +import { ImageInputFieldComponent } from './fields/ImageInputFieldComponent'; +import { LatentsInputFieldComponent } from './fields/LatentsInputFieldComponent'; +import { ModelInputFieldComponent } from './fields/ModelInputFieldComponent'; +import { NumberInputFieldComponent } from './fields/NumberInputFieldComponent'; +import { StringInputFieldComponent } from './fields/StringInputFieldComponent'; + +type InputFieldComponentProps = { + nodeId: string; + field: InputFieldValue; + template: InputFieldTemplate; +}; + +// build an individual input element based on the schema +export const InputFieldComponent = (props: InputFieldComponentProps) => { + const { nodeId, field, template } = props; + const { type, value } = field; + + if (type === 'string' && template.type === 'string') { + return ( + + ); + } + + if (type === 'boolean' && template.type === 'boolean') { + return ( + + ); + } + + if ( + (type === 'integer' && template.type === 'integer') || + (type === 'float' && template.type === 'float') + ) { + return ( + + ); + } + + if (type === 'enum' && template.type === 'enum') { + return ( + + ); + } + + if (type === 'image' && template.type === 'image') { + return ( + + ); + } + + if (type === 'latents' && template.type === 'latents') { + return ( + + ); + } + + if (type === 'model' && template.type === 'model') { + return ( + + ); + } + + if (type === 'array' && template.type === 'array') { + return ( + + ); + } + + return Unknown field type: {type}; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx new file mode 100644 index 0000000000..5f06ee9352 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/InvocationComponent.tsx @@ -0,0 +1,243 @@ +import { NodeProps, useReactFlow } from 'reactflow'; +import { + Box, + Flex, + FormControl, + FormLabel, + Heading, + HStack, + Tooltip, + Icon, + Code, + Text, +} from '@chakra-ui/react'; +import { FaExclamationCircle, FaInfoCircle } from 'react-icons/fa'; +import { InvocationValue } from '../types/types'; +import { InputFieldComponent } from './InputFieldComponent'; +import { FieldHandle } from './FieldHandle'; +import { isEqual, map, size } from 'lodash'; +import { memo, useMemo, useRef } from 'react'; +import { useIsValidConnection } from '../hooks/useIsValidConnection'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { useGetInvocationTemplate } from '../hooks/useInvocationTemplate'; + +const connectedInputFieldsSelector = createSelector( + [(state: RootState) => state.nodes.edges], + (edges) => { + // return edges.map((e) => e.targetHandle); + return edges; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +export const InvocationComponent = memo((props: NodeProps) => { + const { id: nodeId, data, selected } = props; + const { type, inputs, outputs } = data; + + const isValidConnection = useIsValidConnection(); + + const connectedInputs = useAppSelector(connectedInputFieldsSelector); + const getInvocationTemplate = useGetInvocationTemplate(); + // TODO: determine if a field/handle is connected and disable the input if so + + const template = useRef(getInvocationTemplate(type)); + + if (!template.current) { + return ( + + + + + + ); + } + + return ( + + + <> + {nodeId} + + + {template.current.title} + + + + + + {map(inputs, (input, i) => { + const { id: fieldId } = input; + const inputTemplate = template.current?.inputs[input.name]; + + if (!inputTemplate) { + return ( + + + + Unknown input: {input.name} + + + + ); + } + + const isConnected = Boolean( + connectedInputs.filter((connectedInput) => { + return ( + connectedInput.target === nodeId && + connectedInput.targetHandle === input.name + ); + }).length + ); + + return ( + + + + {inputTemplate?.title} + + + + + + + {!['never', 'directOnly'].includes( + inputTemplate?.inputRequirement ?? '' + ) && ( + + )} + + ); + })} + {map(outputs).map((output, i) => { + const outputTemplate = template.current?.outputs[output.name]; + + const isConnected = Boolean( + connectedInputs.filter((connectedInput) => { + return ( + connectedInput.source === nodeId && + connectedInput.sourceHandle === output.name + ); + }).length + ); + + if (!outputTemplate) { + return ( + + + + Unknown output: {output.name} + + + + ); + } + + return ( + + + + {outputTemplate?.title} Output + + + + + ); + })} + + + + + ); +}); + +InvocationComponent.displayName = 'InvocationComponent'; diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx new file mode 100644 index 0000000000..98e3b2d19a --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -0,0 +1,46 @@ +import 'reactflow/dist/style.css'; +import { Box } from '@chakra-ui/react'; +import { ReactFlowProvider } from 'reactflow'; + +import { Flow } from './Flow'; +import { useAppSelector } from 'app/storeHooks'; +import { RootState } from 'app/store'; +import { buildNodesGraph } from '../util/nodesGraphBuilder/buildNodesGraph'; + +const NodeEditor = () => { + const state = useAppSelector((state: RootState) => state); + + const graph = buildNodesGraph(state); + + return ( + + + + + + {JSON.stringify(graph, null, 2)} + + + ); +}; + +export default NodeEditor; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx new file mode 100644 index 0000000000..d9717f14a4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ArrayInputField.tsx.tsx @@ -0,0 +1,14 @@ +import { + ArrayInputFieldTemplate, + ArrayInputFieldValue, +} from 'features/nodes/types'; +import { FaImage, FaList } from 'react-icons/fa'; +import { FieldComponentProps } from './types'; + +export const ArrayInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + return ; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx new file mode 100644 index 0000000000..f9fe404f82 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/BooleanInputFieldComponent.tsx @@ -0,0 +1,31 @@ +import { Switch } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + BooleanInputFieldTemplate, + BooleanInputFieldValue, +} from 'features/nodes/types'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +export const BooleanInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.checked, + }) + ); + }; + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx new file mode 100644 index 0000000000..8de8e17484 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/EnumInputFieldComponent.tsx @@ -0,0 +1,35 @@ +import { Select } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + EnumInputFieldTemplate, + EnumInputFieldValue, +} from 'features/nodes/types'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +export const EnumInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field, template } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.value, + }) + ); + }; + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx new file mode 100644 index 0000000000..599fa61e38 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx @@ -0,0 +1,64 @@ +import { Box, Image, Icon, Flex } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; +import { useGetUrl } from 'common/util/getUrl'; +import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; +import useGetImageByUuid from 'features/gallery/hooks/useGetImageByUuid'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + ImageInputFieldTemplate, + ImageInputFieldValue, +} from 'features/nodes/types'; +import { DragEvent, useCallback, useState } from 'react'; +import { FaImage } from 'react-icons/fa'; +import { ImageType } from 'services/api'; +import { FieldComponentProps } from './types'; + +export const ImageInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + const { value } = field; + + const getImageByNameAndType = useGetImageByNameAndType(); + const dispatch = useAppDispatch(); + const [url, setUrl] = useState(); + const { getUrl } = useGetUrl(); + + const handleDrop = useCallback( + (e: DragEvent) => { + const name = e.dataTransfer.getData('invokeai/imageName'); + const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; + + if (!name || !type) { + return; + } + + const image = getImageByNameAndType(name, type); + + if (!image) { + return; + } + + setUrl(image.url); + + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: { + image_name: name, + image_type: type, + }, + }) + ); + }, + [getImageByNameAndType, dispatch, field.name, nodeId] + ); + + return ( + + } /> + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx new file mode 100644 index 0000000000..ed053aab7c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LatentsInputFieldComponent.tsx @@ -0,0 +1,13 @@ +import { + LatentsInputFieldTemplate, + LatentsInputFieldValue, +} from 'features/nodes/types'; +import { FieldComponentProps } from './types'; + +export const LatentsInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + return null; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx new file mode 100644 index 0000000000..5aaf83a186 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/ModelInputFieldComponent.tsx @@ -0,0 +1,57 @@ +import { Select } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + ModelInputFieldTemplate, + ModelInputFieldValue, +} from 'features/nodes/types'; +import { + selectModelsById, + selectModelsIds, +} from 'features/system/store/modelSlice'; +import { isEqual, map } from 'lodash'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +const availableModelsSelector = createSelector( + [selectModelsIds], + (allModelNames) => { + return { allModelNames }; + // return map(modelList, (_, name) => name); + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +export const ModelInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const { allModelNames } = useAppSelector(availableModelsSelector); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.value, + }) + ); + }; + + return ( + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx new file mode 100644 index 0000000000..57b8527e00 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/NumberInputFieldComponent.tsx @@ -0,0 +1,41 @@ +import { + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, +} from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + FloatInputFieldTemplate, + FloatInputFieldValue, + IntegerInputFieldTemplate, + IntegerInputFieldValue, +} from 'features/nodes/types'; +import { FieldComponentProps } from './types'; + +export const NumberInputFieldComponent = ( + props: FieldComponentProps< + IntegerInputFieldValue | FloatInputFieldValue, + IntegerInputFieldTemplate | FloatInputFieldTemplate + > +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (_: string, value: number) => { + dispatch(fieldValueChanged({ nodeId, fieldName: field.name, value })); + }; + + return ( + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx new file mode 100644 index 0000000000..7ed3b5d435 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/StringInputFieldComponent.tsx @@ -0,0 +1,29 @@ +import { Input } from '@chakra-ui/react'; +import { useAppDispatch } from 'app/storeHooks'; +import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; +import { + StringInputFieldTemplate, + StringInputFieldValue, +} from 'features/nodes/types'; +import { ChangeEvent } from 'react'; +import { FieldComponentProps } from './types'; + +export const StringInputFieldComponent = ( + props: FieldComponentProps +) => { + const { nodeId, field } = props; + + const dispatch = useAppDispatch(); + + const handleValueChanged = (e: ChangeEvent) => { + dispatch( + fieldValueChanged({ + nodeId, + fieldName: field.name, + value: e.target.value, + }) + ); + }; + + return ; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/types.ts b/invokeai/frontend/web/src/features/nodes/components/fields/types.ts new file mode 100644 index 0000000000..fb7e73ad92 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/fields/types.ts @@ -0,0 +1,10 @@ +import { InputFieldTemplate, InputFieldValue } from 'features/nodes/types'; + +export type FieldComponentProps< + V extends InputFieldValue, + T extends InputFieldTemplate +> = { + nodeId: string; + field: V; + template: T; +}; diff --git a/invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts b/invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts new file mode 100644 index 0000000000..46ee5289b2 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/examples/iterationGraph.ts @@ -0,0 +1,52 @@ +export const iterationGraph = { + nodes: { + '0': { + id: '0', + type: 'range', + start: 0, + stop: 5, + step: 1, + }, + '1': { + collection: [], + id: '1', + index: 0, + type: 'iterate', + }, + '2': { + cfg_scale: 7.5, + height: 512, + id: '2', + model: '', + progress_images: false, + prompt: 'dog', + sampler_name: 'k_lms', + seamless: false, + steps: 11, + type: 'txt2img', + width: 512, + }, + }, + edges: [ + { + source: { + field: 'collection', + node_id: '0', + }, + destination: { + field: 'collection', + node_id: '1', + }, + }, + { + source: { + field: 'item', + node_id: '1', + }, + destination: { + field: 'seed', + node_id: '2', + }, + }, + ], +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts new file mode 100644 index 0000000000..19eeac8378 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useBuildInvocation.ts @@ -0,0 +1,78 @@ +import { RootState } from 'app/store'; +import { useAppSelector } from 'app/storeHooks'; +import { reduce } from 'lodash'; +import { Node } from 'reactflow'; +import { AnyInvocationType } from 'services/events/types'; +import { v4 as uuidv4 } from 'uuid'; +import { + InputFieldValue, + InvocationValue, + OutputFieldValue, +} from '../types/types'; +import { buildInputFieldValue } from '../util/fieldValueBuilders'; + +export const useBuildInvocation = () => { + const invocationTemplates = useAppSelector( + (state: RootState) => state.nodes.invocationTemplates + ); + + return (type: AnyInvocationType) => { + const template = invocationTemplates[type]; + + if (template === undefined) { + console.error(`Unable to find template ${type}.`); + return; + } + + const nodeId = uuidv4(); + + const inputs = reduce( + template.inputs, + (inputsAccumulator, inputTemplate, inputName) => { + const fieldId = uuidv4(); + + const inputFieldValue: InputFieldValue = buildInputFieldValue( + fieldId, + inputTemplate + ); + + inputsAccumulator[inputName] = inputFieldValue; + + return inputsAccumulator; + }, + {} as Record + ); + + const outputs = reduce( + template.outputs, + (outputsAccumulator, outputTemplate, outputName) => { + const fieldId = uuidv4(); + + const outputFieldValue: OutputFieldValue = { + id: fieldId, + name: outputName, + type: outputTemplate.type, + }; + + outputsAccumulator[outputName] = outputFieldValue; + + return outputsAccumulator; + }, + {} as Record + ); + + const invocation: Node = { + id: nodeId, + type: 'invocation', + position: { x: 0, y: 0 }, + data: { + id: nodeId, + type, + inputs, + outputs, + }, + }; + + return invocation; + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts new file mode 100644 index 0000000000..f58e82b897 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInvocationTemplate.ts @@ -0,0 +1,16 @@ +import { useAppSelector } from 'app/storeHooks'; +import { invocationTemplatesSelector } from '../store/selectors/invocationTemplatesSelector'; + +export const useGetInvocationTemplate = () => { + const invocationTemplates = useAppSelector(invocationTemplatesSelector); + + return (invocationType: string) => { + const template = invocationTemplates[invocationType]; + + if (!template) { + return; + } + + return template; + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts new file mode 100644 index 0000000000..dec9120d08 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -0,0 +1,93 @@ +import { useCallback } from 'react'; +import { Connection, Node, useReactFlow } from 'reactflow'; +import graphlib from '@dagrejs/graphlib'; +import { InvocationValue } from '../types/types'; + +export const useIsValidConnection = () => { + const flow = useReactFlow(); + + // Check if an in-progress connection is valid + const isValidConnection = useCallback( + ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { + const edges = flow.getEdges(); + const nodes = flow.getNodes(); + + return true; + + // Connection must have valid targets + if (!(source && sourceHandle && target && targetHandle)) { + return false; + } + + // Connection is invalid if target already has a connection + if ( + edges.find((edge) => { + return edge.target === target && edge.targetHandle === 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; + } + + // Connection types must be the same for a connection + if ( + sourceNode.data.outputs[sourceHandle].type !== + targetNode.data.inputs[targetHandle].type + ) { + return false; + } + + // Graphs much be acyclic (no loops!) + + /** + * TODO: use `graphlib.alg.findCycles()` to identify strong connections + * + * this validation func only runs when the cursor hits the second handle of the connection, + * and only on that second handle - so it cannot tell us exhaustively which connections + * are valid. + * + * ideally, we check when the connection starts to calculate all invalid handles at once. + * + * requires making a new graphlib graph - and calling `findCycles()` - for each potential + * handle. instead of using the `isValidConnection` prop, it would use the `onConnectStart` + * prop. + * + * the strong connections should be stored in global state. + * + * then, `isValidConnection` would simple loop through the strong connections and if the + * source and target are in a single strong connection, return false. + * + * and also, we can use this knowledge to style every handle when a connection starts, + * which is otherwise not possible. + */ + + // build a graphlib graph + const g = new graphlib.Graph(); + + nodes.forEach((n) => { + g.setNode(n.id); + }); + + edges.forEach((e) => { + g.setEdge(e.source, e.target); + }); + + // Add the candidate edge to the graph + g.setEdge(source, target); + + return graphlib.alg.isAcyclic(g); + }, + [flow] + ); + + return isValidConnection; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts b/invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts new file mode 100644 index 0000000000..5648283ddd --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/nodesPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { NodesState } from './nodesSlice'; + +/** + * Nodes slice persist blacklist + */ +const itemsToBlacklist: (keyof NodesState)[] = ['schema', 'invocations']; + +export const nodesBlacklist = itemsToBlacklist.map( + (blacklistItem) => `nodes.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts new file mode 100644 index 0000000000..1ce806de57 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -0,0 +1,103 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { OpenAPIV3 } from 'openapi-types'; +import { + addEdge, + applyEdgeChanges, + applyNodeChanges, + Connection, + Edge, + EdgeChange, + Node, + NodeChange, + OnConnectStartParams, +} from 'reactflow'; +import { Graph, ImageField } from 'services/api'; +import { receivedOpenAPISchema } from 'services/thunks/schema'; +import { isFulfilledAnyGraphBuilt } from 'services/thunks/session'; +import { InvocationTemplate, InvocationValue } from '../types/types'; +import { parseSchema } from '../util/parseSchema'; + +export type NodesState = { + nodes: Node[]; + edges: Edge[]; + schema: OpenAPIV3.Document | null; + invocationTemplates: Record; + connectionStartParams: OnConnectStartParams | null; + lastGraph: Graph | null; +}; + +export const initialNodesState: NodesState = { + nodes: [], + edges: [], + schema: null, + invocationTemplates: {}, + connectionStartParams: null, + lastGraph: null, +}; + +const nodesSlice = createSlice({ + name: 'nodes', + initialState: initialNodesState, + reducers: { + nodesChanged: (state, action: PayloadAction) => { + state.nodes = applyNodeChanges(action.payload, state.nodes); + }, + nodeAdded: (state, action: PayloadAction>) => { + state.nodes.push(action.payload); + }, + edgesChanged: (state, action: PayloadAction) => { + state.edges = applyEdgeChanges(action.payload, state.edges); + }, + connectionStarted: (state, action: PayloadAction) => { + state.connectionStartParams = action.payload; + }, + connectionMade: (state, action: PayloadAction) => { + state.edges = addEdge(action.payload, state.edges); + }, + connectionEnded: (state) => { + state.connectionStartParams = null; + }, + fieldValueChanged: ( + state, + action: PayloadAction<{ + nodeId: string; + fieldName: string; + value: + | string + | number + | boolean + | Pick + | undefined; + }> + ) => { + const { nodeId, fieldName, value } = action.payload; + const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId); + + if (nodeIndex > -1) { + state.nodes[nodeIndex].data.inputs[fieldName].value = value; + } + }, + }, + extraReducers(builder) { + builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { + state.schema = action.payload; + state.invocationTemplates = parseSchema(action.payload); + }); + + builder.addMatcher(isFulfilledAnyGraphBuilt, (state, action) => { + state.lastGraph = action.payload; + }); + }, +}); + +export const { + nodesChanged, + edgesChanged, + nodeAdded, + fieldValueChanged, + connectionMade, + connectionStarted, + connectionEnded, +} = nodesSlice.actions; + +export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts b/invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts new file mode 100644 index 0000000000..0ffc4ac5bb --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/selectors/invocationTemplatesSelector.ts @@ -0,0 +1,7 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; + +export const invocationTemplatesSelector = createSelector( + (state: RootState) => state.nodes, + (nodes) => nodes.invocationTemplates +); diff --git a/invokeai/frontend/web/src/features/nodes/types/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts new file mode 100644 index 0000000000..0fb8acf5bf --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -0,0 +1,69 @@ +import { getCSSVar } from '@chakra-ui/utils'; +import { FieldType, FieldUIConfig } from './types'; + +export const HANDLE_TOOLTIP_OPEN_DELAY = 500; + +export const FIELD_TYPE_MAP: Record = { + integer: 'integer', + number: 'float', + string: 'string', + boolean: 'boolean', + enum: 'enum', + ImageField: 'image', + LatentsField: 'latents', + model: 'model', + array: 'array', +}; + +const COLOR_TOKEN_VALUE = 500; + +const getColorTokenCssVariable = (color: string) => + `var(--invokeai-colors-${color}-${COLOR_TOKEN_VALUE})`; + +export const FIELDS: Record = { + integer: { + colorCssVar: getColorTokenCssVariable('red'), + title: 'Integer', + description: 'Integers are whole numbers, without a decimal point.', + }, + float: { + colorCssVar: getColorTokenCssVariable('orange'), + title: 'Float', + description: 'Floats are numbers with a decimal point.', + }, + string: { + colorCssVar: getColorTokenCssVariable('yellow'), + title: 'String', + description: 'Strings are text.', + }, + boolean: { + colorCssVar: getColorTokenCssVariable('green'), + title: 'Boolean', + description: 'Booleans are true or false.', + }, + enum: { + colorCssVar: getColorTokenCssVariable('blue'), + title: 'Enum', + description: 'Enums are values that may be one of a number of options.', + }, + image: { + colorCssVar: getColorTokenCssVariable('purple'), + title: 'Image', + description: 'Images may be passed between nodes.', + }, + latents: { + colorCssVar: getColorTokenCssVariable('pink'), + title: 'Latents', + description: 'Latents may be passed between nodes.', + }, + model: { + colorCssVar: getColorTokenCssVariable('teal'), + title: 'Model', + description: 'Models are models.', + }, + array: { + colorCssVar: getColorTokenCssVariable('gray'), + title: 'Array', + description: 'TODO: Array type description.', + }, +}; diff --git a/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts b/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts new file mode 100644 index 0000000000..99c9a28150 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/typeGuards.ts @@ -0,0 +1,9 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const isReferenceObject = ( + obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject +): obj is OpenAPIV3.ReferenceObject => '$ref' in obj; + +export const isSchemaObject = ( + obj: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject +): obj is OpenAPIV3.SchemaObject => !('$ref' in obj); diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts new file mode 100644 index 0000000000..1f35712d39 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -0,0 +1,296 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { ImageField } from 'services/api'; +import { AnyInvocationType } from 'services/events/types'; + +export type InvocationValue = { + id: string; + type: AnyInvocationType; + inputs: Record; + outputs: Record; +}; + +export type InvocationTemplate = { + /** + * Unique type of the invocation + */ + type: AnyInvocationType; + /** + * Display name of the invocation + */ + title: string; + /** + * Description of the invocation + */ + description: string; + /** + * Invocation tags + */ + tags: string[]; + /** + * Array of invocation inputs + */ + inputs: Record; + // inputs: InputField[]; + /** + * Array of the invocation outputs + */ + outputs: Record; + // outputs: OutputField[]; +}; + +export type FieldUIConfig = { + colorCssVar: string; + title: string; + description: string; +}; + +/** + * The valid invocation field types + */ +export type FieldType = + | 'integer' + | 'float' + | 'string' + | 'boolean' + | 'enum' + | 'image' + | 'latents' + | 'model' + | 'array'; + +/** + * An input field is persisted across reloads as part of the user's local state. + * + * An input field has three properties: + * - `id` a unique identifier + * - `name` the name of the field, which comes from the python dataclass + * - `value` the field's value + */ +export type InputFieldValue = + | IntegerInputFieldValue + | FloatInputFieldValue + | StringInputFieldValue + | BooleanInputFieldValue + | ImageInputFieldValue + | LatentsInputFieldValue + | EnumInputFieldValue + | ModelInputFieldValue + | ArrayInputFieldValue; + +/** + * An input field template is generated on each page load from the OpenAPI schema. + * + * The template provides the field type and other field metadata (e.g. title, description, + * maximum length, pattern to match, etc). + */ +export type InputFieldTemplate = + | IntegerInputFieldTemplate + | FloatInputFieldTemplate + | StringInputFieldTemplate + | BooleanInputFieldTemplate + | ImageInputFieldTemplate + | LatentsInputFieldTemplate + | EnumInputFieldTemplate + | ModelInputFieldTemplate + | ArrayInputFieldTemplate; + +/** + * An output field is persisted across as part of the user's local state. + * + * An output field has two properties: + * - `id` a unique identifier + * - `name` the name of the field, which comes from the python dataclass + */ +export type OutputFieldValue = FieldValueBase; + +/** + * An output field template is generated on each page load from the OpenAPI schema. + * + * The template provides the output field's name, type, title, and description. + */ +export type OutputFieldTemplate = { + name: string; + type: FieldType; + title: string; + description: string; +}; + +/** + * Indicates when/if this field needs an input. + */ +export type InputRequirement = 'always' | 'never' | 'optional'; + +/** + * Indicates the kind of input(s) this field may have. + */ +export type InputKind = 'connection' | 'direct' | 'any'; + +export type FieldValueBase = { + id: string; + name: string; + type: FieldType; +}; + +export type IntegerInputFieldValue = FieldValueBase & { + type: 'integer'; + value?: number; +}; + +export type FloatInputFieldValue = FieldValueBase & { + type: 'float'; + value?: number; +}; + +export type StringInputFieldValue = FieldValueBase & { + type: 'string'; + value?: string; +}; + +export type BooleanInputFieldValue = FieldValueBase & { + type: 'boolean'; + value?: boolean; +}; + +export type EnumInputFieldValue = FieldValueBase & { + type: 'enum'; + value?: number | string; +}; + +export type LatentsInputFieldValue = FieldValueBase & { + type: 'latents'; + value?: undefined; +}; + +export type ImageInputFieldValue = FieldValueBase & { + type: 'image'; + value?: Pick; +}; + +export type ModelInputFieldValue = FieldValueBase & { + type: 'model'; + value?: string; +}; + +export type ArrayInputFieldValue = FieldValueBase & { + type: 'array'; + value?: (string | number)[]; +}; + +export type InputFieldTemplateBase = { + name: string; + title: string; + description: string; + type: FieldType; + inputRequirement: InputRequirement; + inputKind: InputKind; +}; + +export type IntegerInputFieldTemplate = InputFieldTemplateBase & { + type: 'integer'; + default: number; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: boolean; + minimum?: number; + exclusiveMinimum?: boolean; +}; + +export type FloatInputFieldTemplate = InputFieldTemplateBase & { + type: 'float'; + default: number; + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: boolean; + minimum?: number; + exclusiveMinimum?: boolean; +}; + +export type StringInputFieldTemplate = InputFieldTemplateBase & { + type: 'string'; + default: string; + maxLength?: number; + minLength?: number; + pattern?: string; +}; + +export type BooleanInputFieldTemplate = InputFieldTemplateBase & { + default: boolean; + type: 'boolean'; +}; + +export type ImageInputFieldTemplate = InputFieldTemplateBase & { + default: Pick; + type: 'image'; +}; + +export type LatentsInputFieldTemplate = InputFieldTemplateBase & { + default: undefined; + type: 'latents'; +}; + +export type EnumInputFieldTemplate = InputFieldTemplateBase & { + default: string | number; + type: 'enum'; + enumType: 'string' | 'number'; + options: Array; +}; + +export type ModelInputFieldTemplate = InputFieldTemplateBase & { + default: string; + type: 'model'; +}; + +export type ArrayInputFieldTemplate = InputFieldTemplateBase & { + default: (string | number)[]; + type: 'array'; +}; + +/** + * JANKY CUSTOMISATION OF OpenAPI SCHEMA TYPES + */ + +export type TypeHints = { + [fieldName: string]: FieldType; +}; + +export type InvocationSchemaExtra = { + output: OpenAPIV3.ReferenceObject; // the output of the invocation + ui?: { + tags?: string[]; + type_hints?: TypeHints; + title?: string; + }; + title: string; + properties: Omit< + NonNullable, + 'type' + > & { + type: Omit & { + default: AnyInvocationType; + }; + }; +}; + +export type InvocationSchemaType = { + default: string; // the type of the invocation +}; + +export type InvocationBaseSchemaObject = Omit< + OpenAPIV3.BaseSchemaObject, + 'title' | 'type' | 'properties' +> & + InvocationSchemaExtra; + +export interface ArraySchemaObject extends InvocationBaseSchemaObject { + type: OpenAPIV3.ArraySchemaObjectType; + items: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject; +} +export interface NonArraySchemaObject extends InvocationBaseSchemaObject { + type?: OpenAPIV3.NonArraySchemaObjectType; +} + +export type InvocationSchemaObject = ArraySchemaObject | NonArraySchemaObject; + +export const isInvocationSchemaObject = ( + obj: OpenAPIV3.ReferenceObject | InvocationSchemaObject +): obj is InvocationSchemaObject => !('$ref' in obj); diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts new file mode 100644 index 0000000000..e37f446e00 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/fieldTemplateBuilders.ts @@ -0,0 +1,336 @@ +import { reduce } from 'lodash'; +import { OpenAPIV3 } from 'openapi-types'; +import { FIELD_TYPE_MAP } from '../types/constants'; +import { isSchemaObject } from '../types/typeGuards'; +import { + BooleanInputFieldTemplate, + EnumInputFieldTemplate, + FloatInputFieldTemplate, + ImageInputFieldTemplate, + IntegerInputFieldTemplate, + LatentsInputFieldTemplate, + StringInputFieldTemplate, + ModelInputFieldTemplate, + InputFieldTemplateBase, + OutputFieldTemplate, + TypeHints, + FieldType, +} from '../types/types'; + +export type BaseFieldProperties = 'name' | 'title' | 'description'; + +export type BuildInputFieldArg = { + schemaObject: OpenAPIV3.SchemaObject; + baseField: Omit< + InputFieldTemplateBase, + 'type' | 'inputRequirement' | 'inputKind' + >; +}; + +/** + * Transforms an invocation output ref object to field type. + * @param ref The ref string to transform + * @returns The field type. + * + * @example + * refObjectToFieldType({ "$ref": "#/components/schemas/ImageField" }) --> 'ImageField' + */ +export const refObjectToFieldType = ( + refObject: OpenAPIV3.ReferenceObject +): keyof typeof FIELD_TYPE_MAP => refObject.$ref.split('/').slice(-1)[0]; + +const buildIntegerInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): IntegerInputFieldTemplate => { + const template: IntegerInputFieldTemplate = { + ...baseField, + type: 'integer', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? 0, + }; + + if (schemaObject.multipleOf !== undefined) { + template.multipleOf = schemaObject.multipleOf; + } + + if (schemaObject.maximum !== undefined) { + template.maximum = schemaObject.maximum; + } + + if (schemaObject.exclusiveMaximum !== undefined) { + template.exclusiveMaximum = schemaObject.exclusiveMaximum; + } + + if (schemaObject.minimum !== undefined) { + template.minimum = schemaObject.minimum; + } + + if (schemaObject.exclusiveMinimum !== undefined) { + template.exclusiveMinimum = schemaObject.exclusiveMinimum; + } + + return template; +}; + +const buildFloatInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): FloatInputFieldTemplate => { + const template: FloatInputFieldTemplate = { + ...baseField, + type: 'float', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? 0, + }; + + if (schemaObject.multipleOf !== undefined) { + template.multipleOf = schemaObject.multipleOf; + } + + if (schemaObject.maximum !== undefined) { + template.maximum = schemaObject.maximum; + } + + if (schemaObject.exclusiveMaximum !== undefined) { + template.exclusiveMaximum = schemaObject.exclusiveMaximum; + } + + if (schemaObject.minimum !== undefined) { + template.minimum = schemaObject.minimum; + } + + if (schemaObject.exclusiveMinimum !== undefined) { + template.exclusiveMinimum = schemaObject.exclusiveMinimum; + } + + return template; +}; + +const buildStringInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): StringInputFieldTemplate => { + const template: StringInputFieldTemplate = { + ...baseField, + type: 'string', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? '', + }; + + if (schemaObject.minLength !== undefined) { + template.minLength = schemaObject.minLength; + } + + if (schemaObject.maxLength !== undefined) { + template.maxLength = schemaObject.maxLength; + } + + if (schemaObject.pattern !== undefined) { + template.pattern = schemaObject.pattern; + } + + return template; +}; + +const buildBooleanInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): BooleanInputFieldTemplate => { + const template: BooleanInputFieldTemplate = { + ...baseField, + type: 'boolean', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? false, + }; + + return template; +}; + +const buildModelInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): ModelInputFieldTemplate => { + const template: ModelInputFieldTemplate = { + ...baseField, + type: 'model', + inputRequirement: 'always', + inputKind: 'direct', + default: schemaObject.default ?? undefined, + }; + + return template; +}; + +const buildImageInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): ImageInputFieldTemplate => { + const template: ImageInputFieldTemplate = { + ...baseField, + type: 'image', + inputRequirement: 'always', + inputKind: 'any', + default: schemaObject.default ?? undefined, + }; + + return template; +}; + +const buildLatentsInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): LatentsInputFieldTemplate => { + const template: LatentsInputFieldTemplate = { + ...baseField, + type: 'latents', + inputRequirement: 'always', + inputKind: 'connection', + default: schemaObject.default ?? undefined, + }; + + return template; +}; + +const buildEnumInputFieldTemplate = ({ + schemaObject, + baseField, +}: BuildInputFieldArg): EnumInputFieldTemplate => { + const options = schemaObject.enum ?? []; + const template: EnumInputFieldTemplate = { + ...baseField, + type: 'enum', + enumType: (schemaObject.type as 'string' | 'number') ?? 'string', // TODO: dangerous? + options: options, + inputRequirement: 'always', + inputKind: 'direct', + default: schemaObject.default ?? options[0], + }; + + return template; +}; + +export const getFieldType = ( + schemaObject: OpenAPIV3.SchemaObject, + name: string, + typeHints?: TypeHints +): FieldType => { + let rawFieldType = ''; + + if (typeHints && name in typeHints) { + rawFieldType = typeHints[name]; + } else if (!schemaObject.type) { + rawFieldType = refObjectToFieldType( + schemaObject.allOf![0] as OpenAPIV3.ReferenceObject + ); + } else if (schemaObject.enum) { + rawFieldType = 'enum'; + } else if (schemaObject.type) { + rawFieldType = schemaObject.type; + } + + const fieldType = FIELD_TYPE_MAP[rawFieldType]; + + if (!fieldType) { + throw `Field type "${rawFieldType}" is unknown!`; + } + + return fieldType; +}; + +/** + * Builds an input field from an invocation schema property. + * @param schemaObject The schema object + * @returns An input field + */ +export const buildInputFieldTemplate = ( + schemaObject: OpenAPIV3.SchemaObject, + name: string, + typeHints?: TypeHints +) => { + const fieldType = getFieldType(schemaObject, name, typeHints); + + const baseField = { + name, + title: schemaObject.title ?? '', + description: schemaObject.description ?? '', + }; + + if (['image'].includes(fieldType)) { + return buildImageInputFieldTemplate({ schemaObject, baseField }); + } + if (['latents'].includes(fieldType)) { + return buildLatentsInputFieldTemplate({ schemaObject, baseField }); + } + if (['model'].includes(fieldType)) { + return buildModelInputFieldTemplate({ schemaObject, baseField }); + } + if (['enum'].includes(fieldType)) { + return buildEnumInputFieldTemplate({ schemaObject, baseField }); + } + if (['integer'].includes(fieldType)) { + return buildIntegerInputFieldTemplate({ schemaObject, baseField }); + } + if (['number', 'float'].includes(fieldType)) { + return buildFloatInputFieldTemplate({ schemaObject, baseField }); + } + if (['string'].includes(fieldType)) { + return buildStringInputFieldTemplate({ schemaObject, baseField }); + } + if (['boolean'].includes(fieldType)) { + return buildBooleanInputFieldTemplate({ schemaObject, baseField }); + } + + return; +}; + +/** + * Builds invocation output fields from an invocation's output reference object. + * @param openAPI The OpenAPI schema + * @param refObject The output reference object + * @returns A record of outputs + */ +export const buildOutputFieldTemplates = ( + refObject: OpenAPIV3.ReferenceObject, + openAPI: OpenAPIV3.Document, + typeHints?: TypeHints +): Record => { + // extract output schema name from ref + const outputSchemaName = refObject.$ref.split('/').slice(-1)[0]; + + // get the output schema itself + const outputSchema = openAPI.components!.schemas![outputSchemaName]; + + if (isSchemaObject(outputSchema)) { + const outputFields = reduce( + outputSchema.properties as OpenAPIV3.SchemaObject, + (outputsAccumulator, property, propertyName) => { + if ( + !['type', 'id'].includes(propertyName) && + !['object'].includes(property.type) && // TODO: handle objects? + isSchemaObject(property) + ) { + const fieldType = getFieldType(property, propertyName, typeHints); + + outputsAccumulator[propertyName] = { + name: propertyName, + title: property.title ?? '', + description: property.description ?? '', + type: fieldType, + }; + } + + return outputsAccumulator; + }, + {} as Record + ); + + return outputFields; + } + + return {}; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts new file mode 100644 index 0000000000..f2db2b5dc4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/fieldValueBuilders.ts @@ -0,0 +1,57 @@ +import { InputFieldTemplate, InputFieldValue } from '../types/types'; + +export const buildInputFieldValue = ( + id: string, + template: InputFieldTemplate +): InputFieldValue => { + const fieldValue: InputFieldValue = { + id, + name: template.name, + type: template.type, + }; + + if (template.inputRequirement !== 'never') { + if (template.type === 'string') { + fieldValue.value = template.default ?? ''; + } + + if (template.type === 'integer') { + fieldValue.value = template.default ?? 0; + } + + if (template.type === 'float') { + fieldValue.value = template.default ?? 0; + } + + if (template.type === 'boolean') { + fieldValue.value = template.default ?? false; + } + + if (template.type === 'enum') { + if (template.enumType === 'number') { + fieldValue.value = template.default ?? 0; + } + if (template.enumType === 'string') { + fieldValue.value = template.default ?? ''; + } + } + + if (template.type === 'array') { + fieldValue.value = template.default ?? 1; + } + + if (template.type === 'image') { + fieldValue.value = undefined; + } + + if (template.type === 'latents') { + fieldValue.value = undefined; + } + + if (template.type === 'model') { + fieldValue.value = undefined; + } + } + + return fieldValue; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts new file mode 100644 index 0000000000..873dba3ac3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildEdges.ts @@ -0,0 +1,39 @@ +import { + Edge, + ImageToImageInvocation, + IterateInvocation, + RandomRangeInvocation, + RangeInvocation, + TextToImageInvocation, +} from 'services/api'; + +export const buildEdges = ( + baseNode: TextToImageInvocation | ImageToImageInvocation, + 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/linearGraphBuilder/buildImageToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts new file mode 100644 index 0000000000..f04f177d5b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildImageToImageNode.ts @@ -0,0 +1,99 @@ +import { v4 as uuidv4 } from 'uuid'; +import { RootState } from 'app/store'; +import { + Edge, + ImageToImageInvocation, + TextToImageInvocation, +} from 'services/api'; +import { _Image } from 'app/invokeai'; +import { initialImageSelector } from 'features/parameters/store/generationSelectors'; + +export const buildImg2ImgNode = (state: RootState): ImageToImageInvocation => { + const nodeId = uuidv4(); + const { generation, system, models } = state; + + const { selectedModelName } = models; + + const { + prompt, + seed, + steps, + width, + height, + cfgScale, + sampler, + seamless, + img2imgStrength: strength, + shouldFitToWidthHeight: fit, + shouldRandomizeSeed, + } = generation; + + const initialImage = initialImageSelector(state); + + if (!initialImage) { + // TODO: handle this + throw 'no initial image'; + } + + const imageToImageNode: ImageToImageInvocation = { + id: nodeId, + type: 'img2img', + prompt, + steps, + width, + height, + cfg_scale: cfgScale, + scheduler: sampler as ImageToImageInvocation['scheduler'], + seamless, + model: selectedModelName, + progress_images: true, + image: { + image_name: initialImage.name, + image_type: initialImage.type, + }, + strength, + fit, + }; + + if (!shouldRandomizeSeed) { + imageToImageNode.seed = seed; + } + + return imageToImageNode; +}; + +type hiresReturnType = { + node: Record; + edge: Edge; +}; + +export const buildHiResNode = ( + baseNode: Record, + strength?: number +): hiresReturnType => { + const nodeId = uuidv4(); + const baseNodeId = Object.keys(baseNode)[0]; + const baseNodeValues = Object.values(baseNode)[0]; + + return { + node: { + [nodeId]: { + ...baseNodeValues, + id: nodeId, + type: 'img2img', + strength, + fit: true, + }, + }, + edge: { + source: { + field: 'image', + node_id: baseNodeId, + }, + destination: { + field: 'image', + node_id: nodeId, + }, + }, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts new file mode 100644 index 0000000000..6764038da4 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildIterateNode.ts @@ -0,0 +1,13 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { IterateInvocation } from 'services/api'; + +export const buildIterateNode = (): IterateInvocation => { + const nodeId = uuidv4(); + return { + id: nodeId, + type: 'iterate', + collection: [], + index: 0, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts new file mode 100644 index 0000000000..9667dfa2b3 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildLinearGraph.ts @@ -0,0 +1,39 @@ +import { RootState } from 'app/store'; +import { Graph } from 'services/api'; +import { buildImg2ImgNode } from './buildImageToImageNode'; +import { buildTxt2ImgNode } from './buildTextToImageNode'; +import { buildRangeNode } from './buildRangeNode'; +import { buildIterateNode } from './buildIterateNode'; +import { buildEdges } from './buildEdges'; + +/** + * Builds the Linear workflow graph. + */ +export const buildLinearGraph = (state: RootState): Graph => { + // The base node is either a txt2img or img2img node + const baseNode = state.generation.isImageToImageEnabled + ? buildImg2ImgNode(state) + : buildTxt2ImgNode(state); + + // We always range and iterate nodes, no matter the iteration count + // This is required to provide the correct seeds to the backend engine + const rangeNode = buildRangeNode(state); + const iterateNode = buildIterateNode(); + + // Build the edges for the nodes selected. + const edges = buildEdges(baseNode, rangeNode, iterateNode); + + // Assemble! + const graph = { + nodes: { + [rangeNode.id]: rangeNode, + [iterateNode.id]: iterateNode, + [baseNode.id]: baseNode, + }, + edges, + }; + + // TODO: hires fix requires latent space upscaling; we don't have nodes for this yet + + return graph; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts new file mode 100644 index 0000000000..1f87ec785e --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildRangeNode.ts @@ -0,0 +1,26 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { RootState } from 'app/store'; +import { RandomRangeInvocation, RangeInvocation } from 'services/api'; + +export const buildRangeNode = ( + state: RootState +): RangeInvocation | RandomRangeInvocation => { + const nodeId = uuidv4(); + const { shouldRandomizeSeed, iterations, seed } = state.generation; + + if (shouldRandomizeSeed) { + return { + id: nodeId, + type: 'random_range', + size: iterations, + }; + } + + return { + id: nodeId, + type: 'range', + start: seed, + stop: seed + iterations, + }; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts new file mode 100644 index 0000000000..057d35dbcb --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/linearGraphBuilder/buildTextToImageNode.ts @@ -0,0 +1,42 @@ +import { v4 as uuidv4 } from 'uuid'; +import { RootState } from 'app/store'; +import { TextToImageInvocation } from 'services/api'; + +export const buildTxt2ImgNode = (state: RootState): TextToImageInvocation => { + const nodeId = uuidv4(); + const { generation, models } = state; + + const { selectedModelName } = models; + + const { + prompt, + seed, + steps, + width, + height, + cfgScale: cfg_scale, + sampler, + seamless, + shouldRandomizeSeed, + } = generation; + + const textToImageNode: NonNullable = { + id: nodeId, + type: 'txt2img', + prompt, + steps, + width, + height, + cfg_scale, + scheduler: sampler as TextToImageInvocation['scheduler'], + seamless, + model: selectedModelName, + progress_images: true, + }; + + if (!shouldRandomizeSeed) { + textToImageNode.seed = seed; + } + + return textToImageNode; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts b/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts new file mode 100644 index 0000000000..848615277d --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/nodesGraphBuilder/buildNodesGraph.ts @@ -0,0 +1,77 @@ +import { Graph } from 'services/api'; +import { v4 as uuidv4 } from 'uuid'; +import { reduce } from 'lodash'; +import { RootState } from 'app/store'; +import { AnyInvocation } from 'services/events/types'; + +/** + * Builds a graph from the node editor state. + */ +export const buildNodesGraph = (state: RootState): Graph => { + const { nodes, edges } = state.nodes; + + // Reduce the node editor nodes into invocation graph nodes + const parsedNodes = nodes.reduce>( + (nodesAccumulator, node, nodeIndex) => { + const { id, data } = node; + const { type, inputs } = data; + + // Transform each node's inputs to simple key-value pairs + const transformedInputs = reduce( + inputs, + (inputsAccumulator, input, name) => { + inputsAccumulator[name] = input.value; + + return inputsAccumulator; + }, + {} as Record, any> + ); + + // Build this specific node + const graphNode = { + type, + id, + ...transformedInputs, + }; + + // Add it to the nodes object + Object.assign(nodesAccumulator, { + [id]: graphNode, + }); + + return nodesAccumulator; + }, + {} + ); + + // Reduce the node editor edges into invocation graph edges + const parsedEdges = edges.reduce>( + (edgesAccumulator, edge, edgeIndex) => { + const { source, target, sourceHandle, targetHandle } = edge; + + // Format the edges and add to the edges array + edgesAccumulator.push({ + source: { + node_id: source, + field: sourceHandle as string, + }, + destination: { + node_id: target, + field: targetHandle as string, + }, + }); + + return edgesAccumulator; + }, + [] + ); + + // Assemble! + const graph = { + id: uuidv4(), + nodes: parsedNodes, + edges: parsedEdges, + }; + + return graph; +}; diff --git a/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts new file mode 100644 index 0000000000..c4c2b8fcf1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/util/parseSchema.ts @@ -0,0 +1,120 @@ +import { filter, reduce } from 'lodash'; +import { OpenAPIV3 } from 'openapi-types'; +import { isSchemaObject } from '../types/typeGuards'; +import { + InputFieldTemplate, + InvocationSchemaObject, + InvocationTemplate, + isInvocationSchemaObject, + OutputFieldTemplate, +} from '../types/types'; +import { + buildInputFieldTemplate, + buildOutputFieldTemplates, +} from './fieldTemplateBuilders'; + +const invocationBlacklist = ['Graph', 'Collect', 'LoadImage']; + +export const parseSchema = (openAPI: OpenAPIV3.Document) => { + // filter out non-invocation schemas, plus some tricky invocations for now + const filteredSchemas = filter( + openAPI.components!.schemas, + (schema, key) => + key.includes('Invocation') && + !key.includes('InvocationOutput') && + !invocationBlacklist.some((blacklistItem) => key.includes(blacklistItem)) + ) as (OpenAPIV3.ReferenceObject | InvocationSchemaObject)[]; + + const invocations = filteredSchemas.reduce< + Record + >((acc, schema) => { + // only want SchemaObjects + if (isInvocationSchemaObject(schema)) { + const type = schema.properties.type.default; + + const title = + schema.ui?.title ?? + schema.title + .replace('Invocation', '') + .split(/(?=[A-Z])/) // split PascalCase into array + .join(' '); + + const typeHints = schema.ui?.type_hints; + + const inputs = reduce( + schema.properties, + (inputsAccumulator, property, propertyName) => { + if ( + // `type` and `id` are not valid inputs/outputs + !['type', 'id'].includes(propertyName) && + isSchemaObject(property) + ) { + let field: InputFieldTemplate | undefined; + if (propertyName === 'collection') { + field = { + default: property.default ?? [], + name: 'collection', + title: property.title ?? '', + description: property.description ?? '', + type: 'array', + inputRequirement: 'always', + inputKind: 'connection', + }; + } else { + field = buildInputFieldTemplate( + property, + propertyName, + typeHints + ); + } + if (field) { + inputsAccumulator[propertyName] = field; + } + } + return inputsAccumulator; + }, + {} as Record + ); + + const rawOutput = (schema as InvocationSchemaObject).output; + + let outputs: Record; + + // some special handling is needed for collect, iterate and range nodes + if (type === 'iterate') { + // this is guaranteed to be a SchemaObject + const iterationOutput = openAPI.components!.schemas![ + 'IterateInvocationOutput' + ] as OpenAPIV3.SchemaObject; + + outputs = { + item: { + name: 'item', + title: iterationOutput.title ?? '', + description: iterationOutput.description ?? '', + type: 'array', + }, + }; + } else { + outputs = buildOutputFieldTemplates(rawOutput, openAPI, typeHints); + } + + const invocation: InvocationTemplate = { + title, + type, + tags: schema.ui?.tags ?? [], + description: schema.description ?? '', + inputs, + outputs, + }; + + Object.assign(acc, { [type]: invocation }); + } + + return acc; + }, {}); + + console.debug('Generated invocations: ', invocations); + + return invocations; +}; diff --git a/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx b/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx index b742f5a37e..a4f9be7918 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AccordionItems/InvokeAccordionItem.tsx @@ -6,19 +6,18 @@ import { Box, Flex, } from '@chakra-ui/react'; -import { Feature } from 'app/features'; import GuideIcon from 'common/components/GuideIcon'; -import { ReactNode } from 'react'; +import { ParametersAccordionItem } from '../ParametersAccordion'; -export interface InvokeAccordionItemProps { - header: string; - content: ReactNode; - feature?: Feature; - additionalHeaderComponents?: ReactNode; -} +type InvokeAccordionItemProps = { + accordionItem: ParametersAccordionItem; +}; -export default function InvokeAccordionItem(props: InvokeAccordionItemProps) { - const { header, feature, content, additionalHeaderComponents } = props; +export default function InvokeAccordionItem({ + accordionItem, +}: InvokeAccordionItemProps) { + const { header, feature, content, additionalHeaderComponents } = + accordionItem; return ( @@ -32,7 +31,7 @@ export default function InvokeAccordionItem(props: InvokeAccordionItemProps) { - {content} + {content} ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx index 187b23cdff..866038c993 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Canvas/InfillAndScalingSettings.tsx @@ -115,9 +115,7 @@ const InfillAndScalingSettings = () => { onChange={handleChangeBoundingBoxScaleMethod} /> { handleReset={handleResetScaledWidth} /> { onChange={(e) => dispatch(setInfillMethod(e.target.value))} /> state.generation.shouldFitToWidthHeight ); + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + const handleChangeFit = (e: ChangeEvent) => dispatch(setShouldFitToWidthHeight(e.target.checked)); @@ -19,6 +23,7 @@ export default function ImageFit() { return ( + diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx index e4d5a9174d..503b364f1a 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageStrength.tsx @@ -14,6 +14,9 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) { const img2imgStrength = useAppSelector( (state: RootState) => state.generation.img2imgStrength ); + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); const dispatch = useAppDispatch(); @@ -37,6 +40,7 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) { inputWidth={22} withReset handleReset={handleImg2ImgStrengthReset} + isDisabled={!isImageToImageEnabled} /> ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx new file mode 100644 index 0000000000..ad449a5ff3 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle.tsx @@ -0,0 +1,24 @@ +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import IAISwitch from 'common/components/IAISwitch'; +import { isImageToImageEnabledChanged } from 'features/parameters/store/generationSlice'; +import { ChangeEvent } from 'react'; + +export default function ImageToImageToggle() { + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + + const dispatch = useAppDispatch(); + + const handleChange = (e: ChangeEvent) => + dispatch(isImageToImageEnabledChanged(e.target.checked)); + + return ( + + ); +} diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx new file mode 100644 index 0000000000..0a3fd34c95 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/ImageToImage/InitialImagePreview.tsx @@ -0,0 +1,155 @@ +import { Box, Flex, Image, Spinner, Text } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; +import { useGetUrl } from 'common/util/getUrl'; +import useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; +import { selectResultsById } from 'features/gallery/store/resultsSlice'; +import { + clearInitialImage, + initialImageSelected, +} from 'features/parameters/store/generationSlice'; +import { addToast } from 'features/system/store/systemSlice'; +import { isEqual } from 'lodash'; +import { DragEvent, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ImageType } from 'services/api'; +import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; + +const initialImagePreviewSelector = createSelector( + [(state: RootState) => state], + (state) => { + const { initialImage } = state.generation; + const image = selectResultsById(state, initialImage as string); + + return { + initialImage: image, + }; + }, + { memoizeOptions: { resultEqualityCheck: isEqual } } +); + +const InitialImagePreview = () => { + const isImageToImageEnabled = useAppSelector( + (state: RootState) => state.generation.isImageToImageEnabled + ); + const { initialImage } = useAppSelector(initialImagePreviewSelector); + const { getUrl } = useGetUrl(); + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const [isLoaded, setIsLoaded] = useState(false); + const getImageByNameAndType = useGetImageByNameAndType(); + + const onError = () => { + dispatch( + addToast({ + title: t('toast.parametersFailed'), + description: t('toast.parametersFailedDesc'), + status: 'error', + isClosable: true, + }) + ); + dispatch(clearInitialImage()); + setIsLoaded(false); + }; + + const handleDrop = useCallback( + (e: DragEvent) => { + setIsLoaded(false); + const name = e.dataTransfer.getData('invokeai/imageName'); + const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; + + if (!name || !type) { + return; + } + + const image = getImageByNameAndType(name, type); + + if (!image) { + return; + } + + dispatch(initialImageSelected(image.name)); + }, + [getImageByNameAndType, dispatch] + ); + + return ( + + + {initialImage?.url && ( + <> + { + setIsLoaded(true); + }} + fallback={ + + + + } + /> + {isLoaded && ( + + )} + + )} + + {!initialImage?.url && } + + {!isImageToImageEnabled && ( + + + Image to Image is Disabled + + + )} + + ); +}; + +export default InitialImagePreview; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx index 97705da9cc..172acaab68 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Output/HiresSettings.tsx @@ -51,9 +51,7 @@ export const HiresStrength = () => { // inputWidth={22} withReset handleReset={handleHiResStrengthReset} - isSliderDisabled={!hiresFix} - isInputDisabled={!hiresFix} - isResetDisabled={!hiresFix} + isDisabled={!hiresFix} /> ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx index 908c4b5527..0cb5a12524 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleDenoisingStrength.tsx @@ -30,9 +30,7 @@ export default function UpscaleDenoisingStrength() { withSliderMarks withInput withReset - isSliderDisabled={!isESRGANAvailable} - isInputDisabled={!isESRGANAvailable} - isResetDisabled={!isESRGANAvailable} + isDisabled={!isESRGANAvailable} /> ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx index 0f67a3a053..819c4fda57 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Upscale/UpscaleStrength.tsx @@ -27,9 +27,7 @@ export default function UpscaleStrength() { withSliderMarks withInput withReset - isSliderDisabled={!isESRGANAvailable} - isInputDisabled={!isESRGANAvailable} - isResetDisabled={!isESRGANAvailable} + isDisabled={!isESRGANAvailable} /> ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx index eef4c8728c..27a39757f9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Variations/VariationAmount.tsx @@ -24,9 +24,7 @@ export default function VariationAmount() { step={0.01} min={0} max={1} - isSliderDisabled={!shouldGenerateVariations} - isInputDisabled={!shouldGenerateVariations} - isResetDisabled={!shouldGenerateVariations} + isDisabled={!shouldGenerateVariations} onChange={(v) => dispatch(setVariationAmount(v))} handleReset={() => dispatch(setVariationAmount(0.1))} withInput diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx index 0068568402..8dbf70eab5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx @@ -19,9 +19,7 @@ export default function MainHeight() { return shouldUseSliders ? ( { + const { + activeTab, + openLinearAccordionItems, + openUnifiedCanvasAccordionItems, + disabledParameterPanels, + } = uiSlice; + + let openAccordions: number[] = []; + + if (tabMap[activeTab] === 'linear') { + openAccordions = openLinearAccordionItems; + } + + if (tabMap[activeTab] === 'unifiedCanvas') { + openAccordions = openUnifiedCanvasAccordionItems; + } + + return { + openAccordions, + disabledParameterPanels, + }; +}); + +export type ParametersAccordionItem = { + name: string; + header: string; + content: ReactNode; + feature?: Feature; + additionalHeaderComponents?: ReactNode; }; -type ParametersAccordionsType = { - accordionInfo: ParametersAccordionType; +export type ParametersAccordionItems = { + [parametersAccordionKey: string]: ParametersAccordionItem; +}; + +type ParametersAccordionProps = { + accordionItems: ParametersAccordionItems; }; /** * Main container for generation and processing parameters. */ -const ParametersAccordion = (props: ParametersAccordionsType) => { - const { accordionInfo } = props; - - const openAccordions = useAppSelector( - (state: RootState) => state.system.openAccordions +const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => { + const { openAccordions, disabledParameterPanels } = useAppSelector( + parametersAccordionSelector ); const dispatch = useAppDispatch(); - /** - * Stores accordion state in redux so preferred UI setup is retained. - */ - const handleChangeAccordionState = (openAccordions: number | number[]) => - dispatch(setOpenAccordions(openAccordions)); - - const renderAccordions = () => { - const accordionsToRender: ReactElement[] = []; - if (accordionInfo) { - Object.keys(accordionInfo).forEach((key) => { - const { header, feature, content, additionalHeaderComponents } = - accordionInfo[key]; - accordionsToRender.push( - - ); - }); - } - return accordionsToRender; + const handleChangeAccordionState = (openAccordions: number | number[]) => { + dispatch( + openAccordionItemsChanged( + Array.isArray(openAccordions) ? openAccordions : [openAccordions] + ) + ); }; + // Render function for accordion items + const renderAccordionItems = useCallback(() => { + // Filter out disabled accordions + const filteredAccordionItems = filter( + accordionItems, + (item) => disabledParameterPanels.indexOf(item.name) === -1 + ); + + return filteredAccordionItems.map((accordionItem) => ( + + )); + }, [disabledParameterPanels, accordionItems]); + return ( { gap: 2, }} > - {renderAccordions()} + {renderAccordionItems()} ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx index 0992e63ee3..2fb81ae9a0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/CancelButton.tsx @@ -1,5 +1,4 @@ import { createSelector } from '@reduxjs/toolkit'; -import { cancelProcessing } from 'app/socketio/actions'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import IAIIconButton, { IAIIconButtonProps, @@ -9,16 +8,36 @@ import { SystemState, setCancelAfter, setCancelType, + cancelScheduled, + cancelTypeChanged, + CancelType, } from 'features/system/store/systemSlice'; import { isEqual } from 'lodash'; import { useEffect, useCallback, memo } from 'react'; -import { ButtonSpinner, ButtonGroup } from '@chakra-ui/react'; +import { + ButtonSpinner, + ButtonGroup, + Menu, + MenuButton, + MenuList, + MenuOptionGroup, + MenuItemOption, + IconButton, +} from '@chakra-ui/react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { MdCancel, MdCancelScheduleSend } from 'react-icons/md'; +import { + MdArrowDropDown, + MdArrowDropUp, + MdCancel, + MdCancelScheduleSend, +} from 'react-icons/md'; import IAISimpleMenu from 'common/components/IAISimpleMenu'; +import { sessionCanceled } from 'services/thunks/session'; +import { FaChevronDown } from 'react-icons/fa'; +import { BiChevronDown } from 'react-icons/bi'; const cancelButtonSelector = createSelector( systemSelector, @@ -29,8 +48,11 @@ const cancelButtonSelector = createSelector( isCancelable: system.isCancelable, currentIteration: system.currentIteration, totalIterations: system.totalIterations, - cancelType: system.cancelOptions.cancelType, - cancelAfter: system.cancelOptions.cancelAfter, + // cancelType: system.cancelOptions.cancelType, + // cancelAfter: system.cancelOptions.cancelAfter, + sessionId: system.sessionId, + cancelType: system.cancelType, + isCancelScheduled: system.isCancelScheduled, }; }, { @@ -56,16 +78,34 @@ const CancelButton = ( currentIteration, totalIterations, cancelType, - cancelAfter, + isCancelScheduled, + // cancelAfter, + sessionId, } = useAppSelector(cancelButtonSelector); + const handleClickCancel = useCallback(() => { - dispatch(cancelProcessing()); - dispatch(setCancelAfter(null)); - }, [dispatch]); + if (!sessionId) { + return; + } + + if (cancelType === 'scheduled') { + dispatch(cancelScheduled()); + return; + } + + dispatch(sessionCanceled({ sessionId })); + }, [dispatch, sessionId, cancelType]); const { t } = useTranslation(); - const isCancelScheduled = cancelAfter === null ? false : true; + const handleCancelTypeChanged = useCallback( + (value: string | string[]) => { + const newCancelType = Array.isArray(value) ? value[0] : value; + dispatch(cancelTypeChanged(newCancelType as CancelType)); + }, + [dispatch] + ); + // const isCancelScheduled = cancelAfter === null ? false : true; useHotkeys( 'shift+x', @@ -77,22 +117,22 @@ const CancelButton = ( [isConnected, isProcessing, isCancelable] ); - useEffect(() => { - if (cancelAfter !== null && cancelAfter < currentIteration) { - handleClickCancel(); - } - }, [cancelAfter, currentIteration, handleClickCancel]); + // useEffect(() => { + // if (cancelAfter !== null && cancelAfter < currentIteration) { + // handleClickCancel(); + // } + // }, [cancelAfter, currentIteration, handleClickCancel]); - const cancelMenuItems = [ - { - item: t('parameters.cancel.immediate'), - onClick: () => dispatch(setCancelType('immediate')), - }, - { - item: t('parameters.cancel.schedule'), - onClick: () => dispatch(setCancelType('scheduled')), - }, - ]; + // const cancelMenuItems = [ + // { + // item: t('parameters.cancel.immediate'), + // onClick: () => dispatch(cancelTypeChanged('immediate')), + // }, + // { + // item: t('parameters.cancel.schedule'), + // onClick: () => dispatch(cancelTypeChanged('scheduled')), + // }, + // ]; return ( @@ -121,29 +161,40 @@ const CancelButton = ( ? t('parameters.cancel.isScheduled') : t('parameters.cancel.schedule') } - isDisabled={ - !isConnected || - !isProcessing || - !isCancelable || - currentIteration === totalIterations - } - onClick={() => { - // If a cancel request has already been made, and the user clicks again before the next iteration has been processed, stop the request. - if (isCancelScheduled) dispatch(setCancelAfter(null)); - else dispatch(setCancelAfter(currentIteration)); - }} + isDisabled={!isConnected || !isProcessing || !isCancelable} + onClick={handleClickCancel} colorScheme="error" {...rest} /> )} - + + + } + paddingX={0} + paddingY={0} + colorScheme="error" + minWidth={5} + /> + + + + {t('parameters.cancel.immediate')} + + + {t('parameters.cancel.schedule')} + + + + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index b68f245044..d4293c0938 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -11,6 +11,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaPlay } from 'react-icons/fa'; +import { linearGraphBuilt, sessionCreated } from 'services/thunks/session'; interface InvokeButton extends Omit { @@ -24,7 +25,8 @@ export default function InvokeButton(props: InvokeButton) { const activeTabName = useAppSelector(activeTabNameSelector); const handleClickGenerate = () => { - dispatch(generateImage(activeTabName)); + // dispatch(generateImage(activeTabName)); + dispatch(linearGraphBuilt()); }; const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts b/invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts new file mode 100644 index 0000000000..884ed0e079 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/generationPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { GenerationState } from './generationSlice'; + +/** + * Generation slice persist blacklist + */ +const itemsToBlacklist: (keyof GenerationState)[] = []; + +export const generationBlacklist = itemsToBlacklist.map( + (blacklistItem) => `generation.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts index 5cc8e1a592..39550c5ad6 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts @@ -1,5 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store'; +import { gallerySelector } from 'features/gallery/store/gallerySelectors'; +import { + selectResultsById, + selectResultsEntities, +} from 'features/gallery/store/resultsSlice'; +import { selectUploadsById } from 'features/gallery/store/uploadsSlice'; import { isEqual } from 'lodash'; export const generationSelector = (state: RootState) => state.generation; @@ -15,3 +21,15 @@ export const mayGenerateMultipleImagesSelector = createSelector( }, } ); + +export const initialImageSelector = createSelector( + [(state: RootState) => state, generationSelector], + (state, generation) => { + const { initialImage: initialImageName } = generation; + + return ( + selectResultsById(state, initialImageName as string) ?? + selectUploadsById(state, initialImageName as string) + ); + } +); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 1cb3a98204..e4a92f0b10 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -11,7 +11,7 @@ export interface GenerationState { height: number; img2imgStrength: number; infillMethod: string; - initialImage?: InvokeAI.Image | string; // can be an Image or url + initialImage?: InvokeAI._Image | string; // can be an Image or url iterations: number; maskPath: string; perlin: number; @@ -36,6 +36,7 @@ export interface GenerationState { shouldUseSymmetry: boolean; horizontalSymmetrySteps: number; verticalSymmetrySteps: number; + isImageToImageEnabled: boolean; } const initialGenerationState: GenerationState = { @@ -67,6 +68,7 @@ const initialGenerationState: GenerationState = { shouldUseSymmetry: false, horizontalSymmetrySteps: 0, verticalSymmetrySteps: 0, + isImageToImageEnabled: false, }; const initialState: GenerationState = initialGenerationState; @@ -317,12 +319,12 @@ export const generationSlice = createSlice({ setShouldRandomizeSeed: (state, action: PayloadAction) => { state.shouldRandomizeSeed = action.payload; }, - setInitialImage: ( - state, - action: PayloadAction - ) => { - state.initialImage = action.payload; - }, + // setInitialImage: ( + // state, + // action: PayloadAction + // ) => { + // state.initialImage = action.payload; + // }, clearInitialImage: (state) => { state.initialImage = undefined; }, @@ -353,6 +355,13 @@ export const generationSlice = createSlice({ setVerticalSymmetrySteps: (state, action: PayloadAction) => { state.verticalSymmetrySteps = action.payload; }, + initialImageSelected: (state, action: PayloadAction) => { + state.initialImage = action.payload; + state.isImageToImageEnabled = true; + }, + isImageToImageEnabledChanged: (state, action: PayloadAction) => { + state.isImageToImageEnabled = action.payload; + }, }, }); @@ -368,7 +377,7 @@ export const { setHeight, setImg2imgStrength, setInfillMethod, - setInitialImage, + // setInitialImage, setIterations, setMaskPath, setParameter, @@ -394,6 +403,8 @@ export const { setShouldUseSymmetry, setHorizontalSymmetrySteps, setVerticalSymmetrySteps, + initialImageSelected, + isImageToImageEnabledChanged, } = generationSlice.actions; export default generationSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts new file mode 100644 index 0000000000..9b8b3bb475 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { PostprocessingState } from './postprocessingSlice'; + +/** + * Postprocessing slice persist blacklist + */ +const itemsToBlacklist: (keyof PostprocessingState)[] = []; + +export const postprocessingBlacklist = itemsToBlacklist.map( + (blacklistItem) => `postprocessing.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx index 761bce8f98..31d228df35 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx @@ -1,20 +1,27 @@ import { Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; -import { requestModelChange } from 'app/socketio/actions'; +import { ChangeEvent } from 'react'; +import { isEqual } from 'lodash'; +import { useTranslation } from 'react-i18next'; + import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import IAISelect from 'common/components/IAISelect'; -import { isEqual, map } from 'lodash'; - -import { ChangeEvent } from 'react'; -import { useTranslation } from 'react-i18next'; -import { activeModelSelector, systemSelector } from '../store/systemSelectors'; +import { + modelSelected, + selectedModelSelector, + selectModelsIds, +} from '../store/modelSlice'; +import { RootState } from 'app/store'; const selector = createSelector( - [systemSelector], - (system) => { - const { isProcessing, model_list } = system; - const models = map(model_list, (model, key) => key); - return { models, isProcessing }; + [(state: RootState) => state], + (state) => { + const selectedModel = selectedModelSelector(state); + const allModelNames = selectModelsIds(state); + return { + allModelNames, + selectedModel, + }; }, { memoizeOptions: { @@ -26,10 +33,9 @@ const selector = createSelector( const ModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const { models, isProcessing } = useAppSelector(selector); - const activeModel = useAppSelector(activeModelSelector); + const { allModelNames, selectedModel } = useAppSelector(selector); const handleChangeModel = (e: ChangeEvent) => { - dispatch(requestModelChange(e.target.value)); + dispatch(modelSelected(e.target.value)); }; return ( @@ -41,10 +47,9 @@ const ModelSelect = () => { diff --git a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx index f47730d221..a8a87bc39a 100644 --- a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx @@ -80,7 +80,7 @@ const StatusIndicator = () => { cursor={statusIndicatorCursor} onClick={handleClickStatusIndicator} sx={{ - fontSize: 'xs', + fontSize: 'sm', fontWeight: '600', color: `${statusIdentifier}.400`, }} diff --git a/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts b/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts index 62d9179ec2..0c99eec0a4 100644 --- a/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts +++ b/invokeai/frontend/web/src/features/system/hooks/useToastWatcher.ts @@ -1,9 +1,24 @@ -import { useToast } from '@chakra-ui/react'; +import { useToast, UseToastOptions } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { toastQueueSelector } from 'features/system/store/systemSelectors'; import { clearToastQueue } from 'features/system/store/systemSlice'; import { useEffect } from 'react'; +export type MakeToastArg = string | UseToastOptions; + +export const makeToast = (arg: MakeToastArg): UseToastOptions => { + if (typeof arg === 'string') { + return { + title: arg, + status: 'info', + isClosable: true, + duration: 2500, + }; + } + + return { status: 'info', isClosable: true, duration: 2500, ...arg }; +}; + const useToastWatcher = () => { const dispatch = useAppDispatch(); const toastQueue = useAppSelector(toastQueueSelector); diff --git a/invokeai/frontend/web/src/features/system/store/modelSelectors.ts b/invokeai/frontend/web/src/features/system/store/modelSelectors.ts new file mode 100644 index 0000000000..74027d631b --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/modelSelectors.ts @@ -0,0 +1,5 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { reduce } from 'lodash'; + +export const modelSelector = (state: RootState) => state.models; diff --git a/invokeai/frontend/web/src/features/system/store/modelSlice.ts b/invokeai/frontend/web/src/features/system/store/modelSlice.ts new file mode 100644 index 0000000000..843e27a435 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/modelSlice.ts @@ -0,0 +1,80 @@ +import { createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { keys, sample } from 'lodash'; +import { CkptModelInfo, DiffusersModelInfo } from 'services/api'; +import { receivedModels } from 'services/thunks/model'; + +export type Model = (CkptModelInfo | DiffusersModelInfo) & { + name: string; +}; + +export const modelsAdapter = createEntityAdapter({ + selectId: (model) => model.name, + sortComparer: (a, b) => a.name.localeCompare(b.name), +}); + +type AdditionalModelsState = { + selectedModelName: string; +}; + +export const initialModelsState = + modelsAdapter.getInitialState({ + selectedModelName: '', + }); + +export type ModelsState = typeof initialModelsState; + +export const modelsSlice = createSlice({ + name: 'models', + initialState: initialModelsState, + reducers: { + modelAdded: modelsAdapter.upsertOne, + modelSelected: (state, action: PayloadAction) => { + state.selectedModelName = action.payload; + }, + }, + extraReducers(builder) { + /** + * Received Models - FULFILLED + */ + builder.addCase(receivedModels.fulfilled, (state, action) => { + const models = action.payload; + modelsAdapter.setAll(state, models); + + // If the current selected model is `''` or isn't actually in the list of models, + // choose a random model + if ( + !state.selectedModelName || + !keys(models).includes(state.selectedModelName) + ) { + const randomModel = sample(models); + + if (randomModel) { + state.selectedModelName = randomModel.name; + } else { + state.selectedModelName = ''; + } + } + }); + }, +}); + +export const selectedModelSelector = (state: RootState) => { + const { selectedModelName } = state.models; + const selectedModel = selectModelsById(state, selectedModelName); + + return selectedModel ?? null; +}; + +export const { + selectAll: selectModelsAll, + selectById: selectModelsById, + selectEntities: selectModelsEntities, + selectIds: selectModelsIds, + selectTotal: selectModelsTotal, +} = modelsAdapter.getSelectors((state) => state.models); + +export const { modelAdded, modelSelected } = modelsSlice.actions; + +export default modelsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts b/invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts new file mode 100644 index 0000000000..26b61f5a9b --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/modelsPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { ModelsState } from './modelSlice'; + +/** + * Models slice persist blacklist + */ +const itemsToBlacklist: (keyof ModelsState)[] = ['entities', 'ids']; + +export const modelsBlacklist = itemsToBlacklist.map( + (blacklistItem) => `models.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts b/invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts new file mode 100644 index 0000000000..31ac7a43e6 --- /dev/null +++ b/invokeai/frontend/web/src/features/system/store/systemPersistsBlacklist.ts @@ -0,0 +1,26 @@ +import { SystemState } from './systemSlice'; + +/** + * System slice persist blacklist + */ +const itemsToBlacklist: (keyof SystemState)[] = [ + 'currentIteration', + 'currentStatus', + 'currentStep', + 'isCancelable', + 'isConnected', + 'isESRGANAvailable', + 'isGFPGANAvailable', + 'isProcessing', + 'socketId', + 'totalIterations', + 'totalSteps', + 'openModel', + 'isCancelScheduled', + 'sessionId', + 'progressImage', +]; + +export const systemBlacklist = itemsToBlacklist.map( + (blacklistItem) => `system.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 3293869d58..f52a199aec 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -2,7 +2,23 @@ import { ExpandedIndex, UseToastOptions } from '@chakra-ui/react'; import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import * as InvokeAI from 'app/invokeai'; +import { + generatorProgress, + invocationComplete, + invocationError, + invocationStarted, + socketConnected, + socketDisconnected, + socketSubscribed, + socketUnsubscribed, +} from 'services/events/actions'; + import i18n from 'i18n'; +import { isImageOutput } from 'services/types/guards'; +import { ProgressImage } from 'services/events/types'; +import { initialImageSelected } from 'features/parameters/store/generationSlice'; +import { makeToast } from '../hooks/useToastWatcher'; +import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; export type LogLevel = 'info' | 'warning' | 'error'; @@ -56,6 +72,30 @@ export interface SystemState cancelType: CancelType; cancelAfter: number | null; }; + /** + * The current progress image + */ + progressImage: ProgressImage | null; + /** + * The current socket session id + */ + sessionId: string | null; + /** + * Cancel strategy + */ + cancelType: CancelType; + /** + * Whether or not a scheduled cancelation is pending + */ + isCancelScheduled: boolean; + /** + * Array of node IDs that we want to handle when events received + */ + subscribedNodeIds: string[]; + /** + * Whether or not URLs should be transformed to use a different host + */ + shouldTransformUrls: boolean; } const initialSystemState: SystemState = { @@ -98,6 +138,12 @@ const initialSystemState: SystemState = { cancelType: 'immediate', cancelAfter: null, }, + progressImage: null, + sessionId: null, + cancelType: 'immediate', + isCancelScheduled: false, + subscribedNodeIds: [], + shouldTransformUrls: false, }; export const systemSlice = createSlice({ @@ -271,6 +317,203 @@ export const systemSlice = createSlice({ setCancelAfter: (state, action: PayloadAction) => { state.cancelOptions.cancelAfter = action.payload; }, + /** + * A cancel was scheduled + */ + cancelScheduled: (state) => { + state.isCancelScheduled = true; + }, + /** + * The scheduled cancel was aborted + */ + scheduledCancelAborted: (state) => { + state.isCancelScheduled = false; + }, + /** + * The cancel type was changed + */ + cancelTypeChanged: (state, action: PayloadAction) => { + state.cancelType = action.payload; + }, + /** + * The array of subscribed node ids was changed + */ + subscribedNodeIdsSet: (state, action: PayloadAction) => { + state.subscribedNodeIds = action.payload; + }, + /** + * `shouldTransformUrls` was changed + */ + shouldTransformUrlsChanged: (state, action: PayloadAction) => { + state.shouldTransformUrls = action.payload; + }, + }, + extraReducers(builder) { + /** + * Socket Subscribed + */ + builder.addCase(socketSubscribed, (state, action) => { + state.sessionId = action.payload.sessionId; + }); + + /** + * Socket Unsubscribed + */ + builder.addCase(socketUnsubscribed, (state) => { + state.sessionId = null; + }); + + /** + * Socket Connected + */ + builder.addCase(socketConnected, (state, action) => { + const { timestamp } = action.payload; + + state.isConnected = true; + state.currentStatus = i18n.t('common.statusConnected'); + state.log.push({ + timestamp, + message: `Connected to server`, + level: 'info', + }); + state.toastQueue.push( + makeToast({ title: i18n.t('toast.connected'), status: 'success' }) + ); + }); + + /** + * Socket Disconnected + */ + builder.addCase(socketDisconnected, (state, action) => { + const { timestamp } = action.payload; + + state.isConnected = false; + state.currentStatus = i18n.t('common.statusDisconnected'); + state.log.push({ + timestamp, + message: `Disconnected from server`, + level: 'error', + }); + state.toastQueue.push( + makeToast({ title: i18n.t('toast.disconnected'), status: 'error' }) + ); + }); + + /** + * Invocation Started + */ + builder.addCase(invocationStarted, (state) => { + state.isProcessing = true; + state.isCancelable = true; + state.currentStatusHasSteps = false; + state.currentStatus = i18n.t('common.statusGenerating'); + }); + + /** + * Generator Progress + */ + builder.addCase(generatorProgress, (state, action) => { + const { + step, + total_steps, + progress_image, + invocation, + graph_execution_state_id, + } = action.payload.data; + + state.currentStatusHasSteps = true; + state.currentStep = step + 1; // TODO: step starts at -1, think this is a bug + state.totalSteps = total_steps; + state.progressImage = progress_image ?? null; + }); + + /** + * Invocation Complete + */ + builder.addCase(invocationComplete, (state, action) => { + const { data, timestamp } = action.payload; + + state.isProcessing = false; + state.currentStep = 0; + state.totalSteps = 0; + state.progressImage = null; + state.currentStatus = i18n.t('common.statusProcessingComplete'); + + // TODO: handle logging for other invocation types + if (isImageOutput(data.result)) { + state.log.push({ + timestamp, + message: `Generated: ${data.result.image.image_name}`, + level: 'info', + }); + } + }); + + /** + * Invocation Error + */ + builder.addCase(invocationError, (state, action) => { + const { data, timestamp } = action.payload; + + state.log.push({ + timestamp, + message: `Server error: ${data.error}`, + level: 'error', + }); + + state.wasErrorSeen = true; + state.progressImage = null; + state.isProcessing = false; + + state.toastQueue.push( + makeToast({ title: i18n.t('toast.serverError'), status: 'error' }) + ); + + state.log.push({ + timestamp, + message: `Server error: ${data.error}`, + level: 'error', + }); + }); + + /** + * Session Invoked - PENDING + */ + + builder.addCase(sessionInvoked.pending, (state) => { + state.currentStatus = i18n.t('common.statusPreparing'); + }); + + /** + * Session Canceled + */ + builder.addCase(sessionCanceled.fulfilled, (state, action) => { + const { timestamp } = action.payload; + + state.isProcessing = false; + state.isCancelable = false; + state.isCancelScheduled = false; + state.currentStep = 0; + state.totalSteps = 0; + state.progressImage = null; + + state.toastQueue.push( + makeToast({ title: i18n.t('toast.canceled'), status: 'warning' }) + ); + + state.log.push({ + timestamp, + message: `Processing canceled`, + level: 'warning', + }); + }); + + /** + * Initial Image Selected + */ + builder.addCase(initialImageSelected, (state) => { + state.toastQueue.push(makeToast(i18n.t('toast.sentToImageToImage'))); + }); }, }); @@ -306,6 +549,11 @@ export const { setOpenModel, setCancelType, setCancelAfter, + cancelScheduled, + scheduledCancelAborted, + cancelTypeChanged, + subscribedNodeIdsSet, + shouldTransformUrlsChanged, } = systemSlice.actions; export default systemSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx index 6076b0944f..e0dd3eadd3 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingGalleryButton.tsx @@ -15,9 +15,7 @@ const floatingGalleryButtonSelector = createSelector( return { shouldPinGallery, - shouldShowGalleryButton: - (!shouldPinGallery || !shouldShowGallery) && - ['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName), + shouldShowGalleryButton: !shouldPinGallery || !shouldShowGallery, }; }, { memoizeOptions: { resultEqualityCheck: isEqual } } diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index cec9ab2918..06ac904bb1 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector( const shouldShowParametersPanelButton = !canvasBetaLayoutCheck && (!shouldPinParametersPanel || !shouldShowParametersPanel) && - ['txt2img', 'img2img', 'unifiedCanvas'].includes(activeTabName); + ['linear', 'unifiedCanvas'].includes(activeTabName); return { shouldPinParametersPanel, diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 18db74791b..8603d33d06 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -11,29 +11,20 @@ import { } from '@chakra-ui/react'; import { RootState } from 'app/store'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import NodesWIP from 'common/components/WorkInProgress/NodesWIP'; -import { PostProcessingWIP } from 'common/components/WorkInProgress/PostProcessingWIP'; -import TrainingWIP from 'common/components/WorkInProgress/Training'; import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { setActiveTab, togglePanels } from 'features/ui/store/uiSlice'; import { ReactNode, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { - MdDeviceHub, - MdFlashOn, - MdGridOn, - MdPhotoFilter, - MdPhotoLibrary, - MdTextFields, -} from 'react-icons/md'; +import { MdDeviceHub, MdGridOn } from 'react-icons/md'; import { activeTabIndexSelector } from '../store/uiSelectors'; -import ImageToImageWorkarea from 'features/ui/components/tabs/ImageToImage/ImageToImageWorkarea'; -import TextToImageWorkarea from 'features/ui/components/tabs/TextToImage/TextToImageWorkarea'; import UnifiedCanvasWorkarea from 'features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasWorkarea'; import { useTranslation } from 'react-i18next'; import { ResourceKey } from 'i18next'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import NodeEditor from 'features/nodes/components/NodeEditor'; +import LinearWorkarea from './tabs/Linear/LinearWorkarea'; +import { FaImage } from 'react-icons/fa'; export interface InvokeTabInfo { id: InvokeTabName; @@ -45,38 +36,26 @@ const tabIconStyles: ChakraProps['sx'] = { boxSize: 6, }; -const tabInfo: InvokeTabInfo[] = [ - { - id: 'txt2img', - icon: , - workarea: , - }, - { - id: 'img2img', - icon: , - workarea: , - }, - { - id: 'unifiedCanvas', - icon: , - workarea: , - }, - { - id: 'nodes', - icon: , - workarea: , - }, - { - id: 'postprocessing', - icon: , - workarea: , - }, - { - id: 'training', - icon: , - workarea: , - }, -]; +const buildTabs = (disabledTabs: InvokeTabName[]): InvokeTabInfo[] => { + const tabs: InvokeTabInfo[] = [ + { + id: 'linear', + icon: , + workarea: , + }, + { + id: 'unifiedCanvas', + icon: , + workarea: , + }, + { + id: 'nodes', + icon: , + workarea: , + }, + ]; + return tabs.filter((tab) => !disabledTabs.includes(tab.id)); +}; export default function InvokeTabs() { const activeTab = useAppSelector(activeTabIndexSelector); @@ -85,13 +64,10 @@ export default function InvokeTabs() { (state: RootState) => state.lightbox.isLightboxOpen ); - const shouldPinGallery = useAppSelector( - (state: RootState) => state.ui.shouldPinGallery - ); + const { shouldPinGallery, disabledTabs, shouldPinParametersPanel } = + useAppSelector((state: RootState) => state.ui); - const shouldPinParametersPanel = useAppSelector( - (state: RootState) => state.ui.shouldPinParametersPanel - ); + const activeTabs = buildTabs(disabledTabs); const { t } = useTranslation(); @@ -109,18 +85,6 @@ export default function InvokeTabs() { dispatch(setActiveTab(2)); }); - useHotkeys('4', () => { - dispatch(setActiveTab(3)); - }); - - useHotkeys('5', () => { - dispatch(setActiveTab(4)); - }); - - useHotkeys('6', () => { - dispatch(setActiveTab(5)); - }); - // Lightbox Hotkey useHotkeys( 'z', @@ -142,7 +106,7 @@ export default function InvokeTabs() { const tabs = useMemo( () => - tabInfo.map((tab) => ( + activeTabs.map((tab) => ( )), - [t] + [t, activeTabs] ); const tabPanels = useMemo( () => - tabInfo.map((tab) => {tab.workarea}), - [] + activeTabs.map((tab) => {tab.workarea}), + [activeTabs] ); return ( @@ -174,8 +138,11 @@ export default function InvokeTabs() { dispatch(setActiveTab(index)); }} flexGrow={1} + isLazy > - {tabs} + + {tabs} + {tabPanels} ); diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx index ea4cd5ba1a..13f551e904 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeWorkarea.tsx @@ -1,7 +1,7 @@ import { Box, BoxProps, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import { setInitialImage } from 'features/parameters/store/generationSlice'; +import { initialImageSelected } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector, uiSelector, @@ -46,9 +46,7 @@ const InvokeWorkarea = (props: InvokeWorkareaProps) => { const uuid = e.dataTransfer.getData('invokeai/imageUuid'); const image = getImageByUuid(uuid); if (!image) return; - if (activeTabName === 'img2img') { - dispatch(setInitialImage(image)); - } else if (activeTabName === 'unifiedCanvas') { + if (activeTabName === 'unifiedCanvas') { dispatch(setInitialCanvasImage(image)); } }; diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx index 1f0bcaead3..77cffa814a 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanel.tsx @@ -96,7 +96,6 @@ const ParametersPanel = ({ children }: ParametersPanelProps) => { onClose={closeParametersPanel} isPinned={shouldPinParametersPanel || isLightboxOpen} sx={{ - borderColor: 'base.700', p: shouldPinParametersPanel ? 0 : 4, bg: 'base.900', }} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx deleted file mode 100644 index db62d2ed80..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageContent.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { ChakraProps, Flex, Grid } from '@chakra-ui/react'; -import { RootState } from 'app/store'; -import { useAppSelector } from 'app/storeHooks'; -import ImageUploadButton from 'common/components/ImageUploaderButton'; -import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; -import InitImagePreview from './InitImagePreview'; - -const workareaSplitViewStyle: ChakraProps['sx'] = { - flexDirection: 'column', - height: '100%', - width: '100%', - gap: 4, - - padding: 4, -}; - -const ImageToImageContent = () => { - const initialImage = useAppSelector( - (state: RootState) => state.generation.initialImage - ); - - const imageToImageComponent = initialImage ? ( - - - - ) : ( - - ); - - return ( - - - {imageToImageComponent} - - - - - - ); -}; - -export default ImageToImageContent; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx deleted file mode 100644 index 4989ef034b..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageParameters.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import { Feature } from 'app/features'; -import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; -import FaceRestoreToggle from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreToggle'; -import ImageToImageOutputSettings from 'features/parameters/components/AdvancedParameters/Output/ImageToImageOutputSettings'; -import SymmetrySettings from 'features/parameters/components/AdvancedParameters/Output/SymmetrySettings'; -import SymmetryToggle from 'features/parameters/components/AdvancedParameters/Output/SymmetryToggle'; -import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; -import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; -import UpscaleToggle from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleToggle'; -import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; -import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; -import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion from 'features/parameters/components/ParametersAccordion'; -import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; -import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; -import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; -import { useTranslation } from 'react-i18next'; -import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; -import { memo } from 'react'; - -const ImageToImageParameters = () => { - const { t } = useTranslation(); - - const imageToImageAccordions = { - general: { - header: `${t('parameters.general')}`, - feature: undefined, - content: , - }, - imageToImage: { - header: `${t('parameters.imageToImage')}`, - feature: undefined, - content: , - }, - seed: { - header: `${t('parameters.seed')}`, - feature: Feature.SEED, - content: , - }, - variations: { - header: `${t('parameters.variations')}`, - feature: Feature.VARIATIONS, - content: , - additionalHeaderComponents: , - }, - face_restore: { - header: `${t('parameters.faceRestoration')}`, - feature: Feature.FACE_CORRECTION, - content: , - additionalHeaderComponents: , - }, - upscale: { - header: `${t('parameters.upscaling')}`, - feature: Feature.UPSCALE, - content: , - additionalHeaderComponents: , - }, - symmetry: { - header: `${t('parameters.symmetry')}`, - content: , - additionalHeaderComponents: , - }, - other: { - header: `${t('parameters.otherOptions')}`, - feature: Feature.OTHER, - content: , - }, - }; - - return ( - - - - - - - ); -}; - -export default memo(ImageToImageParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx deleted file mode 100644 index ebc6f50ff2..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageWorkarea.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import InvokeWorkarea from 'features/ui/components/InvokeWorkarea'; -import ImageToImageContent from './ImageToImageContent'; -import ImageToImageParameters from './ImageToImageParameters'; - -export default function ImageToImageWorkarea() { - return ( - }> - - - ); -} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx deleted file mode 100644 index 3add963c01..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/InitImagePreview.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Flex, Image, Text, useToast } from '@chakra-ui/react'; -import { RootState } from 'app/store'; -import { useAppDispatch, useAppSelector } from 'app/storeHooks'; -import ImageUploaderIconButton from 'common/components/ImageUploaderIconButton'; -import CurrentImageHidden from 'features/gallery/components/CurrentImageHidden'; -import { clearInitialImage } from 'features/parameters/store/generationSlice'; -import { useTranslation } from 'react-i18next'; - -export default function InitImagePreview() { - const initialImage = useAppSelector( - (state: RootState) => state.generation.initialImage - ); - - const { shouldHidePreview } = useAppSelector((state: RootState) => state.ui); - - const { t } = useTranslation(); - - const dispatch = useAppDispatch(); - - const toast = useToast(); - - const alertMissingInitImage = () => { - toast({ - title: t('toast.parametersFailed'), - description: t('toast.parametersFailedDesc'), - status: 'error', - isClosable: true, - }); - dispatch(clearInitialImage()); - }; - - return ( - <> - - - {t('parameters.initialImage')} - - - - {initialImage && ( - - } - onError={alertMissingInitImage} - /> - - )} - - ); -} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx similarity index 85% rename from invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageContent.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx index 886e3a5331..8860956aeb 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx @@ -1,7 +1,7 @@ import { Box, Flex } from '@chakra-ui/react'; import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; -const TextToImageContent = () => { +const LinearContent = () => { return ( { ); }; -export default TextToImageContent; +export default LinearContent; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx similarity index 65% rename from invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageParameters.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx index 126dd10228..67ec737a12 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx @@ -1,61 +1,61 @@ import { Flex } from '@chakra-ui/react'; import { Feature } from 'app/features'; -import FaceRestoreSettings from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreSettings'; -import FaceRestoreToggle from 'features/parameters/components/AdvancedParameters/FaceRestore/FaceRestoreToggle'; +import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; +import ImageToImageToggle from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle'; import OutputSettings from 'features/parameters/components/AdvancedParameters/Output/OutputSettings'; import SymmetrySettings from 'features/parameters/components/AdvancedParameters/Output/SymmetrySettings'; import SymmetryToggle from 'features/parameters/components/AdvancedParameters/Output/SymmetryToggle'; import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; -import UpscaleSettings from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleSettings'; -import UpscaleToggle from 'features/parameters/components/AdvancedParameters/Upscale/UpscaleToggle'; import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion from 'features/parameters/components/ParametersAccordion'; +import ParametersAccordion, { + ParametersAccordionItems, +} from 'features/parameters/components/ParametersAccordion'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const TextToImageParameters = () => { +const LinearParameters = () => { const { t } = useTranslation(); - const textToImageAccordions = { + const linearAccordions: ParametersAccordionItems = { general: { + name: 'general', header: `${t('parameters.general')}`, feature: undefined, content: , }, seed: { + name: 'seed', header: `${t('parameters.seed')}`, feature: Feature.SEED, content: , }, + imageToImage: { + name: 'imageToImage', + header: `${t('parameters.imageToImage')}`, + feature: undefined, + content: , + additionalHeaderComponents: , + }, variations: { + name: 'variations', header: `${t('parameters.variations')}`, feature: Feature.VARIATIONS, content: , additionalHeaderComponents: , }, - face_restore: { - header: `${t('parameters.faceRestoration')}`, - feature: Feature.FACE_CORRECTION, - content: , - additionalHeaderComponents: , - }, - upscale: { - header: `${t('parameters.upscaling')}`, - feature: Feature.UPSCALE, - content: , - additionalHeaderComponents: , - }, symmetry: { + name: 'symmetry', header: `${t('parameters.symmetry')}`, content: , additionalHeaderComponents: , }, other: { + name: 'other', header: `${t('parameters.otherOptions')}`, feature: Feature.OTHER, content: , @@ -67,9 +67,9 @@ const TextToImageParameters = () => { - + ); }; -export default memo(TextToImageParameters); +export default memo(LinearParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx new file mode 100644 index 0000000000..f75065b6a3 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkarea.tsx @@ -0,0 +1,11 @@ +import InvokeWorkarea from 'features/ui/components/InvokeWorkarea'; +import LinearContent from './LinearContent'; +import LinearParameters from './LinearParameters'; + +export default function LinearWorkarea() { + return ( + }> + + + ); +} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx deleted file mode 100644 index eb95c96be5..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageWorkarea.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import InvokeWorkarea from 'features/ui/components/InvokeWorkarea'; -import TextToImageContent from './TextToImageContent'; -import TextToImageParameters from './TextToImageParameters'; - -export default function TextToImageWorkarea() { - return ( - }> - - - ); -} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index 0d49eafebd..a8c917d675 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -10,7 +10,9 @@ import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; -import ParametersAccordion from 'features/parameters/components/ParametersAccordion'; +import ParametersAccordion, { + ParametersAccordionItems, +} from 'features/parameters/components/ParametersAccordion'; import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; @@ -19,44 +21,52 @@ import { useTranslation } from 'react-i18next'; export default function UnifiedCanvasParameters() { const { t } = useTranslation(); - const unifiedCanvasAccordions = { + const unifiedCanvasAccordions: ParametersAccordionItems = { general: { + name: 'general', header: `${t('parameters.general')}`, feature: undefined, content: , }, unifiedCanvasImg2Img: { + name: 'unifiedCanvasImg2Img', header: `${t('parameters.imageToImage')}`, feature: undefined, content: , }, seed: { + name: 'seed', header: `${t('parameters.seed')}`, feature: Feature.SEED, content: , }, boundingBox: { + name: 'boundingBox', header: `${t('parameters.boundingBoxHeader')}`, feature: Feature.BOUNDING_BOX, content: , }, seamCorrection: { + name: 'seamCorrection', header: `${t('parameters.seamCorrectionHeader')}`, feature: Feature.SEAM_CORRECTION, content: , }, infillAndScaling: { + name: 'infillAndScaling', header: `${t('parameters.infillScalingHeader')}`, feature: Feature.INFILL_AND_SCALING, content: , }, variations: { + name: 'variations', header: `${t('parameters.variations')}`, feature: Feature.VARIATIONS, content: , additionalHeaderComponents: , }, symmetry: { + name: 'symmetry', header: `${t('parameters.symmetry')}`, content: , additionalHeaderComponents: , @@ -68,7 +78,7 @@ export default function UnifiedCanvasParameters() { - + ); } diff --git a/invokeai/frontend/web/src/features/ui/store/extraReducers.ts b/invokeai/frontend/web/src/features/ui/store/extraReducers.ts new file mode 100644 index 0000000000..9b134e1476 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/store/extraReducers.ts @@ -0,0 +1,13 @@ +import { InvokeTabName, tabMap } from './tabMap'; +import { UIState } from './uiTypes'; + +export const setActiveTabReducer = ( + state: UIState, + newActiveTab: number | InvokeTabName +) => { + if (typeof newActiveTab === 'number') { + state.activeTab = newActiveTab; + } else { + state.activeTab = tabMap.indexOf(newActiveTab); + } +}; diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.ts b/invokeai/frontend/web/src/features/ui/store/tabMap.ts index d30799a80f..7584878e02 100644 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.ts +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.ts @@ -1,10 +1,11 @@ export const tabMap = [ - 'txt2img', - 'img2img', + // 'txt2img', + // 'img2img', + 'linear', 'unifiedCanvas', 'nodes', - 'postprocessing', - 'training', + // 'postprocessing', + // 'training', ] as const; export type InvokeTabName = (typeof tabMap)[number]; diff --git a/invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts b/invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts new file mode 100644 index 0000000000..64516c6372 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/store/uiPersistBlacklist.ts @@ -0,0 +1,10 @@ +import { UIState } from './uiTypes'; + +/** + * UI slice persist blacklist + */ +const itemsToBlacklist: (keyof UIState)[] = []; + +export const uiBlacklist = itemsToBlacklist.map( + (blacklistItem) => `ui.${blacklistItem}` +); diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index f90e339697..9cdf26c042 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -1,5 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import { initialImageSelected } from 'features/parameters/store/generationSlice'; +import { setActiveTabReducer } from './extraReducers'; import { InvokeTabName, tabMap } from './tabMap'; import { AddNewModelType, UIState } from './uiTypes'; @@ -17,6 +19,10 @@ const initialtabsState: UIState = { shouldPinGallery: true, shouldShowGallery: true, shouldHidePreview: false, + disabledParameterPanels: [], + disabledTabs: [], + openLinearAccordionItems: [], + openUnifiedCanvasAccordionItems: [], }; const initialState: UIState = initialtabsState; @@ -26,11 +32,7 @@ export const uiSlice = createSlice({ initialState, reducers: { setActiveTab: (state, action: PayloadAction) => { - if (typeof action.payload === 'number') { - state.activeTab = action.payload; - } else { - state.activeTab = tabMap.indexOf(action.payload); - } + setActiveTabReducer(state, action.payload); }, setCurrentTheme: (state, action: PayloadAction) => { state.currentTheme = action.payload; @@ -96,6 +98,21 @@ export const uiSlice = createSlice({ state.shouldShowParametersPanel = true; } }, + setDisabledPanels: (state, action: PayloadAction) => { + state.disabledParameterPanels = action.payload; + }, + setDisabledTabs: (state, action: PayloadAction) => { + state.disabledTabs = action.payload; + }, + openAccordionItemsChanged: (state, action: PayloadAction) => { + if (tabMap[state.activeTab] === 'linear') { + state.openLinearAccordionItems = action.payload; + } + + if (tabMap[state.activeTab] === 'unifiedCanvas') { + state.openUnifiedCanvasAccordionItems = action.payload; + } + }, }, }); @@ -118,6 +135,9 @@ export const { togglePinParametersPanel, toggleParametersPanel, toggleGalleryPanel, + setDisabledPanels, + setDisabledTabs, + openAccordionItemsChanged, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 2a8e8f4231..a1cba603bb 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,3 +1,5 @@ +import { InvokeTabName } from './tabMap'; + export type AddNewModelType = 'ckpt' | 'diffusers' | null; export interface UIState { @@ -14,4 +16,8 @@ export interface UIState { shouldHidePreview: boolean; shouldPinGallery: boolean; shouldShowGallery: boolean; + disabledParameterPanels: string[]; + disabledTabs: InvokeTabName[]; + openLinearAccordionItems: number[]; + openUnifiedCanvasAccordionItems: number[]; } diff --git a/invokeai/frontend/web/src/services/api/core/ApiError.ts b/invokeai/frontend/web/src/services/api/core/ApiError.ts new file mode 100644 index 0000000000..41a9605a3a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/ApiError.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; + +export class ApiError extends Error { + public readonly url: string; + public readonly status: number; + public readonly statusText: string; + public readonly body: any; + public readonly request: ApiRequestOptions; + + constructor(request: ApiRequestOptions, response: ApiResult, message: string) { + super(message); + + this.name = 'ApiError'; + this.url = response.url; + this.status = response.status; + this.statusText = response.statusText; + this.body = response.body; + this.request = request; + } +} diff --git a/invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts b/invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts new file mode 100644 index 0000000000..c9350406a1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/ApiRequestOptions.ts @@ -0,0 +1,16 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiRequestOptions = { + readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; + readonly url: string; + readonly path?: Record; + readonly cookies?: Record; + readonly headers?: Record; + readonly query?: Record; + readonly formData?: Record; + readonly body?: any; + readonly mediaType?: string; + readonly responseHeader?: string; + readonly errors?: Record; +}; diff --git a/invokeai/frontend/web/src/services/api/core/ApiResult.ts b/invokeai/frontend/web/src/services/api/core/ApiResult.ts new file mode 100644 index 0000000000..91f60ae082 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/ApiResult.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiResult = { + readonly url: string; + readonly ok: boolean; + readonly status: number; + readonly statusText: string; + readonly body: any; +}; diff --git a/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts b/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts new file mode 100644 index 0000000000..b923479fea --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/CancelablePromise.ts @@ -0,0 +1,128 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export class CancelError extends Error { + + constructor(message: string) { + super(message); + this.name = 'CancelError'; + } + + public get isCancelled(): boolean { + return true; + } +} + +export interface OnCancel { + readonly isResolved: boolean; + readonly isRejected: boolean; + readonly isCancelled: boolean; + + (cancelHandler: () => void): void; +} + +export class CancelablePromise implements Promise { + readonly [Symbol.toStringTag]!: string; + + private _isResolved: boolean; + private _isRejected: boolean; + private _isCancelled: boolean; + private readonly _cancelHandlers: (() => void)[]; + private readonly _promise: Promise; + private _resolve?: (value: T | PromiseLike) => void; + private _reject?: (reason?: any) => void; + + constructor( + executor: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + onCancel: OnCancel + ) => void + ) { + this._isResolved = false; + this._isRejected = false; + this._isCancelled = false; + this._cancelHandlers = []; + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + + const onResolve = (value: T | PromiseLike): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isResolved = true; + this._resolve?.(value); + }; + + const onReject = (reason?: any): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isRejected = true; + this._reject?.(reason); + }; + + const onCancel = (cancelHandler: () => void): void => { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._cancelHandlers.push(cancelHandler); + }; + + Object.defineProperty(onCancel, 'isResolved', { + get: (): boolean => this._isResolved, + }); + + Object.defineProperty(onCancel, 'isRejected', { + get: (): boolean => this._isRejected, + }); + + Object.defineProperty(onCancel, 'isCancelled', { + get: (): boolean => this._isCancelled, + }); + + return executor(onResolve, onReject, onCancel as OnCancel); + }); + } + + public then( + onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike) | null + ): Promise { + return this._promise.then(onFulfilled, onRejected); + } + + public catch( + onRejected?: ((reason: any) => TResult | PromiseLike) | null + ): Promise { + return this._promise.catch(onRejected); + } + + public finally(onFinally?: (() => void) | null): Promise { + return this._promise.finally(onFinally); + } + + public cancel(): void { + if (this._isResolved || this._isRejected || this._isCancelled) { + return; + } + this._isCancelled = true; + if (this._cancelHandlers.length) { + try { + for (const cancelHandler of this._cancelHandlers) { + cancelHandler(); + } + } catch (error) { + console.warn('Cancellation threw an error', error); + return; + } + } + this._cancelHandlers.length = 0; + this._reject?.(new CancelError('Request aborted')); + } + + public get isCancelled(): boolean { + return this._isCancelled; + } +} diff --git a/invokeai/frontend/web/src/services/api/core/OpenAPI.ts b/invokeai/frontend/web/src/services/api/core/OpenAPI.ts new file mode 100644 index 0000000000..ba65dcd55d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/OpenAPI.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; + +type Resolver = (options: ApiRequestOptions) => Promise; +type Headers = Record; + +export type OpenAPIConfig = { + BASE: string; + VERSION: string; + WITH_CREDENTIALS: boolean; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; + TOKEN?: string | Resolver; + USERNAME?: string | Resolver; + PASSWORD?: string | Resolver; + HEADERS?: Headers | Resolver; + ENCODE_PATH?: (path: string) => string; +}; + +export const OpenAPI: OpenAPIConfig = { + BASE: '', + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: undefined, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; diff --git a/invokeai/frontend/web/src/services/api/core/request.ts b/invokeai/frontend/web/src/services/api/core/request.ts new file mode 100644 index 0000000000..745f687743 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/core/request.ts @@ -0,0 +1,351 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Custom `request.ts` file for OpenAPI code generator. + * + * Patches the request logic in such a way that we can extract headers from requests. + * + * Copied from https://github.com/ferdikoomen/openapi-typescript-codegen/issues/829#issuecomment-1228224477 + * + * This file should be excluded in `tsconfig.json` and ignored by prettier/eslint! + */ + +import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; +import FormData from 'form-data'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +export const HEADERS = Symbol('HEADERS'); + +const isDefined = ( + value: T | null | undefined +): value is Exclude => { + return value !== undefined && value !== null; +}; + +const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +const isSuccess = (status: number): boolean => { + return status >= 200 && status < 300; +}; + +const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +const getQueryString = (params: Record): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach((v) => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver = (options: ApiRequestOptions) => Promise; + +const resolve = async ( + options: ApiRequestOptions, + resolver?: T | Resolver +): Promise => { + if (typeof resolver === 'function') { + return (resolver as Resolver)(options); + } + return resolver; +}; + +const getHeaders = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + formData?: FormData +): Promise> => { + const token = await resolve(options, config.TOKEN); + const username = await resolve(options, config.USERNAME); + const password = await resolve(options, config.PASSWORD); + const additionalHeaders = await resolve(options, config.HEADERS); + const formHeaders = + (typeof formData?.getHeaders === 'function' && formData?.getHeaders()) || + {}; + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + ...formHeaders, + }) + .filter(([_, value]) => isDefined(value)) + .reduce( + (headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), + {} as Record + ); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return headers; +}; + +const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body) { + return options.body; + } + return undefined; +}; + +const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Record, + onCancel: OnCancel +): Promise> => { + const source = axios.CancelToken.source(); + + const requestConfig: AxiosRequestConfig = { + url, + headers, + data: body ?? formData, + method: options.method, + withCredentials: config.WITH_CREDENTIALS, + cancelToken: source.token, + }; + + onCancel(() => source.cancel('The user aborted a request.')); + + try { + return await axios.request(requestConfig); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + return axiosError.response; + } + throw error; + } +}; + +const getResponseHeader = ( + response: AxiosResponse, + responseHeader?: string +): string | undefined => { + if (responseHeader) { + const content = response.headers[responseHeader]; + if (isString(content)) { + return content; + } + } + return undefined; +}; + +const getResponseBody = (response: AxiosResponse): any => { + if (response.status !== 204) { + return response.data; + } + return undefined; +}; + +const catchErrorCodes = ( + options: ApiRequestOptions, + result: ApiResult +): void => { + const errors: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + }; + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + throw new ApiError(options, result, 'Generic Error'); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ +export const request = ( + config: OpenAPIConfig, + options: ApiRequestOptions +): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options, formData); + + if (!onCancel.isCancelled) { + const response = await sendRequest( + config, + options, + url, + body, + formData, + headers, + onCancel + ); + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader( + response, + options.responseHeader + ); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve({ ...result.body, [HEADERS]: response.headers }); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts new file mode 100644 index 0000000000..f1b84f8465 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -0,0 +1,133 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export { ApiError } from './core/ApiError'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI } from './core/OpenAPI'; +export type { OpenAPIConfig } from './core/OpenAPI'; + +export type { AddInvocation } from './models/AddInvocation'; +export type { BlurInvocation } from './models/BlurInvocation'; +export type { Body_upload_image } from './models/Body_upload_image'; +export type { CkptModelInfo } from './models/CkptModelInfo'; +export type { CollectInvocation } from './models/CollectInvocation'; +export type { CollectInvocationOutput } from './models/CollectInvocationOutput'; +export type { CreateModelRequest } from './models/CreateModelRequest'; +export type { CropImageInvocation } from './models/CropImageInvocation'; +export type { CvInpaintInvocation } from './models/CvInpaintInvocation'; +export type { DiffusersModelInfo } from './models/DiffusersModelInfo'; +export type { DivideInvocation } from './models/DivideInvocation'; +export type { Edge } from './models/Edge'; +export type { EdgeConnection } from './models/EdgeConnection'; +export type { Graph } from './models/Graph'; +export type { GraphExecutionState } from './models/GraphExecutionState'; +export type { GraphInvocation } from './models/GraphInvocation'; +export type { GraphInvocationOutput } from './models/GraphInvocationOutput'; +export type { HTTPValidationError } from './models/HTTPValidationError'; +export type { ImageField } from './models/ImageField'; +export type { ImageOutput } from './models/ImageOutput'; +export type { ImageResponse } from './models/ImageResponse'; +export type { ImageResponseMetadata } from './models/ImageResponseMetadata'; +export type { ImageToImageInvocation } from './models/ImageToImageInvocation'; +export type { ImageType } from './models/ImageType'; +export type { InpaintInvocation } from './models/InpaintInvocation'; +export type { IntCollectionOutput } from './models/IntCollectionOutput'; +export type { IntOutput } from './models/IntOutput'; +export type { InverseLerpInvocation } from './models/InverseLerpInvocation'; +export type { InvokeAIMetadata } from './models/InvokeAIMetadata'; +export type { IterateInvocation } from './models/IterateInvocation'; +export type { IterateInvocationOutput } from './models/IterateInvocationOutput'; +export type { LatentsField } from './models/LatentsField'; +export type { LatentsOutput } from './models/LatentsOutput'; +export type { LatentsToImageInvocation } from './models/LatentsToImageInvocation'; +export type { LatentsToLatentsInvocation } from './models/LatentsToLatentsInvocation'; +export type { LerpInvocation } from './models/LerpInvocation'; +export type { LoadImageInvocation } from './models/LoadImageInvocation'; +export type { MaskFromAlphaInvocation } from './models/MaskFromAlphaInvocation'; +export type { MaskOutput } from './models/MaskOutput'; +export type { MetadataImageField } from './models/MetadataImageField'; +export type { MetadataLatentsField } from './models/MetadataLatentsField'; +export type { ModelsList } from './models/ModelsList'; +export type { MultiplyInvocation } from './models/MultiplyInvocation'; +export type { NoiseInvocation } from './models/NoiseInvocation'; +export type { NoiseOutput } from './models/NoiseOutput'; +export type { PaginatedResults_GraphExecutionState_ } from './models/PaginatedResults_GraphExecutionState_'; +export type { PaginatedResults_ImageResponse_ } from './models/PaginatedResults_ImageResponse_'; +export type { ParamIntInvocation } from './models/ParamIntInvocation'; +export type { PasteImageInvocation } from './models/PasteImageInvocation'; +export type { PromptOutput } from './models/PromptOutput'; +export type { RandomRangeInvocation } from './models/RandomRangeInvocation'; +export type { RangeInvocation } from './models/RangeInvocation'; +export type { RestoreFaceInvocation } from './models/RestoreFaceInvocation'; +export type { ShowImageInvocation } from './models/ShowImageInvocation'; +export type { SubtractInvocation } from './models/SubtractInvocation'; +export type { TextToImageInvocation } from './models/TextToImageInvocation'; +export type { TextToLatentsInvocation } from './models/TextToLatentsInvocation'; +export type { UpscaleInvocation } from './models/UpscaleInvocation'; +export type { VaeRepo } from './models/VaeRepo'; +export type { ValidationError } from './models/ValidationError'; + +export { $AddInvocation } from './schemas/$AddInvocation'; +export { $BlurInvocation } from './schemas/$BlurInvocation'; +export { $Body_upload_image } from './schemas/$Body_upload_image'; +export { $CkptModelInfo } from './schemas/$CkptModelInfo'; +export { $CollectInvocation } from './schemas/$CollectInvocation'; +export { $CollectInvocationOutput } from './schemas/$CollectInvocationOutput'; +export { $CreateModelRequest } from './schemas/$CreateModelRequest'; +export { $CropImageInvocation } from './schemas/$CropImageInvocation'; +export { $CvInpaintInvocation } from './schemas/$CvInpaintInvocation'; +export { $DiffusersModelInfo } from './schemas/$DiffusersModelInfo'; +export { $DivideInvocation } from './schemas/$DivideInvocation'; +export { $Edge } from './schemas/$Edge'; +export { $EdgeConnection } from './schemas/$EdgeConnection'; +export { $Graph } from './schemas/$Graph'; +export { $GraphExecutionState } from './schemas/$GraphExecutionState'; +export { $GraphInvocation } from './schemas/$GraphInvocation'; +export { $GraphInvocationOutput } from './schemas/$GraphInvocationOutput'; +export { $HTTPValidationError } from './schemas/$HTTPValidationError'; +export { $ImageField } from './schemas/$ImageField'; +export { $ImageOutput } from './schemas/$ImageOutput'; +export { $ImageResponse } from './schemas/$ImageResponse'; +export { $ImageResponseMetadata } from './schemas/$ImageResponseMetadata'; +export { $ImageToImageInvocation } from './schemas/$ImageToImageInvocation'; +export { $ImageType } from './schemas/$ImageType'; +export { $InpaintInvocation } from './schemas/$InpaintInvocation'; +export { $IntCollectionOutput } from './schemas/$IntCollectionOutput'; +export { $IntOutput } from './schemas/$IntOutput'; +export { $InverseLerpInvocation } from './schemas/$InverseLerpInvocation'; +export { $InvokeAIMetadata } from './schemas/$InvokeAIMetadata'; +export { $IterateInvocation } from './schemas/$IterateInvocation'; +export { $IterateInvocationOutput } from './schemas/$IterateInvocationOutput'; +export { $LatentsField } from './schemas/$LatentsField'; +export { $LatentsOutput } from './schemas/$LatentsOutput'; +export { $LatentsToImageInvocation } from './schemas/$LatentsToImageInvocation'; +export { $LatentsToLatentsInvocation } from './schemas/$LatentsToLatentsInvocation'; +export { $LerpInvocation } from './schemas/$LerpInvocation'; +export { $LoadImageInvocation } from './schemas/$LoadImageInvocation'; +export { $MaskFromAlphaInvocation } from './schemas/$MaskFromAlphaInvocation'; +export { $MaskOutput } from './schemas/$MaskOutput'; +export { $MetadataImageField } from './schemas/$MetadataImageField'; +export { $MetadataLatentsField } from './schemas/$MetadataLatentsField'; +export { $ModelsList } from './schemas/$ModelsList'; +export { $MultiplyInvocation } from './schemas/$MultiplyInvocation'; +export { $NoiseInvocation } from './schemas/$NoiseInvocation'; +export { $NoiseOutput } from './schemas/$NoiseOutput'; +export { $PaginatedResults_GraphExecutionState_ } from './schemas/$PaginatedResults_GraphExecutionState_'; +export { $PaginatedResults_ImageResponse_ } from './schemas/$PaginatedResults_ImageResponse_'; +export { $ParamIntInvocation } from './schemas/$ParamIntInvocation'; +export { $PasteImageInvocation } from './schemas/$PasteImageInvocation'; +export { $PromptOutput } from './schemas/$PromptOutput'; +export { $RandomRangeInvocation } from './schemas/$RandomRangeInvocation'; +export { $RangeInvocation } from './schemas/$RangeInvocation'; +export { $RestoreFaceInvocation } from './schemas/$RestoreFaceInvocation'; +export { $ShowImageInvocation } from './schemas/$ShowImageInvocation'; +export { $SubtractInvocation } from './schemas/$SubtractInvocation'; +export { $TextToImageInvocation } from './schemas/$TextToImageInvocation'; +export { $TextToLatentsInvocation } from './schemas/$TextToLatentsInvocation'; +export { $UpscaleInvocation } from './schemas/$UpscaleInvocation'; +export { $VaeRepo } from './schemas/$VaeRepo'; +export { $ValidationError } from './schemas/$ValidationError'; + +export { ImagesService } from './services/ImagesService'; +export { ModelsService } from './services/ModelsService'; +export { SessionsService } from './services/SessionsService'; diff --git a/invokeai/frontend/web/src/services/api/models/AddInvocation.ts b/invokeai/frontend/web/src/services/api/models/AddInvocation.ts new file mode 100644 index 0000000000..1ff7b010c2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/AddInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Adds two numbers + */ +export type AddInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'add'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts b/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts new file mode 100644 index 0000000000..0643e4b309 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/BlurInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Blurs an image + */ +export type BlurInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'blur'; + /** + * The image to blur + */ + image?: ImageField; + /** + * The blur radius + */ + radius?: number; + /** + * The type of blur + */ + blur_type?: 'gaussian' | 'box'; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/Body_upload_image.ts b/invokeai/frontend/web/src/services/api/models/Body_upload_image.ts new file mode 100644 index 0000000000..b81146d3ab --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/Body_upload_image.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Body_upload_image = { + file: Blob; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts b/invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts new file mode 100644 index 0000000000..2ae7c09674 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CkptModelInfo.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type CkptModelInfo = { + /** + * A description of the model + */ + description?: string; + format?: 'ckpt'; + /** + * The path to the model config + */ + config: string; + /** + * The path to the model weights + */ + weights: string; + /** + * The path to the model VAE + */ + vae: string; + /** + * The width of the model + */ + width?: number; + /** + * The height of the model + */ + height?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts b/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts new file mode 100644 index 0000000000..d250ae4450 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CollectInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Collects values into a collection + */ +export type CollectInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'collect'; + /** + * The item to collect (all inputs must be of the same type) + */ + item?: any; + /** + * The collection, will be provided on execution + */ + collection?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts b/invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts new file mode 100644 index 0000000000..a5976242ea --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CollectInvocationOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Base class for all invocation outputs + */ +export type CollectInvocationOutput = { + type: 'collect_output'; + /** + * The collection of input items + */ + collection: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts b/invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts new file mode 100644 index 0000000000..0b0f52b8fe --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CreateModelRequest.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CkptModelInfo } from './CkptModelInfo'; +import type { DiffusersModelInfo } from './DiffusersModelInfo'; + +export type CreateModelRequest = { + /** + * The name of the model + */ + name: string; + /** + * The model info + */ + info: (CkptModelInfo | DiffusersModelInfo); +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts new file mode 100644 index 0000000000..2676f5cb87 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CropImageInvocation.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Crops an image to a specified box. The box can be outside of the image. + */ +export type CropImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'crop'; + /** + * The image to crop + */ + image?: ImageField; + /** + * The left x coordinate of the crop rectangle + */ + 'x'?: number; + /** + * The top y coordinate of the crop rectangle + */ + 'y'?: number; + /** + * The width of the crop rectangle + */ + width?: number; + /** + * The height of the crop rectangle + */ + height?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts b/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts new file mode 100644 index 0000000000..19342acf8f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/CvInpaintInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Simple inpaint using opencv. + */ +export type CvInpaintInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'cv_inpaint'; + /** + * The image to inpaint + */ + image?: ImageField; + /** + * The mask to use when inpainting + */ + mask?: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts b/invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts new file mode 100644 index 0000000000..5be4801cdd --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/DiffusersModelInfo.ts @@ -0,0 +1,26 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { VaeRepo } from './VaeRepo'; + +export type DiffusersModelInfo = { + /** + * A description of the model + */ + description?: string; + format?: 'diffusers'; + /** + * The VAE repo to use for this model + */ + vae?: VaeRepo; + /** + * The repo ID to use for this model + */ + repo_id?: string; + /** + * The path to the model + */ + path?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts b/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts new file mode 100644 index 0000000000..3cb262e9af --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/DivideInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Divides two numbers + */ +export type DivideInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'div'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/Edge.ts b/invokeai/frontend/web/src/services/api/models/Edge.ts new file mode 100644 index 0000000000..bba275cb26 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/Edge.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { EdgeConnection } from './EdgeConnection'; + +export type Edge = { + /** + * The connection for the edge's from node and field + */ + source: EdgeConnection; + /** + * The connection for the edge's to node and field + */ + destination: EdgeConnection; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/EdgeConnection.ts b/invokeai/frontend/web/src/services/api/models/EdgeConnection.ts new file mode 100644 index 0000000000..ecbddccd76 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/EdgeConnection.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type EdgeConnection = { + /** + * The id of the node for this edge connection + */ + node_id: string; + /** + * The field for this connection + */ + field: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/Graph.ts b/invokeai/frontend/web/src/services/api/models/Graph.ts new file mode 100644 index 0000000000..1e590e4ba9 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/Graph.ts @@ -0,0 +1,49 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { AddInvocation } from './AddInvocation'; +import type { BlurInvocation } from './BlurInvocation'; +import type { CollectInvocation } from './CollectInvocation'; +import type { CropImageInvocation } from './CropImageInvocation'; +import type { CvInpaintInvocation } from './CvInpaintInvocation'; +import type { DivideInvocation } from './DivideInvocation'; +import type { Edge } from './Edge'; +import type { GraphInvocation } from './GraphInvocation'; +import type { ImageToImageInvocation } from './ImageToImageInvocation'; +import type { InpaintInvocation } from './InpaintInvocation'; +import type { InverseLerpInvocation } from './InverseLerpInvocation'; +import type { IterateInvocation } from './IterateInvocation'; +import type { LatentsToImageInvocation } from './LatentsToImageInvocation'; +import type { LatentsToLatentsInvocation } from './LatentsToLatentsInvocation'; +import type { LerpInvocation } from './LerpInvocation'; +import type { LoadImageInvocation } from './LoadImageInvocation'; +import type { MaskFromAlphaInvocation } from './MaskFromAlphaInvocation'; +import type { MultiplyInvocation } from './MultiplyInvocation'; +import type { NoiseInvocation } from './NoiseInvocation'; +import type { ParamIntInvocation } from './ParamIntInvocation'; +import type { PasteImageInvocation } from './PasteImageInvocation'; +import type { RandomRangeInvocation } from './RandomRangeInvocation'; +import type { RangeInvocation } from './RangeInvocation'; +import type { RestoreFaceInvocation } from './RestoreFaceInvocation'; +import type { ShowImageInvocation } from './ShowImageInvocation'; +import type { SubtractInvocation } from './SubtractInvocation'; +import type { TextToImageInvocation } from './TextToImageInvocation'; +import type { TextToLatentsInvocation } from './TextToLatentsInvocation'; +import type { UpscaleInvocation } from './UpscaleInvocation'; + +export type Graph = { + /** + * The id of this graph + */ + id?: string; + /** + * The nodes in this graph + */ + nodes?: Record; + /** + * The connections between nodes and their fields in this graph + */ + edges?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts new file mode 100644 index 0000000000..2243542480 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/GraphExecutionState.ts @@ -0,0 +1,58 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CollectInvocationOutput } from './CollectInvocationOutput'; +import type { Graph } from './Graph'; +import type { GraphInvocationOutput } from './GraphInvocationOutput'; +import type { ImageOutput } from './ImageOutput'; +import type { IntCollectionOutput } from './IntCollectionOutput'; +import type { IntOutput } from './IntOutput'; +import type { IterateInvocationOutput } from './IterateInvocationOutput'; +import type { LatentsOutput } from './LatentsOutput'; +import type { MaskOutput } from './MaskOutput'; +import type { NoiseOutput } from './NoiseOutput'; +import type { PromptOutput } from './PromptOutput'; + +/** + * Tracks the state of a graph execution + */ +export type GraphExecutionState = { + /** + * The id of the execution state + */ + id: string; + /** + * The graph being executed + */ + graph: Graph; + /** + * The expanded graph of activated and executed nodes + */ + execution_graph: Graph; + /** + * The set of node ids that have been executed + */ + executed: Array; + /** + * The list of node ids that have been executed, in order of execution + */ + executed_history: Array; + /** + * The results of node executions + */ + results: Record; + /** + * Errors raised when executing nodes + */ + errors: Record; + /** + * The map of prepared nodes to original graph nodes + */ + prepared_source_mapping: Record; + /** + * The map of original graph nodes to prepared nodes + */ + source_prepared_mapping: Record>; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts b/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts new file mode 100644 index 0000000000..5109a49a68 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/GraphInvocation.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Graph } from './Graph'; + +/** + * A node to process inputs and produce outputs. + * May use dependency injection in __init__ to receive providers. + */ +export type GraphInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'graph'; + /** + * The graph to run + */ + graph?: Graph; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts b/invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts new file mode 100644 index 0000000000..af0aae3edb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/GraphInvocationOutput.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Base class for all invocation outputs + */ +export type GraphInvocationOutput = { + type: 'graph_output'; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts b/invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts new file mode 100644 index 0000000000..5e13adc4e5 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/HTTPValidationError.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ValidationError } from './ValidationError'; + +export type HTTPValidationError = { + detail?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageField.ts b/invokeai/frontend/web/src/services/api/models/ImageField.ts new file mode 100644 index 0000000000..fa22ae8007 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageField.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageType } from './ImageType'; + +/** + * An image field used for passing image objects between invocations + */ +export type ImageField = { + /** + * The type of the image + */ + image_type: ImageType; + /** + * The name of the image + */ + image_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageOutput.ts b/invokeai/frontend/web/src/services/api/models/ImageOutput.ts new file mode 100644 index 0000000000..09b842de26 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageOutput.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Base class for invocations that output an image + */ +export type ImageOutput = { + type: 'image'; + /** + * The output image + */ + image: ImageField; + /** + * The width of the image in pixels + */ + width: number; + /** + * The height of the image in pixels + */ + height: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageResponse.ts b/invokeai/frontend/web/src/services/api/models/ImageResponse.ts new file mode 100644 index 0000000000..688f29bfef --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageResponse.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageResponseMetadata } from './ImageResponseMetadata'; +import type { ImageType } from './ImageType'; + +/** + * The response type for images + */ +export type ImageResponse = { + /** + * The type of the image + */ + image_type: ImageType; + /** + * The name of the image + */ + image_name: string; + /** + * The url of the image + */ + image_url: string; + /** + * The url of the image's thumbnail + */ + thumbnail_url: string; + /** + * The image's metadata + */ + metadata: ImageResponseMetadata; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts b/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts new file mode 100644 index 0000000000..50acf364df --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageResponseMetadata.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { InvokeAIMetadata } from './InvokeAIMetadata'; + +/** + * An image's metadata. Used only in HTTP responses. + */ +export type ImageResponseMetadata = { + /** + * The creation timestamp of the image + */ + created: number; + /** + * The width of the image in pixels + */ + width: number; + /** + * The height of the image in pixels + */ + height: number; + /** + * The image's InvokeAI-specific metadata + */ + invokeai?: InvokeAIMetadata; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts new file mode 100644 index 0000000000..d65ceeee3a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageToImageInvocation.ts @@ -0,0 +1,69 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Generates an image using img2img. + */ +export type ImageToImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'img2img'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * The scheduler to use + */ + scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; + /** + * The input image + */ + image?: ImageField; + /** + * The strength of the original image + */ + strength?: number; + /** + * Whether or not the result should be fit to the aspect ratio of the input image + */ + fit?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ImageType.ts b/invokeai/frontend/web/src/services/api/models/ImageType.ts new file mode 100644 index 0000000000..b6468a1ed0 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ImageType.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An enumeration. + */ +export type ImageType = 'results' | 'intermediates' | 'uploads'; diff --git a/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts new file mode 100644 index 0000000000..7ea6a89f62 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InpaintInvocation.ts @@ -0,0 +1,77 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Generates an image using inpaint. + */ +export type InpaintInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'inpaint'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * The scheduler to use + */ + scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; + /** + * The input image + */ + image?: ImageField; + /** + * The strength of the original image + */ + strength?: number; + /** + * Whether or not the result should be fit to the aspect ratio of the input image + */ + fit?: boolean; + /** + * The mask + */ + mask?: ImageField; + /** + * The amount by which to replace masked areas with latent noise + */ + inpaint_replace?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts b/invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts new file mode 100644 index 0000000000..93a115f980 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IntCollectionOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * A collection of integers + */ +export type IntCollectionOutput = { + type?: 'int_collection'; + /** + * The int collection + */ + collection?: Array; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IntOutput.ts b/invokeai/frontend/web/src/services/api/models/IntOutput.ts new file mode 100644 index 0000000000..eeea6c68b4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IntOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An integer output + */ +export type IntOutput = { + type?: 'int_output'; + /** + * The output integer + */ + 'a'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts b/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts new file mode 100644 index 0000000000..33c59b7bac --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InverseLerpInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Inverse linear interpolation of all pixels of an image + */ +export type InverseLerpInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'ilerp'; + /** + * The image to lerp + */ + image?: ImageField; + /** + * The minimum input value + */ + min?: number; + /** + * The maximum input value + */ + max?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts new file mode 100644 index 0000000000..a6bc3f7744 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/InvokeAIMetadata.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { MetadataImageField } from './MetadataImageField'; +import type { MetadataLatentsField } from './MetadataLatentsField'; + +export type InvokeAIMetadata = { + session_id?: string; + node?: Record; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts b/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts new file mode 100644 index 0000000000..0ff7a1258d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IterateInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * A node to process inputs and produce outputs. + * May use dependency injection in __init__ to receive providers. + */ +export type IterateInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'iterate'; + /** + * The list of items to iterate over + */ + collection?: Array; + /** + * The index, will be provided on executed iterators + */ + index?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts b/invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts new file mode 100644 index 0000000000..ce8d9f8c4b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/IterateInvocationOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Used to connect iteration outputs. Will be expanded to a specific output. + */ +export type IterateInvocationOutput = { + type: 'iterate_output'; + /** + * The item being iterated over + */ + item: any; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsField.ts b/invokeai/frontend/web/src/services/api/models/LatentsField.ts new file mode 100644 index 0000000000..bc6a525f7c --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsField.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * A latents field used for passing latents between invocations + */ +export type LatentsField = { + /** + * The name of the latents + */ + latents_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsOutput.ts b/invokeai/frontend/web/src/services/api/models/LatentsOutput.ts new file mode 100644 index 0000000000..0d417c3db2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Base class for invocations that output latents + */ +export type LatentsOutput = { + type?: 'latent_output'; + /** + * The output latents + */ + latents?: LatentsField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts new file mode 100644 index 0000000000..8acd872e28 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsToImageInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Generates an image from latents. + */ +export type LatentsToImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'l2i'; + /** + * The latents to generate an image from + */ + latents?: LatentsField; + /** + * The model to use + */ + model?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts new file mode 100644 index 0000000000..8210f01bb6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LatentsToLatentsInvocation.ts @@ -0,0 +1,73 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Generates latents using latents as base image. + */ +export type LatentsToLatentsInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'l2l'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The noise to use + */ + noise?: LatentsField; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * The scheduler to use + */ + scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The axes to tile the image on, 'x' and/or 'y' + */ + seamless_axes?: string; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; + /** + * The latents to use as a base image + */ + latents?: LatentsField; + /** + * The strength of the latents to use + */ + strength?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts b/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts new file mode 100644 index 0000000000..f2406c2246 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LerpInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Linear interpolation of all pixels of an image + */ +export type LerpInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'lerp'; + /** + * The image to lerp + */ + image?: ImageField; + /** + * The minimum output value + */ + min?: number; + /** + * The maximum output value + */ + max?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts new file mode 100644 index 0000000000..745a9b44e4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/LoadImageInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageType } from './ImageType'; + +/** + * Load an image and provide it as output. + */ +export type LoadImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'load_image'; + /** + * The type of the image + */ + image_type: ImageType; + /** + * The name of the image + */ + image_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts b/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts new file mode 100644 index 0000000000..e71b1f464b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MaskFromAlphaInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Extracts the alpha channel of an image as a mask. + */ +export type MaskFromAlphaInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'tomask'; + /** + * The image to create the mask from + */ + image?: ImageField; + /** + * Whether or not to invert the mask + */ + invert?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MaskOutput.ts b/invokeai/frontend/web/src/services/api/models/MaskOutput.ts new file mode 100644 index 0000000000..645fb8d1cb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MaskOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Base class for invocations that output a mask + */ +export type MaskOutput = { + type: 'mask'; + /** + * The output mask + */ + mask: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts b/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts new file mode 100644 index 0000000000..0dcae1ccee --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MetadataImageField.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageType } from './ImageType'; + +export type MetadataImageField = { + image_type: ImageType; + image_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts b/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts new file mode 100644 index 0000000000..30b6aebeba --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MetadataLatentsField.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type MetadataLatentsField = { + latents_name: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ModelsList.ts b/invokeai/frontend/web/src/services/api/models/ModelsList.ts new file mode 100644 index 0000000000..7a7449542d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ModelsList.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { CkptModelInfo } from './CkptModelInfo'; +import type { DiffusersModelInfo } from './DiffusersModelInfo'; + +export type ModelsList = { + models: Record; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts b/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts new file mode 100644 index 0000000000..eede8f18d7 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/MultiplyInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Multiplies two numbers + */ +export type MultiplyInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'mul'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts b/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts new file mode 100644 index 0000000000..59e50b76f3 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/NoiseInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Generates latent noise. + */ +export type NoiseInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'noise'; + /** + * The seed to use + */ + seed?: number; + /** + * The width of the resulting noise + */ + width?: number; + /** + * The height of the resulting noise + */ + height?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/NoiseOutput.ts b/invokeai/frontend/web/src/services/api/models/NoiseOutput.ts new file mode 100644 index 0000000000..ff87cb7277 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/NoiseOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Invocation noise output + */ +export type NoiseOutput = { + type?: 'noise_output'; + /** + * The output noise + */ + noise?: LatentsField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts b/invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts new file mode 100644 index 0000000000..dd9f50cd4a --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PaginatedResults_GraphExecutionState_.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { GraphExecutionState } from './GraphExecutionState'; + +/** + * Paginated results + */ +export type PaginatedResults_GraphExecutionState_ = { + /** + * Items + */ + items: Array; + /** + * Current Page + */ + page: number; + /** + * Total number of pages + */ + pages: number; + /** + * Number of items per page + */ + per_page: number; + /** + * Total number of items in result + */ + total: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts b/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts new file mode 100644 index 0000000000..214c7c2f57 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PaginatedResults_ImageResponse_.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageResponse } from './ImageResponse'; + +/** + * Paginated results + */ +export type PaginatedResults_ImageResponse_ = { + /** + * Items + */ + items: Array; + /** + * Current Page + */ + page: number; + /** + * Total number of pages + */ + pages: number; + /** + * Number of items per page + */ + per_page: number; + /** + * Total number of items in result + */ + total: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts b/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts new file mode 100644 index 0000000000..7047310a87 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ParamIntInvocation.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An integer parameter + */ +export type ParamIntInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'param_int'; + /** + * The integer value + */ + 'a'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts new file mode 100644 index 0000000000..8a181ccf07 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PasteImageInvocation.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Pastes an image into another image. + */ +export type PasteImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'paste'; + /** + * The base image + */ + base_image?: ImageField; + /** + * The image to paste + */ + image?: ImageField; + /** + * The mask to use when pasting + */ + mask?: ImageField; + /** + * The left x coordinate at which to paste the image + */ + 'x'?: number; + /** + * The top y coordinate at which to paste the image + */ + 'y'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/PromptOutput.ts b/invokeai/frontend/web/src/services/api/models/PromptOutput.ts new file mode 100644 index 0000000000..5bca3f3037 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/PromptOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Base class for invocations that output a prompt + */ +export type PromptOutput = { + type: 'prompt'; + /** + * The output prompt + */ + prompt: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts new file mode 100644 index 0000000000..55a94ec46d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/RandomRangeInvocation.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Creates a collection of random numbers + */ +export type RandomRangeInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'random_range'; + /** + * The inclusive low value + */ + low?: number; + /** + * The exclusive high value + */ + high?: number; + /** + * The number of values to generate + */ + size?: number; + /** + * The seed for the RNG + */ + seed?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts new file mode 100644 index 0000000000..72bc4806da --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/RangeInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Creates a range + */ +export type RangeInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'range'; + /** + * The start of the range + */ + start?: number; + /** + * The stop of the range + */ + stop?: number; + /** + * The step of the range + */ + step?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts b/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts new file mode 100644 index 0000000000..e03ed01c81 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/RestoreFaceInvocation.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Restores faces in an image. + */ +export type RestoreFaceInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'restore_face'; + /** + * The input image + */ + image?: ImageField; + /** + * The strength of the restoration + */ + strength?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts new file mode 100644 index 0000000000..145895ad75 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ShowImageInvocation.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Displays a provided image, and passes it forward in the pipeline. + */ +export type ShowImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'show_image'; + /** + * The image to show + */ + image?: ImageField; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts b/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts new file mode 100644 index 0000000000..6f2da116a2 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/SubtractInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Subtracts two numbers + */ +export type SubtractInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'sub'; + /** + * The first number + */ + 'a'?: number; + /** + * The second number + */ + 'b'?: number; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts new file mode 100644 index 0000000000..b1ff7a3525 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/TextToImageInvocation.ts @@ -0,0 +1,55 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Generates an image using text2img. + */ +export type TextToImageInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'txt2img'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * The scheduler to use + */ + scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts new file mode 100644 index 0000000000..63754db163 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/TextToLatentsInvocation.ts @@ -0,0 +1,65 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { LatentsField } from './LatentsField'; + +/** + * Generates latents from a prompt. + */ +export type TextToLatentsInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 't2l'; + /** + * The prompt to generate an image from + */ + prompt?: string; + /** + * The seed to use (-1 for a random seed) + */ + seed?: number; + /** + * The noise to use + */ + noise?: LatentsField; + /** + * The number of steps to use to generate the image + */ + steps?: number; + /** + * The width of the resulting image + */ + width?: number; + /** + * The height of the resulting image + */ + height?: number; + /** + * The Classifier-Free Guidance, higher values may result in a result closer to the prompt + */ + cfg_scale?: number; + /** + * The scheduler to use + */ + scheduler?: 'ddim' | 'dpmpp_2' | 'k_dpm_2' | 'k_dpm_2_a' | 'k_dpmpp_2' | 'k_euler' | 'k_euler_a' | 'k_heun' | 'k_lms' | 'plms'; + /** + * Whether or not to generate an image that can tile without seams + */ + seamless?: boolean; + /** + * The axes to tile the image on, 'x' and/or 'y' + */ + seamless_axes?: string; + /** + * The model to use (currently ignored) + */ + model?: string; + /** + * Whether or not to produce progress images during generation + */ + progress_images?: boolean; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts b/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts new file mode 100644 index 0000000000..8416c2454d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/UpscaleInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { ImageField } from './ImageField'; + +/** + * Upscales an image. + */ +export type UpscaleInvocation = { + /** + * The id of this node. Must be unique among all nodes. + */ + id: string; + type?: 'upscale'; + /** + * The input image + */ + image?: ImageField; + /** + * The strength + */ + strength?: number; + /** + * The upscale level + */ + level?: 2 | 4; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/VaeRepo.ts b/invokeai/frontend/web/src/services/api/models/VaeRepo.ts new file mode 100644 index 0000000000..0e233626c6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/VaeRepo.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type VaeRepo = { + /** + * The repo ID to use for this VAE + */ + repo_id: string; + /** + * The path to the VAE + */ + path?: string; + /** + * The subfolder to use for this VAE + */ + subfolder?: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/models/ValidationError.ts b/invokeai/frontend/web/src/services/api/models/ValidationError.ts new file mode 100644 index 0000000000..14e1fdecd0 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/models/ValidationError.ts @@ -0,0 +1,10 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ValidationError = { + loc: Array<(string | number)>; + msg: string; + type: string; +}; + diff --git a/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts new file mode 100644 index 0000000000..3aa74aef3e --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$AddInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $AddInvocation = { + description: `Adds two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts new file mode 100644 index 0000000000..69f5438583 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$BlurInvocation.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $BlurInvocation = { + description: `Blurs an image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to blur`, + contains: [{ + type: 'ImageField', + }], + }, + radius: { + type: 'number', + description: `The blur radius`, + }, + blur_type: { + type: 'Enum', + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts b/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts new file mode 100644 index 0000000000..7d6adf5a84 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$Body_upload_image.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Body_upload_image = { + properties: { + file: { + type: 'binary', + isRequired: true, + format: 'binary', + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts b/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts new file mode 100644 index 0000000000..aeac9a4200 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CkptModelInfo.ts @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CkptModelInfo = { + properties: { + description: { + type: 'string', + description: `A description of the model`, + }, + format: { + type: 'Enum', + }, + config: { + type: 'string', + description: `The path to the model config`, + isRequired: true, + }, + weights: { + type: 'string', + description: `The path to the model weights`, + isRequired: true, + }, + vae: { + type: 'string', + description: `The path to the model VAE`, + isRequired: true, + }, + width: { + type: 'number', + description: `The width of the model`, + }, + height: { + type: 'number', + description: `The height of the model`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts new file mode 100644 index 0000000000..1ab0bb0b9b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CollectInvocation = { + description: `Collects values into a collection`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + item: { + description: `The item to collect (all inputs must be of the same type)`, + properties: { + }, + }, + collection: { + type: 'array', + contains: { + properties: { + }, + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts new file mode 100644 index 0000000000..598ad94eff --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CollectInvocationOutput.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CollectInvocationOutput = { + description: `Base class for all invocation outputs`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + collection: { + type: 'array', + contains: { + properties: { + }, + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts b/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts new file mode 100644 index 0000000000..32593059d8 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CreateModelRequest.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CreateModelRequest = { + properties: { + name: { + type: 'string', + description: `The name of the model`, + isRequired: true, + }, + info: { + type: 'one-of', + description: `The model info`, + contains: [{ + type: 'CkptModelInfo', + }, { + type: 'DiffusersModelInfo', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts new file mode 100644 index 0000000000..f279efe286 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CropImageInvocation.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CropImageInvocation = { + description: `Crops an image to a specified box. The box can be outside of the image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to crop`, + contains: [{ + type: 'ImageField', + }], + }, + 'x': { + type: 'number', + description: `The left x coordinate of the crop rectangle`, + }, + 'y': { + type: 'number', + description: `The top y coordinate of the crop rectangle`, + }, + width: { + type: 'number', + description: `The width of the crop rectangle`, + }, + height: { + type: 'number', + description: `The height of the crop rectangle`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts new file mode 100644 index 0000000000..959484f3ed --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$CvInpaintInvocation.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CvInpaintInvocation = { + description: `Simple inpaint using opencv.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to inpaint`, + contains: [{ + type: 'ImageField', + }], + }, + mask: { + type: 'all-of', + description: `The mask to use when inpainting`, + contains: [{ + type: 'ImageField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts b/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts new file mode 100644 index 0000000000..b2e895b498 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$DiffusersModelInfo.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $DiffusersModelInfo = { + properties: { + description: { + type: 'string', + description: `A description of the model`, + }, + format: { + type: 'Enum', + }, + vae: { + type: 'all-of', + description: `The VAE repo to use for this model`, + contains: [{ + type: 'VaeRepo', + }], + }, + repo_id: { + type: 'string', + description: `The repo ID to use for this model`, + }, + path: { + type: 'string', + description: `The path to the model`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts new file mode 100644 index 0000000000..a6d5998591 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$DivideInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $DivideInvocation = { + description: `Divides two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Edge.ts b/invokeai/frontend/web/src/services/api/schemas/$Edge.ts new file mode 100644 index 0000000000..d7e7028bf1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$Edge.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Edge = { + properties: { + source: { + type: 'all-of', + description: `The connection for the edge's from node and field`, + contains: [{ + type: 'EdgeConnection', + }], + isRequired: true, + }, + destination: { + type: 'all-of', + description: `The connection for the edge's to node and field`, + contains: [{ + type: 'EdgeConnection', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts b/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts new file mode 100644 index 0000000000..a3f325888e --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$EdgeConnection.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $EdgeConnection = { + properties: { + node_id: { + type: 'string', + description: `The id of the node for this edge connection`, + isRequired: true, + }, + field: { + type: 'string', + description: `The field for this connection`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$Graph.ts b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts new file mode 100644 index 0000000000..b431011ba6 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$Graph.ts @@ -0,0 +1,80 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Graph = { + properties: { + id: { + type: 'string', + description: `The id of this graph`, + }, + nodes: { + type: 'dictionary', + contains: { + type: 'one-of', + contains: [{ + type: 'LoadImageInvocation', + }, { + type: 'ShowImageInvocation', + }, { + type: 'CropImageInvocation', + }, { + type: 'PasteImageInvocation', + }, { + type: 'MaskFromAlphaInvocation', + }, { + type: 'BlurInvocation', + }, { + type: 'LerpInvocation', + }, { + type: 'InverseLerpInvocation', + }, { + type: 'NoiseInvocation', + }, { + type: 'TextToLatentsInvocation', + }, { + type: 'LatentsToImageInvocation', + }, { + type: 'AddInvocation', + }, { + type: 'SubtractInvocation', + }, { + type: 'MultiplyInvocation', + }, { + type: 'DivideInvocation', + }, { + type: 'ParamIntInvocation', + }, { + type: 'CvInpaintInvocation', + }, { + type: 'RangeInvocation', + }, { + type: 'RandomRangeInvocation', + }, { + type: 'UpscaleInvocation', + }, { + type: 'RestoreFaceInvocation', + }, { + type: 'TextToImageInvocation', + }, { + type: 'GraphInvocation', + }, { + type: 'IterateInvocation', + }, { + type: 'CollectInvocation', + }, { + type: 'LatentsToLatentsInvocation', + }, { + type: 'ImageToImageInvocation', + }, { + type: 'InpaintInvocation', + }], + }, + }, + edges: { + type: 'array', + contains: { + type: 'Edge', + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts new file mode 100644 index 0000000000..a21419a6a4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$GraphExecutionState.ts @@ -0,0 +1,95 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $GraphExecutionState = { + description: `Tracks the state of a graph execution`, + properties: { + id: { + type: 'string', + description: `The id of the execution state`, + isRequired: true, + }, + graph: { + type: 'all-of', + description: `The graph being executed`, + contains: [{ + type: 'Graph', + }], + isRequired: true, + }, + execution_graph: { + type: 'all-of', + description: `The expanded graph of activated and executed nodes`, + contains: [{ + type: 'Graph', + }], + isRequired: true, + }, + executed: { + type: 'array', + contains: { + type: 'string', + }, + isRequired: true, + }, + executed_history: { + type: 'array', + contains: { + type: 'string', + }, + isRequired: true, + }, + results: { + type: 'dictionary', + contains: { + type: 'one-of', + contains: [{ + type: 'ImageOutput', + }, { + type: 'MaskOutput', + }, { + type: 'LatentsOutput', + }, { + type: 'NoiseOutput', + }, { + type: 'IntOutput', + }, { + type: 'PromptOutput', + }, { + type: 'IntCollectionOutput', + }, { + type: 'GraphInvocationOutput', + }, { + type: 'IterateInvocationOutput', + }, { + type: 'CollectInvocationOutput', + }], + }, + isRequired: true, + }, + errors: { + type: 'dictionary', + contains: { + type: 'string', + }, + isRequired: true, + }, + prepared_source_mapping: { + type: 'dictionary', + contains: { + type: 'string', + }, + isRequired: true, + }, + source_prepared_mapping: { + type: 'dictionary', + contains: { + type: 'array', + contains: { + type: 'string', + }, + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts new file mode 100644 index 0000000000..0b9e4322c8 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $GraphInvocation = { + description: `A node to process inputs and produce outputs. + May use dependency injection in __init__ to receive providers.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + graph: { + type: 'all-of', + description: `The graph to run`, + contains: [{ + type: 'Graph', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts new file mode 100644 index 0000000000..c411e65a85 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$GraphInvocationOutput.ts @@ -0,0 +1,12 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $GraphInvocationOutput = { + description: `Base class for all invocation outputs`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts b/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts new file mode 100644 index 0000000000..0d129d4b67 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$HTTPValidationError.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $HTTPValidationError = { + properties: { + detail: { + type: 'array', + contains: { + type: 'ValidationError', + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts new file mode 100644 index 0000000000..968ac29a45 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageField.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageField = { + description: `An image field used for passing image objects between invocations`, + properties: { + image_type: { + type: 'all-of', + description: `The type of the image`, + contains: [{ + type: 'ImageType', + }], + isRequired: true, + }, + image_name: { + type: 'string', + description: `The name of the image`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts new file mode 100644 index 0000000000..6adbe0d8c1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageOutput.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageOutput = { + description: `Base class for invocations that output an image`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + image: { + type: 'all-of', + description: `The output image`, + contains: [{ + type: 'ImageField', + }], + isRequired: true, + }, + width: { + type: 'number', + description: `The width of the image in pixels`, + isRequired: true, + }, + height: { + type: 'number', + description: `The height of the image in pixels`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts new file mode 100644 index 0000000000..9a3d453536 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageResponse.ts @@ -0,0 +1,39 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageResponse = { + description: `The response type for images`, + properties: { + image_type: { + type: 'all-of', + description: `The type of the image`, + contains: [{ + type: 'ImageType', + }], + isRequired: true, + }, + image_name: { + type: 'string', + description: `The name of the image`, + isRequired: true, + }, + image_url: { + type: 'string', + description: `The url of the image`, + isRequired: true, + }, + thumbnail_url: { + type: 'string', + description: `The url of the image's thumbnail`, + isRequired: true, + }, + metadata: { + type: 'all-of', + description: `The image's metadata`, + contains: [{ + type: 'ImageResponseMetadata', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts new file mode 100644 index 0000000000..d215c8de58 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageResponseMetadata.ts @@ -0,0 +1,30 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageResponseMetadata = { + description: `An image's metadata. Used only in HTTP responses.`, + properties: { + created: { + type: 'number', + description: `The creation timestamp of the image`, + isRequired: true, + }, + width: { + type: 'number', + description: `The width of the image in pixels`, + isRequired: true, + }, + height: { + type: 'number', + description: `The height of the image in pixels`, + isRequired: true, + }, + invokeai: { + type: 'all-of', + description: `The image's InvokeAI-specific metadata`, + contains: [{ + type: 'InvokeAIMetadata', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts new file mode 100644 index 0000000000..4b77f03ca3 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageToImageInvocation.ts @@ -0,0 +1,75 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageToImageInvocation = { + description: `Generates an image using img2img.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength of the original image`, + maximum: 1, + }, + fit: { + type: 'boolean', + description: `Whether or not the result should be fit to the aspect ratio of the input image`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts b/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts new file mode 100644 index 0000000000..92e1f2b218 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ImageType.ts @@ -0,0 +1,6 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ImageType = { + type: 'Enum', +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts new file mode 100644 index 0000000000..ab022825b3 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InpaintInvocation.ts @@ -0,0 +1,87 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InpaintInvocation = { + description: `Generates an image using inpaint.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength of the original image`, + maximum: 1, + }, + fit: { + type: 'boolean', + description: `Whether or not the result should be fit to the aspect ratio of the input image`, + }, + mask: { + type: 'all-of', + description: `The mask`, + contains: [{ + type: 'ImageField', + }], + }, + inpaint_replace: { + type: 'number', + description: `The amount by which to replace masked areas with latent noise`, + maximum: 1, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts new file mode 100644 index 0000000000..caffe0ac87 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IntCollectionOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IntCollectionOutput = { + description: `A collection of integers`, + properties: { + type: { + type: 'Enum', + }, + collection: { + type: 'array', + contains: { + type: 'number', + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts new file mode 100644 index 0000000000..dfb16c1473 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IntOutput.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IntOutput = { + description: `An integer output`, + properties: { + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The output integer`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts new file mode 100644 index 0000000000..43dadca876 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InverseLerpInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InverseLerpInvocation = { + description: `Inverse linear interpolation of all pixels of an image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to lerp`, + contains: [{ + type: 'ImageField', + }], + }, + min: { + type: 'number', + description: `The minimum input value`, + maximum: 255, + }, + max: { + type: 'number', + description: `The maximum input value`, + maximum: 255, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts b/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts new file mode 100644 index 0000000000..2d0b8e2db1 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$InvokeAIMetadata.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $InvokeAIMetadata = { + properties: { + session_id: { + type: 'string', + }, + node: { + type: 'dictionary', + contains: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'number', + }, { + type: 'number', + }, { + type: 'boolean', + }, { + type: 'MetadataImageField', + }, { + type: 'MetadataLatentsField', + }], + }, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts new file mode 100644 index 0000000000..b570b889e4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IterateInvocation = { + description: `A node to process inputs and produce outputs. + May use dependency injection in __init__ to receive providers.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + collection: { + type: 'array', + contains: { + properties: { + }, + }, + }, + index: { + type: 'number', + description: `The index, will be provided on executed iterators`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts new file mode 100644 index 0000000000..826e92346d --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$IterateInvocationOutput.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $IterateInvocationOutput = { + description: `Used to connect iteration outputs. Will be expanded to a specific output.`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + item: { + description: `The item being iterated over`, + properties: { + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts new file mode 100644 index 0000000000..6f81c42883 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsField.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsField = { + description: `A latents field used for passing latents between invocations`, + properties: { + latents_name: { + type: 'string', + description: `The name of the latents`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts new file mode 100644 index 0000000000..2b44f5a438 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsOutput.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsOutput = { + description: `Base class for invocations that output latents`, + properties: { + type: { + type: 'Enum', + }, + latents: { + type: 'all-of', + description: `The output latents`, + contains: [{ + type: 'LatentsField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts new file mode 100644 index 0000000000..971fa3b675 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsToImageInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsToImageInvocation = { + description: `Generates an image from latents.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + latents: { + type: 'all-of', + description: `The latents to generate an image from`, + contains: [{ + type: 'LatentsField', + }], + }, + model: { + type: 'string', + description: `The model to use`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts new file mode 100644 index 0000000000..d27fdc7c1f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LatentsToLatentsInvocation.ts @@ -0,0 +1,81 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LatentsToLatentsInvocation = { + description: `Generates latents using latents as base image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + noise: { + type: 'all-of', + description: `The noise to use`, + contains: [{ + type: 'LatentsField', + }], + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + seamless_axes: { + type: 'string', + description: `The axes to tile the image on, 'x' and/or 'y'`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + latents: { + type: 'all-of', + description: `The latents to use as a base image`, + contains: [{ + type: 'LatentsField', + }], + }, + strength: { + type: 'number', + description: `The strength of the latents to use`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts new file mode 100644 index 0000000000..bafac85817 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LerpInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LerpInvocation = { + description: `Linear interpolation of all pixels of an image`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to lerp`, + contains: [{ + type: 'ImageField', + }], + }, + min: { + type: 'number', + description: `The minimum output value`, + maximum: 255, + }, + max: { + type: 'number', + description: `The maximum output value`, + maximum: 255, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts new file mode 100644 index 0000000000..7b7a0cdffe --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$LoadImageInvocation.ts @@ -0,0 +1,29 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LoadImageInvocation = { + description: `Load an image and provide it as output.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image_type: { + type: 'all-of', + description: `The type of the image`, + contains: [{ + type: 'ImageType', + }], + isRequired: true, + }, + image_name: { + type: 'string', + description: `The name of the image`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts new file mode 100644 index 0000000000..88c2089816 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MaskFromAlphaInvocation.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MaskFromAlphaInvocation = { + description: `Extracts the alpha channel of an image as a mask.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to create the mask from`, + contains: [{ + type: 'ImageField', + }], + }, + invert: { + type: 'boolean', + description: `Whether or not to invert the mask`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts new file mode 100644 index 0000000000..cc9d107ab5 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MaskOutput.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MaskOutput = { + description: `Base class for invocations that output a mask`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + mask: { + type: 'all-of', + description: `The output mask`, + contains: [{ + type: 'ImageField', + }], + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts new file mode 100644 index 0000000000..5e4b1307ed --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MetadataImageField.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MetadataImageField = { + properties: { + image_type: { + type: 'ImageType', + isRequired: true, + }, + image_name: { + type: 'string', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts b/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts new file mode 100644 index 0000000000..c377f26e42 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MetadataLatentsField.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MetadataLatentsField = { + properties: { + latents_name: { + type: 'string', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts b/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts new file mode 100644 index 0000000000..6fa85f6329 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ModelsList.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ModelsList = { + properties: { + models: { + type: 'dictionary', + contains: { + type: 'one-of', + contains: [{ + type: 'CkptModelInfo', + }, { + type: 'DiffusersModelInfo', + }], + }, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts new file mode 100644 index 0000000000..4e8c1d4bbb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$MultiplyInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $MultiplyInvocation = { + description: `Multiplies two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts new file mode 100644 index 0000000000..446e77e747 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$NoiseInvocation.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NoiseInvocation = { + description: `Generates latent noise.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + seed: { + type: 'number', + description: `The seed to use`, + maximum: 4294967295, + }, + width: { + type: 'number', + description: `The width of the resulting noise`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting noise`, + multipleOf: 64, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts new file mode 100644 index 0000000000..b0c3cc1d02 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$NoiseOutput.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $NoiseOutput = { + description: `Invocation noise output`, + properties: { + type: { + type: 'Enum', + }, + noise: { + type: 'all-of', + description: `The output noise`, + contains: [{ + type: 'LatentsField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts new file mode 100644 index 0000000000..ca574eb463 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_GraphExecutionState_.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PaginatedResults_GraphExecutionState_ = { + description: `Paginated results`, + properties: { + items: { + type: 'array', + contains: { + type: 'GraphExecutionState', + }, + isRequired: true, + }, + page: { + type: 'number', + description: `Current Page`, + isRequired: true, + }, + pages: { + type: 'number', + description: `Total number of pages`, + isRequired: true, + }, + per_page: { + type: 'number', + description: `Number of items per page`, + isRequired: true, + }, + total: { + type: 'number', + description: `Total number of items in result`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts new file mode 100644 index 0000000000..113a374f85 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PaginatedResults_ImageResponse_.ts @@ -0,0 +1,35 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PaginatedResults_ImageResponse_ = { + description: `Paginated results`, + properties: { + items: { + type: 'array', + contains: { + type: 'ImageResponse', + }, + isRequired: true, + }, + page: { + type: 'number', + description: `Current Page`, + isRequired: true, + }, + pages: { + type: 'number', + description: `Total number of pages`, + isRequired: true, + }, + per_page: { + type: 'number', + description: `Number of items per page`, + isRequired: true, + }, + total: { + type: 'number', + description: `Total number of items in result`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts new file mode 100644 index 0000000000..a8eac4c450 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ParamIntInvocation.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ParamIntInvocation = { + description: `An integer parameter`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The integer value`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts new file mode 100644 index 0000000000..74bb1edfcb --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PasteImageInvocation.ts @@ -0,0 +1,45 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PasteImageInvocation = { + description: `Pastes an image into another image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + base_image: { + type: 'all-of', + description: `The base image`, + contains: [{ + type: 'ImageField', + }], + }, + image: { + type: 'all-of', + description: `The image to paste`, + contains: [{ + type: 'ImageField', + }], + }, + mask: { + type: 'all-of', + description: `The mask to use when pasting`, + contains: [{ + type: 'ImageField', + }], + }, + 'x': { + type: 'number', + description: `The left x coordinate at which to paste the image`, + }, + 'y': { + type: 'number', + description: `The top y coordinate at which to paste the image`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts b/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts new file mode 100644 index 0000000000..29b800452f --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$PromptOutput.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $PromptOutput = { + description: `Base class for invocations that output a prompt`, + properties: { + type: { + type: 'Enum', + isRequired: true, + }, + prompt: { + type: 'string', + description: `The output prompt`, + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts new file mode 100644 index 0000000000..f13e1a8332 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$RandomRangeInvocation.ts @@ -0,0 +1,33 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $RandomRangeInvocation = { + description: `Creates a collection of random numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + low: { + type: 'number', + description: `The inclusive low value`, + }, + high: { + type: 'number', + description: `The exclusive high value`, + }, + size: { + type: 'number', + description: `The number of values to generate`, + }, + seed: { + type: 'number', + description: `The seed for the RNG`, + maximum: 2147483647, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts new file mode 100644 index 0000000000..f05dae51d4 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$RangeInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $RangeInvocation = { + description: `Creates a range`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + start: { + type: 'number', + description: `The start of the range`, + }, + stop: { + type: 'number', + description: `The stop of the range`, + }, + step: { + type: 'number', + description: `The step of the range`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts new file mode 100644 index 0000000000..a9d10c480b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$RestoreFaceInvocation.ts @@ -0,0 +1,28 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $RestoreFaceInvocation = { + description: `Restores faces in an image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength of the restoration`, + maximum: 1, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts new file mode 100644 index 0000000000..99a8ce0068 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ShowImageInvocation.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ShowImageInvocation = { + description: `Displays a provided image, and passes it forward in the pipeline.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The image to show`, + contains: [{ + type: 'ImageField', + }], + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts new file mode 100644 index 0000000000..be835de13b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$SubtractInvocation.ts @@ -0,0 +1,24 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $SubtractInvocation = { + description: `Subtracts two numbers`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + 'a': { + type: 'number', + description: `The first number`, + }, + 'b': { + type: 'number', + description: `The second number`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts new file mode 100644 index 0000000000..70c5858012 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$TextToImageInvocation.ts @@ -0,0 +1,59 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $TextToImageInvocation = { + description: `Generates an image using text2img.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts new file mode 100644 index 0000000000..7b6dd155ca --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$TextToLatentsInvocation.ts @@ -0,0 +1,70 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $TextToLatentsInvocation = { + description: `Generates latents from a prompt.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + prompt: { + type: 'string', + description: `The prompt to generate an image from`, + }, + seed: { + type: 'number', + description: `The seed to use (-1 for a random seed)`, + maximum: 4294967295, + minimum: -1, + }, + noise: { + type: 'all-of', + description: `The noise to use`, + contains: [{ + type: 'LatentsField', + }], + }, + steps: { + type: 'number', + description: `The number of steps to use to generate the image`, + }, + width: { + type: 'number', + description: `The width of the resulting image`, + multipleOf: 64, + }, + height: { + type: 'number', + description: `The height of the resulting image`, + multipleOf: 64, + }, + cfg_scale: { + type: 'number', + description: `The Classifier-Free Guidance, higher values may result in a result closer to the prompt`, + }, + scheduler: { + type: 'Enum', + }, + seamless: { + type: 'boolean', + description: `Whether or not to generate an image that can tile without seams`, + }, + seamless_axes: { + type: 'string', + description: `The axes to tile the image on, 'x' and/or 'y'`, + }, + model: { + type: 'string', + description: `The model to use (currently ignored)`, + }, + progress_images: { + type: 'boolean', + description: `Whether or not to produce progress images during generation`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts b/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts new file mode 100644 index 0000000000..21f87f1fb7 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$UpscaleInvocation.ts @@ -0,0 +1,31 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $UpscaleInvocation = { + description: `Upscales an image.`, + properties: { + id: { + type: 'string', + description: `The id of this node. Must be unique among all nodes.`, + isRequired: true, + }, + type: { + type: 'Enum', + }, + image: { + type: 'all-of', + description: `The input image`, + contains: [{ + type: 'ImageField', + }], + }, + strength: { + type: 'number', + description: `The strength`, + maximum: 1, + }, + level: { + type: 'Enum', + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts b/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts new file mode 100644 index 0000000000..8b8fbf0968 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$VaeRepo.ts @@ -0,0 +1,20 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $VaeRepo = { + properties: { + repo_id: { + type: 'string', + description: `The repo ID to use for this VAE`, + isRequired: true, + }, + path: { + type: 'string', + description: `The path to the VAE`, + }, + subfolder: { + type: 'string', + description: `The subfolder to use for this VAE`, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts b/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts new file mode 100644 index 0000000000..d4c5c3e471 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/schemas/$ValidationError.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ValidationError = { + properties: { + loc: { + type: 'array', + contains: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'number', + }], + }, + isRequired: true, + }, + msg: { + type: 'string', + isRequired: true, + }, + type: { + type: 'string', + isRequired: true, + }, + }, +} as const; diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts new file mode 100644 index 0000000000..2d0f9435e9 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -0,0 +1,139 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Body_upload_image } from '../models/Body_upload_image'; +import type { ImageResponse } from '../models/ImageResponse'; +import type { ImageType } from '../models/ImageType'; +import type { PaginatedResults_ImageResponse_ } from '../models/PaginatedResults_ImageResponse_'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class ImagesService { + + /** + * Get Image + * Gets a result + * @returns any Successful Response + * @throws ApiError + */ + public static getImage({ + imageType, + imageName, + }: { + /** + * The type of image to get + */ + imageType: ImageType, + /** + * The name of the image to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Thumbnail + * Gets a thumbnail + * @returns any Successful Response + * @throws ApiError + */ + public static getThumbnail({ + imageType, + imageName, + }: { + /** + * The type of image to get + */ + imageType: ImageType, + /** + * The name of the image to get + */ + imageName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/{image_type}/thumbnails/{image_name}', + path: { + 'image_type': imageType, + 'image_name': imageName, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Upload Image + * @returns ImageResponse The image was uploaded successfully + * @throws ApiError + */ + public static uploadImage({ + formData, + }: { + formData: Body_upload_image, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/images/uploads/', + formData: formData, + mediaType: 'multipart/form-data', + errors: { + 415: `Image upload failed`, + 422: `Validation Error`, + }, + }); + } + + /** + * List Images + * Gets a list of images + * @returns PaginatedResults_ImageResponse_ Successful Response + * @throws ApiError + */ + public static listImages({ + imageType, + page, + perPage = 10, + }: { + /** + * The type of images to get + */ + imageType?: ImageType, + /** + * The page of images to get + */ + page?: number, + /** + * The number of images per page + */ + perPage?: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/images/', + query: { + 'image_type': imageType, + 'page': page, + 'per_page': perPage, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/api/services/ModelsService.ts b/invokeai/frontend/web/src/services/api/services/ModelsService.ts new file mode 100644 index 0000000000..3f8ae6bf7b --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/ModelsService.ts @@ -0,0 +1,72 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CreateModelRequest } from '../models/CreateModelRequest'; +import type { ModelsList } from '../models/ModelsList'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class ModelsService { + + /** + * List Models + * Gets a list of models + * @returns ModelsList Successful Response + * @throws ApiError + */ + public static listModels(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/models/', + }); + } + + /** + * Update Model + * Add Model + * @returns any Successful Response + * @throws ApiError + */ + public static updateModel({ + requestBody, + }: { + requestBody: CreateModelRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/models/', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Delete Model + * Delete Model + * @returns any Successful Response + * @throws ApiError + */ + public static delModel({ + modelName, + }: { + modelName: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/models/{model_name}', + path: { + 'model_name': modelName, + }, + errors: { + 404: `Model not found`, + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/api/services/SessionsService.ts b/invokeai/frontend/web/src/services/api/services/SessionsService.ts new file mode 100644 index 0000000000..269092c6d9 --- /dev/null +++ b/invokeai/frontend/web/src/services/api/services/SessionsService.ts @@ -0,0 +1,381 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { AddInvocation } from '../models/AddInvocation'; +import type { BlurInvocation } from '../models/BlurInvocation'; +import type { CollectInvocation } from '../models/CollectInvocation'; +import type { CropImageInvocation } from '../models/CropImageInvocation'; +import type { CvInpaintInvocation } from '../models/CvInpaintInvocation'; +import type { DivideInvocation } from '../models/DivideInvocation'; +import type { Edge } from '../models/Edge'; +import type { Graph } from '../models/Graph'; +import type { GraphExecutionState } from '../models/GraphExecutionState'; +import type { GraphInvocation } from '../models/GraphInvocation'; +import type { ImageToImageInvocation } from '../models/ImageToImageInvocation'; +import type { InpaintInvocation } from '../models/InpaintInvocation'; +import type { InverseLerpInvocation } from '../models/InverseLerpInvocation'; +import type { IterateInvocation } from '../models/IterateInvocation'; +import type { LatentsToImageInvocation } from '../models/LatentsToImageInvocation'; +import type { LatentsToLatentsInvocation } from '../models/LatentsToLatentsInvocation'; +import type { LerpInvocation } from '../models/LerpInvocation'; +import type { LoadImageInvocation } from '../models/LoadImageInvocation'; +import type { MaskFromAlphaInvocation } from '../models/MaskFromAlphaInvocation'; +import type { MultiplyInvocation } from '../models/MultiplyInvocation'; +import type { NoiseInvocation } from '../models/NoiseInvocation'; +import type { PaginatedResults_GraphExecutionState_ } from '../models/PaginatedResults_GraphExecutionState_'; +import type { ParamIntInvocation } from '../models/ParamIntInvocation'; +import type { PasteImageInvocation } from '../models/PasteImageInvocation'; +import type { RandomRangeInvocation } from '../models/RandomRangeInvocation'; +import type { RangeInvocation } from '../models/RangeInvocation'; +import type { RestoreFaceInvocation } from '../models/RestoreFaceInvocation'; +import type { ShowImageInvocation } from '../models/ShowImageInvocation'; +import type { SubtractInvocation } from '../models/SubtractInvocation'; +import type { TextToImageInvocation } from '../models/TextToImageInvocation'; +import type { TextToLatentsInvocation } from '../models/TextToLatentsInvocation'; +import type { UpscaleInvocation } from '../models/UpscaleInvocation'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class SessionsService { + + /** + * List Sessions + * Gets a list of sessions, optionally searching + * @returns PaginatedResults_GraphExecutionState_ Successful Response + * @throws ApiError + */ + public static listSessions({ + page, + perPage = 10, + query = '', + }: { + /** + * The page of results to get + */ + page?: number, + /** + * The number of results per page + */ + perPage?: number, + /** + * The query string to search for + */ + query?: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/sessions/', + query: { + 'page': page, + 'per_page': perPage, + 'query': query, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Create Session + * Creates a new session, optionally initializing it with an invocation graph + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static createSession({ + requestBody, + }: { + requestBody?: Graph, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/sessions/', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid json`, + 422: `Validation Error`, + }, + }); + } + + /** + * Get Session + * Gets a session + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static getSession({ + sessionId, + }: { + /** + * The id of the session to get + */ + sessionId: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/sessions/{session_id}', + path: { + 'session_id': sessionId, + }, + errors: { + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Add Node + * Adds a node to the graph + * @returns string Successful Response + * @throws ApiError + */ + public static addNode({ + sessionId, + requestBody, + }: { + /** + * The id of the session + */ + sessionId: string, + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/sessions/{session_id}/nodes', + path: { + 'session_id': sessionId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Update Node + * Updates a node in the graph and removes all linked edges + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static updateNode({ + sessionId, + nodePath, + requestBody, + }: { + /** + * The id of the session + */ + sessionId: string, + /** + * The path to the node in the graph + */ + nodePath: string, + requestBody: (LoadImageInvocation | ShowImageInvocation | CropImageInvocation | PasteImageInvocation | MaskFromAlphaInvocation | BlurInvocation | LerpInvocation | InverseLerpInvocation | NoiseInvocation | TextToLatentsInvocation | LatentsToImageInvocation | AddInvocation | SubtractInvocation | MultiplyInvocation | DivideInvocation | ParamIntInvocation | CvInpaintInvocation | RangeInvocation | RandomRangeInvocation | UpscaleInvocation | RestoreFaceInvocation | TextToImageInvocation | GraphInvocation | IterateInvocation | CollectInvocation | LatentsToLatentsInvocation | ImageToImageInvocation | InpaintInvocation), + }): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/api/v1/sessions/{session_id}/nodes/{node_path}', + path: { + 'session_id': sessionId, + 'node_path': nodePath, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Delete Node + * Deletes a node in the graph and removes all linked edges + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static deleteNode({ + sessionId, + nodePath, + }: { + /** + * The id of the session + */ + sessionId: string, + /** + * The path to the node to delete + */ + nodePath: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/sessions/{session_id}/nodes/{node_path}', + path: { + 'session_id': sessionId, + 'node_path': nodePath, + }, + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Add Edge + * Adds an edge to the graph + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static addEdge({ + sessionId, + requestBody, + }: { + /** + * The id of the session + */ + sessionId: string, + requestBody: Edge, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/sessions/{session_id}/edges', + path: { + 'session_id': sessionId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Delete Edge + * Deletes an edge from the graph + * @returns GraphExecutionState Successful Response + * @throws ApiError + */ + public static deleteEdge({ + sessionId, + fromNodeId, + fromField, + toNodeId, + toField, + }: { + /** + * The id of the session + */ + sessionId: string, + /** + * The id of the node the edge is coming from + */ + fromNodeId: string, + /** + * The field of the node the edge is coming from + */ + fromField: string, + /** + * The id of the node the edge is going to + */ + toNodeId: string, + /** + * The field of the node the edge is going to + */ + toField: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/sessions/{session_id}/edges/{from_node_id}/{from_field}/{to_node_id}/{to_field}', + path: { + 'session_id': sessionId, + 'from_node_id': fromNodeId, + 'from_field': fromField, + 'to_node_id': toNodeId, + 'to_field': toField, + }, + errors: { + 400: `Invalid node or link`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Invoke Session + * Invokes a session + * @returns any Successful Response + * @throws ApiError + */ + public static invokeSession({ + sessionId, + all = false, + }: { + /** + * The id of the session to invoke + */ + sessionId: string, + /** + * Whether or not to invoke all remaining invocations + */ + all?: boolean, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/api/v1/sessions/{session_id}/invoke', + path: { + 'session_id': sessionId, + }, + query: { + 'all': all, + }, + errors: { + 400: `The session has no invocations ready to invoke`, + 404: `Session not found`, + 422: `Validation Error`, + }, + }); + } + + /** + * Cancel Session Invoke + * Invokes a session + * @returns any Successful Response + * @throws ApiError + */ + public static cancelSessionInvoke({ + sessionId, + }: { + /** + * The id of the session to cancel + */ + sessionId: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/v1/sessions/{session_id}/invoke', + path: { + 'session_id': sessionId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} diff --git a/invokeai/frontend/web/src/services/events/actions.ts b/invokeai/frontend/web/src/services/events/actions.ts new file mode 100644 index 0000000000..79435f52bc --- /dev/null +++ b/invokeai/frontend/web/src/services/events/actions.ts @@ -0,0 +1,50 @@ +import { createAction } from '@reduxjs/toolkit'; +import { + GeneratorProgressEvent, + InvocationCompleteEvent, + InvocationErrorEvent, + InvocationStartedEvent, +} from 'services/events/types'; + +// Common socket action payload data +type BaseSocketPayload = { + timestamp: string; +}; + +// Create actions for each socket event +// Middleware and redux can then respond to them as needed + +export const socketConnected = createAction( + 'socket/socketConnected' +); + +export const socketDisconnected = createAction( + 'socket/socketDisconnected' +); + +export const socketSubscribed = createAction< + BaseSocketPayload & { sessionId: string } +>('socket/socketSubscribed'); + +export const socketUnsubscribed = createAction< + BaseSocketPayload & { sessionId: string } +>('socket/socketUnsubscribed'); + +export const invocationStarted = createAction< + BaseSocketPayload & { data: InvocationStartedEvent } +>('socket/invocationStarted'); + +export const invocationComplete = createAction< + BaseSocketPayload & { data: InvocationCompleteEvent } +>('socket/invocationComplete'); + +export const invocationError = createAction< + BaseSocketPayload & { data: InvocationErrorEvent } +>('socket/invocationError'); + +export const generatorProgress = createAction< + BaseSocketPayload & { data: GeneratorProgressEvent } +>('socket/generatorProgress'); + +// dispatch this when we need to fully reset the socket connection +export const socketReset = createAction('socket/socketReset'); diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts new file mode 100644 index 0000000000..9a462a1f85 --- /dev/null +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -0,0 +1,221 @@ +import { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; +import { io, Socket } from 'socket.io-client'; + +import { + ClientToServerEvents, + ServerToClientEvents, +} from 'services/events/types'; +import { + generatorProgress, + invocationComplete, + invocationError, + invocationStarted, + socketConnected, + socketDisconnected, + socketReset, + socketSubscribed, + socketUnsubscribed, +} from './actions'; +import { + receivedResultImagesPage, + receivedUploadImagesPage, +} from 'services/thunks/gallery'; +import { AppDispatch, RootState } from 'app/store'; +import { getTimestamp } from 'common/util/getTimestamp'; +import { + sessionInvoked, + isFulfilledSessionCreatedAction, + sessionCanceled, +} from 'services/thunks/session'; +import { OpenAPI } from 'services/api'; +import { receivedModels } from 'services/thunks/model'; +import { receivedOpenAPISchema } from 'services/thunks/schema'; + +export const socketMiddleware = () => { + let areListenersSet = false; + + let socketUrl = `ws://${window.location.host}`; + + const socketOptions: Parameters[0] = { + timeout: 60000, + path: '/ws/socket.io', + autoConnect: false, // achtung! removing this breaks the dynamic middleware + }; + + // if building in package mode, replace socket url with open api base url minus the http protocol + if (['nodes', 'package'].includes(import.meta.env.MODE)) { + if (OpenAPI.BASE) { + //eslint-disable-next-line + socketUrl = OpenAPI.BASE.replace(/^https?\:\/\//i, ''); + } + + if (OpenAPI.TOKEN) { + // TODO: handle providing jwt to socket.io + socketOptions.auth = { token: OpenAPI.TOKEN }; + } + } + + const socket: Socket = io( + socketUrl, + socketOptions + ); + + const middleware: Middleware = + (store: MiddlewareAPI) => (next) => (action) => { + const { dispatch, getState } = store; + + // Nothing dispatches `socketReset` actions yet, so this is a noop, but including anyways + if (socketReset.match(action)) { + const { sessionId } = getState().system; + + if (sessionId) { + socket.emit('unsubscribe', { session: sessionId }); + dispatch( + socketUnsubscribed({ sessionId, timestamp: getTimestamp() }) + ); + } + + if (socket.connected) { + socket.disconnect(); + dispatch(socketDisconnected({ timestamp: getTimestamp() })); + } + + socket.removeAllListeners(); + areListenersSet = false; + } + + // Set listeners for `connect` and `disconnect` events once + // Must happen in middleware to get access to `dispatch` + if (!areListenersSet) { + socket.on('connect', () => { + dispatch(socketConnected({ timestamp: getTimestamp() })); + + const { results, uploads, models, nodes } = getState(); + + // These thunks need to be dispatch in middleware; cannot handle in a reducer + if (!results.ids.length) { + dispatch(receivedResultImagesPage()); + } + + if (!uploads.ids.length) { + dispatch(receivedUploadImagesPage()); + } + + if (!models.ids.length) { + dispatch(receivedModels()); + } + + if (!nodes.schema) { + dispatch(receivedOpenAPISchema()); + } + }); + + socket.on('disconnect', () => { + dispatch(socketDisconnected({ timestamp: getTimestamp() })); + }); + + areListenersSet = true; + + // must manually connect + socket.connect(); + } + + // Everything else only happens once we have created a session + if (isFulfilledSessionCreatedAction(action)) { + const oldSessionId = getState().system.sessionId; + + // temp disable event subscription + const shouldHandleEvent = (id: string): boolean => true; + + // const subscribedNodeIds = getState().system.subscribedNodeIds; + // const shouldHandleEvent = (id: string): boolean => { + // if (subscribedNodeIds.length === 1 && subscribedNodeIds[0] === '*') { + // return true; + // } + + // return subscribedNodeIds.includes(id); + // }; + + if (oldSessionId) { + // Unsubscribe when invocations complete + socket.emit('unsubscribe', { + session: oldSessionId, + }); + + dispatch( + socketUnsubscribed({ + sessionId: oldSessionId, + timestamp: getTimestamp(), + }) + ); + + const listenersToRemove: (keyof ServerToClientEvents)[] = [ + 'invocation_started', + 'generator_progress', + 'invocation_error', + 'invocation_complete', + ]; + + // Remove listeners for these events; we need to set them up fresh whenever we subscribe + listenersToRemove.forEach((event: keyof ServerToClientEvents) => { + socket.removeAllListeners(event); + }); + } + + const sessionId = action.payload.id; + + // After a session is created, we immediately subscribe to events and then invoke the session + socket.emit('subscribe', { session: sessionId }); + + // Always dispatch the event actions for other consumers who want to know when we subscribed + dispatch( + socketSubscribed({ + sessionId, + timestamp: getTimestamp(), + }) + ); + + // Set up listeners for the present subscription + socket.on('invocation_started', (data) => { + if (shouldHandleEvent(data.node.id)) { + dispatch(invocationStarted({ data, timestamp: getTimestamp() })); + } + }); + + socket.on('generator_progress', (data) => { + if (shouldHandleEvent(data.node.id)) { + dispatch(generatorProgress({ data, timestamp: getTimestamp() })); + } + }); + + socket.on('invocation_error', (data) => { + if (shouldHandleEvent(data.node.id)) { + dispatch(invocationError({ data, timestamp: getTimestamp() })); + } + }); + + socket.on('invocation_complete', (data) => { + if (shouldHandleEvent(data.node.id)) { + const sessionId = data.graph_execution_state_id; + + const { cancelType, isCancelScheduled } = getState().system; + + // Handle scheduled cancelation + if (cancelType === 'scheduled' && isCancelScheduled) { + dispatch(sessionCanceled({ sessionId })); + } + + dispatch(invocationComplete({ data, timestamp: getTimestamp() })); + } + }); + + // Finally we actually invoke the session, starting processing + dispatch(sessionInvoked({ sessionId })); + } + + // Always pass the action on so other middleware and reducers can handle it + next(action); + }; + + return middleware; +}; diff --git a/invokeai/frontend/web/src/services/events/types.ts b/invokeai/frontend/web/src/services/events/types.ts new file mode 100644 index 0000000000..8452c46340 --- /dev/null +++ b/invokeai/frontend/web/src/services/events/types.ts @@ -0,0 +1,109 @@ +import { Graph, GraphExecutionState } from '../api'; + +/** + * A progress image, we get one for each step in the generation + */ +export type ProgressImage = { + dataURL: string; + width: number; + height: number; +}; + +export type AnyInvocationType = NonNullable< + NonNullable[string]['type'] +>; + +export type AnyInvocation = NonNullable[string]; + +// export type AnyInvocation = { +// id: string; +// type: AnyInvocationType | string; +// [key: string]: any; +// }; + +export type AnyResult = GraphExecutionState['results'][string]; + +/** + * A `generator_progress` socket.io event. + * + * @example socket.on('generator_progress', (data: GeneratorProgressEvent) => { ... } + */ +export type GeneratorProgressEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; + progress_image?: ProgressImage; + step: number; + total_steps: number; +}; + +/** + * A `invocation_complete` socket.io event. + * + * `result` is a discriminated union with a `type` property as the discriminant. + * + * @example socket.on('invocation_complete', (data: InvocationCompleteEvent) => { ... } + */ +export type InvocationCompleteEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; + result: AnyResult; +}; + +/** + * A `invocation_error` socket.io event. + * + * @example socket.on('invocation_error', (data: InvocationErrorEvent) => { ... } + */ +export type InvocationErrorEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; + error: string; +}; + +/** + * A `invocation_started` socket.io event. + * + * @example socket.on('invocation_started', (data: InvocationStartedEvent) => { ... } + */ +export type InvocationStartedEvent = { + graph_execution_state_id: string; + node: AnyInvocation; + source_node_id: string; +}; + +/** + * A `graph_execution_state_complete` socket.io event. + * + * @example socket.on('graph_execution_state_complete', (data: GraphExecutionStateCompleteEvent) => { ... } + */ +export type GraphExecutionStateCompleteEvent = { + graph_execution_state_id: string; +}; + +export type ClientEmitSubscribe = { + session: string; +}; + +export type ClientEmitUnsubscribe = { + session: string; +}; + +export type ServerToClientEvents = { + generator_progress: (payload: GeneratorProgressEvent) => void; + invocation_complete: (payload: InvocationCompleteEvent) => void; + invocation_error: (payload: InvocationErrorEvent) => void; + invocation_started: (payload: InvocationStartedEvent) => void; + graph_execution_state_complete: ( + payload: GraphExecutionStateCompleteEvent + ) => void; +}; + +export type ClientToServerEvents = { + connect: () => void; + disconnect: () => void; + subscribe: (payload: ClientEmitSubscribe) => void; + unsubscribe: (payload: ClientEmitUnsubscribe) => void; +}; diff --git a/invokeai/frontend/web/src/services/fixtures/openapi.json b/invokeai/frontend/web/src/services/fixtures/openapi.json new file mode 100644 index 0000000000..fb2cddf3e7 --- /dev/null +++ b/invokeai/frontend/web/src/services/fixtures/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.0.2","info":{"title":"Invoke AI","description":"An API for invoking AI image operations","version":"1.0.0"},"paths":{"/api/v1/sessions/":{"get":{"tags":["sessions"],"summary":"List Sessions","description":"Gets a list of sessions, optionally searching","operationId":"list_sessions","parameters":[{"description":"The page of results to get","required":false,"schema":{"title":"Page","type":"integer","description":"The page of results to get","default":0},"name":"page","in":"query"},{"description":"The number of results per page","required":false,"schema":{"title":"Per Page","type":"integer","description":"The number of results per page","default":10},"name":"per_page","in":"query"},{"description":"The query string to search for","required":false,"schema":{"title":"Query","type":"string","description":"The query string to search for","default":""},"name":"query","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedResults_GraphExecutionState_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["sessions"],"summary":"Create Session","description":"Creates a new session, optionally initializing it with an invocation graph","operationId":"create_session","requestBody":{"content":{"application/json":{"schema":{"title":"Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The graph to initialize the session with"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid json"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}":{"get":{"tags":["sessions"],"summary":"Get Session","description":"Gets a session","operationId":"get_session","parameters":[{"description":"The id of the session to get","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session to get"},"name":"session_id","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/nodes":{"post":{"tags":["sessions"],"summary":"Add Node","description":"Adds a node to the graph","operationId":"add_node","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Node","anyOf":[{"$ref":"#/components/schemas/LoadImageInvocation"},{"$ref":"#/components/schemas/ShowImageInvocation"},{"$ref":"#/components/schemas/CropImageInvocation"},{"$ref":"#/components/schemas/PasteImageInvocation"},{"$ref":"#/components/schemas/MaskFromAlphaInvocation"},{"$ref":"#/components/schemas/BlurInvocation"},{"$ref":"#/components/schemas/LerpInvocation"},{"$ref":"#/components/schemas/InverseLerpInvocation"},{"$ref":"#/components/schemas/CvInpaintInvocation"},{"$ref":"#/components/schemas/UpscaleInvocation"},{"$ref":"#/components/schemas/RestoreFaceInvocation"},{"$ref":"#/components/schemas/TextToImageInvocation"},{"$ref":"#/components/schemas/GraphInvocation"},{"$ref":"#/components/schemas/IterateInvocation"},{"$ref":"#/components/schemas/CollectInvocation"},{"$ref":"#/components/schemas/ImageToImageInvocation"},{"$ref":"#/components/schemas/InpaintInvocation"}],"description":"The node to add"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response 200 Add Node","type":"string"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/nodes/{node_path}":{"put":{"tags":["sessions"],"summary":"Update Node","description":"Updates a node in the graph and removes all linked edges","operationId":"update_node","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"},{"description":"The path to the node in the graph","required":true,"schema":{"title":"Node Path","type":"string","description":"The path to the node in the graph"},"name":"node_path","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Node","anyOf":[{"$ref":"#/components/schemas/LoadImageInvocation"},{"$ref":"#/components/schemas/ShowImageInvocation"},{"$ref":"#/components/schemas/CropImageInvocation"},{"$ref":"#/components/schemas/PasteImageInvocation"},{"$ref":"#/components/schemas/MaskFromAlphaInvocation"},{"$ref":"#/components/schemas/BlurInvocation"},{"$ref":"#/components/schemas/LerpInvocation"},{"$ref":"#/components/schemas/InverseLerpInvocation"},{"$ref":"#/components/schemas/CvInpaintInvocation"},{"$ref":"#/components/schemas/UpscaleInvocation"},{"$ref":"#/components/schemas/RestoreFaceInvocation"},{"$ref":"#/components/schemas/TextToImageInvocation"},{"$ref":"#/components/schemas/GraphInvocation"},{"$ref":"#/components/schemas/IterateInvocation"},{"$ref":"#/components/schemas/CollectInvocation"},{"$ref":"#/components/schemas/ImageToImageInvocation"},{"$ref":"#/components/schemas/InpaintInvocation"}],"description":"The new node"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sessions"],"summary":"Delete Node","description":"Deletes a node in the graph and removes all linked edges","operationId":"delete_node","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"},{"description":"The path to the node to delete","required":true,"schema":{"title":"Node Path","type":"string","description":"The path to the node to delete"},"name":"node_path","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/edges":{"post":{"tags":["sessions"],"summary":"Add Edge","description":"Adds an edge to the graph","operationId":"add_edge","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Edge","allOf":[{"$ref":"#/components/schemas/Edge"}],"description":"The edge to add"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/edges/{from_node_id}/{from_field}/{to_node_id}/{to_field}":{"delete":{"tags":["sessions"],"summary":"Delete Edge","description":"Deletes an edge from the graph","operationId":"delete_edge","parameters":[{"description":"The id of the session","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session"},"name":"session_id","in":"path"},{"description":"The id of the node the edge is coming from","required":true,"schema":{"title":"From Node Id","type":"string","description":"The id of the node the edge is coming from"},"name":"from_node_id","in":"path"},{"description":"The field of the node the edge is coming from","required":true,"schema":{"title":"From Field","type":"string","description":"The field of the node the edge is coming from"},"name":"from_field","in":"path"},{"description":"The id of the node the edge is going to","required":true,"schema":{"title":"To Node Id","type":"string","description":"The id of the node the edge is going to"},"name":"to_node_id","in":"path"},{"description":"The field of the node the edge is going to","required":true,"schema":{"title":"To Field","type":"string","description":"The field of the node the edge is going to"},"name":"to_field","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GraphExecutionState"}}}},"400":{"description":"Invalid node or link"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sessions/{session_id}/invoke":{"put":{"tags":["sessions"],"summary":"Invoke Session","description":"Invokes a session","operationId":"invoke_session","parameters":[{"description":"The id of the session to invoke","required":true,"schema":{"title":"Session Id","type":"string","description":"The id of the session to invoke"},"name":"session_id","in":"path"},{"description":"Whether or not to invoke all remaining invocations","required":false,"schema":{"title":"All","type":"boolean","description":"Whether or not to invoke all remaining invocations","default":false},"name":"all","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"202":{"description":"The invocation is queued"},"400":{"description":"The session has no invocations ready to invoke"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/images/{image_type}/{image_name}":{"get":{"tags":["images"],"summary":"Get Image","description":"Gets a result","operationId":"get_image","parameters":[{"description":"The type of image to get","required":true,"schema":{"allOf":[{"$ref":"#/components/schemas/ImageType"}],"description":"The type of image to get"},"name":"image_type","in":"path"},{"description":"The name of the image to get","required":true,"schema":{"title":"Image Name","type":"string","description":"The name of the image to get"},"name":"image_name","in":"path"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/images/uploads/":{"post":{"tags":["images"],"summary":"Upload Image","operationId":"upload_image","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_image"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"201":{"description":"The image was uploaded successfully"},"404":{"description":"Session not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"BlurInvocation":{"title":"BlurInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["blur"],"type":"string","default":"blur"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to blur"},"radius":{"title":"Radius","minimum":0.0,"type":"number","description":"The blur radius","default":8.0},"blur_type":{"title":"Blur Type","enum":["gaussian","box"],"type":"string","description":"The type of blur","default":"gaussian"}},"description":"Blurs an image","output":{"$ref":"#/components/schemas/ImageOutput"}},"Body_upload_image":{"title":"Body_upload_image","required":["file"],"type":"object","properties":{"file":{"title":"File","type":"string","format":"binary"}}},"CollectInvocation":{"title":"CollectInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["collect"],"type":"string","default":"collect"},"item":{"title":"Item","description":"The item to collect (all inputs must be of the same type)"},"collection":{"title":"Collection","type":"array","items":{},"description":"The collection, will be provided on execution"}},"description":"Collects values into a collection","output":{"$ref":"#/components/schemas/CollectInvocationOutput"}},"CollectInvocationOutput":{"title":"CollectInvocationOutput","description":"Base class for all invocation outputs","type":"object","properties":{"type":{"title":"Type","default":"collect_output","enum":["collect_output"],"type":"string"},"collection":{"title":"Collection","description":"The collection of input items","type":"array","items":{}}},"required":["collection"]},"CropImageInvocation":{"title":"CropImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["crop"],"type":"string","default":"crop"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to crop"},"x":{"title":"X","type":"integer","description":"The left x coordinate of the crop rectangle","default":0},"y":{"title":"Y","type":"integer","description":"The top y coordinate of the crop rectangle","default":0},"width":{"title":"Width","exclusiveMinimum":0.0,"type":"integer","description":"The width of the crop rectangle","default":512},"height":{"title":"Height","exclusiveMinimum":0.0,"type":"integer","description":"The height of the crop rectangle","default":512}},"description":"Crops an image to a specified box. The box can be outside of the image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"CvInpaintInvocation":{"title":"CvInpaintInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["cv_inpaint"],"type":"string","default":"cv_inpaint"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to inpaint"},"mask":{"title":"Mask","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The mask to use when inpainting"}},"description":"Simple inpaint using opencv.","output":{"$ref":"#/components/schemas/ImageOutput"}},"Edge":{"title":"Edge","required":["source","destination"],"type":"object","properties":{"source":{"title":"Source","allOf":[{"$ref":"#/components/schemas/EdgeConnection"}],"description":"The connection for the edge's from node and field"},"destination":{"title":"Destination","allOf":[{"$ref":"#/components/schemas/EdgeConnection"}],"description":"The connection for the edge's to node and field"}}},"EdgeConnection":{"title":"EdgeConnection","required":["node_id","field"],"type":"object","properties":{"node_id":{"title":"Node Id","type":"string","description":"The id of the node for this edge connection"},"field":{"title":"Field","type":"string","description":"The field for this connection"}}},"Graph":{"title":"Graph","type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this graph"},"nodes":{"title":"Nodes","type":"object","additionalProperties":{"oneOf":[{"$ref":"#/components/schemas/LoadImageInvocation"},{"$ref":"#/components/schemas/ShowImageInvocation"},{"$ref":"#/components/schemas/CropImageInvocation"},{"$ref":"#/components/schemas/PasteImageInvocation"},{"$ref":"#/components/schemas/MaskFromAlphaInvocation"},{"$ref":"#/components/schemas/BlurInvocation"},{"$ref":"#/components/schemas/LerpInvocation"},{"$ref":"#/components/schemas/InverseLerpInvocation"},{"$ref":"#/components/schemas/CvInpaintInvocation"},{"$ref":"#/components/schemas/UpscaleInvocation"},{"$ref":"#/components/schemas/RestoreFaceInvocation"},{"$ref":"#/components/schemas/TextToImageInvocation"},{"$ref":"#/components/schemas/GraphInvocation"},{"$ref":"#/components/schemas/IterateInvocation"},{"$ref":"#/components/schemas/CollectInvocation"},{"$ref":"#/components/schemas/ImageToImageInvocation"},{"$ref":"#/components/schemas/InpaintInvocation"}],"discriminator":{"propertyName":"type","mapping":{"load_image":"#/components/schemas/LoadImageInvocation","show_image":"#/components/schemas/ShowImageInvocation","crop":"#/components/schemas/CropImageInvocation","paste":"#/components/schemas/PasteImageInvocation","tomask":"#/components/schemas/MaskFromAlphaInvocation","blur":"#/components/schemas/BlurInvocation","lerp":"#/components/schemas/LerpInvocation","ilerp":"#/components/schemas/InverseLerpInvocation","cv_inpaint":"#/components/schemas/CvInpaintInvocation","upscale":"#/components/schemas/UpscaleInvocation","restore_face":"#/components/schemas/RestoreFaceInvocation","txt2img":"#/components/schemas/TextToImageInvocation","graph":"#/components/schemas/GraphInvocation","iterate":"#/components/schemas/IterateInvocation","collect":"#/components/schemas/CollectInvocation","img2img":"#/components/schemas/ImageToImageInvocation","inpaint":"#/components/schemas/InpaintInvocation"}}},"description":"The nodes in this graph"},"edges":{"title":"Edges","type":"array","items":{"$ref":"#/components/schemas/Edge"},"description":"The connections between nodes and their fields in this graph"}}},"GraphExecutionState":{"title":"GraphExecutionState","required":["graph"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of the execution state"},"graph":{"title":"Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The graph being executed"},"execution_graph":{"title":"Execution Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The expanded graph of activated and executed nodes"},"executed":{"title":"Executed","uniqueItems":true,"type":"array","items":{"type":"string"},"description":"The set of node ids that have been executed"},"executed_history":{"title":"Executed History","type":"array","items":{"type":"string"},"description":"The list of node ids that have been executed, in order of execution"},"results":{"title":"Results","type":"object","additionalProperties":{"oneOf":[{"$ref":"#/components/schemas/ImageOutput"},{"$ref":"#/components/schemas/MaskOutput"},{"$ref":"#/components/schemas/PromptOutput"},{"$ref":"#/components/schemas/GraphInvocationOutput"},{"$ref":"#/components/schemas/IterateInvocationOutput"},{"$ref":"#/components/schemas/CollectInvocationOutput"}],"discriminator":{"propertyName":"type","mapping":{"image":"#/components/schemas/ImageOutput","mask":"#/components/schemas/MaskOutput","prompt":"#/components/schemas/PromptOutput","graph_output":"#/components/schemas/GraphInvocationOutput","iterate_output":"#/components/schemas/IterateInvocationOutput","collect_output":"#/components/schemas/CollectInvocationOutput"}}},"description":"The results of node executions"},"errors":{"title":"Errors","type":"object","additionalProperties":{"type":"string"},"description":"Errors raised when executing nodes"},"prepared_source_mapping":{"title":"Prepared Source Mapping","type":"object","additionalProperties":{"type":"string"},"description":"The map of prepared nodes to original graph nodes"},"source_prepared_mapping":{"title":"Source Prepared Mapping","type":"object","additionalProperties":{"uniqueItems":true,"type":"array","items":{"type":"string"}},"description":"The map of original graph nodes to prepared nodes"}},"description":"Tracks the state of a graph execution"},"GraphInvocation":{"title":"GraphInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["graph"],"type":"string","default":"graph"},"graph":{"title":"Graph","allOf":[{"$ref":"#/components/schemas/Graph"}],"description":"The graph to run"}},"description":"A node to process inputs and produce outputs.\nMay use dependency injection in __init__ to receive providers.","output":{"$ref":"#/components/schemas/GraphInvocationOutput"}},"GraphInvocationOutput":{"title":"GraphInvocationOutput","description":"Base class for all invocation outputs","type":"object","properties":{"type":{"title":"Type","default":"graph_output","enum":["graph_output"],"type":"string"}}},"HTTPValidationError":{"title":"HTTPValidationError","type":"object","properties":{"detail":{"title":"Detail","type":"array","items":{"$ref":"#/components/schemas/ValidationError"}}}},"ImageField":{"title":"ImageField","description":"An image field used for passing image objects between invocations","type":"object","properties":{"image_type":{"title":"Image Type","description":"The type of the image","default":"results","type":"string"},"image_name":{"title":"Image Name","description":"The name of the image","type":"string"}}},"ImageOutput":{"title":"ImageOutput","description":"Base class for invocations that output an image","type":"object","properties":{"type":{"title":"Type","default":"image","enum":["image"],"type":"string"},"image":{"title":"Image","description":"The output image","allOf":[{"$ref":"#/components/schemas/ImageField"}]}}},"ImageToImageInvocation":{"title":"ImageToImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["img2img"],"type":"string","default":"img2img"},"prompt":{"title":"Prompt","type":"string","description":"The prompt to generate an image from"},"seed":{"title":"Seed","maximum":4294967295.0,"minimum":-1.0,"type":"integer","description":"The seed to use (-1 for a random seed)","default":-1},"steps":{"title":"Steps","exclusiveMinimum":0.0,"type":"integer","description":"The number of steps to use to generate the image","default":10},"width":{"title":"Width","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The width of the resulting image","default":512},"height":{"title":"Height","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The height of the resulting image","default":512},"cfg_scale":{"title":"Cfg Scale","exclusiveMinimum":0.0,"type":"number","description":"The Classifier-Free Guidance, higher values may result in a result closer to the prompt","default":7.5},"sampler_name":{"title":"Sampler Name","enum":["ddim","dpmpp_2","k_dpm_2","k_dpm_2_a","k_dpmpp_2","k_euler","k_euler_a","k_heun","k_lms","plms"],"type":"string","description":"The sampler to use","default":"k_lms"},"seamless":{"title":"Seamless","type":"boolean","description":"Whether or not to generate an image that can tile without seams","default":false},"model":{"title":"Model","type":"string","description":"The model to use (currently ignored)","default":""},"progress_images":{"title":"Progress Images","type":"boolean","description":"Whether or not to produce progress images during generation","default":false},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength of the original image","default":0.75},"fit":{"title":"Fit","type":"boolean","description":"Whether or not the result should be fit to the aspect ratio of the input image","default":true}},"description":"Generates an image using img2img.","output":{"$ref":"#/components/schemas/ImageOutput"}},"ImageType":{"title":"ImageType","enum":["results","intermediates","uploads"],"type":"string","description":"An enumeration."},"InpaintInvocation":{"title":"InpaintInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["inpaint"],"type":"string","default":"inpaint"},"prompt":{"title":"Prompt","type":"string","description":"The prompt to generate an image from"},"seed":{"title":"Seed","maximum":4294967295.0,"minimum":-1.0,"type":"integer","description":"The seed to use (-1 for a random seed)","default":-1},"steps":{"title":"Steps","exclusiveMinimum":0.0,"type":"integer","description":"The number of steps to use to generate the image","default":10},"width":{"title":"Width","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The width of the resulting image","default":512},"height":{"title":"Height","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The height of the resulting image","default":512},"cfg_scale":{"title":"Cfg Scale","exclusiveMinimum":0.0,"type":"number","description":"The Classifier-Free Guidance, higher values may result in a result closer to the prompt","default":7.5},"sampler_name":{"title":"Sampler Name","enum":["ddim","dpmpp_2","k_dpm_2","k_dpm_2_a","k_dpmpp_2","k_euler","k_euler_a","k_heun","k_lms","plms"],"type":"string","description":"The sampler to use","default":"k_lms"},"seamless":{"title":"Seamless","type":"boolean","description":"Whether or not to generate an image that can tile without seams","default":false},"model":{"title":"Model","type":"string","description":"The model to use (currently ignored)","default":""},"progress_images":{"title":"Progress Images","type":"boolean","description":"Whether or not to produce progress images during generation","default":false},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength of the original image","default":0.75},"fit":{"title":"Fit","type":"boolean","description":"Whether or not the result should be fit to the aspect ratio of the input image","default":true},"mask":{"title":"Mask","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The mask"},"inpaint_replace":{"title":"Inpaint Replace","maximum":1.0,"minimum":0.0,"type":"number","description":"The amount by which to replace masked areas with latent noise","default":0.0}},"description":"Generates an image using inpaint.","output":{"$ref":"#/components/schemas/ImageOutput"}},"InverseLerpInvocation":{"title":"InverseLerpInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["ilerp"],"type":"string","default":"ilerp"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to lerp"},"min":{"title":"Min","maximum":255.0,"minimum":0.0,"type":"integer","description":"The minimum input value","default":0},"max":{"title":"Max","maximum":255.0,"minimum":0.0,"type":"integer","description":"The maximum input value","default":255}},"description":"Inverse linear interpolation of all pixels of an image","output":{"$ref":"#/components/schemas/ImageOutput"}},"IterateInvocation":{"title":"IterateInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["iterate"],"type":"string","default":"iterate"},"collection":{"title":"Collection","type":"array","items":{},"description":"The list of items to iterate over"},"index":{"title":"Index","type":"integer","description":"The index, will be provided on executed iterators","default":0}},"description":"A node to process inputs and produce outputs.\nMay use dependency injection in __init__ to receive providers.","output":{"$ref":"#/components/schemas/IterateInvocationOutput"}},"IterateInvocationOutput":{"title":"IterateInvocationOutput","description":"Used to connect iteration outputs. Will be expanded to a specific output.","type":"object","properties":{"type":{"title":"Type","default":"iterate_output","enum":["iterate_output"],"type":"string"},"item":{"title":"Item","description":"The item being iterated over"}}},"LerpInvocation":{"title":"LerpInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["lerp"],"type":"string","default":"lerp"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to lerp"},"min":{"title":"Min","maximum":255.0,"minimum":0.0,"type":"integer","description":"The minimum output value","default":0},"max":{"title":"Max","maximum":255.0,"minimum":0.0,"type":"integer","description":"The maximum output value","default":255}},"description":"Linear interpolation of all pixels of an image","output":{"$ref":"#/components/schemas/ImageOutput"}},"LoadImageInvocation":{"title":"LoadImageInvocation","required":["id","image_type","image_name"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["load_image"],"type":"string","default":"load_image"},"image_type":{"allOf":[{"$ref":"#/components/schemas/ImageType"}],"description":"The type of the image"},"image_name":{"title":"Image Name","type":"string","description":"The name of the image"}},"description":"Load an image from a filename and provide it as output.","output":{"$ref":"#/components/schemas/ImageOutput"}},"MaskFromAlphaInvocation":{"title":"MaskFromAlphaInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["tomask"],"type":"string","default":"tomask"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to create the mask from"},"invert":{"title":"Invert","type":"boolean","description":"Whether or not to invert the mask","default":false}},"description":"Extracts the alpha channel of an image as a mask.","output":{"$ref":"#/components/schemas/MaskOutput"}},"MaskOutput":{"title":"MaskOutput","description":"Base class for invocations that output a mask","type":"object","properties":{"type":{"title":"Type","default":"mask","enum":["mask"],"type":"string"},"mask":{"title":"Mask","description":"The output mask","allOf":[{"$ref":"#/components/schemas/ImageField"}]}}},"PaginatedResults_GraphExecutionState_":{"title":"PaginatedResults[GraphExecutionState]","required":["items","page","pages","per_page","total"],"type":"object","properties":{"items":{"title":"Items","type":"array","items":{"$ref":"#/components/schemas/GraphExecutionState"},"description":"Items"},"page":{"title":"Page","type":"integer","description":"Current Page"},"pages":{"title":"Pages","type":"integer","description":"Total number of pages"},"per_page":{"title":"Per Page","type":"integer","description":"Number of items per page"},"total":{"title":"Total","type":"integer","description":"Total number of items in result"}},"description":"Paginated results"},"PasteImageInvocation":{"title":"PasteImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["paste"],"type":"string","default":"paste"},"base_image":{"title":"Base Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The base image"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to paste"},"mask":{"title":"Mask","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The mask to use when pasting"},"x":{"title":"X","type":"integer","description":"The left x coordinate at which to paste the image","default":0},"y":{"title":"Y","type":"integer","description":"The top y coordinate at which to paste the image","default":0}},"description":"Pastes an image into another image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"PromptOutput":{"title":"PromptOutput","type":"object","properties":{"type":{"title":"Type","enum":["prompt"],"type":"string","default":"prompt"},"prompt":{"title":"Prompt","type":"string","description":"The output prompt"}},"description":"Base class for invocations that output a prompt"},"RestoreFaceInvocation":{"title":"RestoreFaceInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["restore_face"],"type":"string","default":"restore_face"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength of the restoration","default":0.75}},"description":"Restores faces in an image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"ShowImageInvocation":{"title":"ShowImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["show_image"],"type":"string","default":"show_image"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The image to show"}},"description":"Displays a provided image, and passes it forward in the pipeline.","output":{"$ref":"#/components/schemas/ImageOutput"}},"TextToImageInvocation":{"title":"TextToImageInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["txt2img"],"type":"string","default":"txt2img"},"prompt":{"title":"Prompt","type":"string","description":"The prompt to generate an image from"},"seed":{"title":"Seed","maximum":4294967295.0,"minimum":-1.0,"type":"integer","description":"The seed to use (-1 for a random seed)","default":-1},"steps":{"title":"Steps","exclusiveMinimum":0.0,"type":"integer","description":"The number of steps to use to generate the image","default":10},"width":{"title":"Width","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The width of the resulting image","default":512},"height":{"title":"Height","multipleOf":64.0,"exclusiveMinimum":0.0,"type":"integer","description":"The height of the resulting image","default":512},"cfg_scale":{"title":"Cfg Scale","exclusiveMinimum":0.0,"type":"number","description":"The Classifier-Free Guidance, higher values may result in a result closer to the prompt","default":7.5},"sampler_name":{"title":"Sampler Name","enum":["ddim","dpmpp_2","k_dpm_2","k_dpm_2_a","k_dpmpp_2","k_euler","k_euler_a","k_heun","k_lms","plms"],"type":"string","description":"The sampler to use","default":"k_lms"},"seamless":{"title":"Seamless","type":"boolean","description":"Whether or not to generate an image that can tile without seams","default":false},"model":{"title":"Model","type":"string","description":"The model to use (currently ignored)","default":""},"progress_images":{"title":"Progress Images","type":"boolean","description":"Whether or not to produce progress images during generation","default":false}},"description":"Generates an image using text2img.","output":{"$ref":"#/components/schemas/ImageOutput"}},"UpscaleInvocation":{"title":"UpscaleInvocation","required":["id"],"type":"object","properties":{"id":{"title":"Id","type":"string","description":"The id of this node. Must be unique among all nodes."},"type":{"title":"Type","enum":["upscale"],"type":"string","default":"upscale"},"image":{"title":"Image","allOf":[{"$ref":"#/components/schemas/ImageField"}],"description":"The input image"},"strength":{"title":"Strength","maximum":1.0,"exclusiveMinimum":0.0,"type":"number","description":"The strength","default":0.75},"level":{"title":"Level","enum":[2,4],"type":"integer","description":"The upscale level","default":2}},"description":"Upscales an image.","output":{"$ref":"#/components/schemas/ImageOutput"}},"ValidationError":{"title":"ValidationError","required":["loc","msg","type"],"type":"object","properties":{"loc":{"title":"Location","type":"array","items":{"anyOf":[{"type":"string"},{"type":"integer"}]}},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}}}}}} \ No newline at end of file diff --git a/invokeai/frontend/web/src/services/fixtures/request.ts b/invokeai/frontend/web/src/services/fixtures/request.ts new file mode 100644 index 0000000000..745f687743 --- /dev/null +++ b/invokeai/frontend/web/src/services/fixtures/request.ts @@ -0,0 +1,351 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * Custom `request.ts` file for OpenAPI code generator. + * + * Patches the request logic in such a way that we can extract headers from requests. + * + * Copied from https://github.com/ferdikoomen/openapi-typescript-codegen/issues/829#issuecomment-1228224477 + * + * This file should be excluded in `tsconfig.json` and ignored by prettier/eslint! + */ + +import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; +import FormData from 'form-data'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +export const HEADERS = Symbol('HEADERS'); + +const isDefined = ( + value: T | null | undefined +): value is Exclude => { + return value !== undefined && value !== null; +}; + +const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +const isSuccess = (status: number): boolean => { + return status >= 200 && status < 300; +}; + +const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +const getQueryString = (params: Record): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach((v) => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver = (options: ApiRequestOptions) => Promise; + +const resolve = async ( + options: ApiRequestOptions, + resolver?: T | Resolver +): Promise => { + if (typeof resolver === 'function') { + return (resolver as Resolver)(options); + } + return resolver; +}; + +const getHeaders = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + formData?: FormData +): Promise> => { + const token = await resolve(options, config.TOKEN); + const username = await resolve(options, config.USERNAME); + const password = await resolve(options, config.PASSWORD); + const additionalHeaders = await resolve(options, config.HEADERS); + const formHeaders = + (typeof formData?.getHeaders === 'function' && formData?.getHeaders()) || + {}; + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + ...formHeaders, + }) + .filter(([_, value]) => isDefined(value)) + .reduce( + (headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), + {} as Record + ); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return headers; +}; + +const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body) { + return options.body; + } + return undefined; +}; + +const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Record, + onCancel: OnCancel +): Promise> => { + const source = axios.CancelToken.source(); + + const requestConfig: AxiosRequestConfig = { + url, + headers, + data: body ?? formData, + method: options.method, + withCredentials: config.WITH_CREDENTIALS, + cancelToken: source.token, + }; + + onCancel(() => source.cancel('The user aborted a request.')); + + try { + return await axios.request(requestConfig); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + return axiosError.response; + } + throw error; + } +}; + +const getResponseHeader = ( + response: AxiosResponse, + responseHeader?: string +): string | undefined => { + if (responseHeader) { + const content = response.headers[responseHeader]; + if (isString(content)) { + return content; + } + } + return undefined; +}; + +const getResponseBody = (response: AxiosResponse): any => { + if (response.status !== 204) { + return response.data; + } + return undefined; +}; + +const catchErrorCodes = ( + options: ApiRequestOptions, + result: ApiResult +): void => { + const errors: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + }; + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + throw new ApiError(options, result, 'Generic Error'); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @returns CancelablePromise + * @throws ApiError + */ +export const request = ( + config: OpenAPIConfig, + options: ApiRequestOptions +): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options, formData); + + if (!onCancel.isCancelled) { + const response = await sendRequest( + config, + options, + url, + body, + formData, + headers, + onCancel + ); + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader( + response, + options.responseHeader + ); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve({ ...result.body, [HEADERS]: response.headers }); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts new file mode 100644 index 0000000000..3badee2549 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -0,0 +1,30 @@ +import { createAppAsyncThunk } from 'app/storeUtils'; +import { ImagesService } from 'services/api'; + +export const IMAGES_PER_PAGE = 20; + +export const receivedResultImagesPage = createAppAsyncThunk( + 'results/receivedResultImagesPage', + async (_arg, { getState }) => { + const response = await ImagesService.listImages({ + imageType: 'results', + page: getState().results.nextPage, + perPage: IMAGES_PER_PAGE, + }); + + return response; + } +); + +export const receivedUploadImagesPage = createAppAsyncThunk( + 'uploads/receivedUploadImagesPage', + async (_arg, { getState }) => { + const response = await ImagesService.listImages({ + imageType: 'uploads', + page: getState().uploads.nextPage, + perPage: IMAGES_PER_PAGE, + }); + + return response; + } +); diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts new file mode 100644 index 0000000000..7014925d87 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -0,0 +1,36 @@ +import { isFulfilled } from '@reduxjs/toolkit'; +import { createAppAsyncThunk } from 'app/storeUtils'; +import { ImagesService } from 'services/api'; +import { getHeaders } from 'services/util/getHeaders'; + +type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0]; + +/** + * `ImagesService.getImage()` thunk + */ +export const imageReceived = createAppAsyncThunk( + 'api/imageReceived', + async (arg: ImageReceivedArg, _thunkApi) => { + const response = await ImagesService.getImage(arg); + return response; + } +); + +type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0]; + +/** + * `ImagesService.uploadImage()` thunk + */ +export const imageUploaded = createAppAsyncThunk( + 'api/imageUploaded', + async (arg: ImageUploadedArg, _thunkApi) => { + const response = await ImagesService.uploadImage(arg); + const { location } = getHeaders(response); + return { response, location }; + } +); + +/** + * Function to check if an action is a fulfilled `ImagesService.uploadImage()` thunk + */ +export const isFulfilledImageUploadedAction = isFulfilled(imageUploaded); diff --git a/invokeai/frontend/web/src/services/thunks/model.ts b/invokeai/frontend/web/src/services/thunks/model.ts new file mode 100644 index 0000000000..f5ee522593 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/model.ts @@ -0,0 +1,24 @@ +import { createAppAsyncThunk } from 'app/storeUtils'; +import { Model } from 'features/system/store/modelSlice'; +import { reduce } from 'lodash'; +import { ModelsService } from 'services/api'; + +export const IMAGES_PER_PAGE = 20; + +export const receivedModels = createAppAsyncThunk( + 'models/receivedModels', + async (_arg) => { + const response = await ModelsService.listModels(); + const deserializedModels = reduce( + response.models, + (modelsAccumulator, model, modelName) => { + modelsAccumulator[modelName] = { ...model, name: modelName }; + + return modelsAccumulator; + }, + {} as Record + ); + + return deserializedModels; + } +); diff --git a/invokeai/frontend/web/src/services/thunks/schema.ts b/invokeai/frontend/web/src/services/thunks/schema.ts new file mode 100644 index 0000000000..edf237032f --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/schema.ts @@ -0,0 +1,14 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { OpenAPIV3 } from 'openapi-types'; + +export const receivedOpenAPISchema = createAsyncThunk( + 'nodes/receivedOpenAPISchema', + async () => { + const response = await fetch(`openapi.json`); + const jsonData = (await response.json()) as OpenAPIV3.Document; + + console.debug('OpenAPI schema: ', jsonData); + + return jsonData; + } +); diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts new file mode 100644 index 0000000000..a1213ffcc2 --- /dev/null +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -0,0 +1,132 @@ +import { createAppAsyncThunk } from 'app/storeUtils'; +import { SessionsService } from 'services/api'; +import { buildLinearGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph'; +import { isAnyOf, isFulfilled } from '@reduxjs/toolkit'; +import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/buildNodesGraph'; + +export const linearGraphBuilt = createAppAsyncThunk( + 'api/linearGraphBuilt', + async (_, { dispatch, getState }) => { + const graph = buildLinearGraph(getState()); + + dispatch(sessionCreated({ graph })); + + return graph; + } +); + +export const nodesGraphBuilt = createAppAsyncThunk( + 'api/nodesGraphBuilt', + async (_, { dispatch, getState }) => { + const graph = buildNodesGraph(getState()); + + dispatch(sessionCreated({ graph })); + + return graph; + } +); + +export const isFulfilledAnyGraphBuilt = isAnyOf( + linearGraphBuilt.fulfilled, + nodesGraphBuilt.fulfilled +); + +type SessionCreatedArg = { + graph: Parameters< + (typeof SessionsService)['createSession'] + >[0]['requestBody']; +}; + +/** + * `SessionsService.createSession()` thunk + */ +export const sessionCreated = createAppAsyncThunk( + 'api/sessionCreated', + async (arg: SessionCreatedArg, { dispatch, getState }) => { + console.log('Session created, graph: ', arg.graph); + + const response = await SessionsService.createSession({ + requestBody: arg.graph, + }); + + return response; + } +); + +/** + * Function to check if an action is a fulfilled `SessionsService.createSession()` thunk + */ +export const isFulfilledSessionCreatedAction = isFulfilled(sessionCreated); + +type NodeAddedArg = Parameters<(typeof SessionsService)['addNode']>[0]; + +/** + * `SessionsService.addNode()` thunk + */ +export const nodeAdded = createAppAsyncThunk( + 'api/nodeAdded', + async ( + arg: { node: NodeAddedArg['requestBody']; sessionId: string }, + _thunkApi + ) => { + const response = await SessionsService.addNode({ + requestBody: arg.node, + sessionId: arg.sessionId, + }); + + return response; + } +); + +/** + * `SessionsService.invokeSession()` thunk + */ +export const sessionInvoked = createAppAsyncThunk( + 'api/sessionInvoked', + async (arg: { sessionId: string }, _thunkApi) => { + const { sessionId } = arg; + + const response = await SessionsService.invokeSession({ + sessionId, + all: true, + }); + + return response; + } +); + +type SessionCanceledArg = Parameters< + (typeof SessionsService)['cancelSessionInvoke'] +>[0]; + +/** + * `SessionsService.cancelSession()` thunk + */ +export const sessionCanceled = createAppAsyncThunk( + 'api/sessionCanceled', + async (arg: SessionCanceledArg, _thunkApi) => { + const { sessionId } = arg; + + const response = await SessionsService.cancelSessionInvoke({ + sessionId, + }); + + return response; + } +); + +type SessionsListedArg = Parameters< + (typeof SessionsService)['listSessions'] +>[0]; + +/** + * `SessionsService.listSessions()` thunk + */ +export const listedSessions = createAppAsyncThunk( + 'api/listSessions', + async (arg: SessionsListedArg, _thunkApi) => { + const response = await SessionsService.listSessions(arg); + + return response; + } +); diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts new file mode 100644 index 0000000000..5a9d891395 --- /dev/null +++ b/invokeai/frontend/web/src/services/types/guards.ts @@ -0,0 +1,33 @@ +import { + GraphExecutionState, + GraphInvocationOutput, + ImageOutput, + MaskOutput, + PromptOutput, + IterateInvocationOutput, + CollectInvocationOutput, +} from 'services/api'; + +export const isImageOutput = ( + output: GraphExecutionState['results'][string] +): output is ImageOutput => output.type === 'image'; + +export const isMaskOutput = ( + output: GraphExecutionState['results'][string] +): output is MaskOutput => output.type === 'mask'; + +export const isPromptOutput = ( + output: GraphExecutionState['results'][string] +): output is PromptOutput => output.type === 'prompt'; + +export const isGraphOutput = ( + output: GraphExecutionState['results'][string] +): output is GraphInvocationOutput => output.type === 'graph_output'; + +export const isIterateOutput = ( + output: GraphExecutionState['results'][string] +): output is IterateInvocationOutput => output.type === 'iterate_output'; + +export const isCollectOutput = ( + output: GraphExecutionState['results'][string] +): output is CollectInvocationOutput => output.type === 'collect_output'; diff --git a/invokeai/frontend/web/src/services/util/deserializeImageField.ts b/invokeai/frontend/web/src/services/util/deserializeImageField.ts new file mode 100644 index 0000000000..0d50a78e49 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/deserializeImageField.ts @@ -0,0 +1,29 @@ +import { Image } from 'app/invokeai'; +import { ImageField, ImageType } from 'services/api'; +import { AnyInvocation } from 'services/events/types'; + +export const buildImageUrls = ( + imageType: ImageType, + imageName: string +): { url: string; thumbnail: string } => { + const url = `api/v1/images/${imageType}/${imageName}`; + + const thumbnail = `api/v1/images/${imageType}/thumbnails/${ + imageName.split('.')[0] + }.webp`; + + return { + url, + thumbnail, + }; +}; + +export const extractTimestampFromImageName = (imageName: string) => { + const timestamp = imageName.split('_')?.pop()?.split('.')[0]; + + if (timestamp === undefined) { + return 0; + } + + return Number(timestamp); +}; diff --git a/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts b/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts new file mode 100644 index 0000000000..ec90fb6793 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/deserializeImageResponse.ts @@ -0,0 +1,29 @@ +import { Image } from 'app/invokeai'; +import { parseInvokeAIMetadata } from 'common/util/parseMetadata'; +import { ImageResponse } from 'services/api'; + +/** + * Process ImageReponse objects, which we get from the `list_images` endpoint. + */ +export const deserializeImageResponse = ( + imageResponse: ImageResponse +): Image => { + const { image_name, image_type, image_url, metadata, thumbnail_url } = + imageResponse; + + // TODO: parse metadata - just leaving it as-is for now + const { invokeai, ...rest } = metadata; + + const parsedMetadata = parseInvokeAIMetadata(invokeai); + + return { + name: image_name, + type: image_type, + url: image_url, + thumbnail: thumbnail_url, + metadata: { + ...rest, + ...(invokeai ? { invokeai: parsedMetadata } : {}), + }, + }; +}; diff --git a/invokeai/frontend/web/src/services/util/getHeaders.ts b/invokeai/frontend/web/src/services/util/getHeaders.ts new file mode 100644 index 0000000000..510ba35770 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/getHeaders.ts @@ -0,0 +1,12 @@ +import { HEADERS } from '../api/core/request'; + +/** + * Returns the response headers of the response received by the generated API client. + */ +export const getHeaders = (response: any): Record => { + if (!(HEADERS in response)) { + throw new Error('Response does not have headers'); + } + + return response[HEADERS]; +}; diff --git a/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts new file mode 100644 index 0000000000..386ca972b1 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts @@ -0,0 +1,24 @@ +import { Graph, TextToImageInvocation } from '../api'; + +/** + * Make a graph of however many images + */ +export const makeGraphOfXImages = (numberOfImages: string) => + Array.from(Array(numberOfImages)) + .map( + (_val, i): TextToImageInvocation => ({ + id: i.toString(), + type: 'txt2img', + prompt: 'pizza', + steps: 50, + seed: 123, + sampler_name: 'ddim', + }) + ) + .reduce( + (acc, val: TextToImageInvocation) => { + acc.nodes![val.id] = val; + return acc; + }, + { nodes: {} } as Graph + ); diff --git a/invokeai/frontend/web/src/theme/components/progress.ts b/invokeai/frontend/web/src/theme/components/progress.ts index 8a66a6af7e..fa6b5b57c5 100644 --- a/invokeai/frontend/web/src/theme/components/progress.ts +++ b/invokeai/frontend/web/src/theme/components/progress.ts @@ -9,7 +9,9 @@ const { defineMultiStyleConfig, definePartsStyle } = const invokeAIFilledTrack = defineStyle((_props) => ({ bg: 'accent.600', - transition: 'width 0.2s ease-in-out', + // TODO: the animation is nice but looks weird bc it is substantially longer than each step + // so we get to 100% long before it finishes + // transition: 'width 0.2s ease-in-out', _indeterminate: { bgGradient: 'linear(to-r, transparent 0%, accent.600 50%, transparent 100%);', diff --git a/invokeai/frontend/web/src/theme/components/scrollbar.ts b/invokeai/frontend/web/src/theme/components/scrollbar.ts index 5128fb1cb8..6cca67f962 100644 --- a/invokeai/frontend/web/src/theme/components/scrollbar.ts +++ b/invokeai/frontend/web/src/theme/components/scrollbar.ts @@ -1,11 +1,13 @@ -export const no_scrollbar = { +import { ChakraProps } from '@chakra-ui/react'; + +export const no_scrollbar: ChakraProps['sx'] = { '::-webkit-scrollbar': { display: 'none', }, scrollbarWidth: 'none', }; -export const scrollbar = { +export const scrollbar: ChakraProps['sx'] = { scrollbarColor: 'accent.600 transparent', scrollbarWidth: 'thick', '::-webkit-scrollbar': { @@ -26,6 +28,6 @@ export const scrollbar = { borderColor: 'accent.500', }, '::-webkit-scrollbar-button': { - background: 'transaprent', + background: 'transparent', }, }; diff --git a/invokeai/frontend/web/src/theme/components/slider.ts b/invokeai/frontend/web/src/theme/components/slider.ts index adb874b781..ef3d84196e 100644 --- a/invokeai/frontend/web/src/theme/components/slider.ts +++ b/invokeai/frontend/web/src/theme/components/slider.ts @@ -37,6 +37,13 @@ const invokeAIMark = defineStyle((_props) => { }); const invokeAI = definePartsStyle((props) => ({ + container: { + _disabled: { + opacity: 0.6, + cursor: 'default', + pointerEvents: 'none', + }, + }, track: invokeAITrack(props), filledTrack: invokeAIFilledTrack(props), thumb: invokeAIThumb(props), diff --git a/invokeai/frontend/web/tests/metadata.ts b/invokeai/frontend/web/tests/metadata.ts new file mode 100644 index 0000000000..c694ce67c7 --- /dev/null +++ b/invokeai/frontend/web/tests/metadata.ts @@ -0,0 +1,174 @@ +export default {}; + +// python metadata parsing tests to rebuild + +// # def test_is_good_metadata_unchanged(): +// # parsed_metadata = metadata_service._parse_invokeai_metadata(valid_metadata) + +// # expected = deepcopy(valid_metadata) + +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_session_id(): +// # metadata_missing_session_id = deepcopy(valid_metadata) +// # del metadata_missing_session_id["session_id"] + +// # expected = deepcopy(valid_metadata) +// # del expected["session_id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_session_id +// # ) +// # assert metadata_missing_session_id == parsed_metadata + +// # def test_can_parse_invalid_session_id(): +// # metadata_invalid_session_id = deepcopy(valid_metadata) +// # metadata_invalid_session_id["session_id"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["session_id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_session_id +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_node(): +// # metadata_missing_node = deepcopy(valid_metadata) +// # del metadata_missing_node["node"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_missing_node) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_node(): +// # metadata_invalid_node = deepcopy(valid_metadata) +// # metadata_invalid_node["node"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["node"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_invalid_node) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_node_id(): +// # metadata_missing_node_id = deepcopy(valid_metadata) +// # del metadata_missing_node_id["node"]["id"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_node_id +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_node_id(): +// # metadata_invalid_node_id = deepcopy(valid_metadata) +// # metadata_invalid_node_id["node"]["id"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["id"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_node_id +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_node_type(): +// # metadata_missing_node_type = deepcopy(valid_metadata) +// # del metadata_missing_node_type["node"]["type"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["type"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_node_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_node_type(): +// # metadata_invalid_node_type = deepcopy(valid_metadata) +// # metadata_invalid_node_type["node"]["type"] = 123 + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["type"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_node_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_no_node_attrs(): +// # metadata_no_node_attrs = deepcopy(valid_metadata) +// # metadata_no_node_attrs["node"] = {} + +// # expected = deepcopy(valid_metadata) +// # del expected["node"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_no_node_attrs) +// # assert expected == parsed_metadata + +// # def test_can_parse_array_attr(): +// # metadata_array_attr = deepcopy(valid_metadata) +// # metadata_array_attr["node"]["seed"] = [1, 2, 3] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["seed"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata(metadata_array_attr) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_dict_attr(): +// # metadata_invalid_dict_attr = deepcopy(valid_metadata) +// # metadata_invalid_dict_attr["node"]["seed"] = {"a": 1} + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["seed"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_dict_attr +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_missing_image_field_image_type(): +// # metadata_missing_image_field_image_type = deepcopy(valid_metadata) +// # del metadata_missing_image_field_image_type["node"]["image"]["image_type"] + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["image"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_missing_image_field_image_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_image_field_image_type(): +// # metadata_invalid_image_field_image_type = deepcopy(valid_metadata) +// # metadata_invalid_image_field_image_type["node"]["image"][ +// # "image_type" +// # ] = "bad image type" + +// # expected = deepcopy(valid_metadata) +// # del expected["node"]["image"] + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_image_field_image_type +// # ) +// # assert expected == parsed_metadata + +// # def test_can_parse_invalid_latents_field_latents_name(): +// # metadata_invalid_latents_field_latents_name = deepcopy(valid_metadata) +// # metadata_invalid_latents_field_latents_name["node"]["latents"] = { +// # "latents_name": 123 +// # } + +// # expected = deepcopy(valid_metadata) + +// # parsed_metadata = metadata_service._parse_invokeai_metadata( +// # metadata_invalid_latents_field_latents_name +// # ) + +// # assert expected == parsed_metadata diff --git a/invokeai/frontend/web/tsconfig.json b/invokeai/frontend/web/tsconfig.json index 2a75e16e88..9731a64d3d 100644 --- a/invokeai/frontend/web/tsconfig.json +++ b/invokeai/frontend/web/tsconfig.json @@ -18,5 +18,6 @@ "jsx": "react-jsx" }, "include": ["src", "index.d.ts"], + "exclude": ["src/services/fixtures/*"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/invokeai/frontend/web/vite.config.ts b/invokeai/frontend/web/vite.config.ts index a6071341b3..2128593f10 100644 --- a/invokeai/frontend/web/vite.config.ts +++ b/invokeai/frontend/web/vite.config.ts @@ -38,6 +38,23 @@ export default defineConfig(({ mode }) => { target: 'ws://127.0.0.1:9090', ws: true, }, + // 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, + }, }, }, build: { diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 1a40014efe..4c7edbffd7 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -2,6 +2,16 @@ # yarn lockfile v1 +"@apidevtools/json-schema-ref-parser@9.0.9": + version "9.0.9" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" + integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + "@babel/code-frame@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" @@ -897,6 +907,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@dagrejs/graphlib@^2.1.12": + version "2.1.12" + resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.1.12.tgz#97d29eae006e4efcb68863505464e0e3f28fa5c7" + integrity sha512-yHk2G7ZNzDEHhQTlYtbtEy5PqlIoioCxZUKcrlBgubMvrLmewXqSV3v4rhc8RAt5s8lr8PcWbiovEPuORxe2KA== + "@emotion/babel-plugin@^11.10.6": version "11.10.6" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz#a68ee4b019d661d6f37dec4b8903255766925ead" @@ -1218,6 +1233,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1244,7 +1264,72 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== -"@reduxjs/toolkit@^1.9.2": +"@reactflow/background@11.2.0": + version "11.2.0" + resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.2.0.tgz#2a6f89d4f4837d488629d32a2bd5f01708018115" + integrity sha512-Fd8Few2JsLuE/2GaIM6fkxEBaAJvfzi2Lc106HKi/ddX+dZs8NUsSwMsJy1Ajs8b4GbiX8v8axfKpbK6qFMV8w== + dependencies: + "@reactflow/core" "11.7.0" + classcat "^5.0.3" + zustand "^4.3.1" + +"@reactflow/controls@11.1.11": + version "11.1.11" + resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.1.11.tgz#d58e1bd9ddc2ee83fbf96130a7c54f44ca068c09" + integrity sha512-g6WrsszhNkQjzkJ9HbVUBkGGoUy2z8dQVgH6CYQEjuoonD15cWAPGvjyg8vx8oGG7CuktUhWu5JPivL6qjECow== + dependencies: + "@reactflow/core" "11.7.0" + classcat "^5.0.3" + +"@reactflow/core@11.7.0", "@reactflow/core@^11.6.0": + version "11.7.0" + resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.7.0.tgz#6d9bdc0b1de1c9251dd3651135450ab2d42c6562" + integrity sha512-UJcpbNRSupSSoMWh5UmRp6UUr0ug7xVKmMvadnkKKiNi9584q57nz4HMfkqwN3/ESbre7LD043yh2n678d/5FQ== + dependencies: + "@types/d3" "^7.4.0" + "@types/d3-drag" "^3.0.1" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.3.1" + +"@reactflow/minimap@11.5.0": + version "11.5.0" + resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.5.0.tgz#ddce263a41c2e65dd2febc09c26e93764ce76bfc" + integrity sha512-n/3tlaknLpi3zaqCC+tDDPvUTOjd6jglto9V3RB1F2wlaUEbCwmuoR2GYTkiRyZMvuskKyAoQW8+0DX0+cWwsA== + dependencies: + "@reactflow/core" "11.7.0" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.3.1" + +"@reactflow/node-resizer@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.1.0.tgz#7764211a7e00f873eab652937cffba8df7c02b6a" + integrity sha512-DVL8nnWsltP8/iANadAcTaDB4wsEkx2mOLlBEPNE3yc5loSm3u9l5m4enXRcBym61MiMuTtDPzZMyYYQUjuYIg== + dependencies: + "@reactflow/core" "^11.6.0" + classcat "^5.0.4" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + zustand "^4.3.1" + +"@reactflow/node-toolbar@1.1.11": + version "1.1.11" + resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.1.11.tgz#174b235d85de37cffba387af8f6fb315ec1d31d7" + integrity sha512-+hKtx+cvXwfCa9paGxE+G34rWRIIVEh68ZOqAtivClVmfqGzH/sEoGWtIOUyg9OEDNE1nEmZ1NrnpBGSmHHXFg== + dependencies: + "@reactflow/core" "11.7.0" + classcat "^5.0.3" + zustand "^4.3.1" + +"@reduxjs/toolkit@^1.9.3": version "1.9.3" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.3.tgz#27e1a33072b5a312e4f7fa19247fec160bbb2df9" integrity sha512-GU2TNBQVofL09VGmuSioNPQIu6Ml0YLf4EJhgj0AvBadRlCGzUWet8372LjvO4fqKZF2vH1xU0htAa7BrK9pZg== @@ -1365,6 +1450,216 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/d3-array@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.4.tgz#44eebe40be57476cad6a0cd6a85b0f57d54185a2" + integrity sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ== + +"@types/d3-axis@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.2.tgz#96e11d51256baf5bdb2fa73a17d302993e79df07" + integrity sha512-uGC7DBh0TZrU/LY43Fd8Qr+2ja1FKmH07q2FoZFHo1eYl8aj87GhfVoY1saJVJiq24rp1+wpI6BvQJMKgQm8oA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.2.tgz#a610aad5a1e76c375be63e11c5eee1ed9fd2fb40" + integrity sha512-2TEm8KzUG3N7z0TrSKPmbxByBx54M+S9lHoP2J55QuLU0VSQ9mE96EJSAOVNEqd1bbynMjeTS9VHmz8/bSw8rA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.2.tgz#cf6f05ad2d8faaad524e9e6f454b4fd06b200930" + integrity sha512-abT/iLHD3sGZwqMTX1TYCMEulr+wBd0SzyOQnjYNLp7sngdOHYtNkMRI5v3w5thoN+BWtlHVDx2Osvq6fxhZWw== + +"@types/d3-color@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4" + integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA== + +"@types/d3-contour@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.2.tgz#d8a0e4d12ec14f7d2bb6e59f3fbc1a527457d0b2" + integrity sha512-k6/bGDoAGJZnZWaKzeB+9glgXCYGvh6YlluxzBREiVo8f/X2vpTEdgPy9DN7Z2i42PZOZ4JDhVdlTSTSkLDPlQ== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz#006b7bd838baec1511270cb900bf4fc377bbbf41" + integrity sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ== + +"@types/d3-dispatch@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.2.tgz#b2fa80bab3bcead68680766e966f59cd6cb9a69f" + integrity sha512-rxN6sHUXEZYCKV05MEh4z4WpPSqIw+aP7n9ZN6WYAAvZoEAghEK1WeVZMZcHRBwyaKflU43PCUAJNjFxCzPDjg== + +"@types/d3-drag@*", "@types/d3-drag@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.2.tgz#5562da3e7b33d782c2c1f9e65c5e91bb01ee82cf" + integrity sha512-qmODKEDvyKWVHcWWCOVcuVcOwikLVsyc4q4EBJMREsoQnR2Qoc2cZQUyFUPgO9q4S3qdSqJKBsuefv+h0Qy+tw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.1.tgz#c51a3505cee42653454b74a00f8713dc3548c362" + integrity sha512-76pBHCMTvPLt44wFOieouXcGXWOF0AJCceUvaFkxSZEu4VDUdv93JfpMa6VGNFs01FHfuP4a5Ou68eRG1KBfTw== + +"@types/d3-ease@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" + integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== + +"@types/d3-fetch@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.2.tgz#fe1f335243e07c9bd520c9a71756fed8330c54b1" + integrity sha512-gllwYWozWfbep16N9fByNBDTkJW/SyhH6SGRlXloR7WdtAaBui4plTP+gbUgiEot7vGw/ZZop1yDZlgXXSuzjA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.4.tgz#2d50bd2b695f709797e1745644f6bc123e6e5f5a" + integrity sha512-q7xbVLrWcXvSBBEoadowIUJ7sRpS1yvgMWnzHJggFy5cUZBq2HZL5k/pBSm0GdYWS1vs5/EDwMjSKF55PDY4Aw== + +"@types/d3-format@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" + integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg== + +"@types/d3-geo@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.3.tgz#535e5f24be13722964c52354301be09b752f5d6e" + integrity sha512-bK9uZJS3vuDCNeeXQ4z3u0E7OeJZXjUgzFdSOtNtMCJCLvDtWDwfpRVWlyt3y8EvRzI0ccOu9xlMVirawolSCw== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b3a446b5437faededb30ac32b7cc0486559ab1e2" + integrity sha512-9hjRTVoZjRFR6xo8igAJyNXQyPX6Aq++Nhb5ebrUF414dv4jr2MitM2fWiOY475wa3Za7TOS2Gh9fmqEhLTt0A== + +"@types/d3-interpolate@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" + integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== + +"@types/d3-polygon@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93" + integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw== + +"@types/d3-quadtree@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5" + integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw== + +"@types/d3-random@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953" + integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ== + +"@types/d3-scale-chromatic@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" + integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw== + +"@types/d3-scale@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.3.tgz#7a5780e934e52b6f63ad9c24b105e33dd58102b5" + integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*", "@types/d3-selection@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.5.tgz#27cd53b7672d405025e2414d98532d7934c16ebd" + integrity sha512-xCB0z3Hi8eFIqyja3vW8iV01+OHGYR2di/+e+AiOcXIOrY82lcvWW8Ke1DYE/EUVMsBl4Db9RppSBS3X1U6J0w== + +"@types/d3-shape@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.1.tgz#15cc497751dac31192d7aef4e67a8d2c62354b95" + integrity sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946" + integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw== + +"@types/d3-time@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" + integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== + +"@types/d3-timer@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" + integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== + +"@types/d3-transition@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.3.tgz#d4ac37d08703fb039c87f92851a598ba77400402" + integrity sha512-/S90Od8Id1wgQNvIA8iFv9jRhCiZcGhPd2qX0bKF/PS+y0W5CrXKgIiELd2CvG1mlQrWK/qlYh3VxicqG1ZvgA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.2.tgz#067aa6a6ecbc75a78b753cc6f7a7f9f7e4e7d117" + integrity sha512-t09DDJVBI6AkM7N8kuPsnq/3d/ehtRKBN1xSiYjjMCgbiw6HM6Ged5VhvswmhprfKyGvzeTEL/4WBaK9llWvlA== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.0.tgz#fc5cac5b1756fc592a3cf1f3dc881bf08225f515" + integrity sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/dateformat@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-5.0.0.tgz#17ce64b0318f3f36d1c830c58a7a915445f1f93d" @@ -1383,6 +1678,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== +"@types/geojson@*": + version "7946.0.10" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== + "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -1391,7 +1691,7 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/json-schema@*", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== @@ -1413,6 +1713,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== +"@types/lodash@^4.14.194": + version "4.14.194" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" + integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -1768,6 +2073,11 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" @@ -1783,6 +2093,15 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +axios@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" + integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -1887,12 +2206,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -1947,6 +2271,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +classcat@^5.0.3, classcat@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.4.tgz#e12d1dfe6df6427f260f03b80dc63571a5107ba6" + integrity sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -2077,6 +2406,13 @@ colorette@^2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^2.16.0, commander@^2.20.0, commander@^2.20.3, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2206,6 +2542,68 @@ csstype@^3.0.11, csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-ease@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-interpolate@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +"d3-timer@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + date-fns@^2.29.1: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" @@ -2270,6 +2668,11 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + dependency-tree@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-9.0.0.tgz#9288dd6daf35f6510c1ea30d9894b75369aa50a2" @@ -2952,6 +3355,11 @@ focus-lock@^0.11.6: dependencies: tslib "^2.0.3" +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -2959,6 +3367,15 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + formik@^2.2.9: version "2.2.9" resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" @@ -2988,6 +3405,15 @@ framesync@6.1.2: dependencies: tslib "2.4.0" +fs-extra@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.0.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -3200,6 +3626,18 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3681,6 +4119,13 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-ref-parser@^9.0.9: + version "9.0.9" + resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#66ea538e7450b12af342fa3d5b8458bc1e1e013f" + integrity sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.9" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -3945,6 +4390,18 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4019,6 +4476,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4160,6 +4622,22 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-types@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.0.tgz#bd01acc937b73c9f6db2ac2031bf0231e21ebff0" + integrity sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA== + +openapi-typescript-codegen@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.23.0.tgz#702a651eefc536b27e87e4ad54a80a31d36487f0" + integrity sha512-gOJXy5g3H3HlLpVNN+USrNK2i2KYBmDczk9Xk34u6JorwrGiDJZUj+al4S+i9TXdfUQ/ZaLxE59Xf3wqkxGfqA== + dependencies: + camelcase "^6.3.0" + commander "^9.3.0" + fs-extra "^10.1.0" + handlebars "^4.7.7" + json-schema-ref-parser "^9.0.9" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -4449,6 +4927,11 @@ prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -4661,6 +5144,18 @@ react@^18.2.0: dependencies: loose-envify "^1.1.0" +reactflow@^11.7.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.7.0.tgz#821642ce9ce4a3a2fa6469053520cb032ff03ef4" + integrity sha512-bjfJV1iQZ+VwIlvsnd4TbXNs6kuJ5ONscud6fNkF8RY/oU2VUZpdjA3q1zwmgnjmJcIhxuBiBI5VLGajYx/Ozg== + dependencies: + "@reactflow/background" "11.2.0" + "@reactflow/controls" "11.1.11" + "@reactflow/core" "11.7.0" + "@reactflow/minimap" "11.5.0" + "@reactflow/node-resizer" "2.1.0" + "@reactflow/node-toolbar" "1.1.11" + readable-stream@^3.4.0: version "3.6.1" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62" @@ -4682,6 +5177,11 @@ redux-deep-persist@^1.0.7: resolved "https://registry.yarnpkg.com/redux-deep-persist/-/redux-deep-persist-1.0.7.tgz#fbbd00dcee6111b42624c9d8590b7e124b94476e" integrity sha512-PsD5UXbfCFvDruIPIHKAyaZ3wPhEWBMU8Rtcr/c1pXJT8aYoKbgKUS8JBkaWc3EB1ONlnLTdDDmnC/TOD39hqA== +redux-dynamic-middlewares@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/redux-dynamic-middlewares/-/redux-dynamic-middlewares-2.2.0.tgz#6835dd6d4f2fd975266376b45dcae0141320ae97" + integrity sha512-GHESQC+Y0PV98ZBoaC6br6cDOsNiM1Cu4UleGMqMWCXX03jIr3BoozYVrRkLVVAl4sC216chakMnZOu6SwNdGA== + redux-persist@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" @@ -5027,7 +5527,7 @@ source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.0, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -5377,15 +5877,20 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@4.9.5, typescript@^4.0.0, typescript@^4.5.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + typescript@^3.9.10, typescript@^3.9.5, typescript@^3.9.7: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== -typescript@^4.0.0, typescript@^4.5.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +uglify-js@^3.1.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== unbox-primitive@^1.0.2: version "1.0.2" @@ -5468,7 +5973,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.0.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -5597,6 +6102,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -5697,3 +6207,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.3.1: + version "4.3.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce" + integrity sha512-dY8ERwB9Nd21ellgkBZFhudER8KVlelZm8388B5nDAXhO/+FZDhYMuRnqDgu5SYyRgz/iaf8RKnbUs/cHfOGlQ== + dependencies: + use-sync-external-store "1.2.0" diff --git a/tests/nodes/test_graph_execution_state.py b/tests/nodes/test_graph_execution_state.py index f65129797e..2476786e41 100644 --- a/tests/nodes/test_graph_execution_state.py +++ b/tests/nodes/test_graph_execution_state.py @@ -27,6 +27,7 @@ def mock_services(): events = None, # type: ignore images = None, # type: ignore latents = None, # type: ignore + metadata = None, # type: ignore queue = MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=sqlite_memory, table_name="graphs" diff --git a/tests/nodes/test_invoker.py b/tests/nodes/test_invoker.py index 46d532b9f7..d187c1b171 100644 --- a/tests/nodes/test_invoker.py +++ b/tests/nodes/test_invoker.py @@ -25,6 +25,7 @@ def mock_services() -> InvocationServices: events = TestEventService(), images = None, # type: ignore latents = None, # type: ignore + metadata = None, # type: ignore queue = MemoryInvocationQueue(), graph_library=SqliteItemStorage[LibraryGraph]( filename=sqlite_memory, table_name="graphs" diff --git a/tests/nodes/test_nodes.py b/tests/nodes/test_nodes.py index d16d67d815..e334953d7e 100644 --- a/tests/nodes/test_nodes.py +++ b/tests/nodes/test_nodes.py @@ -49,7 +49,7 @@ class ImageTestInvocation(BaseInvocation): prompt: str = Field(default = "") def invoke(self, context: InvocationContext) -> ImageTestInvocationOutput: - return ImageTestInvocationOutput(image=ImageField(image_name=self.id)) + return ImageTestInvocationOutput(image=ImageField(image_name=self.id, width=512, height=512, mode="", info={})) class PromptCollectionTestInvocationOutput(BaseInvocationOutput): type: Literal['test_prompt_collection_output'] = 'test_prompt_collection_output' diff --git a/tests/nodes/test_png_metadata_service.py b/tests/nodes/test_png_metadata_service.py new file mode 100644 index 0000000000..3075af5a4b --- /dev/null +++ b/tests/nodes/test_png_metadata_service.py @@ -0,0 +1,55 @@ +import json +import os + +from PIL import Image, PngImagePlugin + +from invokeai.app.invocations.generate import TextToImageInvocation +from invokeai.app.services.metadata import PngMetadataService + +valid_metadata = { + "session_id": "1", + "node": { + "id": "1", + "type": "txt2img", + "prompt": "dog", + "seed": 178785523, + "steps": 30, + "width": 512, + "height": 512, + "cfg_scale": 7.5, + "scheduler": "k_lms", + "seamless": False, + "model": "stable-diffusion-1.5", + "progress_images": True, + }, +} + +metadata_service = PngMetadataService() + + +def test_can_load_and_parse_invokeai_metadata(tmp_path): + raw_metadata = {"session_id": "123", "node": {"id": "456", "type": "test_type"}} + + temp_image = Image.new("RGB", (512, 512)) + temp_image_path = os.path.join(tmp_path, "test.png") + + pnginfo = PngImagePlugin.PngInfo() + pnginfo.add_text("invokeai", json.dumps(raw_metadata)) + + temp_image.save(temp_image_path, pnginfo=pnginfo) + + image = Image.open(temp_image_path) + + loaded_metadata = metadata_service.get_metadata(image) + + assert loaded_metadata is not None + assert raw_metadata == loaded_metadata + + +def test_can_build_invokeai_metadata(): + session_id = valid_metadata["session_id"] + node = TextToImageInvocation(**valid_metadata["node"]) + + metadata = metadata_service.build_metadata(session_id=session_id, node=node) + + assert valid_metadata == metadata