Skip to content

Commit 9757bb0

Browse files
refactor(ui): canvas flow (#8069)
2 parents 23b0a4a + 38ccd8e commit 9757bb0

File tree

539 files changed

+12842
-8993
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

539 files changed

+12842
-8993
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ cython_debug/
180180
# Scratch folder
181181
.scratch/
182182
.vscode/
183+
.zed/
183184

184185
# source installer files
185186
installer/*zip

docs/contributing/frontend/workflows.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ Migration logic is in [migrations.ts].
297297
<!-- links -->
298298

299299
[pydantic]: https://github.com/pydantic/pydantic 'pydantic'
300-
[zod]: https://github.com/colinhacks/zod 'zod'
300+
[zod]: https://github.com/colinhacks/zod 'zod/v4'
301301
[openapi-types]: https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-types 'openapi-types'
302302
[reactflow]: https://github.com/xyflow/xyflow 'reactflow'
303303
[reactflow-concepts]: https://reactflow.dev/learn/concepts/terms-and-definitions

invokeai/app/api/routers/board_images.py

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
from fastapi import Body, HTTPException
22
from fastapi.routing import APIRouter
3-
from pydantic import BaseModel, Field
43

54
from invokeai.app.api.dependencies import ApiDependencies
5+
from invokeai.app.services.images.images_common import AddImagesToBoardResult, RemoveImagesFromBoardResult
66

77
board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
88

99

10-
class AddImagesToBoardResult(BaseModel):
11-
board_id: str = Field(description="The id of the board the images were added to")
12-
added_image_names: list[str] = Field(description="The image names that were added to the board")
13-
14-
15-
class RemoveImagesFromBoardResult(BaseModel):
16-
removed_image_names: list[str] = Field(description="The image names that were removed from their board")
17-
18-
1910
@board_images_router.post(
2011
"/",
2112
operation_id="add_image_to_board",
2213
responses={
2314
201: {"description": "The image was added to a board successfully"},
2415
},
2516
status_code=201,
17+
response_model=AddImagesToBoardResult,
2618
)
2719
async def add_image_to_board(
2820
board_id: str = Body(description="The id of the board to add to"),
2921
image_name: str = Body(description="The name of the image to add"),
30-
):
22+
) -> AddImagesToBoardResult:
3123
"""Creates a board_image"""
3224
try:
33-
result = ApiDependencies.invoker.services.board_images.add_image_to_board(
34-
board_id=board_id, image_name=image_name
25+
added_images: set[str] = set()
26+
affected_boards: set[str] = set()
27+
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
28+
ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
29+
added_images.add(image_name)
30+
affected_boards.add(board_id)
31+
affected_boards.add(old_board_id)
32+
33+
return AddImagesToBoardResult(
34+
added_images=list(added_images),
35+
affected_boards=list(affected_boards),
3536
)
36-
return result
3737
except Exception:
3838
raise HTTPException(status_code=500, detail="Failed to add image to board")
3939

@@ -45,14 +45,25 @@ async def add_image_to_board(
4545
201: {"description": "The image was removed from the board successfully"},
4646
},
4747
status_code=201,
48+
response_model=RemoveImagesFromBoardResult,
4849
)
4950
async def remove_image_from_board(
5051
image_name: str = Body(description="The name of the image to remove", embed=True),
51-
):
52+
) -> RemoveImagesFromBoardResult:
5253
"""Removes an image from its board, if it had one"""
5354
try:
54-
result = ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
55-
return result
55+
removed_images: set[str] = set()
56+
affected_boards: set[str] = set()
57+
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
58+
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
59+
removed_images.add(image_name)
60+
affected_boards.add("none")
61+
affected_boards.add(old_board_id)
62+
return RemoveImagesFromBoardResult(
63+
removed_images=list(removed_images),
64+
affected_boards=list(affected_boards),
65+
)
66+
5667
except Exception:
5768
raise HTTPException(status_code=500, detail="Failed to remove image from board")
5869

@@ -72,16 +83,25 @@ async def add_images_to_board(
7283
) -> AddImagesToBoardResult:
7384
"""Adds a list of images to a board"""
7485
try:
75-
added_image_names: list[str] = []
86+
added_images: set[str] = set()
87+
affected_boards: set[str] = set()
7688
for image_name in image_names:
7789
try:
90+
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
7891
ApiDependencies.invoker.services.board_images.add_image_to_board(
79-
board_id=board_id, image_name=image_name
92+
board_id=board_id,
93+
image_name=image_name,
8094
)
81-
added_image_names.append(image_name)
95+
added_images.add(image_name)
96+
affected_boards.add(board_id)
97+
affected_boards.add(old_board_id)
98+
8299
except Exception:
83100
pass
84-
return AddImagesToBoardResult(board_id=board_id, added_image_names=added_image_names)
101+
return AddImagesToBoardResult(
102+
added_images=list(added_images),
103+
affected_boards=list(affected_boards),
104+
)
85105
except Exception:
86106
raise HTTPException(status_code=500, detail="Failed to add images to board")
87107

@@ -100,13 +120,20 @@ async def remove_images_from_board(
100120
) -> RemoveImagesFromBoardResult:
101121
"""Removes a list of images from their board, if they had one"""
102122
try:
103-
removed_image_names: list[str] = []
123+
removed_images: set[str] = set()
124+
affected_boards: set[str] = set()
104125
for image_name in image_names:
105126
try:
127+
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
106128
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
107-
removed_image_names.append(image_name)
129+
removed_images.add(image_name)
130+
affected_boards.add("none")
131+
affected_boards.add(old_board_id)
108132
except Exception:
109133
pass
110-
return RemoveImagesFromBoardResult(removed_image_names=removed_image_names)
134+
return RemoveImagesFromBoardResult(
135+
removed_images=list(removed_images),
136+
affected_boards=list(affected_boards),
137+
)
111138
except Exception:
112139
raise HTTPException(status_code=500, detail="Failed to remove images from board")

invokeai/app/api/routers/images.py

Lines changed: 128 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@
1414
from invokeai.app.invocations.fields import MetadataField
1515
from invokeai.app.services.image_records.image_records_common import (
1616
ImageCategory,
17+
ImageNamesResult,
1718
ImageRecordChanges,
1819
ResourceOrigin,
1920
)
20-
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO
21+
from invokeai.app.services.images.images_common import (
22+
DeleteImagesResult,
23+
ImageDTO,
24+
ImageUrlsDTO,
25+
StarredImagesResult,
26+
UnstarredImagesResult,
27+
)
2128
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
2229
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
2330
from invokeai.app.util.controlnet_utils import heuristic_resize_fast
@@ -153,18 +160,30 @@ async def create_image_upload_entry(
153160
raise HTTPException(status_code=501, detail="Not implemented")
154161

155162

156-
@images_router.delete("/i/{image_name}", operation_id="delete_image")
163+
@images_router.delete("/i/{image_name}", operation_id="delete_image", response_model=DeleteImagesResult)
157164
async def delete_image(
158165
image_name: str = Path(description="The name of the image to delete"),
159-
) -> None:
166+
) -> DeleteImagesResult:
160167
"""Deletes an image"""
161168

169+
deleted_images: set[str] = set()
170+
affected_boards: set[str] = set()
171+
162172
try:
173+
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
174+
board_id = image_dto.board_id or "none"
163175
ApiDependencies.invoker.services.images.delete(image_name)
176+
deleted_images.add(image_name)
177+
affected_boards.add(board_id)
164178
except Exception:
165179
# TODO: Does this need any exception handling at all?
166180
pass
167181

182+
return DeleteImagesResult(
183+
deleted_images=list(deleted_images),
184+
affected_boards=list(affected_boards),
185+
)
186+
168187

169188
@images_router.delete("/intermediates", operation_id="clear_intermediates")
170189
async def clear_intermediates() -> int:
@@ -376,46 +395,52 @@ async def list_image_dtos(
376395
return image_dtos
377396

378397

379-
class DeleteImagesFromListResult(BaseModel):
380-
deleted_images: list[str]
381-
382-
383-
@images_router.post("/delete", operation_id="delete_images_from_list", response_model=DeleteImagesFromListResult)
398+
@images_router.post("/delete", operation_id="delete_images_from_list", response_model=DeleteImagesResult)
384399
async def delete_images_from_list(
385400
image_names: list[str] = Body(description="The list of names of images to delete", embed=True),
386-
) -> DeleteImagesFromListResult:
401+
) -> DeleteImagesResult:
387402
try:
388-
deleted_images: list[str] = []
403+
deleted_images: set[str] = set()
404+
affected_boards: set[str] = set()
389405
for image_name in image_names:
390406
try:
407+
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
408+
board_id = image_dto.board_id or "none"
391409
ApiDependencies.invoker.services.images.delete(image_name)
392-
deleted_images.append(image_name)
410+
deleted_images.add(image_name)
411+
affected_boards.add(board_id)
393412
except Exception:
394413
pass
395-
return DeleteImagesFromListResult(deleted_images=deleted_images)
414+
return DeleteImagesResult(
415+
deleted_images=list(deleted_images),
416+
affected_boards=list(affected_boards),
417+
)
396418
except Exception:
397419
raise HTTPException(status_code=500, detail="Failed to delete images")
398420

399421

400-
@images_router.delete(
401-
"/uncategorized", operation_id="delete_uncategorized_images", response_model=DeleteImagesFromListResult
402-
)
403-
async def delete_uncategorized_images() -> DeleteImagesFromListResult:
422+
@images_router.delete("/uncategorized", operation_id="delete_uncategorized_images", response_model=DeleteImagesResult)
423+
async def delete_uncategorized_images() -> DeleteImagesResult:
404424
"""Deletes all images that are uncategorized"""
405425

406426
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
407427
board_id="none", categories=None, is_intermediate=None
408428
)
409429

410430
try:
411-
deleted_images: list[str] = []
431+
deleted_images: set[str] = set()
432+
affected_boards: set[str] = set()
412433
for image_name in image_names:
413434
try:
414435
ApiDependencies.invoker.services.images.delete(image_name)
415-
deleted_images.append(image_name)
436+
deleted_images.add(image_name)
437+
affected_boards.add("none")
416438
except Exception:
417439
pass
418-
return DeleteImagesFromListResult(deleted_images=deleted_images)
440+
return DeleteImagesResult(
441+
deleted_images=list(deleted_images),
442+
affected_boards=list(affected_boards),
443+
)
419444
except Exception:
420445
raise HTTPException(status_code=500, detail="Failed to delete images")
421446

@@ -424,36 +449,50 @@ class ImagesUpdatedFromListResult(BaseModel):
424449
updated_image_names: list[str] = Field(description="The image names that were updated")
425450

426451

427-
@images_router.post("/star", operation_id="star_images_in_list", response_model=ImagesUpdatedFromListResult)
452+
@images_router.post("/star", operation_id="star_images_in_list", response_model=StarredImagesResult)
428453
async def star_images_in_list(
429454
image_names: list[str] = Body(description="The list of names of images to star", embed=True),
430-
) -> ImagesUpdatedFromListResult:
455+
) -> StarredImagesResult:
431456
try:
432-
updated_image_names: list[str] = []
457+
starred_images: set[str] = set()
458+
affected_boards: set[str] = set()
433459
for image_name in image_names:
434460
try:
435-
ApiDependencies.invoker.services.images.update(image_name, changes=ImageRecordChanges(starred=True))
436-
updated_image_names.append(image_name)
461+
updated_image_dto = ApiDependencies.invoker.services.images.update(
462+
image_name, changes=ImageRecordChanges(starred=True)
463+
)
464+
starred_images.add(image_name)
465+
affected_boards.add(updated_image_dto.board_id or "none")
437466
except Exception:
438467
pass
439-
return ImagesUpdatedFromListResult(updated_image_names=updated_image_names)
468+
return StarredImagesResult(
469+
starred_images=list(starred_images),
470+
affected_boards=list(affected_boards),
471+
)
440472
except Exception:
441473
raise HTTPException(status_code=500, detail="Failed to star images")
442474

443475

444-
@images_router.post("/unstar", operation_id="unstar_images_in_list", response_model=ImagesUpdatedFromListResult)
476+
@images_router.post("/unstar", operation_id="unstar_images_in_list", response_model=UnstarredImagesResult)
445477
async def unstar_images_in_list(
446478
image_names: list[str] = Body(description="The list of names of images to unstar", embed=True),
447-
) -> ImagesUpdatedFromListResult:
479+
) -> UnstarredImagesResult:
448480
try:
449-
updated_image_names: list[str] = []
481+
unstarred_images: set[str] = set()
482+
affected_boards: set[str] = set()
450483
for image_name in image_names:
451484
try:
452-
ApiDependencies.invoker.services.images.update(image_name, changes=ImageRecordChanges(starred=False))
453-
updated_image_names.append(image_name)
485+
updated_image_dto = ApiDependencies.invoker.services.images.update(
486+
image_name, changes=ImageRecordChanges(starred=False)
487+
)
488+
unstarred_images.add(image_name)
489+
affected_boards.add(updated_image_dto.board_id or "none")
454490
except Exception:
455491
pass
456-
return ImagesUpdatedFromListResult(updated_image_names=updated_image_names)
492+
return UnstarredImagesResult(
493+
unstarred_images=list(unstarred_images),
494+
affected_boards=list(affected_boards),
495+
)
457496
except Exception:
458497
raise HTTPException(status_code=500, detail="Failed to unstar images")
459498

@@ -524,3 +563,61 @@ async def get_bulk_download_item(
524563
return response
525564
except Exception:
526565
raise HTTPException(status_code=404)
566+
567+
568+
@images_router.get("/names", operation_id="get_image_names")
569+
async def get_image_names(
570+
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
571+
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
572+
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
573+
board_id: Optional[str] = Query(
574+
default=None,
575+
description="The board id to filter by. Use 'none' to find images without a board.",
576+
),
577+
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
578+
starred_first: bool = Query(default=True, description="Whether to sort by starred images first"),
579+
search_term: Optional[str] = Query(default=None, description="The term to search for"),
580+
) -> ImageNamesResult:
581+
"""Gets ordered list of image names with metadata for optimistic updates"""
582+
583+
try:
584+
result = ApiDependencies.invoker.services.images.get_image_names(
585+
starred_first=starred_first,
586+
order_dir=order_dir,
587+
image_origin=image_origin,
588+
categories=categories,
589+
is_intermediate=is_intermediate,
590+
board_id=board_id,
591+
search_term=search_term,
592+
)
593+
return result
594+
except Exception:
595+
raise HTTPException(status_code=500, detail="Failed to get image names")
596+
597+
598+
@images_router.post(
599+
"/images_by_names",
600+
operation_id="get_images_by_names",
601+
responses={200: {"model": list[ImageDTO]}},
602+
)
603+
async def get_images_by_names(
604+
image_names: list[str] = Body(embed=True, description="Object containing list of image names to fetch DTOs for"),
605+
) -> list[ImageDTO]:
606+
"""Gets image DTOs for the specified image names. Maintains order of input names."""
607+
608+
try:
609+
image_service = ApiDependencies.invoker.services.images
610+
611+
# Fetch DTOs preserving the order of requested names
612+
image_dtos: list[ImageDTO] = []
613+
for name in image_names:
614+
try:
615+
dto = image_service.get_dto(name)
616+
image_dtos.append(dto)
617+
except Exception:
618+
# Skip missing images - they may have been deleted between name fetch and DTO fetch
619+
continue
620+
621+
return image_dtos
622+
except Exception:
623+
raise HTTPException(status_code=500, detail="Failed to get image DTOs")

0 commit comments

Comments
 (0)