Skip to content

Commit 78cc5a7

Browse files
feat(nodes): versioning (#4449)
## What type of PR is this? (check all applicable) - [ ] Refactor - [x] Feature - [ ] Bug Fix - [ ] Optimization - [ ] Documentation Update - [ ] Community Node Submission ## Have you discussed this change with the InvokeAI team? - [x] Yes - [ ] No, because: ## Have you updated all relevant documentation? - [x] Yes - [ ] No ## Description This PR is based on #4423 and should not be merged until it is merged. [feat(nodes): add version to node schemas](c179d4c) The `@invocation` decorator is extended with an optional `version` arg. On execution of the decorator, the version string is parsed using the `semver` package (this was an indirect dependency and has been added to `pyproject.toml`). All built-in nodes are set with `version="1.0.0"`. The version is added to the OpenAPI Schema for consumption by the client. [feat(ui): handle node versions](03de3e4) - Node versions are now added to node templates - Node data (including in workflows) include the version of the node - On loading a workflow, we check to see if the node and template versions match exactly. If not, a warning is logged to console. - The node info icon (top-right corner of node, which you may click to open the notes editor) now shows the version and mentions any issues. - Some workflow validation logic has been shifted around and is now executed in a redux listener. ## Related Tickets & Documents <!-- For pull requests that relate or close an issue, please include them below. For example having the text: "closes #1234" would connect the current pull request to issue 1234. And when we merge the pull request, Github will automatically close the issue. --> - Closes #4393 ## QA Instructions, Screenshots, Recordings <!-- Please provide steps on how to test changes, any hardware or software specifications as well as any other pertinent information. --> Loading old workflows should prompt a warning, and the node status icon should indicate some action is needed. ## [optional] Are there any post deployment tasks we need to perform? I've updated the default workflows: - Bump workflow versions from 1.0 to 1.0.1 - Add versions for all nodes in the workflows - Test workflows [Default Workflows.zip](https://github.com/invoke-ai/InvokeAI/files/12511911/Default.Workflows.zip) I'm not sure where these are being stored right now @Millu
2 parents 1f6c868 + 438bc70 commit 78cc5a7

File tree

38 files changed

+504
-164
lines changed

38 files changed

+504
-164
lines changed

docs/contributing/INVOCATIONS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,12 @@ copy-paste the template above.
244244
We can use the `@invocation` decorator to provide some additional info to the
245245
UI, like a custom title, tags and category.
246246

247+
We also encourage providing a version. This must be a
248+
[semver](https://semver.org/) version string ("$MAJOR.$MINOR.$PATCH"). The UI
249+
will let users know if their workflow is using a mismatched version of the node.
250+
247251
```python
248-
@invocation("resize", title="My Resizer", tags=["resize", "image"], category="My Invocations")
252+
@invocation("resize", title="My Resizer", tags=["resize", "image"], category="My Invocations", version="1.0.0")
249253
class ResizeInvocation(BaseInvocation):
250254
"""Resizes an image"""
251255

@@ -279,8 +283,6 @@ take a look a at our [contributing nodes overview](contributingNodes).
279283

280284
## Advanced
281285

282-
-->
283-
284286
### Custom Output Types
285287

286288
Like with custom inputs, sometimes you might find yourself needing custom

invokeai/app/invocations/baseinvocation.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@
2626
from pydantic import BaseModel, Field, validator
2727
from pydantic.fields import Undefined, ModelField
2828
from pydantic.typing import NoArgAnyCallable
29+
import semver
2930

3031
if TYPE_CHECKING:
3132
from ..services.invocation_services import InvocationServices
3233

3334

35+
class InvalidVersionError(ValueError):
36+
pass
37+
38+
3439
class FieldDescriptions:
3540
denoising_start = "When to start denoising, expressed a percentage of total steps"
3641
denoising_end = "When to stop denoising, expressed a percentage of total steps"
@@ -401,6 +406,9 @@ class UIConfigBase(BaseModel):
401406
tags: Optional[list[str]] = Field(default_factory=None, description="The node's tags")
402407
title: Optional[str] = Field(default=None, description="The node's display name")
403408
category: Optional[str] = Field(default=None, description="The node's category")
409+
version: Optional[str] = Field(
410+
default=None, description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".'
411+
)
404412

405413

406414
class InvocationContext:
@@ -499,6 +507,8 @@ def schema_extra(schema: dict[str, Any], model_class: Type[BaseModel]) -> None:
499507
schema["tags"] = uiconfig.tags
500508
if uiconfig and hasattr(uiconfig, "category"):
501509
schema["category"] = uiconfig.category
510+
if uiconfig and hasattr(uiconfig, "version"):
511+
schema["version"] = uiconfig.version
502512
if "required" not in schema or not isinstance(schema["required"], list):
503513
schema["required"] = list()
504514
schema["required"].extend(["type", "id"])
@@ -567,7 +577,11 @@ def validate_workflow_is_json(cls, v):
567577

568578

569579
def invocation(
570-
invocation_type: str, title: Optional[str] = None, tags: Optional[list[str]] = None, category: Optional[str] = None
580+
invocation_type: str,
581+
title: Optional[str] = None,
582+
tags: Optional[list[str]] = None,
583+
category: Optional[str] = None,
584+
version: Optional[str] = None,
571585
) -> Callable[[Type[GenericBaseInvocation]], Type[GenericBaseInvocation]]:
572586
"""
573587
Adds metadata to an invocation.
@@ -594,6 +608,12 @@ def wrapper(cls: Type[GenericBaseInvocation]) -> Type[GenericBaseInvocation]:
594608
cls.UIConfig.tags = tags
595609
if category is not None:
596610
cls.UIConfig.category = category
611+
if version is not None:
612+
try:
613+
semver.Version.parse(version)
614+
except ValueError as e:
615+
raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
616+
cls.UIConfig.version = version
597617

598618
# Add the invocation type to the pydantic model of the invocation
599619
invocation_type_annotation = Literal[invocation_type] # type: ignore

invokeai/app/invocations/collections.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation
1111

1212

13-
@invocation("range", title="Integer Range", tags=["collection", "integer", "range"], category="collections")
13+
@invocation(
14+
"range", title="Integer Range", tags=["collection", "integer", "range"], category="collections", version="1.0.0"
15+
)
1416
class RangeInvocation(BaseInvocation):
1517
"""Creates a range of numbers from start to stop with step"""
1618

@@ -33,6 +35,7 @@ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
3335
title="Integer Range of Size",
3436
tags=["collection", "integer", "size", "range"],
3537
category="collections",
38+
version="1.0.0",
3639
)
3740
class RangeOfSizeInvocation(BaseInvocation):
3841
"""Creates a range from start to start + size with step"""
@@ -50,6 +53,7 @@ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
5053
title="Random Range",
5154
tags=["range", "integer", "random", "collection"],
5255
category="collections",
56+
version="1.0.0",
5357
)
5458
class RandomRangeInvocation(BaseInvocation):
5559
"""Creates a collection of random numbers"""

invokeai/app/invocations/compel.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ConditioningFieldData:
4444
# PerpNeg = "perp_neg"
4545

4646

47-
@invocation("compel", title="Prompt", tags=["prompt", "compel"], category="conditioning")
47+
@invocation("compel", title="Prompt", tags=["prompt", "compel"], category="conditioning", version="1.0.0")
4848
class CompelInvocation(BaseInvocation):
4949
"""Parse prompt using compel package to conditioning."""
5050

@@ -267,6 +267,7 @@ def _lora_loader():
267267
title="SDXL Prompt",
268268
tags=["sdxl", "compel", "prompt"],
269269
category="conditioning",
270+
version="1.0.0",
270271
)
271272
class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
272273
"""Parse prompt using compel package to conditioning."""
@@ -351,6 +352,7 @@ def invoke(self, context: InvocationContext) -> ConditioningOutput:
351352
title="SDXL Refiner Prompt",
352353
tags=["sdxl", "compel", "prompt"],
353354
category="conditioning",
355+
version="1.0.0",
354356
)
355357
class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
356358
"""Parse prompt using compel package to conditioning."""
@@ -403,7 +405,7 @@ class ClipSkipInvocationOutput(BaseInvocationOutput):
403405
clip: ClipField = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
404406

405407

406-
@invocation("clip_skip", title="CLIP Skip", tags=["clipskip", "clip", "skip"], category="conditioning")
408+
@invocation("clip_skip", title="CLIP Skip", tags=["clipskip", "clip", "skip"], category="conditioning", version="1.0.0")
407409
class ClipSkipInvocation(BaseInvocation):
408410
"""Skip layers in clip text_encoder model."""
409411

invokeai/app/invocations/controlnet_image_processors.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class ControlOutput(BaseInvocationOutput):
9595
control: ControlField = OutputField(description=FieldDescriptions.control)
9696

9797

98-
@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet")
98+
@invocation("controlnet", title="ControlNet", tags=["controlnet"], category="controlnet", version="1.0.0")
9999
class ControlNetInvocation(BaseInvocation):
100100
"""Collects ControlNet info to pass to other nodes"""
101101

@@ -127,7 +127,9 @@ def invoke(self, context: InvocationContext) -> ControlOutput:
127127
)
128128

129129

130-
@invocation("image_processor", title="Base Image Processor", tags=["controlnet"], category="controlnet")
130+
@invocation(
131+
"image_processor", title="Base Image Processor", tags=["controlnet"], category="controlnet", version="1.0.0"
132+
)
131133
class ImageProcessorInvocation(BaseInvocation):
132134
"""Base class for invocations that preprocess images for ControlNet"""
133135

@@ -171,6 +173,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput:
171173
title="Canny Processor",
172174
tags=["controlnet", "canny"],
173175
category="controlnet",
176+
version="1.0.0",
174177
)
175178
class CannyImageProcessorInvocation(ImageProcessorInvocation):
176179
"""Canny edge detection for ControlNet"""
@@ -193,6 +196,7 @@ def run_processor(self, image):
193196
title="HED (softedge) Processor",
194197
tags=["controlnet", "hed", "softedge"],
195198
category="controlnet",
199+
version="1.0.0",
196200
)
197201
class HedImageProcessorInvocation(ImageProcessorInvocation):
198202
"""Applies HED edge detection to image"""
@@ -221,6 +225,7 @@ def run_processor(self, image):
221225
title="Lineart Processor",
222226
tags=["controlnet", "lineart"],
223227
category="controlnet",
228+
version="1.0.0",
224229
)
225230
class LineartImageProcessorInvocation(ImageProcessorInvocation):
226231
"""Applies line art processing to image"""
@@ -242,6 +247,7 @@ def run_processor(self, image):
242247
title="Lineart Anime Processor",
243248
tags=["controlnet", "lineart", "anime"],
244249
category="controlnet",
250+
version="1.0.0",
245251
)
246252
class LineartAnimeImageProcessorInvocation(ImageProcessorInvocation):
247253
"""Applies line art anime processing to image"""
@@ -264,6 +270,7 @@ def run_processor(self, image):
264270
title="Openpose Processor",
265271
tags=["controlnet", "openpose", "pose"],
266272
category="controlnet",
273+
version="1.0.0",
267274
)
268275
class OpenposeImageProcessorInvocation(ImageProcessorInvocation):
269276
"""Applies Openpose processing to image"""
@@ -288,6 +295,7 @@ def run_processor(self, image):
288295
title="Midas Depth Processor",
289296
tags=["controlnet", "midas"],
290297
category="controlnet",
298+
version="1.0.0",
291299
)
292300
class MidasDepthImageProcessorInvocation(ImageProcessorInvocation):
293301
"""Applies Midas depth processing to image"""
@@ -314,6 +322,7 @@ def run_processor(self, image):
314322
title="Normal BAE Processor",
315323
tags=["controlnet"],
316324
category="controlnet",
325+
version="1.0.0",
317326
)
318327
class NormalbaeImageProcessorInvocation(ImageProcessorInvocation):
319328
"""Applies NormalBae processing to image"""
@@ -329,7 +338,9 @@ def run_processor(self, image):
329338
return processed_image
330339

331340

332-
@invocation("mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet")
341+
@invocation(
342+
"mlsd_image_processor", title="MLSD Processor", tags=["controlnet", "mlsd"], category="controlnet", version="1.0.0"
343+
)
333344
class MlsdImageProcessorInvocation(ImageProcessorInvocation):
334345
"""Applies MLSD processing to image"""
335346

@@ -350,7 +361,9 @@ def run_processor(self, image):
350361
return processed_image
351362

352363

353-
@invocation("pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet")
364+
@invocation(
365+
"pidi_image_processor", title="PIDI Processor", tags=["controlnet", "pidi"], category="controlnet", version="1.0.0"
366+
)
354367
class PidiImageProcessorInvocation(ImageProcessorInvocation):
355368
"""Applies PIDI processing to image"""
356369

@@ -376,6 +389,7 @@ def run_processor(self, image):
376389
title="Content Shuffle Processor",
377390
tags=["controlnet", "contentshuffle"],
378391
category="controlnet",
392+
version="1.0.0",
379393
)
380394
class ContentShuffleImageProcessorInvocation(ImageProcessorInvocation):
381395
"""Applies content shuffle processing to image"""
@@ -405,6 +419,7 @@ def run_processor(self, image):
405419
title="Zoe (Depth) Processor",
406420
tags=["controlnet", "zoe", "depth"],
407421
category="controlnet",
422+
version="1.0.0",
408423
)
409424
class ZoeDepthImageProcessorInvocation(ImageProcessorInvocation):
410425
"""Applies Zoe depth processing to image"""
@@ -420,6 +435,7 @@ def run_processor(self, image):
420435
title="Mediapipe Face Processor",
421436
tags=["controlnet", "mediapipe", "face"],
422437
category="controlnet",
438+
version="1.0.0",
423439
)
424440
class MediapipeFaceProcessorInvocation(ImageProcessorInvocation):
425441
"""Applies mediapipe face processing to image"""
@@ -442,6 +458,7 @@ def run_processor(self, image):
442458
title="Leres (Depth) Processor",
443459
tags=["controlnet", "leres", "depth"],
444460
category="controlnet",
461+
version="1.0.0",
445462
)
446463
class LeresImageProcessorInvocation(ImageProcessorInvocation):
447464
"""Applies leres processing to image"""
@@ -470,6 +487,7 @@ def run_processor(self, image):
470487
title="Tile Resample Processor",
471488
tags=["controlnet", "tile"],
472489
category="controlnet",
490+
version="1.0.0",
473491
)
474492
class TileResamplerProcessorInvocation(ImageProcessorInvocation):
475493
"""Tile resampler processor"""
@@ -509,6 +527,7 @@ def run_processor(self, img):
509527
title="Segment Anything Processor",
510528
tags=["controlnet", "segmentanything"],
511529
category="controlnet",
530+
version="1.0.0",
512531
)
513532
class SegmentAnythingProcessorInvocation(ImageProcessorInvocation):
514533
"""Applies segment anything processing to image"""

invokeai/app/invocations/cv.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@
1010
from .baseinvocation import BaseInvocation, InputField, InvocationContext, invocation
1111

1212

13-
@invocation(
14-
"cv_inpaint",
15-
title="OpenCV Inpaint",
16-
tags=["opencv", "inpaint"],
17-
category="inpaint",
18-
)
13+
@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.0.0")
1914
class CvInpaintInvocation(BaseInvocation):
2015
"""Simple inpaint using opencv."""
2116

0 commit comments

Comments
 (0)