Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

230 lines
8.2 KiB
Python

from collections.abc import Mapping
from copy import deepcopy
from typing import Any, get_args, get_origin
from pydantic import BaseModel
from pydantic_core import PydanticUndefined
from invokeai.app.invocations.baseinvocation import InvocationRegistry
from invokeai.app.invocations.call_saved_workflow import parse_call_saved_workflow_dynamic_input
from invokeai.app.invocations.fields import ImageField
from invokeai.app.services.session_processor.workflow_call_batch import build_child_workflow_sessions
from invokeai.app.services.session_queue.session_queue_common import TooManySessionsError
from invokeai.app.services.shared.graph import Graph, GraphExecutionState, WorkflowCallFrame
from invokeai.app.services.shared.workflow_call_compatibility_common import (
WorkflowCallCompatibility,
WorkflowCallCompatibilityReason,
)
from invokeai.app.services.shared.workflow_graph_builder import (
InvalidWorkflowInputError,
UnsupportedWorkflowNodeError,
get_exposed_workflow_input_names,
)
def _count_workflow_return_nodes(workflow: dict[str, Any]) -> int:
workflow_return_count = 0
for node in workflow.get("nodes", []):
if not isinstance(node, dict) or node.get("type") != "invocation":
continue
data = node.get("data")
if isinstance(data, dict) and data.get("type") == "workflow_return":
workflow_return_count += 1
return workflow_return_count
def _is_mapping(value: Any) -> bool:
return isinstance(value, Mapping)
def _build_placeholder_model(annotation: type[BaseModel]) -> Any:
values: dict[str, Any] = {}
for field_name, field_info in annotation.model_fields.items():
if field_info.default is not PydanticUndefined:
values[field_name] = deepcopy(field_info.default)
continue
if field_info.default_factory is not None:
values[field_name] = field_info.default_factory()
continue
placeholder = _get_placeholder_for_annotation(field_info.annotation)
if placeholder is None:
return None
values[field_name] = placeholder
return annotation.model_construct(**values)
def _get_placeholder_for_annotation(annotation: Any) -> Any:
origin = get_origin(annotation)
if origin is not None:
if origin is list:
return []
if origin is dict:
return {}
if origin is tuple:
return []
if origin is set:
return []
args = [arg for arg in get_args(annotation) if arg is not type(None)]
if args:
return _get_placeholder_for_annotation(args[0])
return None
if annotation is Any:
return {}
if annotation is str:
return ""
if annotation is int:
return 0
if annotation is float:
return 0.0
if annotation is bool:
return False
if annotation is ImageField:
return ImageField(image_name="compatibility-placeholder")
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
return _build_placeholder_model(annotation)
return None
def _build_compatibility_workflow_inputs(workflow: dict[str, Any]) -> dict[str, Any]:
workflow_inputs: dict[str, Any] = {}
workflow_nodes = workflow.get("nodes", [])
if not isinstance(workflow_nodes, list):
return workflow_inputs
nodes_by_id = {
node.get("id"): node
for node in workflow_nodes
if _is_mapping(node) and isinstance(node.get("id"), str) and _is_mapping(node.get("data"))
}
for input_name in get_exposed_workflow_input_names(workflow):
node_id, field_name = parse_call_saved_workflow_dynamic_input(input_name)
node = nodes_by_id.get(node_id)
if not _is_mapping(node):
continue
node_data = node.get("data")
if not _is_mapping(node_data):
continue
node_type = node_data.get("type")
if not isinstance(node_type, str):
continue
invocation_class = InvocationRegistry.get_invocation_for_type(node_type)
if invocation_class is None:
continue
field_info = invocation_class.model_fields.get(field_name)
if field_info is None:
continue
if field_info.default is not PydanticUndefined:
workflow_inputs[input_name] = deepcopy(field_info.default)
continue
if field_info.default_factory is not None:
workflow_inputs[input_name] = field_info.default_factory()
continue
placeholder = _get_placeholder_for_annotation(field_info.annotation)
if placeholder is not None:
workflow_inputs[input_name] = placeholder
return workflow_inputs
def _is_unsupported_batch_input_message(message: str) -> bool:
return any(
marker in message
for marker in (
"batch child workflow",
"batch group",
"batch input",
"batch inputs",
"batch node",
"batch-special child workflow nodes",
"connected batch",
"generator-backed batch",
)
)
def get_workflow_call_compatibility(
*,
workflow: dict[str, Any],
workflow_id: str,
services: Any,
user_id: str | None,
maximum_children: int,
resolve_generator_items: bool = True,
) -> WorkflowCallCompatibility:
workflow_return_count = _count_workflow_return_nodes(workflow)
if workflow_return_count == 0:
return WorkflowCallCompatibility(
is_callable=False,
reason=WorkflowCallCompatibilityReason.MissingWorkflowReturn,
message="The workflow must contain exactly one workflow_return node.",
)
if workflow_return_count > 1:
return WorkflowCallCompatibility(
is_callable=False,
reason=WorkflowCallCompatibilityReason.MultipleWorkflowReturn,
message="The workflow must not contain more than one workflow_return node.",
)
try:
workflow_inputs = _build_compatibility_workflow_inputs(workflow)
build_child_workflow_sessions(
parent_session=GraphExecutionState(graph=Graph()),
workflow=workflow,
workflow_inputs=workflow_inputs,
call_frame=WorkflowCallFrame(
prepared_call_node_id="compatibility-call",
source_call_node_id="compatibility-call",
workflow_id=workflow_id,
depth=1,
),
maximum_children=maximum_children,
services=services,
user_id=user_id,
resolve_generator_items=resolve_generator_items,
)
except InvalidWorkflowInputError as e:
return WorkflowCallCompatibility(
is_callable=False,
reason=WorkflowCallCompatibilityReason.InvalidInputs,
message=str(e),
)
except UnsupportedWorkflowNodeError as e:
message = str(e)
reason = WorkflowCallCompatibilityReason.UnsupportedNode
if _is_unsupported_batch_input_message(message):
reason = WorkflowCallCompatibilityReason.UnsupportedBatchInput
elif "exactly one workflow_return" in message and workflow_return_count == 0:
reason = WorkflowCallCompatibilityReason.MissingWorkflowReturn
elif "exactly one workflow_return" in message:
reason = WorkflowCallCompatibilityReason.InvalidGraph
return WorkflowCallCompatibility(
is_callable=False,
reason=reason,
message=message,
)
except TooManySessionsError as e:
return WorkflowCallCompatibility(
is_callable=False,
reason=WorkflowCallCompatibilityReason.ExceedsCapacity,
message=str(e),
)
except ValueError as e:
return WorkflowCallCompatibility(
is_callable=False,
reason=WorkflowCallCompatibilityReason.InvalidGraph,
message=str(e),
)
except Exception as e:
return WorkflowCallCompatibility(
is_callable=False,
reason=WorkflowCallCompatibilityReason.Unknown,
message=str(e),
)
return WorkflowCallCompatibility(
is_callable=True,
reason=WorkflowCallCompatibilityReason.Ok,
)