feat: workflow library - system graphs - wip

This commit is contained in:
psychedelicious 2023-12-01 19:36:38 +11:00
parent 0a25efd054
commit b0350e9bc8
22 changed files with 1167 additions and 185 deletions

View File

@ -30,6 +30,7 @@ from ..services.session_queue.session_queue_sqlite import SqliteSessionQueue
from ..services.shared.default_graphs import create_system_graphs
from ..services.shared.graph import GraphExecutionState, LibraryGraph
from ..services.shared.sqlite.sqlite_database import SqliteDatabase
from ..services.shared.system_workflows import create_system_workflows
from ..services.urls.urls_default import LocalUrlService
from ..services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from .events import FastAPIEventService
@ -123,6 +124,7 @@ class ApiDependencies:
)
create_system_graphs(services.graph_library)
create_system_workflows(workflow_records=services.workflow_records, logger=logger)
db.clean()
ApiDependencies.invoker = Invoker(services)

View File

@ -7,6 +7,7 @@ from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.workflow_records.workflow_records_common import (
Workflow,
WorkflowCategory,
WorkflowNotFoundError,
WorkflowRecordDTO,
WorkflowRecordListItemDTO,
@ -70,7 +71,7 @@ async def create_workflow(
workflow: WorkflowWithoutID = Body(description="The workflow to create", embed=True),
) -> WorkflowRecordDTO:
"""Creates a workflow"""
return ApiDependencies.invoker.services.workflow_records.create(workflow)
return ApiDependencies.invoker.services.workflow_records.create(workflow=workflow)
@workflows_router.get(
@ -85,9 +86,10 @@ async def list_workflows(
per_page: int = Query(default=10, description="The number of workflows per page"),
order_by: WorkflowRecordOrderBy = Query(default=WorkflowRecordOrderBy.Name, description="The order by"),
direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The order by"),
category: WorkflowCategory = Query(default=WorkflowCategory.User, description="The category to get"),
filter_text: Optional[str] = Query(default=None, description="The name to filter by"),
) -> PaginatedResults[WorkflowRecordListItemDTO]:
"""Gets a page of workflows"""
return ApiDependencies.invoker.services.workflow_records.get_many(
page=page, per_page=per_page, order_by=order_by, direction=direction, filter_text=filter_text
page=page, per_page=per_page, order_by=order_by, direction=direction, filter_text=filter_text, category=category
)

View File

@ -0,0 +1,787 @@
{
"id": "af7030e2-c64f-4f03-b387-9634bb54ae5f",
"name": "Text to Image - SD1.5",
"author": "InvokeAI",
"description": "Sample text to image workflow for Stable Diffusion 1.5/2",
"version": "1.1.0",
"contact": "invoke@invoke.ai",
"tags": "text2image, SD1.5, SD2, default",
"notes": "",
"exposedFields": [
{
"nodeId": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"fieldName": "model"
},
{
"nodeId": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"fieldName": "prompt"
},
{
"nodeId": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"fieldName": "prompt"
},
{
"nodeId": "55705012-79b9-4aac-9f26-c0b10309785b",
"fieldName": "width"
},
{
"nodeId": "55705012-79b9-4aac-9f26-c0b10309785b",
"fieldName": "height"
}
],
"meta": {
"category": "system",
"version": "2.0.0"
},
"nodes": [
{
"id": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"type": "invocation",
"data": {
"id": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"type": "compel",
"label": "Negative Compel Prompt",
"isOpen": true,
"notes": "",
"isIntermediate": true,
"useCache": true,
"version": "1.0.0",
"nodePack": "invokeai",
"inputs": {
"prompt": {
"id": "7739aff6-26cb-4016-8897-5a1fb2305e4e",
"name": "prompt",
"fieldKind": "input",
"label": "Negative Prompt",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "StringField"
},
"value": ""
},
"clip": {
"id": "48d23dce-a6ae-472a-9f8c-22a714ea5ce0",
"name": "clip",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ClipField"
}
}
},
"outputs": {
"conditioning": {
"id": "37cf3a9d-f6b7-4b64-8ff6-2558c5ecc447",
"name": "conditioning",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ConditioningField"
}
}
}
},
"width": 320,
"height": 259,
"position": {
"x": 1000,
"y": 350
}
},
{
"id": "55705012-79b9-4aac-9f26-c0b10309785b",
"type": "invocation",
"data": {
"id": "55705012-79b9-4aac-9f26-c0b10309785b",
"type": "noise",
"label": "",
"isOpen": true,
"notes": "",
"isIntermediate": true,
"useCache": true,
"version": "1.0.1",
"nodePack": "invokeai",
"inputs": {
"seed": {
"id": "6431737c-918a-425d-a3b4-5d57e2f35d4d",
"name": "seed",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
},
"value": 0
},
"width": {
"id": "38fc5b66-fe6e-47c8-bba9-daf58e454ed7",
"name": "width",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
},
"value": 512
},
"height": {
"id": "16298330-e2bf-4872-a514-d6923df53cbb",
"name": "height",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
},
"value": 512
},
"use_cpu": {
"id": "c7c436d3-7a7a-4e76-91e4-c6deb271623c",
"name": "use_cpu",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "BooleanField"
},
"value": true
}
},
"outputs": {
"noise": {
"id": "50f650dc-0184-4e23-a927-0497a96fe954",
"name": "noise",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "LatentsField"
}
},
"width": {
"id": "bb8a452b-133d-42d1-ae4a-3843d7e4109a",
"name": "width",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
}
},
"height": {
"id": "35cfaa12-3b8b-4b7a-a884-327ff3abddd9",
"name": "height",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
}
}
}
},
"width": 320,
"height": 388,
"position": {
"x": 600,
"y": 325
}
},
{
"id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"type": "invocation",
"data": {
"id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"type": "main_model_loader",
"label": "",
"isOpen": true,
"notes": "",
"isIntermediate": true,
"useCache": true,
"version": "1.0.0",
"nodePack": "invokeai",
"inputs": {
"model": {
"id": "993eabd2-40fd-44fe-bce7-5d0c7075ddab",
"name": "model",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "MainModelField"
},
"value": {
"model_name": "stable-diffusion-v1-5",
"base_model": "sd-1",
"model_type": "main"
}
}
},
"outputs": {
"unet": {
"id": "5c18c9db-328d-46d0-8cb9-143391c410be",
"name": "unet",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "UNetField"
}
},
"clip": {
"id": "6effcac0-ec2f-4bf5-a49e-a2c29cf921f4",
"name": "clip",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ClipField"
}
},
"vae": {
"id": "57683ba3-f5f5-4f58-b9a2-4b83dacad4a1",
"name": "vae",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "VaeField"
}
}
}
},
"width": 320,
"height": 226,
"position": {
"x": 600,
"y": 25
}
},
{
"id": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"type": "invocation",
"data": {
"id": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"type": "compel",
"label": "Positive Compel Prompt",
"isOpen": true,
"notes": "",
"isIntermediate": true,
"useCache": true,
"version": "1.0.0",
"nodePack": "invokeai",
"inputs": {
"prompt": {
"id": "7739aff6-26cb-4016-8897-5a1fb2305e4e",
"name": "prompt",
"fieldKind": "input",
"label": "Positive Prompt",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "StringField"
},
"value": "Super cute tiger cub, national geographic award-winning photograph"
},
"clip": {
"id": "48d23dce-a6ae-472a-9f8c-22a714ea5ce0",
"name": "clip",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ClipField"
}
}
},
"outputs": {
"conditioning": {
"id": "37cf3a9d-f6b7-4b64-8ff6-2558c5ecc447",
"name": "conditioning",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ConditioningField"
}
}
}
},
"width": 320,
"height": 259,
"position": {
"x": 1000,
"y": 25
}
},
{
"id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
"type": "invocation",
"data": {
"id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
"type": "rand_int",
"label": "Random Seed",
"isOpen": false,
"notes": "",
"isIntermediate": true,
"useCache": false,
"version": "1.0.0",
"nodePack": "invokeai",
"inputs": {
"low": {
"id": "3ec65a37-60ba-4b6c-a0b2-553dd7a84b84",
"name": "low",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
},
"value": 0
},
"high": {
"id": "085f853a-1a5f-494d-8bec-e4ba29a3f2d1",
"name": "high",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
},
"value": 2147483647
}
},
"outputs": {
"value": {
"id": "812ade4d-7699-4261-b9fc-a6c9d2ab55ee",
"name": "value",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
}
}
}
},
"width": 320,
"height": 32,
"position": {
"x": 600,
"y": 275
}
},
{
"id": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
"type": "invocation",
"data": {
"id": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
"type": "denoise_latents",
"label": "",
"isOpen": true,
"notes": "",
"isIntermediate": true,
"useCache": true,
"version": "1.4.0",
"nodePack": "invokeai",
"inputs": {
"positive_conditioning": {
"id": "90b7f4f8-ada7-4028-8100-d2e54f192052",
"name": "positive_conditioning",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ConditioningField"
}
},
"negative_conditioning": {
"id": "9393779e-796c-4f64-b740-902a1177bf53",
"name": "negative_conditioning",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ConditioningField"
}
},
"noise": {
"id": "8e17f1e5-4f98-40b1-b7f4-86aeeb4554c1",
"name": "noise",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "LatentsField"
}
},
"steps": {
"id": "9b63302d-6bd2-42c9-ac13-9b1afb51af88",
"name": "steps",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
},
"value": 50
},
"cfg_scale": {
"id": "87dd04d3-870e-49e1-98bf-af003a810109",
"name": "cfg_scale",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": true,
"name": "FloatField"
},
"value": 7.5
},
"denoising_start": {
"id": "f369d80f-4931-4740-9bcd-9f0620719fab",
"name": "denoising_start",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "FloatField"
},
"value": 0
},
"denoising_end": {
"id": "747d10e5-6f02-445c-994c-0604d814de8c",
"name": "denoising_end",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "FloatField"
},
"value": 1
},
"scheduler": {
"id": "1de84a4e-3a24-4ec8-862b-16ce49633b9b",
"name": "scheduler",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "SchedulerField"
},
"value": "unipc"
},
"unet": {
"id": "ffa6fef4-3ce2-4bdb-9296-9a834849489b",
"name": "unet",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "UNetField"
}
},
"control": {
"id": "077b64cb-34be-4fcc-83f2-e399807a02bd",
"name": "control",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": true,
"name": "ControlField"
}
},
"ip_adapter": {
"id": "1d6948f7-3a65-4a65-a20c-768b287251aa",
"name": "ip_adapter",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": true,
"name": "IPAdapterField"
}
},
"t2i_adapter": {
"id": "75e67b09-952f-4083-aaf4-6b804d690412",
"name": "t2i_adapter",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": true,
"name": "T2IAdapterField"
}
},
"latents": {
"id": "334d4ba3-5a99-4195-82c5-86fb3f4f7d43",
"name": "latents",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "LatentsField"
}
},
"denoise_mask": {
"id": "0d3dbdbf-b014-4e95-8b18-ff2ff9cb0bfa",
"name": "denoise_mask",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "DenoiseMaskField"
}
}
},
"outputs": {
"latents": {
"id": "70fa5bbc-0c38-41bb-861a-74d6d78d2f38",
"name": "latents",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "LatentsField"
}
},
"width": {
"id": "98ee0e6c-82aa-4e8f-8be5-dc5f00ee47f0",
"name": "width",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
}
},
"height": {
"id": "e8cb184a-5e1a-47c8-9695-4b8979564f5d",
"name": "height",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
}
}
}
},
"width": 320,
"height": 646,
"position": {
"x": 1400,
"y": 25
}
},
{
"id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
"type": "invocation",
"data": {
"id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
"type": "l2i",
"label": "",
"isOpen": true,
"notes": "",
"isIntermediate": false,
"useCache": true,
"version": "1.2.0",
"nodePack": "invokeai",
"inputs": {
"metadata": {
"id": "ab375f12-0042-4410-9182-29e30db82c85",
"name": "metadata",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "MetadataField"
}
},
"latents": {
"id": "3a7e7efd-bff5-47d7-9d48-615127afee78",
"name": "latents",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "LatentsField"
}
},
"vae": {
"id": "a1f5f7a1-0795-4d58-b036-7820c0b0ef2b",
"name": "vae",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "VaeField"
}
},
"tiled": {
"id": "da52059a-0cee-4668-942f-519aa794d739",
"name": "tiled",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "BooleanField"
},
"value": false
},
"fp32": {
"id": "c4841df3-b24e-4140-be3b-ccd454c2522c",
"name": "fp32",
"fieldKind": "input",
"label": "",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "BooleanField"
},
"value": true
}
},
"outputs": {
"image": {
"id": "72d667d0-cf85-459d-abf2-28bd8b823fe7",
"name": "image",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "ImageField"
}
},
"width": {
"id": "c8c907d8-1066-49d1-b9a6-83bdcd53addc",
"name": "width",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
}
},
"height": {
"id": "230f359c-b4ea-436c-b372-332d7dcdca85",
"name": "height",
"fieldKind": "output",
"type": {
"isCollection": false,
"isCollectionOrScalar": false,
"name": "IntegerField"
}
}
}
},
"width": 320,
"height": 266,
"position": {
"x": 1800,
"y": 25
}
}
],
"edges": [
{
"id": "reactflow__edge-ea94bc37-d995-4a83-aa99-4af42479f2f2value-55705012-79b9-4aac-9f26-c0b10309785bseed",
"source": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
"target": "55705012-79b9-4aac-9f26-c0b10309785b",
"type": "default",
"sourceHandle": "value",
"targetHandle": "seed"
},
{
"id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-7d8bf987-284f-413a-b2fd-d825445a5d6cclip",
"source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"target": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"type": "default",
"sourceHandle": "clip",
"targetHandle": "clip"
},
{
"id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-93dc02a4-d05b-48ed-b99c-c9b616af3402clip",
"source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"target": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"type": "default",
"sourceHandle": "clip",
"targetHandle": "clip"
},
{
"id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-eea2702a-19fb-45b5-9d75-56b4211ec03cnoise",
"source": "55705012-79b9-4aac-9f26-c0b10309785b",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
"type": "default",
"sourceHandle": "noise",
"targetHandle": "noise"
},
{
"id": "reactflow__edge-7d8bf987-284f-413a-b2fd-d825445a5d6cconditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cpositive_conditioning",
"source": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
"type": "default",
"sourceHandle": "conditioning",
"targetHandle": "positive_conditioning"
},
{
"id": "reactflow__edge-93dc02a4-d05b-48ed-b99c-c9b616af3402conditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cnegative_conditioning",
"source": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
"type": "default",
"sourceHandle": "conditioning",
"targetHandle": "negative_conditioning"
},
{
"id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8unet-eea2702a-19fb-45b5-9d75-56b4211ec03cunet",
"source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
"type": "default",
"sourceHandle": "unet",
"targetHandle": "unet"
},
{
"id": "reactflow__edge-eea2702a-19fb-45b5-9d75-56b4211ec03clatents-58c957f5-0d01-41fc-a803-b2bbf0413d4flatents",
"source": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
"target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
"type": "default",
"sourceHandle": "latents",
"targetHandle": "latents"
},
{
"id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8vae-58c957f5-0d01-41fc-a803-b2bbf0413d4fvae",
"source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
"target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
"type": "default",
"sourceHandle": "vae",
"targetHandle": "vae"
}
]
}

View File

@ -0,0 +1,33 @@
from logging import Logger
from pathlib import Path
import semver
import invokeai.app.assets.workflows as system_workflows_dir
from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowNotFoundError, WorkflowValidator
system_workflows = Path(system_workflows_dir.__path__[0]).glob("*.json")
def create_system_workflows(workflow_records: WorkflowRecordsStorageBase, logger: Logger) -> None:
"""Creates the system workflows."""
for workflow_filename in system_workflows:
with open(workflow_filename, "rb") as f:
workflow_bytes = f.read()
if workflow_bytes is None:
raise ValueError(f"Could not find system workflow: {workflow_filename}")
new_workflow = WorkflowValidator.validate_json(workflow_bytes)
try:
installed_workflow = workflow_records.get(new_workflow.id).workflow
installed_version = semver.Version.parse(installed_workflow.version)
new_version = semver.Version.parse(new_workflow.version)
if new_version.compare(installed_version) > 0:
logger.info(f"Updating system workflow: {new_workflow.name}")
workflow_records._add_system_workflow(new_workflow)
except WorkflowNotFoundError:
logger.info(f"Installing system workflow: {new_workflow.name}")
workflow_records._add_system_workflow(new_workflow)

View File

@ -5,6 +5,7 @@ from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.workflow_records.workflow_records_common import (
Workflow,
WorkflowCategory,
WorkflowRecordDTO,
WorkflowRecordListItemDTO,
WorkflowRecordOrderBy,
@ -42,7 +43,13 @@ class WorkflowRecordsStorageBase(ABC):
per_page: int,
order_by: WorkflowRecordOrderBy,
direction: SQLiteDirection,
category: WorkflowCategory,
filter_text: Optional[str],
) -> PaginatedResults[WorkflowRecordListItemDTO]:
"""Gets many workflows."""
pass
@abstractmethod
def _add_system_workflow(self, workflow: Workflow) -> None:
"""Adds a system workflow. Internal use only."""
pass

View File

@ -16,8 +16,27 @@ class ExposedField(BaseModel):
fieldName: str
class WorkflowNotFoundError(Exception):
"""Raised when a workflow is not found"""
class WorkflowRecordOrderBy(str, Enum, metaclass=MetaEnum):
"""The order by options for workflow records"""
CreatedAt = "created_at"
UpdatedAt = "updated_at"
OpenedAt = "opened_at"
Name = "name"
class WorkflowCategory(str, Enum, metaclass=MetaEnum):
User = "user"
System = "system"
class WorkflowMeta(BaseModel):
version: str = Field(description="The version of the workflow schema.")
category: WorkflowCategory = Field(description="The category of the workflow (user or system).")
@field_validator("version")
def validate_version(cls, version: str):
@ -56,14 +75,17 @@ class Workflow(WorkflowWithoutID):
WorkflowValidator = TypeAdapter(Workflow)
class WorkflowRecordDTO(BaseModel):
class WorkflowRecordDTOBase(BaseModel):
workflow_id: str = Field(description="The id of the workflow.")
workflow: Workflow = Field(description="The workflow.")
name: str = Field(description="The name of the workflow.")
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.")
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.")
opened_at: Union[datetime.datetime, str] = Field(description="The opened timestamp of the workflow.")
class WorkflowRecordDTO(WorkflowRecordDTOBase):
workflow: Workflow = Field(description="The workflow.")
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "WorkflowRecordDTO":
data["workflow"] = WorkflowValidator.validate_json(data.get("workflow", ""))
@ -73,26 +95,8 @@ class WorkflowRecordDTO(BaseModel):
WorkflowRecordDTOValidator = TypeAdapter(WorkflowRecordDTO)
class WorkflowRecordListItemDTO(BaseModel):
workflow_id: str = Field(description="The id of the workflow.")
name: str = Field(description="The name of the workflow.")
class WorkflowRecordListItemDTO(WorkflowRecordDTOBase):
description: str = Field(description="The description of the workflow.")
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.")
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.")
opened_at: Union[datetime.datetime, str] = Field(description="The opened timestamp of the workflow.")
WorkflowRecordListItemDTOValidator = TypeAdapter(WorkflowRecordListItemDTO)
class WorkflowNotFoundError(Exception):
"""Raised when a workflow is not found"""
class WorkflowRecordOrderBy(str, Enum, metaclass=MetaEnum):
"""The order by options for workflow records"""
CreatedAt = "created_at"
UpdatedAt = "updated_at"
OpenedAt = "opened_at"
Name = "name"

View File

@ -7,6 +7,7 @@ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase
from invokeai.app.services.workflow_records.workflow_records_common import (
Workflow,
WorkflowCategory,
WorkflowNotFoundError,
WorkflowRecordDTO,
WorkflowRecordListItemDTO,
@ -40,6 +41,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
""",
(workflow_id,),
)
self._conn.commit()
self._cursor.execute(
"""--sql
SELECT workflow_id, workflow, name, created_at, updated_at, opened_at
@ -60,6 +62,8 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
def create(self, workflow: WorkflowWithoutID) -> WorkflowRecordDTO:
try:
# Only user workflows may be created by this method
assert workflow.meta.category is WorkflowCategory.User
workflow_with_id = WorkflowValidator.validate_python(workflow.model_dump())
self._lock.acquire()
self._cursor.execute(
@ -70,10 +74,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
)
VALUES (?, ?);
""",
(
workflow_with_id.id,
workflow_with_id.model_dump_json(),
),
(workflow_with_id.id, workflow_with_id.model_dump_json()),
)
self._conn.commit()
except Exception:
@ -90,7 +91,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
"""--sql
UPDATE workflow_library
SET workflow = ?
WHERE workflow_id = ?;
WHERE workflow_id = ? AND category = "user";
""",
(workflow.model_dump_json(), workflow.id),
)
@ -108,7 +109,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
self._cursor.execute(
"""--sql
DELETE from workflow_library
WHERE workflow_id = ?;
WHERE workflow_id = ? AND category = "user";
""",
(workflow_id,),
)
@ -126,6 +127,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
per_page: int,
order_by: WorkflowRecordOrderBy,
direction: SQLiteDirection,
category: WorkflowCategory,
filter_text: Optional[str] = None,
) -> PaginatedResults[WorkflowRecordListItemDTO]:
try:
@ -133,24 +135,27 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
# sanitize!
assert order_by in WorkflowRecordOrderBy
assert direction in SQLiteDirection
count_query = "SELECT COUNT(*) FROM workflow_library"
assert category in WorkflowCategory
count_query = "SELECT COUNT(*) FROM workflow_library WHERE category = ?"
main_query = """
SELECT
workflow_id,
category,
name,
description,
created_at,
updated_at,
opened_at
FROM workflow_library
WHERE category = ?
"""
main_params = []
count_params = []
main_params = [category.value]
count_params = [category.value]
stripped_filter_name = filter_text.strip() if filter_text else None
if stripped_filter_name:
filter_string = "%" + stripped_filter_name + "%"
main_query += " WHERE name LIKE ? OR description LIKE ? "
count_query += " WHERE name LIKE ? OR description LIKE ?;"
main_query += " AND name LIKE ? OR description LIKE ? "
count_query += " AND name LIKE ? OR description LIKE ?;"
main_params.extend([filter_string, filter_string])
count_params.extend([filter_string, filter_string])
@ -177,6 +182,28 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
finally:
self._lock.release()
def _add_system_workflow(self, workflow: Workflow) -> None:
try:
self._lock.acquire()
# Only system workflows may be created by this method
assert workflow.meta.category is WorkflowCategory.System
self._cursor.execute(
"""--sql
INSERT OR REPLACE INTO workflow_library (
workflow_id,
workflow
)
VALUES (?, ?);
""",
(workflow.id, workflow.model_dump_json()),
)
self._conn.commit()
except Exception:
self._conn.rollback()
raise
finally:
self._lock.release()
def _create_tables(self) -> None:
try:
self._lock.acquire()
@ -191,6 +218,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
-- updated manually when retrieving workflow
opened_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
-- Generated columns, needed for indexing and searching
category TEXT GENERATED ALWAYS as (json_extract(workflow, '$.meta.category')) VIRTUAL NOT NULL,
name TEXT GENERATED ALWAYS as (json_extract(workflow, '$.name')) VIRTUAL NOT NULL,
description TEXT GENERATED ALWAYS as (json_extract(workflow, '$.description')) VIRTUAL NOT NULL
);
@ -225,6 +253,11 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
CREATE INDEX IF NOT EXISTS idx_workflow_library_opened_at ON workflow_library(opened_at);
"""
)
self._cursor.execute(
"""--sql
CREATE INDEX IF NOT EXISTS idx_workflow_library_category ON workflow_library(category);
"""
)
self._cursor.execute(
"""--sql
CREATE INDEX IF NOT EXISTS idx_workflow_library_name ON workflow_library(name);

View File

@ -128,6 +128,8 @@
"random": "Random",
"reportBugLabel": "Report Bug",
"safetensors": "Safetensors",
"save": "Save",
"saveAs": "Save As",
"settingsLabel": "Settings",
"simple": "Simple",
"somethingWentWrong": "Something went wrong",
@ -1625,7 +1627,7 @@
"unnamedWorkflow": "Unnamed Workflow",
"downloadWorkflow": "Download Workflow",
"saveWorkflow": "Save Workflow",
"duplicateWorkflow": "Duplicate Workflow",
"saveWorkflowAs": "Save Workflow As",
"problemSavingWorkflow": "Problem Saving Workflow",
"workflowSaved": "Workflow Saved",
"noRecentWorkflows": "No Recent Workflows",
@ -1635,6 +1637,7 @@
"loading": "Loading Workflows",
"noDescription": "No description",
"searchWorkflows": "Search Workflows",
"clearWorkflowSearchFilter": "Clear Workflow Search Filter"
"clearWorkflowSearchFilter": "Clear Workflow Search Filter",
"workflowName": "Workflow Name"
}
}

View File

@ -4,7 +4,7 @@ import DownloadWorkflowButton from 'features/workflowLibrary/components/Download
import UploadWorkflowButton from 'features/workflowLibrary/components/LoadWorkflowFromFileButton';
import ResetWorkflowButton from 'features/workflowLibrary/components/ResetWorkflowButton';
import SaveWorkflowButton from 'features/workflowLibrary/components/SaveWorkflowButton';
import DuplicateWorkflowButton from 'features/workflowLibrary/components/DuplicateWorkflowButton';
import SaveWorkflowAsButton from 'features/workflowLibrary/components/SaveWorkflowAsButton';
const TopCenterPanel = () => {
return (
@ -20,7 +20,7 @@ const TopCenterPanel = () => {
<DownloadWorkflowButton />
<UploadWorkflowButton />
<SaveWorkflowButton />
<DuplicateWorkflowButton />
<SaveWorkflowAsButton />
<ResetWorkflowButton />
</Flex>
);

View File

@ -1,33 +1,4 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { cloneDeep, forEach, isEqual, uniqBy } from 'lodash-es';
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
Connection,
Edge,
EdgeChange,
EdgeRemoveChange,
getConnectedEdges,
getIncomers,
getOutgoers,
Node,
NodeChange,
OnConnectStartParams,
SelectionMode,
updateEdge,
Viewport,
XYPosition,
} from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import {
appSocketGeneratorProgress,
appSocketInvocationComplete,
appSocketInvocationError,
appSocketInvocationStarted,
appSocketQueueItemStatusChanged,
} from 'services/events/actions';
import { v4 as uuidv4 } from 'uuid';
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
import {
BoardFieldValue,
@ -58,6 +29,35 @@ import {
zNodeStatus,
} from 'features/nodes/types/invocation';
import { WorkflowV2 } from 'features/nodes/types/workflow';
import { cloneDeep, forEach, isEqual, uniqBy } from 'lodash-es';
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
Connection,
Edge,
EdgeChange,
EdgeRemoveChange,
getConnectedEdges,
getIncomers,
getOutgoers,
Node,
NodeChange,
OnConnectStartParams,
SelectionMode,
updateEdge,
Viewport,
XYPosition,
} from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import {
appSocketGeneratorProgress,
appSocketInvocationComplete,
appSocketInvocationError,
appSocketInvocationStarted,
appSocketQueueItemStatusChanged,
} from 'services/events/actions';
import { v4 as uuidv4 } from 'uuid';
import { NodesState } from './types';
import { findConnectionToValidHandle } from './util/findConnectionToValidHandle';
import { findUnoccupiedPosition } from './util/findUnoccupiedPosition';
@ -81,7 +81,7 @@ const INITIAL_WORKFLOW: WorkflowV2 = {
nodes: [],
edges: [],
exposedFields: [],
meta: { version: '2.0.0' },
meta: { version: '2.0.0', category: 'user' },
};
export const initialNodesState: NodesState = {

View File

@ -13,6 +13,9 @@ export type XYPosition = z.infer<typeof zXYPosition>;
export const zDimension = z.number().gt(0).nullish();
export type Dimension = z.infer<typeof zDimension>;
export const zWorkflowCategory = z.enum(['user', 'system']);
export type WorkflowCategory = z.infer<typeof zWorkflowCategory>;
// #endregion
// #region Workflow Nodes
@ -85,6 +88,7 @@ export const zWorkflowV2 = z.object({
edges: z.array(zWorkflowEdge),
exposedFields: z.array(zFieldIdentifier),
meta: z.object({
category: zWorkflowCategory.default('user'),
version: z.literal('2.0.0'),
}),
});

View File

@ -8,11 +8,15 @@ import {
import { fromZodError } from 'zod-validation-error';
import { parseify } from 'common/util/serialize';
import i18n from 'i18next';
import { cloneDeep } from 'lodash-es';
export const buildWorkflow = (nodesState: NodesState): WorkflowV2 => {
const { workflow: workflowMeta, nodes, edges } = nodesState;
const workflow: WorkflowV2 = {
...workflowMeta,
const workflow = cloneDeep(nodesState.workflow);
const nodes = cloneDeep(nodesState.nodes);
const edges = cloneDeep(nodesState.edges);
const newWorkflow: WorkflowV2 = {
...workflow,
nodes: [],
edges: [],
};
@ -30,7 +34,7 @@ export const buildWorkflow = (nodesState: NodesState): WorkflowV2 => {
logger('nodes').warn({ node: parseify(node) }, message);
return;
}
workflow.nodes.push(result.data);
newWorkflow.nodes.push(result.data);
});
edges.forEach((edge) => {
@ -42,8 +46,8 @@ export const buildWorkflow = (nodesState: NodesState): WorkflowV2 => {
logger('nodes').warn({ edge: parseify(edge) }, message);
return;
}
workflow.edges.push(result.data);
newWorkflow.edges.push(result.data);
});
return workflow;
return newWorkflow;
};

View File

@ -66,6 +66,7 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
});
// Bump version
(workflowToMigrate as unknown as WorkflowV2).meta.version = '2.0.0';
(workflowToMigrate as unknown as WorkflowV2).meta.category = 'user';
// Parsing strips out any extra properties not in the latest version
return zWorkflowV2.parse(workflowToMigrate);
};

View File

@ -39,6 +39,13 @@ export const validateWorkflow = (
// Parse the raw workflow data & migrate it to the latest version
const _workflow = parseAndMigrateWorkflow(workflow);
// System workflows are only allowed to be used as templates.
// If a system workflow is loaded, change its category to user and remove its ID so that we can save it as a user workflow.
if (_workflow.meta.category === 'system') {
_workflow.meta.category = 'user';
_workflow.id = undefined;
}
// Now we can validate the graph
const { nodes, edges } = _workflow;
const warnings: WorkflowWarning[] = [];

View File

@ -1,21 +0,0 @@
import IAIIconButton from 'common/components/IAIIconButton';
import { useDuplicateLibraryWorkflow } from 'features/workflowLibrary/hooks/useDuplicateWorkflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaClone } from 'react-icons/fa';
const DuplicateLibraryWorkflowButton = () => {
const { t } = useTranslation();
const { duplicateWorkflow, isLoading } = useDuplicateLibraryWorkflow();
return (
<IAIIconButton
icon={<FaClone />}
onClick={duplicateWorkflow}
isLoading={isLoading}
tooltip={t('workflows.duplicateWorkflow')}
aria-label={t('workflows.duplicateWorkflow')}
/>
);
};
export default memo(DuplicateLibraryWorkflowButton);

View File

@ -0,0 +1,83 @@
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
FormControl,
FormLabel,
Input,
useDisclosure,
} from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import { useSaveWorkflowAs } from 'features/workflowLibrary/hooks/useDuplicateWorkflow';
import { ChangeEvent, memo, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaClone } from 'react-icons/fa';
const SaveWorkflowAsButton = () => {
const currentName = useAppSelector((state) => state.nodes.workflow.name);
const { t } = useTranslation();
const { saveWorkflowAs, isLoading } = useSaveWorkflowAs();
const [name, setName] = useState(currentName.trim());
const { isOpen, onOpen, onClose } = useDisclosure();
const inputRef = useRef<HTMLInputElement>(null);
const onSave = useCallback(async () => {
saveWorkflowAs({ name, onSuccess: onClose, onError: onClose });
}, [name, onClose, saveWorkflowAs]);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
}, []);
return (
<>
<IAIIconButton
icon={<FaClone />}
onClick={onOpen}
isLoading={isLoading}
tooltip={t('workflows.saveWorkflowAs')}
aria-label={t('workflows.saveWorkflowAs')}
/>
<AlertDialog
isOpen={isOpen}
onClose={onClose}
leastDestructiveRef={inputRef}
isCentered
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t('workflows.saveWorkflowAs')}
</AlertDialogHeader>
<AlertDialogBody>
<FormControl>
<FormLabel>{t('workflows.workflowName')}</FormLabel>
<Input
ref={inputRef}
value={name}
onChange={onChange}
placeholder={t('workflows.workflowName')}
/>
</FormControl>
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton onClick={onClose}>{t('common.cancel')}</IAIButton>
<IAIButton colorScheme="accent" onClick={onSave} ml={3}>
{t('common.saveAs')}
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
);
};
export default memo(SaveWorkflowAsButton);

View File

@ -1,33 +1,15 @@
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
import WorkflowLibraryList from 'features/workflowLibrary/components/WorkflowLibraryList';
import WorkflowLibraryListWrapper from 'features/workflowLibrary/components/WorkflowLibraryListWrapper';
import WorkflowLibrarySystemList from 'features/workflowLibrary/components/WorkflowLibrarySystemList';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import WorkflowLibraryUserList from './WorkflowLibraryUserList';
const WorkflowLibraryContent = () => {
const { t } = useTranslation();
return (
<Tabs w="full" h="full" isLazy>
<TabList w="10rem" layerStyle="second" borderRadius="base" p={2}>
<Tab>{t('workflows.user')}</Tab>
<Tab>{t('workflows.system')}</Tab>
</TabList>
<TabPanels>
<TabPanel>
<WorkflowLibraryListWrapper>
<WorkflowLibraryUserList />
</WorkflowLibraryListWrapper>
</TabPanel>
<TabPanel>
<WorkflowLibraryListWrapper>
<WorkflowLibrarySystemList />
</WorkflowLibraryListWrapper>
</TabPanel>
</TabPanels>
</Tabs>
<WorkflowLibraryListWrapper>
<WorkflowLibraryList />
</WorkflowLibraryListWrapper>
);
};

View File

@ -1,8 +1,8 @@
import { CloseIcon } from '@chakra-ui/icons';
import {
ButtonGroup,
Divider,
Flex,
Heading,
IconButton,
Input,
InputGroup,
@ -10,12 +10,14 @@ import {
Spacer,
} from '@chakra-ui/react';
import { SelectItem } from '@mantine/core';
import IAIButton from 'common/components/IAIButton';
import {
IAINoContentFallback,
IAINoContentFallbackWithSpinner,
} from 'common/components/IAIImageFallback';
import IAIMantineSelect from 'common/components/IAIMantineSelect';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import { WorkflowCategory } from 'features/nodes/types/workflow';
import WorkflowLibraryListItem from 'features/workflowLibrary/components/WorkflowLibraryListItem';
import WorkflowLibraryPagination from 'features/workflowLibrary/components/WorkflowLibraryPagination';
import { ChangeEvent, KeyboardEvent, memo, useCallback, useState } from 'react';
@ -40,19 +42,19 @@ const DIRECTION_DATA: SelectItem[] = [
const WorkflowLibraryList = () => {
const { t } = useTranslation();
const [category, setCategory] = useState<WorkflowCategory>('user');
const [page, setPage] = useState(0);
const [filter_text, setFilterText] = useState('');
const [order_by, setOrderBy] = useState<WorkflowRecordOrderBy>('opened_at');
const [direction, setDirection] = useState<SQLiteDirection>('ASC');
const [debouncedFilterText] = useDebounce(filter_text, 500, {
leading: true,
});
const [debouncedFilterText] = useDebounce(filter_text, 500);
const { data, isLoading, isError, isFetching } = useListWorkflowsQuery({
page,
per_page: PER_PAGE,
order_by,
direction,
filter_name: debouncedFilterText,
category,
filter_text: debouncedFilterText,
});
const handleChangeOrderBy = useCallback(
@ -102,6 +104,14 @@ const WorkflowLibraryList = () => {
[]
);
const handleSetUserCategory = useCallback(() => {
setCategory('user');
}, []);
const handleSetSystemCategory = useCallback(() => {
setCategory('system');
}, []);
if (isLoading) {
return <IAINoContentFallbackWithSpinner label={t('workflows.loading')} />;
}
@ -113,7 +123,22 @@ const WorkflowLibraryList = () => {
return (
<>
<Flex gap={4} alignItems="center" h={10} flexShrink={0} flexGrow={0}>
<Heading size="md">{t('workflows.userWorkflows')}</Heading>
<ButtonGroup>
<IAIButton
variant={category === 'user' ? undefined : 'ghost'}
onClick={handleSetUserCategory}
isChecked={category === 'user'}
>
{t('workflows.userWorkflows')}
</IAIButton>
<IAIButton
variant={category === 'system' ? undefined : 'ghost'}
onClick={handleSetSystemCategory}
isChecked={category === 'system'}
>
{t('workflows.systemWorkflows')}
</IAIButton>
</ButtonGroup>
<Spacer />
<InputGroup w="20rem">
<Input
@ -166,7 +191,7 @@ const WorkflowLibraryList = () => {
<Divider />
{data.items.length ? (
<ScrollableContent>
<Flex w="full" h="full" gap={2} flexDir="column">
<Flex w="full" h="full" gap={2} px={1} flexDir="column">
{data.items.map((w) => (
<WorkflowLibraryListItem key={w.workflow_id} workflowDTO={w} />
))}

View File

@ -72,14 +72,16 @@ const WorkflowLibraryListItem = ({ workflowDTO }: Props) => {
>
{t('common.load')}
</IAIButton>
<IAIButton
colorScheme="error"
onClick={handleDeleteWorkflow}
isLoading={deleteWorkflowResult.isLoading}
aria-label={t('workflows.deleteWorkflow')}
>
{t('common.delete')}
</IAIButton>
{workflowDTO.category === 'user' && (
<IAIButton
colorScheme="error"
onClick={handleDeleteWorkflow}
isLoading={deleteWorkflowResult.isLoading}
aria-label={t('workflows.deleteWorkflow')}
>
{t('common.delete')}
</IAIButton>
)}
</Flex>
</Flex>
);

View File

@ -3,35 +3,47 @@ import { useAppDispatch } from 'app/store/storeHooks';
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { zWorkflowV2 } from 'features/nodes/types/workflow';
import { omit } from 'lodash-es';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCreateWorkflowMutation } from 'services/api/endpoints/workflows';
export const useDuplicateLibraryWorkflow = () => {
type Arg = {
name: string;
onSuccess?: () => void;
onError?: () => void;
};
export const useSaveWorkflowAs = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const workflow = useWorkflow();
const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation();
const toaster = useAppToaster();
const duplicateWorkflow = useCallback(async () => {
try {
const data = await createWorkflow(omit(workflow, 'id')).unwrap();
const createdWorkflow = zWorkflowV2.parse(data.workflow);
dispatch(workflowLoaded(createdWorkflow));
toaster({
title: t('workflows.workflowSaved'),
status: 'success',
});
} catch (e) {
toaster({
title: t('workflows.problemSavingWorkflow'),
status: 'error',
});
}
}, [workflow, dispatch, toaster, t, createWorkflow]);
const saveWorkflowAs = useCallback(
async ({ name: newName, onSuccess, onError }: Arg) => {
try {
workflow.id = undefined;
workflow.name = newName;
const data = await createWorkflow(workflow).unwrap();
const createdWorkflow = zWorkflowV2.parse(data.workflow);
dispatch(workflowLoaded(createdWorkflow));
onSuccess && onSuccess();
toaster({
title: t('workflows.workflowSaved'),
status: 'success',
});
} catch (e) {
onError && onError();
toaster({
title: t('workflows.problemSavingWorkflow'),
status: 'error',
});
}
},
[workflow, dispatch, toaster, t, createWorkflow]
);
return {
duplicateWorkflow,
saveWorkflowAs,
isLoading: createWorkflowResult.isLoading,
isError: createWorkflowResult.isError,
};

File diff suppressed because one or more lines are too long

View File

@ -165,6 +165,7 @@ version = { attr = "invokeai.version.__version__" }
[tool.setuptools.package-data]
"invokeai.app.assets" = ["**/*.png"]
"invokeai.app.assets.workflows" = ["**/*.json"]
"invokeai.assets.fonts" = ["**/*.ttf"]
"invokeai.backend" = ["**.png"]
"invokeai.configs" = ["*.example", "**/*.yaml", "*.txt"]