InvokeAI 6.9.0rc3__py3-none-any.whl → 6.10.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invokeai/app/api/dependencies.py +2 -0
- invokeai/app/api/routers/model_manager.py +91 -2
- invokeai/app/api/routers/workflows.py +9 -0
- invokeai/app/invocations/fields.py +19 -0
- invokeai/app/invocations/image_to_latents.py +23 -5
- invokeai/app/invocations/latents_to_image.py +2 -25
- invokeai/app/invocations/metadata.py +9 -1
- invokeai/app/invocations/model.py +8 -0
- invokeai/app/invocations/primitives.py +12 -0
- invokeai/app/invocations/prompt_template.py +57 -0
- invokeai/app/invocations/z_image_control.py +112 -0
- invokeai/app/invocations/z_image_denoise.py +610 -0
- invokeai/app/invocations/z_image_image_to_latents.py +102 -0
- invokeai/app/invocations/z_image_latents_to_image.py +103 -0
- invokeai/app/invocations/z_image_lora_loader.py +153 -0
- invokeai/app/invocations/z_image_model_loader.py +135 -0
- invokeai/app/invocations/z_image_text_encoder.py +197 -0
- invokeai/app/services/model_install/model_install_common.py +14 -1
- invokeai/app/services/model_install/model_install_default.py +119 -19
- invokeai/app/services/model_records/model_records_base.py +12 -0
- invokeai/app/services/model_records/model_records_sql.py +17 -0
- invokeai/app/services/shared/graph.py +132 -77
- invokeai/app/services/workflow_records/workflow_records_base.py +8 -0
- invokeai/app/services/workflow_records/workflow_records_sqlite.py +42 -0
- invokeai/app/util/step_callback.py +3 -0
- invokeai/backend/model_manager/configs/controlnet.py +47 -1
- invokeai/backend/model_manager/configs/factory.py +26 -1
- invokeai/backend/model_manager/configs/lora.py +43 -1
- invokeai/backend/model_manager/configs/main.py +113 -0
- invokeai/backend/model_manager/configs/qwen3_encoder.py +156 -0
- invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_diffusers_rms_norm.py +40 -0
- invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_layer_norm.py +25 -0
- invokeai/backend/model_manager/load/model_cache/torch_module_autocast/torch_module_autocast.py +11 -2
- invokeai/backend/model_manager/load/model_loaders/lora.py +11 -0
- invokeai/backend/model_manager/load/model_loaders/z_image.py +935 -0
- invokeai/backend/model_manager/load/model_util.py +6 -1
- invokeai/backend/model_manager/metadata/metadata_base.py +12 -5
- invokeai/backend/model_manager/model_on_disk.py +3 -0
- invokeai/backend/model_manager/starter_models.py +70 -0
- invokeai/backend/model_manager/taxonomy.py +5 -0
- invokeai/backend/model_manager/util/select_hf_files.py +23 -8
- invokeai/backend/patches/layer_patcher.py +34 -16
- invokeai/backend/patches/layers/lora_layer_base.py +2 -1
- invokeai/backend/patches/lora_conversions/flux_aitoolkit_lora_conversion_utils.py +17 -2
- invokeai/backend/patches/lora_conversions/flux_xlabs_lora_conversion_utils.py +92 -0
- invokeai/backend/patches/lora_conversions/formats.py +5 -0
- invokeai/backend/patches/lora_conversions/z_image_lora_constants.py +8 -0
- invokeai/backend/patches/lora_conversions/z_image_lora_conversion_utils.py +155 -0
- invokeai/backend/quantization/gguf/ggml_tensor.py +27 -4
- invokeai/backend/quantization/gguf/loaders.py +47 -12
- invokeai/backend/stable_diffusion/diffusion/conditioning_data.py +13 -0
- invokeai/backend/util/devices.py +25 -0
- invokeai/backend/util/hotfixes.py +2 -2
- invokeai/backend/z_image/__init__.py +16 -0
- invokeai/backend/z_image/extensions/__init__.py +1 -0
- invokeai/backend/z_image/extensions/regional_prompting_extension.py +207 -0
- invokeai/backend/z_image/text_conditioning.py +74 -0
- invokeai/backend/z_image/z_image_control_adapter.py +238 -0
- invokeai/backend/z_image/z_image_control_transformer.py +643 -0
- invokeai/backend/z_image/z_image_controlnet_extension.py +531 -0
- invokeai/backend/z_image/z_image_patchify_utils.py +135 -0
- invokeai/backend/z_image/z_image_transformer_patch.py +234 -0
- invokeai/frontend/web/dist/assets/App-CYhlZO3Q.js +161 -0
- invokeai/frontend/web/dist/assets/{browser-ponyfill-CN1j0ARZ.js → browser-ponyfill-DHZxq1nk.js} +1 -1
- invokeai/frontend/web/dist/assets/index-dgSJAY--.js +530 -0
- invokeai/frontend/web/dist/index.html +1 -1
- invokeai/frontend/web/dist/locales/de.json +24 -6
- invokeai/frontend/web/dist/locales/en.json +70 -1
- invokeai/frontend/web/dist/locales/es.json +0 -5
- invokeai/frontend/web/dist/locales/fr.json +0 -6
- invokeai/frontend/web/dist/locales/it.json +17 -64
- invokeai/frontend/web/dist/locales/ja.json +379 -44
- invokeai/frontend/web/dist/locales/ru.json +0 -6
- invokeai/frontend/web/dist/locales/vi.json +7 -54
- invokeai/frontend/web/dist/locales/zh-CN.json +0 -6
- invokeai/version/invokeai_version.py +1 -1
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/METADATA +3 -3
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/RECORD +84 -60
- invokeai/frontend/web/dist/assets/App-Cn9UyjoV.js +0 -161
- invokeai/frontend/web/dist/assets/index-BDrf9CL-.js +0 -530
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/WHEEL +0 -0
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/entry_points.txt +0 -0
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/licenses/LICENSE +0 -0
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/licenses/LICENSE-SD1+SD2.txt +0 -0
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/licenses/LICENSE-SDXL.txt +0 -0
- {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import itertools
|
|
5
|
-
from
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Any, Deque, Iterable, Optional, Type, TypeVar, Union, get_args, get_origin
|
|
6
7
|
|
|
7
8
|
import networkx as nx
|
|
8
9
|
from pydantic import (
|
|
@@ -10,6 +11,7 @@ from pydantic import (
|
|
|
10
11
|
ConfigDict,
|
|
11
12
|
GetCoreSchemaHandler,
|
|
12
13
|
GetJsonSchemaHandler,
|
|
14
|
+
PrivateAttr,
|
|
13
15
|
ValidationError,
|
|
14
16
|
field_validator,
|
|
15
17
|
)
|
|
@@ -33,6 +35,10 @@ from invokeai.app.util.misc import uuid_string
|
|
|
33
35
|
# in 3.10 this would be "from types import NoneType"
|
|
34
36
|
NoneType = type(None)
|
|
35
37
|
|
|
38
|
+
# Port name constants
|
|
39
|
+
ITEM_FIELD = "item"
|
|
40
|
+
COLLECTION_FIELD = "collection"
|
|
41
|
+
|
|
36
42
|
|
|
37
43
|
class EdgeConnection(BaseModel):
|
|
38
44
|
node_id: str = Field(description="The id of the node for this edge connection")
|
|
@@ -395,7 +401,7 @@ class Graph(BaseModel):
|
|
|
395
401
|
|
|
396
402
|
try:
|
|
397
403
|
self.edges.remove(edge)
|
|
398
|
-
except
|
|
404
|
+
except ValueError:
|
|
399
405
|
pass
|
|
400
406
|
|
|
401
407
|
def validate_self(self) -> None:
|
|
@@ -414,7 +420,8 @@ class Graph(BaseModel):
|
|
|
414
420
|
|
|
415
421
|
# Validate that all node ids are unique
|
|
416
422
|
node_ids = [n.id for n in self.nodes.values()]
|
|
417
|
-
|
|
423
|
+
seen = set()
|
|
424
|
+
duplicate_node_ids = {nid for nid in node_ids if (nid in seen) or seen.add(nid)}
|
|
418
425
|
if duplicate_node_ids:
|
|
419
426
|
raise DuplicateNodeIdError(f"Node ids must be unique, found duplicates {duplicate_node_ids}")
|
|
420
427
|
|
|
@@ -529,19 +536,19 @@ class Graph(BaseModel):
|
|
|
529
536
|
raise InvalidEdgeError(f"Field types are incompatible ({edge})")
|
|
530
537
|
|
|
531
538
|
# Validate if iterator output type matches iterator input type (if this edge results in both being set)
|
|
532
|
-
if isinstance(to_node, IterateInvocation) and edge.destination.field ==
|
|
539
|
+
if isinstance(to_node, IterateInvocation) and edge.destination.field == COLLECTION_FIELD:
|
|
533
540
|
err = self._is_iterator_connection_valid(edge.destination.node_id, new_input=edge.source)
|
|
534
541
|
if err is not None:
|
|
535
542
|
raise InvalidEdgeError(f"Iterator input type does not match iterator output type ({edge}): {err}")
|
|
536
543
|
|
|
537
544
|
# Validate if iterator input type matches output type (if this edge results in both being set)
|
|
538
|
-
if isinstance(from_node, IterateInvocation) and edge.source.field ==
|
|
545
|
+
if isinstance(from_node, IterateInvocation) and edge.source.field == ITEM_FIELD:
|
|
539
546
|
err = self._is_iterator_connection_valid(edge.source.node_id, new_output=edge.destination)
|
|
540
547
|
if err is not None:
|
|
541
548
|
raise InvalidEdgeError(f"Iterator output type does not match iterator input type ({edge}): {err}")
|
|
542
549
|
|
|
543
550
|
# Validate if collector input type matches output type (if this edge results in both being set)
|
|
544
|
-
if isinstance(to_node, CollectInvocation) and edge.destination.field ==
|
|
551
|
+
if isinstance(to_node, CollectInvocation) and edge.destination.field == ITEM_FIELD:
|
|
545
552
|
err = self._is_collector_connection_valid(edge.destination.node_id, new_input=edge.source)
|
|
546
553
|
if err is not None:
|
|
547
554
|
raise InvalidEdgeError(f"Collector output type does not match collector input type ({edge}): {err}")
|
|
@@ -549,7 +556,7 @@ class Graph(BaseModel):
|
|
|
549
556
|
# Validate if collector output type matches input type (if this edge results in both being set) - skip if the destination field is not Any or list[Any]
|
|
550
557
|
if (
|
|
551
558
|
isinstance(from_node, CollectInvocation)
|
|
552
|
-
and edge.source.field ==
|
|
559
|
+
and edge.source.field == COLLECTION_FIELD
|
|
553
560
|
and not self._is_destination_field_list_of_Any(edge)
|
|
554
561
|
and not self._is_destination_field_Any(edge)
|
|
555
562
|
):
|
|
@@ -639,8 +646,8 @@ class Graph(BaseModel):
|
|
|
639
646
|
new_input: Optional[EdgeConnection] = None,
|
|
640
647
|
new_output: Optional[EdgeConnection] = None,
|
|
641
648
|
) -> str | None:
|
|
642
|
-
inputs = [e.source for e in self._get_input_edges(node_id,
|
|
643
|
-
outputs = [e.destination for e in self._get_output_edges(node_id,
|
|
649
|
+
inputs = [e.source for e in self._get_input_edges(node_id, COLLECTION_FIELD)]
|
|
650
|
+
outputs = [e.destination for e in self._get_output_edges(node_id, ITEM_FIELD)]
|
|
644
651
|
|
|
645
652
|
if new_input is not None:
|
|
646
653
|
inputs.append(new_input)
|
|
@@ -670,7 +677,7 @@ class Graph(BaseModel):
|
|
|
670
677
|
if isinstance(input_node, CollectInvocation):
|
|
671
678
|
# Traverse the graph to find the first collector input edge. Collectors validate that their collection
|
|
672
679
|
# inputs are all of the same type, so we can use the first input edge to determine the collector's type
|
|
673
|
-
first_collector_input_edge = self._get_input_edges(input_node.id,
|
|
680
|
+
first_collector_input_edge = self._get_input_edges(input_node.id, ITEM_FIELD)[0]
|
|
674
681
|
first_collector_input_type = get_output_field_type(
|
|
675
682
|
self.get_node(first_collector_input_edge.source.node_id), first_collector_input_edge.source.field
|
|
676
683
|
)
|
|
@@ -690,8 +697,8 @@ class Graph(BaseModel):
|
|
|
690
697
|
new_input: Optional[EdgeConnection] = None,
|
|
691
698
|
new_output: Optional[EdgeConnection] = None,
|
|
692
699
|
) -> str | None:
|
|
693
|
-
inputs = [e.source for e in self._get_input_edges(node_id,
|
|
694
|
-
outputs = [e.destination for e in self._get_output_edges(node_id,
|
|
700
|
+
inputs = [e.source for e in self._get_input_edges(node_id, ITEM_FIELD)]
|
|
701
|
+
outputs = [e.destination for e in self._get_output_edges(node_id, COLLECTION_FIELD)]
|
|
695
702
|
|
|
696
703
|
if new_input is not None:
|
|
697
704
|
inputs.append(new_input)
|
|
@@ -761,7 +768,7 @@ class Graph(BaseModel):
|
|
|
761
768
|
# TODO: figure out if iteration nodes need to be expanded
|
|
762
769
|
|
|
763
770
|
unique_edges = {(e.source.node_id, e.destination.node_id) for e in self.edges}
|
|
764
|
-
g.add_edges_from(
|
|
771
|
+
g.add_edges_from(unique_edges)
|
|
765
772
|
return g
|
|
766
773
|
|
|
767
774
|
|
|
@@ -802,6 +809,41 @@ class GraphExecutionState(BaseModel):
|
|
|
802
809
|
description="The map of original graph nodes to prepared nodes",
|
|
803
810
|
default_factory=dict,
|
|
804
811
|
)
|
|
812
|
+
# Ready queues grouped by node class name (internal only)
|
|
813
|
+
_ready_queues: dict[str, Deque[str]] = PrivateAttr(default_factory=dict)
|
|
814
|
+
# Current class being drained; stays until its queue empties
|
|
815
|
+
_active_class: Optional[str] = PrivateAttr(default=None)
|
|
816
|
+
# Optional priority; others follow in name order
|
|
817
|
+
ready_order: list[str] = Field(default_factory=list)
|
|
818
|
+
indegree: dict[str, int] = Field(default_factory=dict, description="Remaining unmet input count for exec nodes")
|
|
819
|
+
|
|
820
|
+
def _type_key(self, node_obj: BaseInvocation) -> str:
|
|
821
|
+
return node_obj.__class__.__name__
|
|
822
|
+
|
|
823
|
+
def _queue_for(self, cls_name: str) -> Deque[str]:
|
|
824
|
+
q = self._ready_queues.get(cls_name)
|
|
825
|
+
if q is None:
|
|
826
|
+
q = deque()
|
|
827
|
+
self._ready_queues[cls_name] = q
|
|
828
|
+
return q
|
|
829
|
+
|
|
830
|
+
def set_ready_order(self, order: Iterable[Type[BaseInvocation] | str]) -> None:
|
|
831
|
+
names: list[str] = []
|
|
832
|
+
for x in order:
|
|
833
|
+
names.append(x.__name__ if hasattr(x, "__name__") else str(x))
|
|
834
|
+
self.ready_order = names
|
|
835
|
+
|
|
836
|
+
def _enqueue_if_ready(self, nid: str) -> None:
|
|
837
|
+
"""Push nid to its class queue if unmet inputs == 0."""
|
|
838
|
+
# Invariants: exec node exists and has an indegree entry
|
|
839
|
+
if nid not in self.execution_graph.nodes:
|
|
840
|
+
raise KeyError(f"exec node {nid} missing from execution_graph")
|
|
841
|
+
if nid not in self.indegree:
|
|
842
|
+
raise KeyError(f"indegree missing for exec node {nid}")
|
|
843
|
+
if self.indegree[nid] != 0 or nid in self.executed:
|
|
844
|
+
return
|
|
845
|
+
node_obj = self.execution_graph.nodes[nid]
|
|
846
|
+
self._queue_for(self._type_key(node_obj)).append(nid)
|
|
805
847
|
|
|
806
848
|
model_config = ConfigDict(
|
|
807
849
|
json_schema_extra={
|
|
@@ -834,12 +876,14 @@ class GraphExecutionState(BaseModel):
|
|
|
834
876
|
# If there are no prepared nodes, prepare some nodes
|
|
835
877
|
next_node = self._get_next_node()
|
|
836
878
|
if next_node is None:
|
|
837
|
-
|
|
879
|
+
base_g = self.graph.nx_graph_flat()
|
|
880
|
+
prepared_id = self._prepare(base_g)
|
|
838
881
|
|
|
839
882
|
# Prepare as many nodes as we can
|
|
840
883
|
while prepared_id is not None:
|
|
841
|
-
prepared_id = self._prepare()
|
|
842
|
-
next_node
|
|
884
|
+
prepared_id = self._prepare(base_g)
|
|
885
|
+
if next_node is None:
|
|
886
|
+
next_node = self._get_next_node()
|
|
843
887
|
|
|
844
888
|
# Get values from edges
|
|
845
889
|
if next_node is not None:
|
|
@@ -869,6 +913,18 @@ class GraphExecutionState(BaseModel):
|
|
|
869
913
|
self.executed.add(source_node)
|
|
870
914
|
self.executed_history.append(source_node)
|
|
871
915
|
|
|
916
|
+
# Decrement children indegree and enqueue when ready
|
|
917
|
+
for e in self.execution_graph._get_output_edges(node_id):
|
|
918
|
+
child = e.destination.node_id
|
|
919
|
+
if child not in self.indegree:
|
|
920
|
+
raise KeyError(f"indegree missing for exec node {child}")
|
|
921
|
+
# Only decrement if there's something to satisfy
|
|
922
|
+
if self.indegree[child] == 0:
|
|
923
|
+
raise RuntimeError(f"indegree underflow for {child} from parent {node_id}")
|
|
924
|
+
self.indegree[child] -= 1
|
|
925
|
+
if self.indegree[child] == 0:
|
|
926
|
+
self._enqueue_if_ready(child)
|
|
927
|
+
|
|
872
928
|
def set_node_error(self, node_id: str, error: str):
|
|
873
929
|
"""Marks a node as errored"""
|
|
874
930
|
self.errors[node_id] = error
|
|
@@ -892,7 +948,7 @@ class GraphExecutionState(BaseModel):
|
|
|
892
948
|
# If this is an iterator node, we must create a copy for each iteration
|
|
893
949
|
if isinstance(node, IterateInvocation):
|
|
894
950
|
# Get input collection edge (should error if there are no inputs)
|
|
895
|
-
input_collection_edge = next(iter(self.graph._get_input_edges(node_id,
|
|
951
|
+
input_collection_edge = next(iter(self.graph._get_input_edges(node_id, COLLECTION_FIELD)))
|
|
896
952
|
input_collection_prepared_node_id = next(
|
|
897
953
|
n[1] for n in iteration_node_map if n[0] == input_collection_edge.source.node_id
|
|
898
954
|
)
|
|
@@ -922,7 +978,7 @@ class GraphExecutionState(BaseModel):
|
|
|
922
978
|
# Create a new node (or one for each iteration of this iterator)
|
|
923
979
|
for i in range(self_iteration_count) if self_iteration_count > 0 else [-1]:
|
|
924
980
|
# Create a new node
|
|
925
|
-
new_node =
|
|
981
|
+
new_node = node.model_copy(deep=True)
|
|
926
982
|
|
|
927
983
|
# Create the node id (use a random uuid)
|
|
928
984
|
new_node.id = uuid_string()
|
|
@@ -946,53 +1002,55 @@ class GraphExecutionState(BaseModel):
|
|
|
946
1002
|
)
|
|
947
1003
|
self.execution_graph.add_edge(new_edge)
|
|
948
1004
|
|
|
1005
|
+
# Initialize indegree as unmet inputs only and enqueue if ready
|
|
1006
|
+
inputs = self.execution_graph._get_input_edges(new_node.id)
|
|
1007
|
+
unmet = sum(1 for e in inputs if e.source.node_id not in self.executed)
|
|
1008
|
+
self.indegree[new_node.id] = unmet
|
|
1009
|
+
self._enqueue_if_ready(new_node.id)
|
|
1010
|
+
|
|
949
1011
|
new_nodes.append(new_node.id)
|
|
950
1012
|
|
|
951
1013
|
return new_nodes
|
|
952
1014
|
|
|
953
|
-
def _iterator_graph(self) -> nx.DiGraph:
|
|
1015
|
+
def _iterator_graph(self, base: Optional[nx.DiGraph] = None) -> nx.DiGraph:
|
|
954
1016
|
"""Gets a DiGraph with edges to collectors removed so an ancestor search produces all active iterators for any node"""
|
|
955
|
-
g = self.graph.nx_graph_flat()
|
|
1017
|
+
g = base.copy() if base is not None else self.graph.nx_graph_flat()
|
|
956
1018
|
collectors = (n for n in self.graph.nodes if isinstance(self.graph.get_node(n), CollectInvocation))
|
|
957
1019
|
for c in collectors:
|
|
958
1020
|
g.remove_edges_from(list(g.in_edges(c)))
|
|
959
1021
|
return g
|
|
960
1022
|
|
|
961
|
-
def _get_node_iterators(self, node_id: str) -> list[str]:
|
|
1023
|
+
def _get_node_iterators(self, node_id: str, it_graph: Optional[nx.DiGraph] = None) -> list[str]:
|
|
962
1024
|
"""Gets iterators for a node"""
|
|
963
|
-
g = self._iterator_graph()
|
|
964
|
-
|
|
965
|
-
return iterators
|
|
1025
|
+
g = it_graph or self._iterator_graph()
|
|
1026
|
+
return [n for n in nx.ancestors(g, node_id) if isinstance(self.graph.get_node(n), IterateInvocation)]
|
|
966
1027
|
|
|
967
|
-
def _prepare(self) -> Optional[str]:
|
|
1028
|
+
def _prepare(self, base_g: Optional[nx.DiGraph] = None) -> Optional[str]:
|
|
968
1029
|
# Get flattened source graph
|
|
969
|
-
g = self.graph.nx_graph_flat()
|
|
1030
|
+
g = base_g or self.graph.nx_graph_flat()
|
|
970
1031
|
|
|
971
1032
|
# Find next node that:
|
|
972
1033
|
# - was not already prepared
|
|
973
1034
|
# - is not an iterate node whose inputs have not been executed
|
|
974
1035
|
# - does not have an unexecuted iterate ancestor
|
|
975
1036
|
sorted_nodes = nx.topological_sort(g)
|
|
1037
|
+
|
|
1038
|
+
def unprepared(n: str) -> bool:
|
|
1039
|
+
return n not in self.source_prepared_mapping
|
|
1040
|
+
|
|
1041
|
+
def iter_inputs_ready(n: str) -> bool:
|
|
1042
|
+
if not isinstance(self.graph.get_node(n), IterateInvocation):
|
|
1043
|
+
return True
|
|
1044
|
+
return all(u in self.executed for u, _ in g.in_edges(n))
|
|
1045
|
+
|
|
1046
|
+
def no_unexecuted_iter_ancestors(n: str) -> bool:
|
|
1047
|
+
return not any(
|
|
1048
|
+
isinstance(self.graph.get_node(a), IterateInvocation) and a not in self.executed
|
|
1049
|
+
for a in nx.ancestors(g, n)
|
|
1050
|
+
)
|
|
1051
|
+
|
|
976
1052
|
next_node_id = next(
|
|
977
|
-
(
|
|
978
|
-
n
|
|
979
|
-
for n in sorted_nodes
|
|
980
|
-
# exclude nodes that have already been prepared
|
|
981
|
-
if n not in self.source_prepared_mapping
|
|
982
|
-
# exclude iterate nodes whose inputs have not been executed
|
|
983
|
-
and not (
|
|
984
|
-
isinstance(self.graph.get_node(n), IterateInvocation) # `n` is an iterate node...
|
|
985
|
-
and not all((e[0] in self.executed for e in g.in_edges(n))) # ...that has unexecuted inputs
|
|
986
|
-
)
|
|
987
|
-
# exclude nodes who have unexecuted iterate ancestors
|
|
988
|
-
and not any(
|
|
989
|
-
(
|
|
990
|
-
isinstance(self.graph.get_node(a), IterateInvocation) # `a` is an iterate ancestor of `n`...
|
|
991
|
-
and a not in self.executed # ...that is not executed
|
|
992
|
-
for a in nx.ancestors(g, n) # for all ancestors `a` of node `n`
|
|
993
|
-
)
|
|
994
|
-
)
|
|
995
|
-
),
|
|
1053
|
+
(n for n in sorted_nodes if unprepared(n) and iter_inputs_ready(n) and no_unexecuted_iter_ancestors(n)),
|
|
996
1054
|
None,
|
|
997
1055
|
)
|
|
998
1056
|
|
|
@@ -1000,7 +1058,7 @@ class GraphExecutionState(BaseModel):
|
|
|
1000
1058
|
return None
|
|
1001
1059
|
|
|
1002
1060
|
# Get all parents of the next node
|
|
1003
|
-
next_node_parents = [
|
|
1061
|
+
next_node_parents = [u for u, _ in g.in_edges(next_node_id)]
|
|
1004
1062
|
|
|
1005
1063
|
# Create execution nodes
|
|
1006
1064
|
next_node = self.graph.get_node(next_node_id)
|
|
@@ -1018,7 +1076,8 @@ class GraphExecutionState(BaseModel):
|
|
|
1018
1076
|
else: # Iterators or normal nodes
|
|
1019
1077
|
# Get all iterator combinations for this node
|
|
1020
1078
|
# Will produce a list of lists of prepared iterator nodes, from which results can be iterated
|
|
1021
|
-
|
|
1079
|
+
it_g = self._iterator_graph(g)
|
|
1080
|
+
iterator_nodes = self._get_node_iterators(next_node_id, it_g)
|
|
1022
1081
|
iterator_nodes_prepared = [list(self.source_prepared_mapping[n]) for n in iterator_nodes]
|
|
1023
1082
|
iterator_node_prepared_combinations = list(itertools.product(*iterator_nodes_prepared))
|
|
1024
1083
|
|
|
@@ -1066,45 +1125,41 @@ class GraphExecutionState(BaseModel):
|
|
|
1066
1125
|
)
|
|
1067
1126
|
|
|
1068
1127
|
def _get_next_node(self) -> Optional[BaseInvocation]:
|
|
1069
|
-
"""Gets the
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
for
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
if node not in self.executed and all((e[0] in self.executed for e in g.in_edges(node))):
|
|
1094
|
-
return self.execution_graph.nodes[node]
|
|
1095
|
-
|
|
1096
|
-
# If no node is found, return None
|
|
1128
|
+
"""Gets the next ready node: FIFO within class, drain class before switching."""
|
|
1129
|
+
# 1) Continue draining the active class
|
|
1130
|
+
if self._active_class:
|
|
1131
|
+
q = self._ready_queues.get(self._active_class)
|
|
1132
|
+
while q:
|
|
1133
|
+
nid = q.popleft()
|
|
1134
|
+
if nid not in self.executed:
|
|
1135
|
+
return self.execution_graph.nodes[nid]
|
|
1136
|
+
# emptied: release active class
|
|
1137
|
+
self._active_class = None
|
|
1138
|
+
|
|
1139
|
+
# 2) Pick next class by priority, then by class name
|
|
1140
|
+
seen = set(self.ready_order)
|
|
1141
|
+
for cls_name in self.ready_order:
|
|
1142
|
+
q = self._ready_queues.get(cls_name)
|
|
1143
|
+
if q:
|
|
1144
|
+
self._active_class = cls_name
|
|
1145
|
+
# recurse to drain newly set active class
|
|
1146
|
+
return self._get_next_node()
|
|
1147
|
+
for cls_name in sorted(k for k in self._ready_queues.keys() if k not in seen):
|
|
1148
|
+
q = self._ready_queues[cls_name]
|
|
1149
|
+
if q:
|
|
1150
|
+
self._active_class = cls_name
|
|
1151
|
+
return self._get_next_node()
|
|
1097
1152
|
return None
|
|
1098
1153
|
|
|
1099
1154
|
def _prepare_inputs(self, node: BaseInvocation):
|
|
1100
|
-
input_edges =
|
|
1155
|
+
input_edges = self.execution_graph._get_input_edges(node.id)
|
|
1101
1156
|
# Inputs must be deep-copied, else if a node mutates the object, other nodes that get the same input
|
|
1102
1157
|
# will see the mutation.
|
|
1103
1158
|
if isinstance(node, CollectInvocation):
|
|
1104
1159
|
output_collection = [
|
|
1105
1160
|
copydeep(getattr(self.results[edge.source.node_id], edge.source.field))
|
|
1106
1161
|
for edge in input_edges
|
|
1107
|
-
if edge.destination.field ==
|
|
1162
|
+
if edge.destination.field == ITEM_FIELD
|
|
1108
1163
|
]
|
|
1109
1164
|
node.collection = output_collection
|
|
1110
1165
|
else:
|
|
@@ -74,3 +74,11 @@ class WorkflowRecordsStorageBase(ABC):
|
|
|
74
74
|
def update_opened_at(self, workflow_id: str) -> None:
|
|
75
75
|
"""Open a workflow."""
|
|
76
76
|
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def get_all_tags(
|
|
80
|
+
self,
|
|
81
|
+
categories: Optional[list[WorkflowCategory]] = None,
|
|
82
|
+
) -> list[str]:
|
|
83
|
+
"""Gets all unique tags from workflows."""
|
|
84
|
+
pass
|
|
@@ -332,6 +332,48 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
|
|
|
332
332
|
(workflow_id,),
|
|
333
333
|
)
|
|
334
334
|
|
|
335
|
+
def get_all_tags(
|
|
336
|
+
self,
|
|
337
|
+
categories: Optional[list[WorkflowCategory]] = None,
|
|
338
|
+
) -> list[str]:
|
|
339
|
+
with self._db.transaction() as cursor:
|
|
340
|
+
conditions: list[str] = []
|
|
341
|
+
params: list[str] = []
|
|
342
|
+
|
|
343
|
+
# Only get workflows that have tags
|
|
344
|
+
conditions.append("tags IS NOT NULL AND tags != ''")
|
|
345
|
+
|
|
346
|
+
if categories:
|
|
347
|
+
assert all(c in WorkflowCategory for c in categories)
|
|
348
|
+
placeholders = ", ".join("?" for _ in categories)
|
|
349
|
+
conditions.append(f"category IN ({placeholders})")
|
|
350
|
+
params.extend([category.value for category in categories])
|
|
351
|
+
|
|
352
|
+
stmt = """--sql
|
|
353
|
+
SELECT DISTINCT tags
|
|
354
|
+
FROM workflow_library
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
if conditions:
|
|
358
|
+
stmt += " WHERE " + " AND ".join(conditions)
|
|
359
|
+
|
|
360
|
+
cursor.execute(stmt, params)
|
|
361
|
+
rows = cursor.fetchall()
|
|
362
|
+
|
|
363
|
+
# Parse comma-separated tags and collect unique tags
|
|
364
|
+
all_tags: set[str] = set()
|
|
365
|
+
|
|
366
|
+
for row in rows:
|
|
367
|
+
tags_value = row[0]
|
|
368
|
+
if tags_value and isinstance(tags_value, str):
|
|
369
|
+
# Tags are stored as comma-separated string
|
|
370
|
+
for tag in tags_value.split(","):
|
|
371
|
+
tag_stripped = tag.strip()
|
|
372
|
+
if tag_stripped:
|
|
373
|
+
all_tags.add(tag_stripped)
|
|
374
|
+
|
|
375
|
+
return sorted(all_tags)
|
|
376
|
+
|
|
335
377
|
def _sync_default_workflows(self) -> None:
|
|
336
378
|
"""Syncs default workflows to the database. Internal use only."""
|
|
337
379
|
|
|
@@ -164,6 +164,9 @@ def diffusion_step_callback(
|
|
|
164
164
|
latent_rgb_factors = COGVIEW4_LATENT_RGB_FACTORS
|
|
165
165
|
elif base_model == BaseModelType.Flux:
|
|
166
166
|
latent_rgb_factors = FLUX_LATENT_RGB_FACTORS
|
|
167
|
+
elif base_model == BaseModelType.ZImage:
|
|
168
|
+
# Z-Image uses FLUX-compatible VAE with 16 latent channels
|
|
169
|
+
latent_rgb_factors = FLUX_LATENT_RGB_FACTORS
|
|
167
170
|
else:
|
|
168
171
|
raise ValueError(f"Unsupported base model: {base_model}")
|
|
169
172
|
|
|
@@ -88,7 +88,9 @@ class ControlNet_Diffusers_Config_Base(Diffusers_Config_Base):
|
|
|
88
88
|
|
|
89
89
|
cls._validate_base(mod)
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
repo_variant = {"repo_variant": override_fields.get("repo_variant", cls._get_repo_variant_or_raise(mod))}
|
|
92
|
+
args = override_fields | repo_variant
|
|
93
|
+
return cls(**args)
|
|
92
94
|
|
|
93
95
|
@classmethod
|
|
94
96
|
def _validate_base(cls, mod: ModelOnDisk) -> None:
|
|
@@ -228,3 +230,47 @@ class ControlNet_Checkpoint_SDXL_Config(ControlNet_Checkpoint_Config_Base, Confi
|
|
|
228
230
|
|
|
229
231
|
class ControlNet_Checkpoint_FLUX_Config(ControlNet_Checkpoint_Config_Base, Config_Base):
|
|
230
232
|
base: Literal[BaseModelType.Flux] = Field(default=BaseModelType.Flux)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _has_z_image_control_keys(state_dict: dict) -> bool:
|
|
236
|
+
"""Check if state dict contains Z-Image Control specific keys."""
|
|
237
|
+
z_image_control_keys = {"control_layers", "control_all_x_embedder", "control_noise_refiner"}
|
|
238
|
+
for key in state_dict.keys():
|
|
239
|
+
if isinstance(key, str):
|
|
240
|
+
prefix = key.split(".")[0]
|
|
241
|
+
if prefix in z_image_control_keys:
|
|
242
|
+
return True
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class ControlNet_Checkpoint_ZImage_Config(Checkpoint_Config_Base, Config_Base):
|
|
247
|
+
"""Model config for Z-Image Control adapter models (Safetensors checkpoint).
|
|
248
|
+
|
|
249
|
+
Z-Image Control models are standalone adapters containing only the control layers
|
|
250
|
+
(control_layers, control_all_x_embedder, control_noise_refiner) that extend
|
|
251
|
+
the base Z-Image transformer with spatial conditioning capabilities.
|
|
252
|
+
|
|
253
|
+
Supports: Canny, HED, Depth, Pose, MLSD.
|
|
254
|
+
Recommended control_context_scale: 0.65-0.80.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
type: Literal[ModelType.ControlNet] = Field(default=ModelType.ControlNet)
|
|
258
|
+
format: Literal[ModelFormat.Checkpoint] = Field(default=ModelFormat.Checkpoint)
|
|
259
|
+
base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage)
|
|
260
|
+
default_settings: ControlAdapterDefaultSettings | None = Field(None)
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -> Self:
|
|
264
|
+
raise_if_not_file(mod)
|
|
265
|
+
|
|
266
|
+
raise_for_override_fields(cls, override_fields)
|
|
267
|
+
|
|
268
|
+
cls._validate_looks_like_z_image_control(mod)
|
|
269
|
+
|
|
270
|
+
return cls(**override_fields)
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def _validate_looks_like_z_image_control(cls, mod: ModelOnDisk) -> None:
|
|
274
|
+
state_dict = mod.load_state_dict()
|
|
275
|
+
if not _has_z_image_control_keys(state_dict):
|
|
276
|
+
raise NotAMatchError("state dict does not look like a Z-Image Control model")
|
|
@@ -20,6 +20,7 @@ from invokeai.backend.model_manager.configs.controlnet import (
|
|
|
20
20
|
ControlNet_Checkpoint_SD1_Config,
|
|
21
21
|
ControlNet_Checkpoint_SD2_Config,
|
|
22
22
|
ControlNet_Checkpoint_SDXL_Config,
|
|
23
|
+
ControlNet_Checkpoint_ZImage_Config,
|
|
23
24
|
ControlNet_Diffusers_FLUX_Config,
|
|
24
25
|
ControlNet_Diffusers_SD1_Config,
|
|
25
26
|
ControlNet_Diffusers_SD2_Config,
|
|
@@ -43,10 +44,12 @@ from invokeai.backend.model_manager.configs.lora import (
|
|
|
43
44
|
LoRA_Diffusers_SD1_Config,
|
|
44
45
|
LoRA_Diffusers_SD2_Config,
|
|
45
46
|
LoRA_Diffusers_SDXL_Config,
|
|
47
|
+
LoRA_Diffusers_ZImage_Config,
|
|
46
48
|
LoRA_LyCORIS_FLUX_Config,
|
|
47
49
|
LoRA_LyCORIS_SD1_Config,
|
|
48
50
|
LoRA_LyCORIS_SD2_Config,
|
|
49
51
|
LoRA_LyCORIS_SDXL_Config,
|
|
52
|
+
LoRA_LyCORIS_ZImage_Config,
|
|
50
53
|
LoRA_OMI_FLUX_Config,
|
|
51
54
|
LoRA_OMI_SDXL_Config,
|
|
52
55
|
LoraModelDefaultSettings,
|
|
@@ -58,15 +61,23 @@ from invokeai.backend.model_manager.configs.main import (
|
|
|
58
61
|
Main_Checkpoint_SD2_Config,
|
|
59
62
|
Main_Checkpoint_SDXL_Config,
|
|
60
63
|
Main_Checkpoint_SDXLRefiner_Config,
|
|
64
|
+
Main_Checkpoint_ZImage_Config,
|
|
61
65
|
Main_Diffusers_CogView4_Config,
|
|
62
66
|
Main_Diffusers_SD1_Config,
|
|
63
67
|
Main_Diffusers_SD2_Config,
|
|
64
68
|
Main_Diffusers_SD3_Config,
|
|
65
69
|
Main_Diffusers_SDXL_Config,
|
|
66
70
|
Main_Diffusers_SDXLRefiner_Config,
|
|
71
|
+
Main_Diffusers_ZImage_Config,
|
|
67
72
|
Main_GGUF_FLUX_Config,
|
|
73
|
+
Main_GGUF_ZImage_Config,
|
|
68
74
|
MainModelDefaultSettings,
|
|
69
75
|
)
|
|
76
|
+
from invokeai.backend.model_manager.configs.qwen3_encoder import (
|
|
77
|
+
Qwen3Encoder_Checkpoint_Config,
|
|
78
|
+
Qwen3Encoder_GGUF_Config,
|
|
79
|
+
Qwen3Encoder_Qwen3Encoder_Config,
|
|
80
|
+
)
|
|
70
81
|
from invokeai.backend.model_manager.configs.siglip import SigLIP_Diffusers_Config
|
|
71
82
|
from invokeai.backend.model_manager.configs.spandrel import Spandrel_Checkpoint_Config
|
|
72
83
|
from invokeai.backend.model_manager.configs.t2i_adapter import (
|
|
@@ -138,15 +149,18 @@ AnyModelConfig = Annotated[
|
|
|
138
149
|
Annotated[Main_Diffusers_SDXLRefiner_Config, Main_Diffusers_SDXLRefiner_Config.get_tag()],
|
|
139
150
|
Annotated[Main_Diffusers_SD3_Config, Main_Diffusers_SD3_Config.get_tag()],
|
|
140
151
|
Annotated[Main_Diffusers_CogView4_Config, Main_Diffusers_CogView4_Config.get_tag()],
|
|
152
|
+
Annotated[Main_Diffusers_ZImage_Config, Main_Diffusers_ZImage_Config.get_tag()],
|
|
141
153
|
# Main (Pipeline) - checkpoint format
|
|
142
154
|
Annotated[Main_Checkpoint_SD1_Config, Main_Checkpoint_SD1_Config.get_tag()],
|
|
143
155
|
Annotated[Main_Checkpoint_SD2_Config, Main_Checkpoint_SD2_Config.get_tag()],
|
|
144
156
|
Annotated[Main_Checkpoint_SDXL_Config, Main_Checkpoint_SDXL_Config.get_tag()],
|
|
145
157
|
Annotated[Main_Checkpoint_SDXLRefiner_Config, Main_Checkpoint_SDXLRefiner_Config.get_tag()],
|
|
146
158
|
Annotated[Main_Checkpoint_FLUX_Config, Main_Checkpoint_FLUX_Config.get_tag()],
|
|
159
|
+
Annotated[Main_Checkpoint_ZImage_Config, Main_Checkpoint_ZImage_Config.get_tag()],
|
|
147
160
|
# Main (Pipeline) - quantized formats
|
|
148
161
|
Annotated[Main_BnBNF4_FLUX_Config, Main_BnBNF4_FLUX_Config.get_tag()],
|
|
149
162
|
Annotated[Main_GGUF_FLUX_Config, Main_GGUF_FLUX_Config.get_tag()],
|
|
163
|
+
Annotated[Main_GGUF_ZImage_Config, Main_GGUF_ZImage_Config.get_tag()],
|
|
150
164
|
# VAE - checkpoint format
|
|
151
165
|
Annotated[VAE_Checkpoint_SD1_Config, VAE_Checkpoint_SD1_Config.get_tag()],
|
|
152
166
|
Annotated[VAE_Checkpoint_SD2_Config, VAE_Checkpoint_SD2_Config.get_tag()],
|
|
@@ -160,6 +174,7 @@ AnyModelConfig = Annotated[
|
|
|
160
174
|
Annotated[ControlNet_Checkpoint_SD2_Config, ControlNet_Checkpoint_SD2_Config.get_tag()],
|
|
161
175
|
Annotated[ControlNet_Checkpoint_SDXL_Config, ControlNet_Checkpoint_SDXL_Config.get_tag()],
|
|
162
176
|
Annotated[ControlNet_Checkpoint_FLUX_Config, ControlNet_Checkpoint_FLUX_Config.get_tag()],
|
|
177
|
+
Annotated[ControlNet_Checkpoint_ZImage_Config, ControlNet_Checkpoint_ZImage_Config.get_tag()],
|
|
163
178
|
# ControlNet - diffusers format
|
|
164
179
|
Annotated[ControlNet_Diffusers_SD1_Config, ControlNet_Diffusers_SD1_Config.get_tag()],
|
|
165
180
|
Annotated[ControlNet_Diffusers_SD2_Config, ControlNet_Diffusers_SD2_Config.get_tag()],
|
|
@@ -170,6 +185,7 @@ AnyModelConfig = Annotated[
|
|
|
170
185
|
Annotated[LoRA_LyCORIS_SD2_Config, LoRA_LyCORIS_SD2_Config.get_tag()],
|
|
171
186
|
Annotated[LoRA_LyCORIS_SDXL_Config, LoRA_LyCORIS_SDXL_Config.get_tag()],
|
|
172
187
|
Annotated[LoRA_LyCORIS_FLUX_Config, LoRA_LyCORIS_FLUX_Config.get_tag()],
|
|
188
|
+
Annotated[LoRA_LyCORIS_ZImage_Config, LoRA_LyCORIS_ZImage_Config.get_tag()],
|
|
173
189
|
# LoRA - OMI format
|
|
174
190
|
Annotated[LoRA_OMI_SDXL_Config, LoRA_OMI_SDXL_Config.get_tag()],
|
|
175
191
|
Annotated[LoRA_OMI_FLUX_Config, LoRA_OMI_FLUX_Config.get_tag()],
|
|
@@ -178,11 +194,16 @@ AnyModelConfig = Annotated[
|
|
|
178
194
|
Annotated[LoRA_Diffusers_SD2_Config, LoRA_Diffusers_SD2_Config.get_tag()],
|
|
179
195
|
Annotated[LoRA_Diffusers_SDXL_Config, LoRA_Diffusers_SDXL_Config.get_tag()],
|
|
180
196
|
Annotated[LoRA_Diffusers_FLUX_Config, LoRA_Diffusers_FLUX_Config.get_tag()],
|
|
197
|
+
Annotated[LoRA_Diffusers_ZImage_Config, LoRA_Diffusers_ZImage_Config.get_tag()],
|
|
181
198
|
# ControlLoRA - diffusers format
|
|
182
199
|
Annotated[ControlLoRA_LyCORIS_FLUX_Config, ControlLoRA_LyCORIS_FLUX_Config.get_tag()],
|
|
183
200
|
# T5 Encoder - all formats
|
|
184
201
|
Annotated[T5Encoder_T5Encoder_Config, T5Encoder_T5Encoder_Config.get_tag()],
|
|
185
202
|
Annotated[T5Encoder_BnBLLMint8_Config, T5Encoder_BnBLLMint8_Config.get_tag()],
|
|
203
|
+
# Qwen3 Encoder
|
|
204
|
+
Annotated[Qwen3Encoder_Qwen3Encoder_Config, Qwen3Encoder_Qwen3Encoder_Config.get_tag()],
|
|
205
|
+
Annotated[Qwen3Encoder_Checkpoint_Config, Qwen3Encoder_Checkpoint_Config.get_tag()],
|
|
206
|
+
Annotated[Qwen3Encoder_GGUF_Config, Qwen3Encoder_GGUF_Config.get_tag()],
|
|
186
207
|
# TI - file format
|
|
187
208
|
Annotated[TI_File_SD1_Config, TI_File_SD1_Config.get_tag()],
|
|
188
209
|
Annotated[TI_File_SD2_Config, TI_File_SD2_Config.get_tag()],
|
|
@@ -333,7 +354,11 @@ class ModelConfigFactory:
|
|
|
333
354
|
# For directories, do a quick file count check with early exit
|
|
334
355
|
total_files = 0
|
|
335
356
|
# Ignore hidden files and directories
|
|
336
|
-
paths_to_check = (
|
|
357
|
+
paths_to_check = (
|
|
358
|
+
p
|
|
359
|
+
for p in path.rglob("*")
|
|
360
|
+
if not p.name.startswith(".") and not any(part.startswith(".") for part in p.parts)
|
|
361
|
+
)
|
|
337
362
|
for item in paths_to_check:
|
|
338
363
|
if item.is_file():
|
|
339
364
|
total_files += 1
|