feat: refactor services folder/module structure
Refactor services folder/module structure.
**Motivation**
While working on our services I've repeatedly encountered circular imports and a general lack of clarity regarding where to put things. The structure introduced goes a long way towards resolving those issues, setting us up for a clean structure going forward.
**Services**
Services are now in their own folder with a few files:
- `services/{service_name}/__init__.py`: init as needed, mostly empty now
- `services/{service_name}/{service_name}_base.py`: the base class for the service
- `services/{service_name}/{service_name}_{impl_type}.py`: the default concrete implementation of the service - typically one of `sqlite`, `default`, or `memory`
- `services/{service_name}/{service_name}_common.py`: any common items - models, exceptions, utilities, etc
Though it's a bit verbose to have the service name both as the folder name and the prefix for files, I found it is _extremely_ confusing to have all of the base classes just be named `base.py`. So, at the cost of some verbosity when importing things, I've included the service name in the filename.
There are some minor logic changes. For example, in `InvocationProcessor`, instead of assigning the model manager service to a variable to be used later in the file, the service is used directly via the `Invoker`.
**Shared**
Things that are used across disparate services are in `services/shared/`:
- `default_graphs.py`: previously in `services/`
- `graphs.py`: previously in `services/`
- `paginatation`: generic pagination models used in a few services
- `sqlite`: the `SqliteDatabase` class, other sqlite-specific things
2023-09-24 08:11:07 +00:00
|
|
|
from typing import Optional
|
2023-07-12 15:14:22 +00:00
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
from PIL.Image import Image as PILImageType
|
2023-05-21 12:15:44 +00:00
|
|
|
|
2023-10-17 08:42:02 +00:00
|
|
|
from invokeai.app.invocations.baseinvocation import MetadataField, WorkflowField
|
feat: refactor services folder/module structure
Refactor services folder/module structure.
**Motivation**
While working on our services I've repeatedly encountered circular imports and a general lack of clarity regarding where to put things. The structure introduced goes a long way towards resolving those issues, setting us up for a clean structure going forward.
**Services**
Services are now in their own folder with a few files:
- `services/{service_name}/__init__.py`: init as needed, mostly empty now
- `services/{service_name}/{service_name}_base.py`: the base class for the service
- `services/{service_name}/{service_name}_{impl_type}.py`: the default concrete implementation of the service - typically one of `sqlite`, `default`, or `memory`
- `services/{service_name}/{service_name}_common.py`: any common items - models, exceptions, utilities, etc
Though it's a bit verbose to have the service name both as the folder name and the prefix for files, I found it is _extremely_ confusing to have all of the base classes just be named `base.py`. So, at the cost of some verbosity when importing things, I've included the service name in the filename.
There are some minor logic changes. For example, in `InvocationProcessor`, instead of assigning the model manager service to a variable to be used later in the file, the service is used directly via the `Invoker`.
**Shared**
Things that are used across disparate services are in `services/shared/`:
- `default_graphs.py`: previously in `services/`
- `graphs.py`: previously in `services/`
- `paginatation`: generic pagination models used in a few services
- `sqlite`: the `SqliteDatabase` class, other sqlite-specific things
2023-09-24 08:11:07 +00:00
|
|
|
from invokeai.app.services.invoker import Invoker
|
|
|
|
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
|
|
|
|
|
|
|
from ..image_files.image_files_common import (
|
2023-07-20 05:44:22 +00:00
|
|
|
ImageFileDeleteException,
|
|
|
|
ImageFileNotFoundException,
|
|
|
|
ImageFileSaveException,
|
|
|
|
)
|
feat: refactor services folder/module structure
Refactor services folder/module structure.
**Motivation**
While working on our services I've repeatedly encountered circular imports and a general lack of clarity regarding where to put things. The structure introduced goes a long way towards resolving those issues, setting us up for a clean structure going forward.
**Services**
Services are now in their own folder with a few files:
- `services/{service_name}/__init__.py`: init as needed, mostly empty now
- `services/{service_name}/{service_name}_base.py`: the base class for the service
- `services/{service_name}/{service_name}_{impl_type}.py`: the default concrete implementation of the service - typically one of `sqlite`, `default`, or `memory`
- `services/{service_name}/{service_name}_common.py`: any common items - models, exceptions, utilities, etc
Though it's a bit verbose to have the service name both as the folder name and the prefix for files, I found it is _extremely_ confusing to have all of the base classes just be named `base.py`. So, at the cost of some verbosity when importing things, I've included the service name in the filename.
There are some minor logic changes. For example, in `InvocationProcessor`, instead of assigning the model manager service to a variable to be used later in the file, the service is used directly via the `Invoker`.
**Shared**
Things that are used across disparate services are in `services/shared/`:
- `default_graphs.py`: previously in `services/`
- `graphs.py`: previously in `services/`
- `paginatation`: generic pagination models used in a few services
- `sqlite`: the `SqliteDatabase` class, other sqlite-specific things
2023-09-24 08:11:07 +00:00
|
|
|
from ..image_records.image_records_common import (
|
|
|
|
ImageCategory,
|
|
|
|
ImageRecord,
|
|
|
|
ImageRecordChanges,
|
2023-07-20 05:44:22 +00:00
|
|
|
ImageRecordDeleteException,
|
|
|
|
ImageRecordNotFoundException,
|
|
|
|
ImageRecordSaveException,
|
feat: refactor services folder/module structure
Refactor services folder/module structure.
**Motivation**
While working on our services I've repeatedly encountered circular imports and a general lack of clarity regarding where to put things. The structure introduced goes a long way towards resolving those issues, setting us up for a clean structure going forward.
**Services**
Services are now in their own folder with a few files:
- `services/{service_name}/__init__.py`: init as needed, mostly empty now
- `services/{service_name}/{service_name}_base.py`: the base class for the service
- `services/{service_name}/{service_name}_{impl_type}.py`: the default concrete implementation of the service - typically one of `sqlite`, `default`, or `memory`
- `services/{service_name}/{service_name}_common.py`: any common items - models, exceptions, utilities, etc
Though it's a bit verbose to have the service name both as the folder name and the prefix for files, I found it is _extremely_ confusing to have all of the base classes just be named `base.py`. So, at the cost of some verbosity when importing things, I've included the service name in the filename.
There are some minor logic changes. For example, in `InvocationProcessor`, instead of assigning the model manager service to a variable to be used later in the file, the service is used directly via the `Invoker`.
**Shared**
Things that are used across disparate services are in `services/shared/`:
- `default_graphs.py`: previously in `services/`
- `graphs.py`: previously in `services/`
- `paginatation`: generic pagination models used in a few services
- `sqlite`: the `SqliteDatabase` class, other sqlite-specific things
2023-09-24 08:11:07 +00:00
|
|
|
InvalidImageCategoryException,
|
|
|
|
InvalidOriginException,
|
|
|
|
ResourceOrigin,
|
2023-07-20 05:44:22 +00:00
|
|
|
)
|
feat: refactor services folder/module structure
Refactor services folder/module structure.
**Motivation**
While working on our services I've repeatedly encountered circular imports and a general lack of clarity regarding where to put things. The structure introduced goes a long way towards resolving those issues, setting us up for a clean structure going forward.
**Services**
Services are now in their own folder with a few files:
- `services/{service_name}/__init__.py`: init as needed, mostly empty now
- `services/{service_name}/{service_name}_base.py`: the base class for the service
- `services/{service_name}/{service_name}_{impl_type}.py`: the default concrete implementation of the service - typically one of `sqlite`, `default`, or `memory`
- `services/{service_name}/{service_name}_common.py`: any common items - models, exceptions, utilities, etc
Though it's a bit verbose to have the service name both as the folder name and the prefix for files, I found it is _extremely_ confusing to have all of the base classes just be named `base.py`. So, at the cost of some verbosity when importing things, I've included the service name in the filename.
There are some minor logic changes. For example, in `InvocationProcessor`, instead of assigning the model manager service to a variable to be used later in the file, the service is used directly via the `Invoker`.
**Shared**
Things that are used across disparate services are in `services/shared/`:
- `default_graphs.py`: previously in `services/`
- `graphs.py`: previously in `services/`
- `paginatation`: generic pagination models used in a few services
- `sqlite`: the `SqliteDatabase` class, other sqlite-specific things
2023-09-24 08:11:07 +00:00
|
|
|
from .images_base import ImageServiceABC
|
|
|
|
from .images_common import ImageDTO, image_record_to_dto
|
2023-06-26 19:53:21 +00:00
|
|
|
|
2023-05-21 12:15:44 +00:00
|
|
|
|
|
|
|
class ImageService(ImageServiceABC):
|
2023-09-24 05:12:51 +00:00
|
|
|
__invoker: Invoker
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-09-24 05:12:51 +00:00
|
|
|
def start(self, invoker: Invoker) -> None:
|
|
|
|
self.__invoker = invoker
|
2023-05-17 09:13:53 +00:00
|
|
|
|
|
|
|
def create(
|
|
|
|
self,
|
|
|
|
image: PILImageType,
|
2023-05-27 11:39:20 +00:00
|
|
|
image_origin: ResourceOrigin,
|
2023-05-17 09:13:53 +00:00
|
|
|
image_category: ImageCategory,
|
2023-05-21 10:05:33 +00:00
|
|
|
node_id: Optional[str] = None,
|
|
|
|
session_id: Optional[str] = None,
|
2023-07-21 07:44:55 +00:00
|
|
|
board_id: Optional[str] = None,
|
feat(api): chore: pydantic & fastapi upgrade
Upgrade pydantic and fastapi to latest.
- pydantic~=2.4.2
- fastapi~=103.2
- fastapi-events~=0.9.1
**Big Changes**
There are a number of logic changes needed to support pydantic v2. Most changes are very simple, like using the new methods to serialized and deserialize models, but there are a few more complex changes.
**Invocations**
The biggest change relates to invocation creation, instantiation and validation.
Because pydantic v2 moves all validation logic into the rust pydantic-core, we may no longer directly stick our fingers into the validation pie.
Previously, we (ab)used models and fields to allow invocation fields to be optional at instantiation, but required when `invoke()` is called. We directly manipulated the fields and invocation models when calling `invoke()`.
With pydantic v2, this is much more involved. Changes to the python wrapper do not propagate down to the rust validation logic - you have to rebuild the model. This causes problem with concurrent access to the invocation classes and is not a free operation.
This logic has been totally refactored and we do not need to change the model any more. The details are in `baseinvocation.py`, in the `InputField` function and `BaseInvocation.invoke_internal()` method.
In the end, this implementation is cleaner.
**Invocation Fields**
In pydantic v2, you can no longer directly add or remove fields from a model.
Previously, we did this to add the `type` field to invocations.
**Invocation Decorators**
With pydantic v2, we instead use the imperative `create_model()` API to create a new model with the additional field. This is done in `baseinvocation.py` in the `invocation()` wrapper.
A similar technique is used for `invocation_output()`.
**Minor Changes**
There are a number of minor changes around the pydantic v2 models API.
**Protected `model_` Namespace**
All models' pydantic-provided methods and attributes are prefixed with `model_` and this is considered a protected namespace. This causes some conflict, because "model" means something to us, and we have a ton of pydantic models with attributes starting with "model_".
Forunately, there are no direct conflicts. However, in any pydantic model where we define an attribute or method that starts with "model_", we must tell set the protected namespaces to an empty tuple.
```py
class IPAdapterModelField(BaseModel):
model_name: str = Field(description="Name of the IP-Adapter model")
base_model: BaseModelType = Field(description="Base model")
model_config = ConfigDict(protected_namespaces=())
```
**Model Serialization**
Pydantic models no longer have `Model.dict()` or `Model.json()`.
Instead, we use `Model.model_dump()` or `Model.model_dump_json()`.
**Model Deserialization**
Pydantic models no longer have `Model.parse_obj()` or `Model.parse_raw()`, and there are no `parse_raw_as()` or `parse_obj_as()` functions.
Instead, you need to create a `TypeAdapter` object to parse python objects or JSON into a model.
```py
adapter_graph = TypeAdapter(Graph)
deserialized_graph_from_json = adapter_graph.validate_json(graph_json)
deserialized_graph_from_dict = adapter_graph.validate_python(graph_dict)
```
**Field Customisation**
Pydantic `Field`s no longer accept arbitrary args.
Now, you must put all additional arbitrary args in a `json_schema_extra` arg on the field.
**Schema Customisation**
FastAPI and pydantic schema generation now follows the OpenAPI version 3.1 spec.
This necessitates two changes:
- Our schema customization logic has been revised
- Schema parsing to build node templates has been revised
The specific aren't important, but this does present additional surface area for bugs.
**Performance Improvements**
Pydantic v2 is a full rewrite with a rust backend. This offers a substantial performance improvement (pydantic claims 5x to 50x depending on the task). We'll notice this the most during serialization and deserialization of sessions/graphs, which happens very very often - a couple times per node.
I haven't done any benchmarks, but anecdotally, graph execution is much faster. Also, very larges graphs - like with massive iterators - are much, much faster.
2023-09-24 08:11:07 +00:00
|
|
|
is_intermediate: Optional[bool] = False,
|
2023-10-17 06:23:10 +00:00
|
|
|
metadata: Optional[MetadataField] = None,
|
|
|
|
workflow: Optional[WorkflowField] = None,
|
2023-05-21 10:05:33 +00:00
|
|
|
) -> ImageDTO:
|
2023-05-27 11:39:20 +00:00
|
|
|
if image_origin not in ResourceOrigin:
|
|
|
|
raise InvalidOriginException
|
2023-05-23 08:59:43 +00:00
|
|
|
|
|
|
|
if image_category not in ImageCategory:
|
|
|
|
raise InvalidImageCategoryException
|
|
|
|
|
2023-09-24 05:12:51 +00:00
|
|
|
image_name = self.__invoker.services.names.create_image_name()
|
2023-05-21 12:15:44 +00:00
|
|
|
|
2023-05-23 08:59:43 +00:00
|
|
|
(width, height) = image.size
|
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
try:
|
2023-10-17 06:23:10 +00:00
|
|
|
if workflow is not None:
|
|
|
|
created_workflow = self.__invoker.services.workflow_records.create(workflow)
|
|
|
|
workflow_id = created_workflow.model_dump()["id"]
|
|
|
|
else:
|
|
|
|
workflow_id = None
|
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
# TODO: Consider using a transaction here to ensure consistency between storage and database
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.image_records.save(
|
2023-05-23 08:59:43 +00:00
|
|
|
# Non-nullable fields
|
2023-05-17 09:13:53 +00:00
|
|
|
image_name=image_name,
|
2023-05-27 11:39:20 +00:00
|
|
|
image_origin=image_origin,
|
2023-05-17 09:13:53 +00:00
|
|
|
image_category=image_category,
|
2023-05-23 08:59:43 +00:00
|
|
|
width=width,
|
|
|
|
height=height,
|
2023-05-25 13:47:18 +00:00
|
|
|
# Meta fields
|
|
|
|
is_intermediate=is_intermediate,
|
2023-05-23 08:59:43 +00:00
|
|
|
# Nullable fields
|
2023-05-17 09:13:53 +00:00
|
|
|
node_id=node_id,
|
|
|
|
metadata=metadata,
|
2023-07-12 15:14:22 +00:00
|
|
|
session_id=session_id,
|
2023-05-23 08:59:43 +00:00
|
|
|
)
|
2023-07-21 07:44:55 +00:00
|
|
|
if board_id is not None:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.board_image_records.add_image_to_board(board_id=board_id, image_name=image_name)
|
2023-10-18 07:16:36 +00:00
|
|
|
if workflow_id is not None:
|
|
|
|
self.__invoker.services.workflow_image_records.create(workflow_id=workflow_id, image_name=image_name)
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.image_files.save(
|
|
|
|
image_name=image_name, image=image, metadata=metadata, workflow=workflow
|
|
|
|
)
|
2023-06-16 05:52:32 +00:00
|
|
|
image_dto = self.get_dto(image_name)
|
2023-05-17 09:13:53 +00:00
|
|
|
|
feat(backend): selective invalidation for invocation cache
This change enhances the invocation cache logic to delete cache entries when the resources to which they refer are deleted.
For example, a cached output may refer to "some_image.png". If that image is deleted, and this particular cache entry is later retrieved by a node, that node's successors will receive references to the now non-existent "some_image.png". When they attempt to use that image, they will fail.
To resolve this, we need to invalidate the cache when the resources to which it refers are deleted. Two options:
- Invalidate the whole cache on every image/latents/etc delete
- Selectively invalidate cache entries when their resources are deleted
Node outputs can be any shape, with any number of resource references in arbitrarily nested pydantic models. Traversing that structure to identify resources is not trivial.
But invalidating the whole cache is a bit heavy-handed. It would be nice to be more selective.
Simple solution:
- Invocation outputs' resource references are always string identifiers - like the image's or latents' name
- Invocation outputs can be stringified, which includes said identifiers
- When the invocation is cached, we store the stringified output alongside the "live" output classes
- When a resource is deleted, pass its identifier to the cache service, which can then invalidate any cache entries that refer to it
The images and latents storage services have been outfitted with `on_deleted()` callbacks, and the cache service registers itself to handle those events. This logic was copied from `ItemStorageABC`.
`on_changed()` callback are also added to the images and latents services, though these are not currently used. Just following the existing pattern.
2023-09-20 08:26:47 +00:00
|
|
|
self._on_changed(image_dto)
|
2023-06-16 05:52:32 +00:00
|
|
|
return image_dto
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordSaveException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to save image record")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageFileSaveException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to save image file")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error(f"Problem saving image record and file: {str(e)}")
|
2023-05-21 12:15:44 +00:00
|
|
|
raise e
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-25 13:47:18 +00:00
|
|
|
def update(
|
|
|
|
self,
|
|
|
|
image_name: str,
|
|
|
|
changes: ImageRecordChanges,
|
|
|
|
) -> ImageDTO:
|
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.image_records.update(image_name, changes)
|
feat(backend): selective invalidation for invocation cache
This change enhances the invocation cache logic to delete cache entries when the resources to which they refer are deleted.
For example, a cached output may refer to "some_image.png". If that image is deleted, and this particular cache entry is later retrieved by a node, that node's successors will receive references to the now non-existent "some_image.png". When they attempt to use that image, they will fail.
To resolve this, we need to invalidate the cache when the resources to which it refers are deleted. Two options:
- Invalidate the whole cache on every image/latents/etc delete
- Selectively invalidate cache entries when their resources are deleted
Node outputs can be any shape, with any number of resource references in arbitrarily nested pydantic models. Traversing that structure to identify resources is not trivial.
But invalidating the whole cache is a bit heavy-handed. It would be nice to be more selective.
Simple solution:
- Invocation outputs' resource references are always string identifiers - like the image's or latents' name
- Invocation outputs can be stringified, which includes said identifiers
- When the invocation is cached, we store the stringified output alongside the "live" output classes
- When a resource is deleted, pass its identifier to the cache service, which can then invalidate any cache entries that refer to it
The images and latents storage services have been outfitted with `on_deleted()` callbacks, and the cache service registers itself to handle those events. This logic was copied from `ItemStorageABC`.
`on_changed()` callback are also added to the images and latents services, though these are not currently used. Just following the existing pattern.
2023-09-20 08:26:47 +00:00
|
|
|
image_dto = self.get_dto(image_name)
|
|
|
|
self._on_changed(image_dto)
|
|
|
|
return image_dto
|
2023-05-25 13:47:18 +00:00
|
|
|
except ImageRecordSaveException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to update image record")
|
2023-05-25 13:47:18 +00:00
|
|
|
raise
|
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem updating image record")
|
2023-05-25 13:47:18 +00:00
|
|
|
raise e
|
|
|
|
|
2023-06-14 11:40:09 +00:00
|
|
|
def get_pil_image(self, image_name: str) -> PILImageType:
|
2023-05-17 09:13:53 +00:00
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
return self.__invoker.services.image_files.get(image_name)
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageFileNotFoundException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to get image file")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem getting image file")
|
2023-05-21 12:15:44 +00:00
|
|
|
raise e
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-06-14 11:40:09 +00:00
|
|
|
def get_record(self, image_name: str) -> ImageRecord:
|
2023-05-17 09:13:53 +00:00
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
return self.__invoker.services.image_records.get(image_name)
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordNotFoundException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Image record not found")
|
2023-05-21 10:05:33 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem getting image record")
|
2023-05-21 12:15:44 +00:00
|
|
|
raise e
|
|
|
|
|
2023-06-14 11:40:09 +00:00
|
|
|
def get_dto(self, image_name: str) -> ImageDTO:
|
2023-05-21 10:05:33 +00:00
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
image_record = self.__invoker.services.image_records.get(image_name)
|
2023-05-21 10:05:33 +00:00
|
|
|
|
|
|
|
image_dto = image_record_to_dto(
|
2023-10-18 07:16:36 +00:00
|
|
|
image_record=image_record,
|
|
|
|
image_url=self.__invoker.services.urls.get_image_url(image_name),
|
|
|
|
thumbnail_url=self.__invoker.services.urls.get_image_url(image_name, True),
|
|
|
|
board_id=self.__invoker.services.board_image_records.get_board_for_image(image_name),
|
|
|
|
workflow_id=self.__invoker.services.workflow_image_records.get_workflow_for_image(image_name),
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
2023-05-21 10:05:33 +00:00
|
|
|
|
|
|
|
return image_dto
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordNotFoundException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Image record not found")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem getting image DTO")
|
2023-05-21 12:15:44 +00:00
|
|
|
raise e
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-10-17 06:23:10 +00:00
|
|
|
def get_metadata(self, image_name: str) -> Optional[MetadataField]:
|
2023-07-12 15:14:22 +00:00
|
|
|
try:
|
2023-10-17 06:23:10 +00:00
|
|
|
return self.__invoker.services.image_records.get_metadata(image_name)
|
2023-07-12 15:14:22 +00:00
|
|
|
except ImageRecordNotFoundException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Image record not found")
|
2023-07-12 15:14:22 +00:00
|
|
|
raise
|
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem getting image DTO")
|
2023-07-12 15:14:22 +00:00
|
|
|
raise e
|
|
|
|
|
2023-10-18 07:16:36 +00:00
|
|
|
def get_workflow(self, image_name: str) -> Optional[WorkflowField]:
|
|
|
|
try:
|
|
|
|
workflow_id = self.__invoker.services.workflow_image_records.get_workflow_for_image(image_name)
|
|
|
|
if workflow_id is None:
|
|
|
|
return None
|
|
|
|
return self.__invoker.services.workflow_records.get(workflow_id)
|
|
|
|
except ImageRecordNotFoundException:
|
|
|
|
self.__invoker.services.logger.error("Image record not found")
|
|
|
|
raise
|
|
|
|
except Exception as e:
|
|
|
|
self.__invoker.services.logger.error("Problem getting image DTO")
|
|
|
|
raise e
|
|
|
|
|
2023-06-14 11:40:09 +00:00
|
|
|
def get_path(self, image_name: str, thumbnail: bool = False) -> str:
|
2023-05-22 05:48:12 +00:00
|
|
|
try:
|
feat(api): chore: pydantic & fastapi upgrade
Upgrade pydantic and fastapi to latest.
- pydantic~=2.4.2
- fastapi~=103.2
- fastapi-events~=0.9.1
**Big Changes**
There are a number of logic changes needed to support pydantic v2. Most changes are very simple, like using the new methods to serialized and deserialize models, but there are a few more complex changes.
**Invocations**
The biggest change relates to invocation creation, instantiation and validation.
Because pydantic v2 moves all validation logic into the rust pydantic-core, we may no longer directly stick our fingers into the validation pie.
Previously, we (ab)used models and fields to allow invocation fields to be optional at instantiation, but required when `invoke()` is called. We directly manipulated the fields and invocation models when calling `invoke()`.
With pydantic v2, this is much more involved. Changes to the python wrapper do not propagate down to the rust validation logic - you have to rebuild the model. This causes problem with concurrent access to the invocation classes and is not a free operation.
This logic has been totally refactored and we do not need to change the model any more. The details are in `baseinvocation.py`, in the `InputField` function and `BaseInvocation.invoke_internal()` method.
In the end, this implementation is cleaner.
**Invocation Fields**
In pydantic v2, you can no longer directly add or remove fields from a model.
Previously, we did this to add the `type` field to invocations.
**Invocation Decorators**
With pydantic v2, we instead use the imperative `create_model()` API to create a new model with the additional field. This is done in `baseinvocation.py` in the `invocation()` wrapper.
A similar technique is used for `invocation_output()`.
**Minor Changes**
There are a number of minor changes around the pydantic v2 models API.
**Protected `model_` Namespace**
All models' pydantic-provided methods and attributes are prefixed with `model_` and this is considered a protected namespace. This causes some conflict, because "model" means something to us, and we have a ton of pydantic models with attributes starting with "model_".
Forunately, there are no direct conflicts. However, in any pydantic model where we define an attribute or method that starts with "model_", we must tell set the protected namespaces to an empty tuple.
```py
class IPAdapterModelField(BaseModel):
model_name: str = Field(description="Name of the IP-Adapter model")
base_model: BaseModelType = Field(description="Base model")
model_config = ConfigDict(protected_namespaces=())
```
**Model Serialization**
Pydantic models no longer have `Model.dict()` or `Model.json()`.
Instead, we use `Model.model_dump()` or `Model.model_dump_json()`.
**Model Deserialization**
Pydantic models no longer have `Model.parse_obj()` or `Model.parse_raw()`, and there are no `parse_raw_as()` or `parse_obj_as()` functions.
Instead, you need to create a `TypeAdapter` object to parse python objects or JSON into a model.
```py
adapter_graph = TypeAdapter(Graph)
deserialized_graph_from_json = adapter_graph.validate_json(graph_json)
deserialized_graph_from_dict = adapter_graph.validate_python(graph_dict)
```
**Field Customisation**
Pydantic `Field`s no longer accept arbitrary args.
Now, you must put all additional arbitrary args in a `json_schema_extra` arg on the field.
**Schema Customisation**
FastAPI and pydantic schema generation now follows the OpenAPI version 3.1 spec.
This necessitates two changes:
- Our schema customization logic has been revised
- Schema parsing to build node templates has been revised
The specific aren't important, but this does present additional surface area for bugs.
**Performance Improvements**
Pydantic v2 is a full rewrite with a rust backend. This offers a substantial performance improvement (pydantic claims 5x to 50x depending on the task). We'll notice this the most during serialization and deserialization of sessions/graphs, which happens very very often - a couple times per node.
I haven't done any benchmarks, but anecdotally, graph execution is much faster. Also, very larges graphs - like with massive iterators - are much, much faster.
2023-09-24 08:11:07 +00:00
|
|
|
return str(self.__invoker.services.image_files.get_path(image_name, thumbnail))
|
2023-05-22 05:48:12 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem getting image path")
|
2023-05-22 05:48:12 +00:00
|
|
|
raise e
|
|
|
|
|
2023-05-23 12:57:29 +00:00
|
|
|
def validate_path(self, path: str) -> bool:
|
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
return self.__invoker.services.image_files.validate_path(path)
|
2023-05-23 12:57:29 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem validating image path")
|
2023-05-23 12:57:29 +00:00
|
|
|
raise e
|
|
|
|
|
2023-06-14 11:40:09 +00:00
|
|
|
def get_url(self, image_name: str, thumbnail: bool = False) -> str:
|
2023-05-22 05:48:12 +00:00
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
return self.__invoker.services.urls.get_image_url(image_name, thumbnail)
|
2023-05-22 05:48:12 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem getting image path")
|
2023-05-22 05:48:12 +00:00
|
|
|
raise e
|
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
def get_many(
|
|
|
|
self,
|
2023-05-28 08:59:14 +00:00
|
|
|
offset: int = 0,
|
|
|
|
limit: int = 10,
|
2023-05-27 11:39:20 +00:00
|
|
|
image_origin: Optional[ResourceOrigin] = None,
|
2023-05-28 08:59:14 +00:00
|
|
|
categories: Optional[list[ImageCategory]] = None,
|
2023-05-27 08:32:16 +00:00
|
|
|
is_intermediate: Optional[bool] = None,
|
2023-06-21 09:56:19 +00:00
|
|
|
board_id: Optional[str] = None,
|
2023-05-28 08:59:14 +00:00
|
|
|
) -> OffsetPaginatedResults[ImageDTO]:
|
2023-05-17 09:13:53 +00:00
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
results = self.__invoker.services.image_records.get_many(
|
2023-05-28 08:59:14 +00:00
|
|
|
offset,
|
|
|
|
limit,
|
2023-05-27 11:39:20 +00:00
|
|
|
image_origin,
|
2023-05-28 08:59:14 +00:00
|
|
|
categories,
|
2023-05-26 23:17:06 +00:00
|
|
|
is_intermediate,
|
2023-06-21 09:56:19 +00:00
|
|
|
board_id,
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
|
|
|
|
2023-11-10 23:44:43 +00:00
|
|
|
image_dtos = [image_record_to_dto(
|
2023-10-18 07:16:36 +00:00
|
|
|
image_record=r,
|
|
|
|
image_url=self.__invoker.services.urls.get_image_url(r.image_name),
|
|
|
|
thumbnail_url=self.__invoker.services.urls.get_image_url(r.image_name, True),
|
|
|
|
board_id=self.__invoker.services.board_image_records.get_board_for_image(r.image_name),
|
|
|
|
workflow_id=self.__invoker.services.workflow_image_records.get_workflow_for_image(r.image_name),
|
2023-11-10 23:44:43 +00:00
|
|
|
) for r in results.items]
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-28 08:59:14 +00:00
|
|
|
return OffsetPaginatedResults[ImageDTO](
|
2023-05-21 10:05:33 +00:00
|
|
|
items=image_dtos,
|
2023-05-28 08:59:14 +00:00
|
|
|
offset=results.offset,
|
|
|
|
limit=results.limit,
|
2023-05-21 10:05:33 +00:00
|
|
|
total=results.total,
|
|
|
|
)
|
2023-05-17 09:13:53 +00:00
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem getting paginated image DTOs")
|
2023-05-21 12:15:44 +00:00
|
|
|
raise e
|
|
|
|
|
2023-06-14 11:40:09 +00:00
|
|
|
def delete(self, image_name: str):
|
2023-05-21 12:15:44 +00:00
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.image_files.delete(image_name)
|
|
|
|
self.__invoker.services.image_records.delete(image_name)
|
feat(backend): selective invalidation for invocation cache
This change enhances the invocation cache logic to delete cache entries when the resources to which they refer are deleted.
For example, a cached output may refer to "some_image.png". If that image is deleted, and this particular cache entry is later retrieved by a node, that node's successors will receive references to the now non-existent "some_image.png". When they attempt to use that image, they will fail.
To resolve this, we need to invalidate the cache when the resources to which it refers are deleted. Two options:
- Invalidate the whole cache on every image/latents/etc delete
- Selectively invalidate cache entries when their resources are deleted
Node outputs can be any shape, with any number of resource references in arbitrarily nested pydantic models. Traversing that structure to identify resources is not trivial.
But invalidating the whole cache is a bit heavy-handed. It would be nice to be more selective.
Simple solution:
- Invocation outputs' resource references are always string identifiers - like the image's or latents' name
- Invocation outputs can be stringified, which includes said identifiers
- When the invocation is cached, we store the stringified output alongside the "live" output classes
- When a resource is deleted, pass its identifier to the cache service, which can then invalidate any cache entries that refer to it
The images and latents storage services have been outfitted with `on_deleted()` callbacks, and the cache service registers itself to handle those events. This logic was copied from `ItemStorageABC`.
`on_changed()` callback are also added to the images and latents services, though these are not currently used. Just following the existing pattern.
2023-09-20 08:26:47 +00:00
|
|
|
self._on_deleted(image_name)
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordDeleteException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to delete image record")
|
2023-05-21 12:15:44 +00:00
|
|
|
raise
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageFileDeleteException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to delete image file")
|
2023-05-21 12:15:44 +00:00
|
|
|
raise
|
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem deleting image record and file")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise e
|
|
|
|
|
2023-06-26 19:53:21 +00:00
|
|
|
def delete_images_on_board(self, board_id: str):
|
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
image_names = self.__invoker.services.board_image_records.get_all_board_image_names_for_board(board_id)
|
feat(ui): another go at gallery (#3791)
* feat(ui): migrate listImages to RTK query using createEntityAdapter
- see comments in `endpoints/images.ts` for explanation of the caching
- so far, only manually updating `all` images when new image is generated. no other manual cache updates are implemented, but will be needed.
- fixed some weirdness with loading state components (like the spinners in gallery)
- added `useThumbnailFallback` for `IAIDndImage`, this displays the tiny webp thumbnail while the full-size images load
- comment out some old thunk related stuff in gallerySlice, which is no longer needed
* feat(ui): add manual cache updates for board changes (wip)
- update RTK Query caches when adding/removing single image to/from board
- work more on migrating all image-related operations to RTK Query
* update AddImagesToBoardContext so that it works when user uses context menu + modal
* handle case where no image is selected
* get assets working for main list and boards - dnd only
* feat(ui): migrate image uploads to RTK Query
- minor refactor of `ImageUploader` and `useImageUploadButton` hooks, simplify some logic
- style filesystem upload overlay to match existing UI
- replace all old `imageUploaded` thunks with `uploadImage` RTK Query calls, update associated logic including canvas related uploads
- simplify `PostUploadAction`s that only need to display user input
* feat(ui): remove `receivedPageOfImages` thunks
* feat(ui): remove `receivedImageUrls` thunk
* feat(ui): finish removing all images thunks
stuff now broken:
- image usage
- delete board images
- on first load, no image selected
* feat(ui): simplify `updateImage` cache manipulation
- we don't actually ever change categories, so we can remove a lot of logic
* feat(ui): simplify canvas autosave
- instead of using a network request to set the canvas generation as not intermediate, we can just do that in the graph
* feat(ui): simplify & handle edge cases in cache updates
* feat(db, api): support `board_id='none'` for `get_many` images queries
This allows us to get all images that are not on a board.
* chore(ui): regen types
* feat(ui): add `All Assets`, `No Board` boards
Restructure boards:
- `all images` is all images
- `all assets` is all assets
- `no board` is all images/assets without a board set
- user boards may have images and assets
Update caching logic
- much simpler without every board having sub-views of images and assets
- update drag and drop operations for all possible interactions
* chore(ui): regen types
* feat(ui): move download to top of context menu
* feat(ui): improve drop overlay styles
* fix(ui): fix image not selected on first load
- listen for first load of all images board, then select the first image
* feat(ui): refactor board deletion
api changes:
- add route to list all image names for a board. this is required to handle board + image deletion. we need to know every image in the board to determine the image usage across the app. this is fetched only when the delete board and images modal is opened so it's as efficient as it can be.
- update the delete board route to respond with a list of deleted `board_images` and `images`, as image names. this is needed to perform accurate clientside state & cache updates after deleting.
db changes:
- remove unused `board_images` service method to get paginated images dtos for a board. this is now done thru the list images endpoint & images service. needs a small logic change on `images.delete_images_on_board`
ui changes:
- simplify the delete board modal - no context, just minor prop drilling. this is feasible for boards only because the components that need to trigger and manipulate the modal are very close together in the tree
- add cache updates for `deleteBoard` & `deleteBoardAndImages` mutations
- the only thing we cannot do directly is on `deleteBoardAndImages`, update the `No Board` board. we'd need to insert image dtos that we may not have loaded. instead, i am just invalidating the tags for that `listImages` cache. so when you `deleteBoardAndImages`, the `No Board` will re-fetch the initial image limit. i think this is more efficient than e.g. fetching all image dtos to insert then inserting them.
- handle image usage for `deleteBoardAndImages`
- update all (i think/hope) the little bits and pieces in the UI to accomodate these changes
* fix(ui): fix board selection logic
* feat(ui): add delete board modal loading state
* fix(ui): use thumbnails for board cover images
* fix(ui): fix race condition with board selection
when selecting a board that doesn't have any images loaded, we need to wait until the images haveloaded before selecting the first image.
this logic is debounced to ~1000ms.
* feat(ui): name 'No Board' correctly, change icon
* fix(ui): do not cache listAllImageNames query
if we cache it, we can end up with stale image usage during deletion.
we could of course manually update the cache as we are doing elsewhere. but because this is a relatively infrequent network request, i'd like to trade increased cache mgmt complexity here for increased resource usage.
* feat(ui): reduce drag preview opacity, remove border
* fix(ui): fix incorrect queryArg used in `deleteImage` and `updateImage` cache updates
* fix(ui): fix doubled open in new tab
* fix(ui): fix new generations not getting added to 'No Board'
* fix(ui): fix board id not changing on new image when autosave enabled
* fix(ui): context menu when selection is 0
need to revise how context menu is triggered later, when we approach multi select
* fix(ui): fix deleting does not update counts for all images and all assets
* fix(ui): fix all assets board name in boards list collapse button
* fix(ui): ensure we never go under 0 for total board count
* fix(ui): fix text overflow on board names
---------
Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
2023-07-19 16:06:38 +00:00
|
|
|
for image_name in image_names:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.image_files.delete(image_name)
|
|
|
|
self.__invoker.services.image_records.delete_many(image_names)
|
feat(backend): selective invalidation for invocation cache
This change enhances the invocation cache logic to delete cache entries when the resources to which they refer are deleted.
For example, a cached output may refer to "some_image.png". If that image is deleted, and this particular cache entry is later retrieved by a node, that node's successors will receive references to the now non-existent "some_image.png". When they attempt to use that image, they will fail.
To resolve this, we need to invalidate the cache when the resources to which it refers are deleted. Two options:
- Invalidate the whole cache on every image/latents/etc delete
- Selectively invalidate cache entries when their resources are deleted
Node outputs can be any shape, with any number of resource references in arbitrarily nested pydantic models. Traversing that structure to identify resources is not trivial.
But invalidating the whole cache is a bit heavy-handed. It would be nice to be more selective.
Simple solution:
- Invocation outputs' resource references are always string identifiers - like the image's or latents' name
- Invocation outputs can be stringified, which includes said identifiers
- When the invocation is cached, we store the stringified output alongside the "live" output classes
- When a resource is deleted, pass its identifier to the cache service, which can then invalidate any cache entries that refer to it
The images and latents storage services have been outfitted with `on_deleted()` callbacks, and the cache service registers itself to handle those events. This logic was copied from `ItemStorageABC`.
`on_changed()` callback are also added to the images and latents services, though these are not currently used. Just following the existing pattern.
2023-09-20 08:26:47 +00:00
|
|
|
for image_name in image_names:
|
|
|
|
self._on_deleted(image_name)
|
2023-06-26 19:53:21 +00:00
|
|
|
except ImageRecordDeleteException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to delete image records")
|
2023-06-26 19:53:21 +00:00
|
|
|
raise
|
|
|
|
except ImageFileDeleteException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to delete image files")
|
2023-06-26 19:53:21 +00:00
|
|
|
raise
|
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem deleting image records and files")
|
2023-06-26 19:53:21 +00:00
|
|
|
raise e
|
2023-07-20 05:44:22 +00:00
|
|
|
|
|
|
|
def delete_intermediates(self) -> int:
|
2023-07-19 14:55:29 +00:00
|
|
|
try:
|
2023-09-24 05:12:51 +00:00
|
|
|
image_names = self.__invoker.services.image_records.delete_intermediates()
|
2023-07-20 05:44:22 +00:00
|
|
|
count = len(image_names)
|
|
|
|
for image_name in image_names:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.image_files.delete(image_name)
|
feat(backend): selective invalidation for invocation cache
This change enhances the invocation cache logic to delete cache entries when the resources to which they refer are deleted.
For example, a cached output may refer to "some_image.png". If that image is deleted, and this particular cache entry is later retrieved by a node, that node's successors will receive references to the now non-existent "some_image.png". When they attempt to use that image, they will fail.
To resolve this, we need to invalidate the cache when the resources to which it refers are deleted. Two options:
- Invalidate the whole cache on every image/latents/etc delete
- Selectively invalidate cache entries when their resources are deleted
Node outputs can be any shape, with any number of resource references in arbitrarily nested pydantic models. Traversing that structure to identify resources is not trivial.
But invalidating the whole cache is a bit heavy-handed. It would be nice to be more selective.
Simple solution:
- Invocation outputs' resource references are always string identifiers - like the image's or latents' name
- Invocation outputs can be stringified, which includes said identifiers
- When the invocation is cached, we store the stringified output alongside the "live" output classes
- When a resource is deleted, pass its identifier to the cache service, which can then invalidate any cache entries that refer to it
The images and latents storage services have been outfitted with `on_deleted()` callbacks, and the cache service registers itself to handle those events. This logic was copied from `ItemStorageABC`.
`on_changed()` callback are also added to the images and latents services, though these are not currently used. Just following the existing pattern.
2023-09-20 08:26:47 +00:00
|
|
|
self._on_deleted(image_name)
|
2023-07-19 14:55:29 +00:00
|
|
|
return count
|
|
|
|
except ImageRecordDeleteException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to delete image records")
|
2023-07-19 14:55:29 +00:00
|
|
|
raise
|
|
|
|
except ImageFileDeleteException:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Failed to delete image files")
|
2023-07-19 14:55:29 +00:00
|
|
|
raise
|
|
|
|
except Exception as e:
|
2023-09-24 05:12:51 +00:00
|
|
|
self.__invoker.services.logger.error("Problem deleting image records and files")
|
2023-07-19 14:55:29 +00:00
|
|
|
raise e
|
2023-10-17 07:28:36 +00:00
|
|
|
|
|
|
|
def get_intermediates_count(self) -> int:
|
|
|
|
try:
|
|
|
|
return self.__invoker.services.image_records.get_intermediates_count()
|
|
|
|
except Exception as e:
|
|
|
|
self.__invoker.services.logger.error("Problem getting intermediates count")
|
|
|
|
raise e
|