Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
aa089e8
tidy(nodes): move all field things to fields.py
psychedelicious Jan 13, 2024
f0e60a4
feat(nodes): restricts invocation context power
psychedelicious Jan 13, 2024
97a6c6e
feat: add pyright config
psychedelicious Jan 13, 2024
7e5ba27
feat(nodes): update all invocations to use new invocation context
psychedelicious Jan 13, 2024
4aa7bee
docs: update INVOCATIONS.md
psychedelicious Jan 13, 2024
ac2eb16
tests: fix tests for new invocation context
psychedelicious Jan 13, 2024
8baf3f7
feat(nodes): tidy `invocation_context.py`, improve comments
psychedelicious Jan 13, 2024
183c9c4
chore: ruff
psychedelicious Jan 13, 2024
5c7ed24
feat(nodes): restore previous invocation context methods with depreca…
psychedelicious Jan 14, 2024
3ceee2b
tests: fix missing arg for InvocationContext
psychedelicious Jan 14, 2024
3de4390
feat(nodes): move `ConditioningFieldData` to `conditioning_data.py`
psychedelicious Jan 14, 2024
a7e23af
feat(nodes): create invocation_api.py
psychedelicious Jan 14, 2024
cc295a9
feat: tweak pyright config
psychedelicious Jan 16, 2024
ae421fb
feat(nodes): do not freeze InvocationContextData, prevents it from be…
psychedelicious Jan 16, 2024
483bdbc
fix(nodes): restore type annotations for `InvocationContext`
psychedelicious Feb 5, 2024
e52434c
feat(nodes): add boards interface to invocation context
psychedelicious Feb 5, 2024
bf48e8a
feat(nodes): export more things from `invocation_api"
psychedelicious Feb 5, 2024
3f5ab02
chore(nodes): add comments for ConfigInterface
psychedelicious Feb 5, 2024
9c1e52b
tests(nodes): fix mock InvocationContext
psychedelicious Feb 5, 2024
afbe889
fix(nodes): restore missing context type annotations
psychedelicious Feb 5, 2024
b4c7748
feat(nodes): do not hide `services` in invocation context interfaces
psychedelicious Feb 7, 2024
889a26c
feat(nodes): cache invocation interface config
psychedelicious Feb 7, 2024
d7b7dcc
feat(nodes): context.__services -> context._services
psychedelicious Feb 7, 2024
aff4675
feat(nodes): context.data -> context._data
psychedelicious Feb 7, 2024
b1ba18b
fix(nodes): do not freeze or cache config in context wrapper
psychedelicious Feb 7, 2024
e36d925
fix(ui): remove original l2i node in HRF graph
psychedelicious Feb 7, 2024
1a191c4
remove unused configdict import
psychedelicious Feb 7, 2024
c16eba7
feat(nodes): add `WithBoard` field helper class
psychedelicious Feb 7, 2024
a3faa37
chore(ui): regen types
psychedelicious Feb 7, 2024
8e2b61e
feat(ui): revise graphs to not use `LinearUIOutputInvocation`
psychedelicious Feb 7, 2024
a5db204
tidy(nodes): remove unnecessary, shadowing class attr declarations
psychedelicious Feb 7, 2024
db6bc73
fix(nodes): rearrange fields.py to avoid needing forward refs
psychedelicious Feb 7, 2024
2932652
tidy(nodes): delete onnx.py
psychedelicious Feb 7, 2024
de0b725
feat(nodes): replace latents service with tensors and conditioning se…
psychedelicious Feb 7, 2024
a7f91b3
tidy(nodes): do not refer to files as latents in `PickleStorageTorch`
psychedelicious Feb 7, 2024
0266946
fix(nodes): add super init to `PickleStorageTorch`
psychedelicious Feb 7, 2024
fd30cb4
feat(nodes): ItemStorageABC typevar no longer bound to pydantic.BaseM…
psychedelicious Feb 7, 2024
25386a7
tidy(nodes): do not refer to files as latents in `PickleStorageTorch`…
psychedelicious Feb 7, 2024
fe0391c
feat(nodes): use `ItemStorageABC` for tensors and conditioning
psychedelicious Feb 7, 2024
7fe5283
feat(nodes): create helper function to generate the item ID
psychedelicious Feb 7, 2024
54a6745
feat(nodes): support custom save and load functions in `ItemStorageEp…
psychedelicious Feb 7, 2024
8b6e322
feat(nodes): support custom exception in ephemeral disk storage
psychedelicious Feb 7, 2024
0642902
revert(nodes): revert making tensors/conditioning use item storage
psychedelicious Feb 7, 2024
55fa785
tidy(nodes): remove object serializer on_saved
psychedelicious Feb 7, 2024
83ddcc5
feat(nodes): allow `_delete_all` in obj serializer to be called at an…
psychedelicious Feb 7, 2024
7a2b606
tests: add object serializer tests
psychedelicious Feb 7, 2024
1f27ddc
tidy(nodes): minor spelling correction
psychedelicious Feb 7, 2024
bcb85e1
tests: fix broken tests
psychedelicious Feb 7, 2024
bc5f356
feat(nodes): use LATENT_SCALE_FACTOR const in tensor output builders
psychedelicious Feb 7, 2024
8892df1
Revert "feat(nodes): use LATENT_SCALE_FACTOR const in tensor output b…
psychedelicious Feb 7, 2024
cb0b389
tidy(nodes): clarify comment
psychedelicious Feb 7, 2024
ed772a7
fix(nodes): use `metadata`/`board_id` if provided by user, overriding…
psychedelicious Feb 8, 2024
c58f8c3
feat(nodes): make delete on startup configurable for obj serializer
psychedelicious Feb 8, 2024
ff249a2
tidy(nodes): do not store unnecessarily store invoker
psychedelicious Feb 9, 2024
c1e5cd5
tidy(nodes): "latents" -> "obj"
psychedelicious Feb 9, 2024
6bb2dda
chore(nodes): fix pyright ignore
psychedelicious Feb 9, 2024
a7207ed
chore(nodes): update ObjectSerializerForwardCache docstring
psychedelicious Feb 9, 2024
199ddd6
tests: test ObjectSerializerDisk class name extraction
psychedelicious Feb 10, 2024
b7ffd36
feat(nodes): use TemporaryDirectory to handle ephemeral storage in Ob…
psychedelicious Feb 10, 2024
ba7b1b2
feat(nodes): extract LATENT_SCALE_FACTOR to constants.py
psychedelicious Feb 10, 2024
2005411
feat(nodes): use LATENT_SCALE_FACTOR in primitives.py, noise.py
psychedelicious Feb 10, 2024
083a4f3
chore(backend): rename `ModelInfo` -> `LoadedModelInfo`
psychedelicious Feb 10, 2024
8fb77e4
chore(nodes): export model-related objects from invocation_api
psychedelicious Feb 10, 2024
321b939
chore(nodes): remove deprecation logic for nodes API
psychedelicious Feb 10, 2024
1bbd13e
chore(nodes): "SAMPLER_NAME_VALUES" -> "SCHEDULER_NAME_VALUES"
psychedelicious Feb 10, 2024
6c4eeaa
feat(nodes): add more missing exports to invocation_api
psychedelicious Feb 10, 2024
c76a6bd
chore(ui): regen types
psychedelicious Feb 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 33 additions & 58 deletions docs/contributing/INVOCATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ complex functionality.

## Invocations Directory

InvokeAI Nodes can be found in the `invokeai/app/invocations` directory. These can be used as examples to create your own nodes.
InvokeAI Nodes can be found in the `invokeai/app/invocations` directory. These
can be used as examples to create your own nodes.

New nodes should be added to a subfolder in `nodes` direction found at the root level of the InvokeAI installation location. Nodes added to this folder will be able to be used upon application startup.
New nodes should be added to a subfolder in `nodes` direction found at the root
level of the InvokeAI installation location. Nodes added to this folder will be
able to be used upon application startup.

Example `nodes` subfolder structure:

Example `nodes` subfolder structure:
```py
├── __init__.py # Invoke-managed custom node loader
Expand All @@ -30,14 +34,14 @@ Example `nodes` subfolder structure:
└── fancy_node.py
```

Each node folder must have an `__init__.py` file that imports its nodes. Only nodes imported in the `__init__.py` file are loaded.
See the README in the nodes folder for more examples:
Each node folder must have an `__init__.py` file that imports its nodes. Only
nodes imported in the `__init__.py` file are loaded. See the README in the nodes
folder for more examples:

```py
from .cool_node import CoolInvocation
```


## Creating A New Invocation

In order to understand the process of creating a new Invocation, let us actually
Expand Down Expand Up @@ -131,7 +135,6 @@ from invokeai.app.invocations.primitives import ImageField
class ResizeInvocation(BaseInvocation):
'''Resizes an image'''

# Inputs
image: ImageField = InputField(description="The input image")
width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image")
height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")
Expand Down Expand Up @@ -167,7 +170,6 @@ from invokeai.app.invocations.primitives import ImageField
class ResizeInvocation(BaseInvocation):
'''Resizes an image'''

# Inputs
image: ImageField = InputField(description="The input image")
width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image")
height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")
Expand Down Expand Up @@ -197,7 +199,6 @@ from invokeai.app.invocations.image import ImageOutput
class ResizeInvocation(BaseInvocation):
'''Resizes an image'''

# Inputs
image: ImageField = InputField(description="The input image")
width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image")
height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")
Expand Down Expand Up @@ -229,30 +230,17 @@ class ResizeInvocation(BaseInvocation):
height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")

def invoke(self, context: InvocationContext) -> ImageOutput:
# Load the image using InvokeAI's predefined Image Service. Returns the PIL image.
image = context.services.images.get_pil_image(self.image.image_name)
# Load the input image as a PIL image
image = context.images.get_pil(self.image.image_name)

# Resizing the image
# Resize the image
resized_image = image.resize((self.width, self.height))

# Save the image using InvokeAI's predefined Image Service. Returns the prepared PIL image.
output_image = context.services.images.create(
image=resized_image,
image_origin=ResourceOrigin.INTERNAL,
image_category=ImageCategory.GENERAL,
node_id=self.id,
session_id=context.graph_execution_state_id,
is_intermediate=self.is_intermediate,
)

# Returning the Image
return ImageOutput(
image=ImageField(
image_name=output_image.image_name,
),
width=output_image.width,
height=output_image.height,
)
# Save the image
image_dto = context.images.save(image=resized_image)

# Return an ImageOutput
return ImageOutput.build(image_dto)
```

**Note:** Do not be overwhelmed by the `ImageOutput` process. InvokeAI has a
Expand Down Expand Up @@ -343,27 +331,25 @@ class ImageColorStringOutput(BaseInvocationOutput):

That's all there is to it.

<!-- TODO: DANGER - we probably do not want people to create their own field types, because this requires a lot of work on the frontend to accomodate.

### Custom Input Fields

Now that you know how to create your own Invocations, let us dive into slightly
more advanced topics.

While creating your own Invocations, you might run into a scenario where the
existing input types in InvokeAI do not meet your requirements. In such cases,
you can create your own input types.
existing fields in InvokeAI do not meet your requirements. In such cases, you
can create your own fields.

Let us create one as an example. Let us say we want to create a color input
field that represents a color code. But before we start on that here are some
general good practices to keep in mind.

**Good Practices**
### Best Practices

- There is no naming convention for input fields but we highly recommend that
you name it something appropriate like `ColorField`.
- It is not mandatory but it is heavily recommended to add a relevant
`docstring` to describe your input field.
`docstring` to describe your field.
- Keep your field in the same file as the Invocation that it is made for or in
another file where it is relevant.

Expand All @@ -378,10 +364,13 @@ class ColorField(BaseModel):
pass
```

Perfect. Now let us create our custom inputs for our field. This is exactly
similar how you created input fields for your Invocation. All the same rules
apply. Let us create four fields representing the _red(r)_, _blue(b)_,
_green(g)_ and _alpha(a)_ channel of the color.
Perfect. Now let us create the properties for our field. This is similar to how
you created input fields for your Invocation. All the same rules apply. Let us
create four fields representing the _red(r)_, _blue(b)_, _green(g)_ and
_alpha(a)_ channel of the color.

> Technically, the properties are _also_ called fields - but in this case, it
> refers to a `pydantic` field.

```python
class ColorField(BaseModel):
Expand All @@ -396,25 +385,11 @@ That's it. We now have a new input field type that we can use in our Invocations
like this.

```python
color: ColorField = Field(default=ColorField(r=0, g=0, b=0, a=0), description='Background color of an image')
color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=0), description='Background color of an image')
```

### Custom Components For Frontend

Every backend input type should have a corresponding frontend component so the
UI knows what to render when you use a particular field type.

If you are using existing field types, we already have components for those. So
you don't have to worry about creating anything new. But this might not always
be the case. Sometimes you might want to create new field types and have the
frontend UI deal with it in a different way.
### Using the custom field

This is where we venture into the world of React and Javascript and create our
own new components for our Invocations. Do not fear the world of JS. It's
actually pretty straightforward.
When you start the UI, your custom field will be automatically recognized.

Let us create a new component for our custom color field we created above. When
we use a color field, let us say we want the UI to display a color picker for
the user to pick from rather than entering values. That is what we will build
now.
-->
Custom fields only support connection inputs in the Workflow Editor.
20 changes: 16 additions & 4 deletions invokeai/app/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

from logging import Logger

import torch

from invokeai.app.services.item_storage.item_storage_memory import ItemStorageMemory
from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk
from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache
from invokeai.app.services.shared.sqlite.sqlite_util import init_db
from invokeai.backend.model_manager.metadata import ModelMetadataStore
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
from invokeai.backend.util.logging import InvokeAILogger
from invokeai.version.invokeai_version import __version__

Expand All @@ -23,8 +28,6 @@
from ..services.invocation_services import InvocationServices
from ..services.invocation_stats.invocation_stats_default import InvocationStatsService
from ..services.invoker import Invoker
from ..services.latents_storage.latents_storage_disk import DiskLatentsStorage
from ..services.latents_storage.latents_storage_forward_cache import ForwardCacheLatentsStorage
from ..services.model_install import ModelInstallService
from ..services.model_manager.model_manager_default import ModelManagerService
from ..services.model_records import ModelRecordServiceSQL
Expand Down Expand Up @@ -68,6 +71,9 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger
logger.debug(f"Internet connectivity is {config.internet_available}")

output_folder = config.output_path
if output_folder is None:
raise ValueError("Output folder is not set")

image_files = DiskImageFileStorage(f"{output_folder}/images")

db = init_db(config=config, logger=logger, image_files=image_files)
Expand All @@ -84,7 +90,12 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger
image_records = SqliteImageRecordStorage(db=db)
images = ImageService()
invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size)
latents = ForwardCacheLatentsStorage(DiskLatentsStorage(f"{output_folder}/latents"))
tensors = ObjectSerializerForwardCache(
ObjectSerializerDisk[torch.Tensor](output_folder / "tensors", ephemeral=True)
)
conditioning = ObjectSerializerForwardCache(
ObjectSerializerDisk[ConditioningFieldData](output_folder / "conditioning", ephemeral=True)
)
model_manager = ModelManagerService(config, logger)
model_record_service = ModelRecordServiceSQL(db=db)
download_queue_service = DownloadQueueService(event_bus=events)
Expand Down Expand Up @@ -117,7 +128,6 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger
image_records=image_records,
images=images,
invocation_cache=invocation_cache,
latents=latents,
logger=logger,
model_manager=model_manager,
model_records=model_record_service,
Expand All @@ -131,6 +141,8 @@ def initialize(config: InvokeAIAppConfig, event_handler_id: int, logger: Logger
session_queue=session_queue,
urls=urls,
workflow_records=workflow_records,
tensors=tensors,
conditioning=conditioning,
)

ApiDependencies.invoker = Invoker(services)
Expand Down
2 changes: 1 addition & 1 deletion invokeai/app/api/routers/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from PIL import Image
from pydantic import BaseModel, Field, ValidationError

from invokeai.app.invocations.baseinvocation import MetadataField, MetadataFieldValidator
from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageRecordChanges, ResourceOrigin
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
Expand Down
3 changes: 1 addition & 2 deletions invokeai/app/api_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
from invokeai.version.invokeai_version import __version__

from .invocations.fields import InputFieldJSONSchemaExtra, OutputFieldJSONSchemaExtra
from .services.config import InvokeAIAppConfig

app_config = InvokeAIAppConfig.get_config()
Expand Down Expand Up @@ -57,8 +58,6 @@
from .api.sockets import SocketIO
from .invocations.baseinvocation import (
BaseInvocation,
InputFieldJSONSchemaExtra,
OutputFieldJSONSchemaExtra,
UIConfigBase,
)

Expand Down
Loading