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.
Files changed (86) hide show
  1. invokeai/app/api/dependencies.py +2 -0
  2. invokeai/app/api/routers/model_manager.py +91 -2
  3. invokeai/app/api/routers/workflows.py +9 -0
  4. invokeai/app/invocations/fields.py +19 -0
  5. invokeai/app/invocations/image_to_latents.py +23 -5
  6. invokeai/app/invocations/latents_to_image.py +2 -25
  7. invokeai/app/invocations/metadata.py +9 -1
  8. invokeai/app/invocations/model.py +8 -0
  9. invokeai/app/invocations/primitives.py +12 -0
  10. invokeai/app/invocations/prompt_template.py +57 -0
  11. invokeai/app/invocations/z_image_control.py +112 -0
  12. invokeai/app/invocations/z_image_denoise.py +610 -0
  13. invokeai/app/invocations/z_image_image_to_latents.py +102 -0
  14. invokeai/app/invocations/z_image_latents_to_image.py +103 -0
  15. invokeai/app/invocations/z_image_lora_loader.py +153 -0
  16. invokeai/app/invocations/z_image_model_loader.py +135 -0
  17. invokeai/app/invocations/z_image_text_encoder.py +197 -0
  18. invokeai/app/services/model_install/model_install_common.py +14 -1
  19. invokeai/app/services/model_install/model_install_default.py +119 -19
  20. invokeai/app/services/model_records/model_records_base.py +12 -0
  21. invokeai/app/services/model_records/model_records_sql.py +17 -0
  22. invokeai/app/services/shared/graph.py +132 -77
  23. invokeai/app/services/workflow_records/workflow_records_base.py +8 -0
  24. invokeai/app/services/workflow_records/workflow_records_sqlite.py +42 -0
  25. invokeai/app/util/step_callback.py +3 -0
  26. invokeai/backend/model_manager/configs/controlnet.py +47 -1
  27. invokeai/backend/model_manager/configs/factory.py +26 -1
  28. invokeai/backend/model_manager/configs/lora.py +43 -1
  29. invokeai/backend/model_manager/configs/main.py +113 -0
  30. invokeai/backend/model_manager/configs/qwen3_encoder.py +156 -0
  31. invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_diffusers_rms_norm.py +40 -0
  32. invokeai/backend/model_manager/load/model_cache/torch_module_autocast/custom_modules/custom_layer_norm.py +25 -0
  33. invokeai/backend/model_manager/load/model_cache/torch_module_autocast/torch_module_autocast.py +11 -2
  34. invokeai/backend/model_manager/load/model_loaders/lora.py +11 -0
  35. invokeai/backend/model_manager/load/model_loaders/z_image.py +935 -0
  36. invokeai/backend/model_manager/load/model_util.py +6 -1
  37. invokeai/backend/model_manager/metadata/metadata_base.py +12 -5
  38. invokeai/backend/model_manager/model_on_disk.py +3 -0
  39. invokeai/backend/model_manager/starter_models.py +70 -0
  40. invokeai/backend/model_manager/taxonomy.py +5 -0
  41. invokeai/backend/model_manager/util/select_hf_files.py +23 -8
  42. invokeai/backend/patches/layer_patcher.py +34 -16
  43. invokeai/backend/patches/layers/lora_layer_base.py +2 -1
  44. invokeai/backend/patches/lora_conversions/flux_aitoolkit_lora_conversion_utils.py +17 -2
  45. invokeai/backend/patches/lora_conversions/flux_xlabs_lora_conversion_utils.py +92 -0
  46. invokeai/backend/patches/lora_conversions/formats.py +5 -0
  47. invokeai/backend/patches/lora_conversions/z_image_lora_constants.py +8 -0
  48. invokeai/backend/patches/lora_conversions/z_image_lora_conversion_utils.py +155 -0
  49. invokeai/backend/quantization/gguf/ggml_tensor.py +27 -4
  50. invokeai/backend/quantization/gguf/loaders.py +47 -12
  51. invokeai/backend/stable_diffusion/diffusion/conditioning_data.py +13 -0
  52. invokeai/backend/util/devices.py +25 -0
  53. invokeai/backend/util/hotfixes.py +2 -2
  54. invokeai/backend/z_image/__init__.py +16 -0
  55. invokeai/backend/z_image/extensions/__init__.py +1 -0
  56. invokeai/backend/z_image/extensions/regional_prompting_extension.py +207 -0
  57. invokeai/backend/z_image/text_conditioning.py +74 -0
  58. invokeai/backend/z_image/z_image_control_adapter.py +238 -0
  59. invokeai/backend/z_image/z_image_control_transformer.py +643 -0
  60. invokeai/backend/z_image/z_image_controlnet_extension.py +531 -0
  61. invokeai/backend/z_image/z_image_patchify_utils.py +135 -0
  62. invokeai/backend/z_image/z_image_transformer_patch.py +234 -0
  63. invokeai/frontend/web/dist/assets/App-CYhlZO3Q.js +161 -0
  64. invokeai/frontend/web/dist/assets/{browser-ponyfill-CN1j0ARZ.js → browser-ponyfill-DHZxq1nk.js} +1 -1
  65. invokeai/frontend/web/dist/assets/index-dgSJAY--.js +530 -0
  66. invokeai/frontend/web/dist/index.html +1 -1
  67. invokeai/frontend/web/dist/locales/de.json +24 -6
  68. invokeai/frontend/web/dist/locales/en.json +70 -1
  69. invokeai/frontend/web/dist/locales/es.json +0 -5
  70. invokeai/frontend/web/dist/locales/fr.json +0 -6
  71. invokeai/frontend/web/dist/locales/it.json +17 -64
  72. invokeai/frontend/web/dist/locales/ja.json +379 -44
  73. invokeai/frontend/web/dist/locales/ru.json +0 -6
  74. invokeai/frontend/web/dist/locales/vi.json +7 -54
  75. invokeai/frontend/web/dist/locales/zh-CN.json +0 -6
  76. invokeai/version/invokeai_version.py +1 -1
  77. {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/METADATA +3 -3
  78. {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/RECORD +84 -60
  79. invokeai/frontend/web/dist/assets/App-Cn9UyjoV.js +0 -161
  80. invokeai/frontend/web/dist/assets/index-BDrf9CL-.js +0 -530
  81. {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/WHEEL +0 -0
  82. {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/entry_points.txt +0 -0
  83. {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/licenses/LICENSE +0 -0
  84. {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/licenses/LICENSE-SD1+SD2.txt +0 -0
  85. {invokeai-6.9.0rc3.dist-info → invokeai-6.10.0rc1.dist-info}/licenses/LICENSE-SDXL.txt +0 -0
  86. {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 typing import Any, Optional, TypeVar, Union, get_args, get_origin
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 KeyError:
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
- duplicate_node_ids = {node_id for node_id in node_ids if node_ids.count(node_id) >= 2}
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 == "collection":
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 == "item":
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 == "item":
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 == "collection"
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, "collection")]
643
- outputs = [e.destination for e in self._get_output_edges(node_id, "item")]
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, "item")[0]
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, "item")]
694
- outputs = [e.destination for e in self._get_output_edges(node_id, "collection")]
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([(e[0], e[1]) for e in unique_edges])
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
- prepared_id = self._prepare()
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 = self._get_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, "collection")))
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 = copy.deepcopy(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
- iterators = [n for n in nx.ancestors(g, node_id) if isinstance(self.graph.get_node(n), IterateInvocation)]
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 = [e[0] for e in g.in_edges(next_node_id)]
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
- iterator_nodes = self._get_node_iterators(next_node_id)
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 deepest node that is ready to be executed"""
1070
- g = self.execution_graph.nx_graph()
1071
-
1072
- # Perform a topological sort using depth-first search
1073
- topo_order = list(nx.dfs_postorder_nodes(g))
1074
-
1075
- # Get all IterateInvocation nodes
1076
- iterate_nodes = [n for n in topo_order if isinstance(self.execution_graph.nodes[n], IterateInvocation)]
1077
-
1078
- # Sort the IterateInvocation nodes based on their index attribute
1079
- iterate_nodes.sort(key=lambda x: self.execution_graph.nodes[x].index)
1080
-
1081
- # Prioritize IterateInvocation nodes and their children
1082
- for iterate_node in iterate_nodes:
1083
- if iterate_node not in self.executed and all((e[0] in self.executed for e in g.in_edges(iterate_node))):
1084
- return self.execution_graph.nodes[iterate_node]
1085
-
1086
- # Check the children of the IterateInvocation node
1087
- for child_node in nx.dfs_postorder_nodes(g, iterate_node):
1088
- if child_node not in self.executed and all((e[0] in self.executed for e in g.in_edges(child_node))):
1089
- return self.execution_graph.nodes[child_node]
1090
-
1091
- # If no IterateInvocation node or its children are ready, return the first ready node in the topological order
1092
- for node in topo_order:
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 = [e for e in self.execution_graph.edges if e.destination.node_id == node.id]
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 == "item"
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
- return cls(**override_fields)
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 = (p for p in path.rglob("*") if not p.name.startswith("."))
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