diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py index 3314d72620e..dd7b2c87b0a 100644 --- a/invokeai/app/invocations/infill.py +++ b/invokeai/app/invocations/infill.py @@ -127,13 +127,16 @@ def infill(self, image: Image.Image): return infilled +LAMA_MODEL_URL = "https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt" + + @invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2") class LaMaInfillInvocation(InfillImageProcessorInvocation): """Infills transparent areas of an image using the LaMa model""" def infill(self, image: Image.Image): with self._context.models.load_remote_model( - source="https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt", + source=LAMA_MODEL_URL, loader=LaMA.load_jit_model, ) as model: lama = LaMA(model) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 3ef28452ceb..71658b5276f 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1809,7 +1809,7 @@ "cannotPublish": "Cannot publish workflow", "publishWarnings": "Warnings", "errorWorkflowHasUnsavedChanges": "Workflow has unsaved changes", - "errorWorkflowHasBatchOrGeneratorNodes": "Workflow has batch and/or generator nodes", + "errorWorkflowHasUnpublishableNodes": "Workflow has batch, generator, or metadata extraction nodes", "errorWorkflowHasInvalidGraph": "Workflow graph invalid (hover Invoke button for details)", "errorWorkflowHasNoOutputNode": "No output node selected", "warningWorkflowHasNoPublishableInputFields": "No publishable input fields selected - published workflow will run with only default values", diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx index 0b9fdb57ffe..73f24b73512 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx @@ -26,6 +26,7 @@ import { $isSelectingOutputNode, $outputNodeId, $validationRunData, + selectHasUnpublishableNodes, usePublishInputs, } from 'features/nodes/components/sidePanel/workflow/publish'; import { useInputFieldTemplateTitleOrThrow } from 'features/nodes/hooks/useInputFieldTemplateTitleOrThrow'; @@ -36,7 +37,6 @@ import { useNodeUserTitleOrThrow } from 'features/nodes/hooks/useNodeUserTitleOr import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames'; import { useOutputFieldTemplate } from 'features/nodes/hooks/useOutputFieldTemplate'; import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode'; -import { selectHasBatchOrGeneratorNodes } from 'features/nodes/store/selectors'; import { useEnqueueWorkflows } from 'features/queue/hooks/useEnqueueWorkflows'; import { $isReadyToEnqueue } from 'features/queue/store/readiness'; import { selectAllowPublishWorkflows } from 'features/system/store/configSlice'; @@ -201,7 +201,7 @@ const PublishWorkflowButton = memo(() => { const isReadyToDoValidationRun = useStore($isReadyToDoValidationRun); const isReadyToEnqueue = useStore($isReadyToEnqueue); const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges(); - const hasBatchOrGeneratorNodes = useAppSelector(selectHasBatchOrGeneratorNodes); + const hasUnpublishableNodes = useAppSelector(selectHasUnpublishableNodes); const outputNodeId = useStore($outputNodeId); const isSelectingOutputNode = useStore($isSelectingOutputNode); const inputs = usePublishInputs(); @@ -249,7 +249,7 @@ const PublishWorkflowButton = memo(() => { return ( 0} @@ -261,7 +261,7 @@ const PublishWorkflowButton = memo(() => { !allowPublishWorkflows || !isReadyToEnqueue || doesWorkflowHaveUnsavedChanges || - hasBatchOrGeneratorNodes || + hasUnpublishableNodes || !isReadyToDoValidationRun || !(outputNodeId !== null && !isSelectingOutputNode) } @@ -330,7 +330,7 @@ export const StartPublishFlowButton = memo(() => { const allowPublishWorkflows = useAppSelector(selectAllowPublishWorkflows); const isReadyToEnqueue = useStore($isReadyToEnqueue); const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges(); - const hasBatchOrGeneratorNodes = useAppSelector(selectHasBatchOrGeneratorNodes); + const hasUnpublishableNodes = useAppSelector(selectHasUnpublishableNodes); const inputs = usePublishInputs(); const onClick = useCallback(() => { @@ -340,7 +340,7 @@ export const StartPublishFlowButton = memo(() => { return ( 0} @@ -352,7 +352,7 @@ export const StartPublishFlowButton = memo(() => { variant="ghost" size="sm" isDisabled={ - !allowPublishWorkflows || !isReadyToEnqueue || doesWorkflowHaveUnsavedChanges || hasBatchOrGeneratorNodes + !allowPublishWorkflows || !isReadyToEnqueue || doesWorkflowHaveUnsavedChanges || hasUnpublishableNodes } > {t('workflows.builder.publish')} @@ -366,7 +366,7 @@ StartPublishFlowButton.displayName = 'StartPublishFlowButton'; const PublishTooltip = memo( ({ isWorkflowSaved, - hasBatchOrGeneratorNodes, + hasUnpublishableNodes, isReadyToEnqueue, hasOutputNode, hasPublishableInputs, @@ -374,7 +374,7 @@ const PublishTooltip = memo( children, }: PropsWithChildren<{ isWorkflowSaved: boolean; - hasBatchOrGeneratorNodes: boolean; + hasUnpublishableNodes: boolean; isReadyToEnqueue: boolean; hasOutputNode: boolean; hasPublishableInputs: boolean; @@ -396,8 +396,8 @@ const PublishTooltip = memo( if (!isWorkflowSaved) { _errors.push(t('workflows.builder.errorWorkflowHasUnsavedChanges')); } - if (hasBatchOrGeneratorNodes) { - _errors.push(t('workflows.builder.errorWorkflowHasBatchOrGeneratorNodes')); + if (hasUnpublishableNodes) { + _errors.push(t('workflows.builder.errorWorkflowHasUnpublishableNodes')); } if (!isReadyToEnqueue) { _errors.push(t('workflows.builder.errorWorkflowHasInvalidGraph')); @@ -406,7 +406,7 @@ const PublishTooltip = memo( _errors.push(t('workflows.builder.errorWorkflowHasNoOutputNode')); } return _errors; - }, [hasBatchOrGeneratorNodes, hasOutputNode, isReadyToEnqueue, isWorkflowSaved, t]); + }, [hasUnpublishableNodes, hasOutputNode, isReadyToEnqueue, isWorkflowSaved, t]); if (errors.length === 0 && warnings.length === 0) { return children; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/publish.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/publish.ts index 1706a02bf79..2c3d9695417 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/publish.ts +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/publish.ts @@ -4,6 +4,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; import { $templates } from 'features/nodes/store/nodesSlice'; import { + selectNodes, selectNodesSlice, selectWorkflowFormNodeFieldFieldIdentifiersDeduped, selectWorkflowId, @@ -11,7 +12,7 @@ import { import type { Templates } from 'features/nodes/store/types'; import type { FieldIdentifier } from 'features/nodes/types/field'; import { isBoardFieldType } from 'features/nodes/types/field'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { isBatchNode, isGeneratorNode, isInvocationNode } from 'features/nodes/types/invocation'; import { atom, computed } from 'nanostores'; import { useMemo } from 'react'; import { useGetBatchStatusQuery } from 'services/api/endpoints/queue'; @@ -108,3 +109,31 @@ export const useIsWorkflowPublished = () => { return isPublished; }; + +// These nodes are not allowed to be in published workflows because they dynamically generate model identifiers +const NODE_TYPE_PUBLISH_DENYLIST = [ + 'metadata_to_model', + 'metadata_to_sdxl_model', + 'metadata_to_vae', + 'metadata_to_lora_collection', + 'metadata_to_loras', + 'metadata_to_sdlx_loras', + 'metadata_to_controlnets', + 'metadata_to_ip_adapters', + 'metadata_to_t2i_adapters', +]; + +export const selectHasUnpublishableNodes = createSelector(selectNodes, (nodes) => { + for (const node of nodes) { + if (!isInvocationNode(node)) { + return true; + } + if (isBatchNode(node) || isGeneratorNode(node)) { + return true; + } + if (NODE_TYPE_PUBLISH_DENYLIST.includes(node.data.type)) { + return true; + } + } + return false; +}); diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts index cf835210f16..271c95606de 100644 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -5,7 +5,7 @@ import { getElement } from 'features/nodes/components/sidePanel/builder/form-man import type { NodesState } from 'features/nodes/store/types'; import type { FieldInputInstance } from 'features/nodes/types/field'; import type { AnyNode, InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation'; -import { isBatchNode, isGeneratorNode, isInvocationNode } from 'features/nodes/types/invocation'; +import { isInvocationNode } from 'features/nodes/types/invocation'; import { isContainerElement, isNodeFieldElement } from 'features/nodes/types/workflow'; import { uniqBy } from 'lodash-es'; import { assert } from 'tsafe'; @@ -85,10 +85,6 @@ export const selectMayRedo = createSelector( (nodes) => nodes.future.length > 0 ); -export const selectHasBatchOrGeneratorNodes = createSelector(selectNodes, (nodes) => - nodes.filter(isInvocationNode).some((node) => isBatchNode(node) || isGeneratorNode(node)) -); - export const selectWorkflowName = createNodesSelector((nodes) => nodes.name); export const selectWorkflowId = createNodesSelector((workflow) => workflow.id);