From c74b9a40afa259886d74484816b8aaaac20779ba Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:57:07 +1000 Subject: [PATCH 01/12] feat(app): add get_uncategorized_image_counts method on board_records service --- .../board_records/board_records_base.py | 7 ++++- .../board_records/board_records_common.py | 5 ++++ .../board_records/board_records_sqlite.py | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/board_records/board_records_base.py b/invokeai/app/services/board_records/board_records_base.py index 9d16dacf60b..7bfe6ada6fd 100644 --- a/invokeai/app/services/board_records/board_records_base.py +++ b/invokeai/app/services/board_records/board_records_base.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord +from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord, UncategorizedImageCounts from invokeai.app.services.shared.pagination import OffsetPaginatedResults @@ -48,3 +48,8 @@ def get_many( def get_all(self, include_archived: bool = False) -> list[BoardRecord]: """Gets all board records.""" pass + + @abstractmethod + def get_uncategorized_image_counts(self) -> UncategorizedImageCounts: + """Gets count of images and assets for uncategorized images (images with no board assocation).""" + pass diff --git a/invokeai/app/services/board_records/board_records_common.py b/invokeai/app/services/board_records/board_records_common.py index 0dda8a8b6b6..1c25aab5650 100644 --- a/invokeai/app/services/board_records/board_records_common.py +++ b/invokeai/app/services/board_records/board_records_common.py @@ -79,3 +79,8 @@ class BoardRecordDeleteException(Exception): def __init__(self, message="Board record not deleted"): super().__init__(message) + + +class UncategorizedImageCounts(BaseModel): + image_count: int = Field(description="The number of uncategorized images.") + asset_count: int = Field(description="The number of uncategorized assets.") diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index c64e060b953..c5167824cd3 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -9,6 +9,7 @@ BoardRecordDeleteException, BoardRecordNotFoundException, BoardRecordSaveException, + UncategorizedImageCounts, deserialize_board_record, ) from invokeai.app.services.shared.pagination import OffsetPaginatedResults @@ -228,3 +229,28 @@ def get_all(self, include_archived: bool = False) -> list[BoardRecord]: raise e finally: self._lock.release() + + def get_uncategorized_image_counts(self) -> UncategorizedImageCounts: + try: + self._lock.acquire() + query = """ + SELECT + CASE + WHEN i.image_category = 'general' THEN 'images' + ELSE 'assets' + END AS category_type, + COUNT(*) AS unassigned_count + FROM images i + LEFT JOIN board_images bi ON i.image_name = bi.image_name + WHERE i.image_category IN ('general', 'control', 'mask', 'user', 'other') + AND bi.board_id IS NULL + AND i.is_intermediate = 0 + GROUP BY category_type; + """ + self._cursor.execute(query) + results = self._cursor.fetchall() + image_count = results[0][1] + asset_count = results[1][1] + return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count) + finally: + self._lock.release() From a95aa6cc16de47896b2ab09c3406e850a019257e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:57:23 +1000 Subject: [PATCH 02/12] feat(api): add get_uncategorized_image_counts endpoint --- invokeai/app/api/routers/boards.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py index 926c0f7fd22..d5b1acb5144 100644 --- a/invokeai/app/api/routers/boards.py +++ b/invokeai/app/api/routers/boards.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Field from invokeai.app.api.dependencies import ApiDependencies -from invokeai.app.services.board_records.board_records_common import BoardChanges +from invokeai.app.services.board_records.board_records_common import BoardChanges, UncategorizedImageCounts from invokeai.app.services.boards.boards_common import BoardDTO from invokeai.app.services.shared.pagination import OffsetPaginatedResults @@ -146,3 +146,14 @@ async def list_all_board_image_names( board_id, ) return image_names + + +@boards_router.get( + "/uncategorized/counts", + operation_id="get_uncategorized_image_counts", + response_model=UncategorizedImageCounts, +) +async def get_uncategorized_image_counts() -> UncategorizedImageCounts: + """Gets count of images and assets for uncategorized images (images with no board assocation)""" + + return ApiDependencies.invoker.services.board_records.get_uncategorized_image_counts() From c05f97d8cac3df938685bac1b45fe33486b7e775 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:13:26 +1000 Subject: [PATCH 03/12] feat(app): refactor board record to include image & asset counts and cover image This _substantially_ reduces the number of queries required to list all boards. A single query now gets one, all, or a page of boards, including counts and cover image name. - Add helpers to build the queries, which share a common base with some joins. - Update `BoardRecord` to include the counts. - Update `BoardDTO`, which is now identical to `BoardRecord`. I opted to not remove `BoardDTO` because it is used in many places. - Update boards high-level service and board records services accordingly. --- .../board_records/board_records_common.py | 110 +++++++++++++++++- .../board_records/board_records_sqlite.py | 70 ++--------- invokeai/app/services/boards/boards_common.py | 21 +--- .../app/services/boards/boards_default.py | 45 +------ 4 files changed, 125 insertions(+), 121 deletions(-) diff --git a/invokeai/app/services/board_records/board_records_common.py b/invokeai/app/services/board_records/board_records_common.py index 1c25aab5650..80a4c3313a7 100644 --- a/invokeai/app/services/board_records/board_records_common.py +++ b/invokeai/app/services/board_records/board_records_common.py @@ -1,11 +1,103 @@ from datetime import datetime -from typing import Optional, Union +from typing import Any, Optional, Union +from attr import dataclass from pydantic import BaseModel, Field from invokeai.app.util.misc import get_iso_timestamp from invokeai.app.util.model_exclude_null import BaseModelExcludeNull +# This query is missing a GROUP BY clause, which is required for the query to be valid. +BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY = """ + SELECT b.board_id, + b.board_name, + b.created_at, + b.updated_at, + b.archived, + COUNT( + CASE + WHEN i.image_category in ('general') + AND i.is_intermediate = 0 THEN 1 + END + ) AS image_count, + COUNT( + CASE + WHEN i.image_category in ('control', 'mask', 'user', 'other') + AND i.is_intermediate = 0 THEN 1 + END + ) AS asset_count, + ( + SELECT bi.image_name + FROM board_images bi + JOIN images i ON bi.image_name = i.image_name + WHERE bi.board_id = b.board_id + AND i.is_intermediate = 0 + ORDER BY i.created_at DESC + LIMIT 1 + ) AS cover_image_name + FROM boards b + LEFT JOIN board_images bi ON b.board_id = bi.board_id + LEFT JOIN images i ON bi.image_name = i.image_name + """ + + +@dataclass +class PaginatedBoardRecordsQueries: + main_query: str + total_count_query: str + + +def get_paginated_list_board_records_query(include_archived: bool) -> PaginatedBoardRecordsQueries: + """Gets a query to retrieve a paginated list of board records.""" + + archived_condition = "WHERE b.archived = 0" if not include_archived else "" + + # The GROUP BY must be added _after_ the WHERE clause! + main_query = f""" + {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + {archived_condition} + GROUP BY b.board_id, + b.board_name, + b.created_at, + b.updated_at + ORDER BY b.created_at DESC + LIMIT ? OFFSET ?; + """ + + total_count_query = f""" + SELECT COUNT(*) + FROM boards b + {archived_condition}; + """ + + return PaginatedBoardRecordsQueries(main_query=main_query, total_count_query=total_count_query) + + +def get_list_all_board_records_query(include_archived: bool) -> str: + """Gets a query to retrieve all board records.""" + + archived_condition = "WHERE b.archived = 0" if not include_archived else "" + + # The GROUP BY must be added _after_ the WHERE clause! + return f""" + {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + {archived_condition} + GROUP BY b.board_id, + b.board_name, + b.created_at, + b.updated_at + ORDER BY b.created_at DESC; + """ + + +def get_board_record_query() -> str: + """Gets a query to retrieve a board record.""" + + return f""" + {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + WHERE b.board_id = ?; + """ + class BoardRecord(BaseModelExcludeNull): """Deserialized board record.""" @@ -26,21 +118,25 @@ class BoardRecord(BaseModelExcludeNull): """Whether or not the board is archived.""" is_private: Optional[bool] = Field(default=None, description="Whether the board is private.") """Whether the board is private.""" + image_count: int = Field(description="The number of images in the board.") + asset_count: int = Field(description="The number of assets in the board.") -def deserialize_board_record(board_dict: dict) -> BoardRecord: +def deserialize_board_record(board_dict: dict[str, Any]) -> BoardRecord: """Deserializes a board record.""" # Retrieve all the values, setting "reasonable" defaults if they are not present. board_id = board_dict.get("board_id", "unknown") board_name = board_dict.get("board_name", "unknown") - cover_image_name = board_dict.get("cover_image_name", "unknown") + cover_image_name = board_dict.get("cover_image_name", None) created_at = board_dict.get("created_at", get_iso_timestamp()) updated_at = board_dict.get("updated_at", get_iso_timestamp()) deleted_at = board_dict.get("deleted_at", get_iso_timestamp()) archived = board_dict.get("archived", False) is_private = board_dict.get("is_private", False) + image_count = board_dict.get("image_count", 0) + asset_count = board_dict.get("asset_count", 0) return BoardRecord( board_id=board_id, @@ -51,6 +147,8 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord: deleted_at=deleted_at, archived=archived, is_private=is_private, + image_count=image_count, + asset_count=asset_count, ) @@ -63,21 +161,21 @@ class BoardChanges(BaseModel, extra="forbid"): class BoardRecordNotFoundException(Exception): """Raised when an board record is not found.""" - def __init__(self, message="Board record not found"): + def __init__(self, message: str = "Board record not found"): super().__init__(message) class BoardRecordSaveException(Exception): """Raised when an board record cannot be saved.""" - def __init__(self, message="Board record not saved"): + def __init__(self, message: str = "Board record not saved"): super().__init__(message) class BoardRecordDeleteException(Exception): """Raised when an board record cannot be deleted.""" - def __init__(self, message="Board record not deleted"): + def __init__(self, message: str = "Board record not deleted"): super().__init__(message) diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index c5167824cd3..27b47ea57dd 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -11,6 +11,9 @@ BoardRecordSaveException, UncategorizedImageCounts, deserialize_board_record, + get_board_record_query, + get_list_all_board_records_query, + get_paginated_list_board_records_query, ) from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase @@ -77,11 +80,7 @@ def get( try: self._lock.acquire() self._cursor.execute( - """--sql - SELECT * - FROM boards - WHERE board_id = ?; - """, + get_board_record_query(), (board_id,), ) @@ -93,7 +92,7 @@ def get( self._lock.release() if result is None: raise BoardRecordNotFoundException - return BoardRecord(**dict(result)) + return deserialize_board_record(dict(result)) def update( self, @@ -150,45 +149,17 @@ def get_many( try: self._lock.acquire() - # Build base query - base_query = """ - SELECT * - FROM boards - {archived_filter} - ORDER BY created_at DESC - LIMIT ? OFFSET ?; - """ - - # Determine archived filter condition - if include_archived: - archived_filter = "" - else: - archived_filter = "WHERE archived = 0" - - final_query = base_query.format(archived_filter=archived_filter) + queries = get_paginated_list_board_records_query(include_archived=include_archived) - # Execute query to fetch boards - self._cursor.execute(final_query, (limit, offset)) + self._cursor.execute( + queries.main_query, + (limit, offset), + ) result = cast(list[sqlite3.Row], self._cursor.fetchall()) boards = [deserialize_board_record(dict(r)) for r in result] - # Determine count query - if include_archived: - count_query = """ - SELECT COUNT(*) - FROM boards; - """ - else: - count_query = """ - SELECT COUNT(*) - FROM boards - WHERE archived = 0; - """ - - # Execute count query - self._cursor.execute(count_query) - + self._cursor.execute(queries.total_count_query) count = cast(int, self._cursor.fetchone()[0]) return OffsetPaginatedResults[BoardRecord](items=boards, offset=offset, limit=limit, total=count) @@ -202,26 +173,9 @@ def get_many( def get_all(self, include_archived: bool = False) -> list[BoardRecord]: try: self._lock.acquire() - - base_query = """ - SELECT * - FROM boards - {archived_filter} - ORDER BY created_at DESC - """ - - if include_archived: - archived_filter = "" - else: - archived_filter = "WHERE archived = 0" - - final_query = base_query.format(archived_filter=archived_filter) - - self._cursor.execute(final_query) - + self._cursor.execute(get_list_all_board_records_query(include_archived=include_archived)) result = cast(list[sqlite3.Row], self._cursor.fetchall()) boards = [deserialize_board_record(dict(r)) for r in result] - return boards except sqlite3.Error as e: diff --git a/invokeai/app/services/boards/boards_common.py b/invokeai/app/services/boards/boards_common.py index 15d0b3c37f5..1e9337a3edf 100644 --- a/invokeai/app/services/boards/boards_common.py +++ b/invokeai/app/services/boards/boards_common.py @@ -1,23 +1,8 @@ -from typing import Optional - -from pydantic import Field - from invokeai.app.services.board_records.board_records_common import BoardRecord +# TODO(psyche): BoardDTO is now identical to BoardRecord. We should consider removing it. class BoardDTO(BoardRecord): - """Deserialized board record with cover image URL and image count.""" - - cover_image_name: Optional[str] = Field(description="The name of the board's cover image.") - """The URL of the thumbnail of the most recent image in the board.""" - image_count: int = Field(description="The number of images in the board.") - """The number of images in the board.""" - + """Deserialized board record.""" -def board_record_to_dto(board_record: BoardRecord, cover_image_name: Optional[str], image_count: int) -> BoardDTO: - """Converts a board record to a board DTO.""" - return BoardDTO( - **board_record.model_dump(exclude={"cover_image_name"}), - cover_image_name=cover_image_name, - image_count=image_count, - ) + pass diff --git a/invokeai/app/services/boards/boards_default.py b/invokeai/app/services/boards/boards_default.py index 97fd3059a93..abf38e8ea71 100644 --- a/invokeai/app/services/boards/boards_default.py +++ b/invokeai/app/services/boards/boards_default.py @@ -1,6 +1,6 @@ from invokeai.app.services.board_records.board_records_common import BoardChanges from invokeai.app.services.boards.boards_base import BoardServiceABC -from invokeai.app.services.boards.boards_common import BoardDTO, board_record_to_dto +from invokeai.app.services.boards.boards_common import BoardDTO from invokeai.app.services.invoker import Invoker from invokeai.app.services.shared.pagination import OffsetPaginatedResults @@ -16,17 +16,11 @@ def create( board_name: str, ) -> BoardDTO: board_record = self.__invoker.services.board_records.save(board_name) - return board_record_to_dto(board_record, None, 0) + return BoardDTO.model_validate(board_record.model_dump()) def get_dto(self, board_id: str) -> BoardDTO: board_record = self.__invoker.services.board_records.get(board_id) - cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id) - if cover_image: - cover_image_name = cover_image.image_name - else: - cover_image_name = None - image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id) - return board_record_to_dto(board_record, cover_image_name, image_count) + return BoardDTO.model_validate(board_record.model_dump()) def update( self, @@ -34,14 +28,7 @@ def update( changes: BoardChanges, ) -> BoardDTO: board_record = self.__invoker.services.board_records.update(board_id, changes) - cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id) - if cover_image: - cover_image_name = cover_image.image_name - else: - cover_image_name = None - - image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id) - return board_record_to_dto(board_record, cover_image_name, image_count) + return BoardDTO.model_validate(board_record.model_dump()) def delete(self, board_id: str) -> None: self.__invoker.services.board_records.delete(board_id) @@ -50,30 +37,10 @@ def get_many( self, offset: int = 0, limit: int = 10, include_archived: bool = False ) -> OffsetPaginatedResults[BoardDTO]: board_records = self.__invoker.services.board_records.get_many(offset, limit, include_archived) - board_dtos = [] - for r in board_records.items: - cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id) - if cover_image: - cover_image_name = cover_image.image_name - else: - cover_image_name = None - - image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id) - board_dtos.append(board_record_to_dto(r, cover_image_name, image_count)) - + board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records.items] return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos)) def get_all(self, include_archived: bool = False) -> list[BoardDTO]: board_records = self.__invoker.services.board_records.get_all(include_archived) - board_dtos = [] - for r in board_records: - cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id) - if cover_image: - cover_image_name = cover_image.image_name - else: - cover_image_name = None - - image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id) - board_dtos.append(board_record_to_dto(r, cover_image_name, image_count)) - + board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records] return board_dtos From 0484f458b694e2ee82b79d41eabfd58abed0fd0a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:13:43 +1000 Subject: [PATCH 04/12] tests(app): fix bulk downloads test --- .../bulk_download/test_bulk_download.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py index 223ecc88632..48842d9a4bd 100644 --- a/tests/app/services/bulk_download/test_bulk_download.py +++ b/tests/app/services/bulk_download/test_bulk_download.py @@ -127,7 +127,16 @@ def test_generate_id_with_board_id(monkeypatch: Any, mock_invoker: Invoker): def mock_board_get(*args, **kwargs): return BoardRecord( - board_id="12345", board_name="test_board_name", created_at="None", updated_at="None", archived=False + board_id="12345", + board_name="test_board_name", + created_at="None", + updated_at="None", + archived=False, + asset_count=0, + image_count=0, + cover_image_name="asdf.png", + deleted_at=None, + is_private=False, ) monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get) @@ -156,7 +165,16 @@ def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: Imag def mock_board_get(*args, **kwargs): return BoardRecord( - board_id="12345", board_name="test_board_name", created_at="None", updated_at="None", archived=False + board_id="12345", + board_name="test_board_name", + created_at="None", + updated_at="None", + archived=False, + asset_count=0, + image_count=0, + cover_image_name="asdf.png", + deleted_at=None, + is_private=False, ) monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get) From 2843a6a227fdb71f8eb61f8f01e291f5bf254f16 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:13:53 +1000 Subject: [PATCH 05/12] chore(ui): typegen chore(ui): typegen --- .../frontend/web/src/services/api/schema.ts | 283 ++++++++++-------- 1 file changed, 161 insertions(+), 122 deletions(-) diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 7780ccbdc85..2fa360140f3 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -333,6 +333,13 @@ export type paths = { */ get: operations["list_all_board_image_names"]; }; + "/api/v1/boards/uncategorized/counts": { + /** + * Get Uncategorized Image Counts + * @description Gets count of images and assets for uncategorized images (images with no board assocation) + */ + get: operations["get_uncategorized_image_counts"]; + }; "/api/v1/board_images/": { /** * Add Image To Board @@ -1020,7 +1027,7 @@ export type components = { }; /** * BoardDTO - * @description Deserialized board record with cover image URL and image count. + * @description Deserialized board record. */ BoardDTO: { /** @@ -1050,9 +1057,9 @@ export type components = { deleted_at?: string | null; /** * Cover Image Name - * @description The name of the board's cover image. + * @description The name of the cover image of the board. */ - cover_image_name: string | null; + cover_image_name?: string | null; /** * Archived * @description Whether or not the board is archived. @@ -1068,6 +1075,11 @@ export type components = { * @description The number of images in the board. */ image_count: number; + /** + * Asset Count + * @description The number of assets in the board. + */ + asset_count: number; }; /** * BoardField @@ -7304,145 +7316,145 @@ export type components = { project_id: string | null; }; InvocationOutputMap: { - rectangle_mask: components["schemas"]["MaskOutput"]; - hed_image_processor: components["schemas"]["ImageOutput"]; - compel: components["schemas"]["ConditioningOutput"]; - img_resize: components["schemas"]["ImageOutput"]; - ideal_size: components["schemas"]["IdealSizeOutput"]; - rand_int: components["schemas"]["IntegerOutput"]; + img_channel_offset: components["schemas"]["ImageOutput"]; + metadata: components["schemas"]["MetadataOutput"]; clip_skip: components["schemas"]["CLIPSkipInvocationOutput"]; - string_collection: components["schemas"]["StringCollectionOutput"]; - create_gradient_mask: components["schemas"]["GradientMaskOutput"]; - round_float: components["schemas"]["FloatOutput"]; - scheduler: components["schemas"]["SchedulerOutput"]; - main_model_loader: components["schemas"]["ModelLoaderOutput"]; - string_split: components["schemas"]["String2Output"]; - mask_from_id: components["schemas"]["ImageOutput"]; - collect: components["schemas"]["CollectInvocationOutput"]; - heuristic_resize: components["schemas"]["ImageOutput"]; - tomask: components["schemas"]["ImageOutput"]; - boolean_collection: components["schemas"]["BooleanCollectionOutput"]; - core_metadata: components["schemas"]["MetadataOutput"]; - canny_image_processor: components["schemas"]["ImageOutput"]; - string_replace: components["schemas"]["StringOutput"]; - face_mask_detection: components["schemas"]["FaceMaskOutput"]; - integer: components["schemas"]["IntegerOutput"]; - img_watermark: components["schemas"]["ImageOutput"]; - img_crop: components["schemas"]["ImageOutput"]; - t2i_adapter: components["schemas"]["T2IAdapterOutput"]; - create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; + canvas_paste_back: components["schemas"]["ImageOutput"]; + seamless: components["schemas"]["SeamlessModeOutput"]; + blank_image: components["schemas"]["ImageOutput"]; + dynamic_prompt: components["schemas"]["StringCollectionOutput"]; + step_param_easing: components["schemas"]["FloatCollectionOutput"]; + latents_collection: components["schemas"]["LatentsCollectionOutput"]; + normalbae_image_processor: components["schemas"]["ImageOutput"]; rand_float: components["schemas"]["FloatOutput"]; + lora_loader: components["schemas"]["LoRALoaderOutput"]; + collect: components["schemas"]["CollectInvocationOutput"]; + infill_rgba: components["schemas"]["ImageOutput"]; + img_lerp: components["schemas"]["ImageOutput"]; + integer_math: components["schemas"]["IntegerOutput"]; + conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + mask_from_id: components["schemas"]["ImageOutput"]; + mlsd_image_processor: components["schemas"]["ImageOutput"]; zoe_depth_image_processor: components["schemas"]["ImageOutput"]; - face_off: components["schemas"]["FaceOffOutput"]; + ideal_size: components["schemas"]["IdealSizeOutput"]; + conditioning: components["schemas"]["ConditioningOutput"]; + img_resize: components["schemas"]["ImageOutput"]; + integer_collection: components["schemas"]["IntegerCollectionOutput"]; + float_range: components["schemas"]["FloatCollectionOutput"]; tile_to_properties: components["schemas"]["TileToPropertiesOutput"]; + alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + img_watermark: components["schemas"]["ImageOutput"]; + merge_tiles_to_image: components["schemas"]["ImageOutput"]; + merge_metadata: components["schemas"]["MetadataOutput"]; + round_float: components["schemas"]["FloatOutput"]; + denoise_latents: components["schemas"]["LatentsOutput"]; + string_join_three: components["schemas"]["StringOutput"]; + img_blur: components["schemas"]["ImageOutput"]; color_map_image_processor: components["schemas"]["ImageOutput"]; - lineart_anime_image_processor: components["schemas"]["ImageOutput"]; - face_identifier: components["schemas"]["ImageOutput"]; - float_math: components["schemas"]["FloatOutput"]; - mediapipe_face_processor: components["schemas"]["ImageOutput"]; - img_channel_multiply: components["schemas"]["ImageOutput"]; - metadata_item: components["schemas"]["MetadataItemOutput"]; - img_ilerp: components["schemas"]["ImageOutput"]; - conditioning: components["schemas"]["ConditioningOutput"]; - pidi_image_processor: components["schemas"]["ImageOutput"]; - seamless: components["schemas"]["SeamlessModeOutput"]; - latents: components["schemas"]["LatentsOutput"]; - img_chan: components["schemas"]["ImageOutput"]; + img_scale: components["schemas"]["ImageOutput"]; + infill_tile: components["schemas"]["ImageOutput"]; + add: components["schemas"]["IntegerOutput"]; + img_paste: components["schemas"]["ImageOutput"]; + img_crop: components["schemas"]["ImageOutput"]; + cv_inpaint: components["schemas"]["ImageOutput"]; + image_collection: components["schemas"]["ImageCollectionOutput"]; + img_pad_crop: components["schemas"]["ImageOutput"]; + canny_image_processor: components["schemas"]["ImageOutput"]; model_identifier: components["schemas"]["ModelIdentifierOutput"]; - noise: components["schemas"]["NoiseOutput"]; - string_join: components["schemas"]["StringOutput"]; - blank_image: components["schemas"]["ImageOutput"]; - calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; - invert_tensor_mask: components["schemas"]["MaskOutput"]; - save_image: components["schemas"]["ImageOutput"]; - unsharp_mask: components["schemas"]["ImageOutput"]; + i2l: components["schemas"]["LatentsOutput"]; + face_mask_detection: components["schemas"]["FaceMaskOutput"]; + img_channel_multiply: components["schemas"]["ImageOutput"]; + sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; + img_mul: components["schemas"]["ImageOutput"]; + tomask: components["schemas"]["ImageOutput"]; image_mask_to_tensor: components["schemas"]["MaskOutput"]; - step_param_easing: components["schemas"]["FloatCollectionOutput"]; - merge_tiles_to_image: components["schemas"]["ImageOutput"]; - integer_collection: components["schemas"]["IntegerCollectionOutput"]; - calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; - integer_math: components["schemas"]["IntegerOutput"]; - range: components["schemas"]["IntegerCollectionOutput"]; - prompt_from_file: components["schemas"]["StringCollectionOutput"]; - segment_anything_processor: components["schemas"]["ImageOutput"]; - freeu: components["schemas"]["UNetOutput"]; - sub: components["schemas"]["IntegerOutput"]; - lresize: components["schemas"]["LatentsOutput"]; - float: components["schemas"]["FloatOutput"]; - float_collection: components["schemas"]["FloatCollectionOutput"]; - dynamic_prompt: components["schemas"]["StringCollectionOutput"]; - infill_lama: components["schemas"]["ImageOutput"]; + face_identifier: components["schemas"]["ImageOutput"]; + noise: components["schemas"]["NoiseOutput"]; l2i: components["schemas"]["ImageOutput"]; - img_lerp: components["schemas"]["ImageOutput"]; + mul: components["schemas"]["IntegerOutput"]; + sub: components["schemas"]["IntegerOutput"]; + main_model_loader: components["schemas"]["ModelLoaderOutput"]; + controlnet: components["schemas"]["ControlOutput"]; ip_adapter: components["schemas"]["IPAdapterOutput"]; - lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; - color: components["schemas"]["ColorOutput"]; - tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; - cv_inpaint: components["schemas"]["ImageOutput"]; lscale: components["schemas"]["LatentsOutput"]; - string: components["schemas"]["StringOutput"]; + sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + latents: components["schemas"]["LatentsOutput"]; + string_split: components["schemas"]["String2Output"]; sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"]; - string_join_three: components["schemas"]["StringOutput"]; - midas_depth_image_processor: components["schemas"]["ImageOutput"]; esrgan: components["schemas"]["ImageOutput"]; - sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; - mul: components["schemas"]["IntegerOutput"]; - normalbae_image_processor: components["schemas"]["ImageOutput"]; - infill_rgba: components["schemas"]["ImageOutput"]; - sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"]; - vae_loader: components["schemas"]["VAEOutput"]; - float_to_int: components["schemas"]["IntegerOutput"]; + dw_openpose_image_processor: components["schemas"]["ImageOutput"]; + compel: components["schemas"]["ConditioningOutput"]; + sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; + sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; + tile_image_processor: components["schemas"]["ImageOutput"]; + mediapipe_face_processor: components["schemas"]["ImageOutput"]; + metadata_item: components["schemas"]["MetadataItemOutput"]; + float_math: components["schemas"]["FloatOutput"]; + prompt_from_file: components["schemas"]["StringCollectionOutput"]; + pidi_image_processor: components["schemas"]["ImageOutput"]; + content_shuffle_image_processor: components["schemas"]["ImageOutput"]; + lineart_anime_image_processor: components["schemas"]["ImageOutput"]; + t2i_adapter: components["schemas"]["T2IAdapterOutput"]; + integer: components["schemas"]["IntegerOutput"]; + unsharp_mask: components["schemas"]["ImageOutput"]; + range: components["schemas"]["IntegerCollectionOutput"]; + string: components["schemas"]["StringOutput"]; + show_image: components["schemas"]["ImageOutput"]; + image: components["schemas"]["ImageOutput"]; + heuristic_resize: components["schemas"]["ImageOutput"]; + div: components["schemas"]["IntegerOutput"]; + rand_int: components["schemas"]["IntegerOutput"]; + float: components["schemas"]["FloatOutput"]; + img_conv: components["schemas"]["ImageOutput"]; + mask_combine: components["schemas"]["ImageOutput"]; + random_range: components["schemas"]["IntegerCollectionOutput"]; + boolean_collection: components["schemas"]["BooleanCollectionOutput"]; + pair_tile_image: components["schemas"]["PairTileImageOutput"]; + save_image: components["schemas"]["ImageOutput"]; lora_selector: components["schemas"]["LoRASelectorOutput"]; - crop_latents: components["schemas"]["LatentsOutput"]; - img_mul: components["schemas"]["ImageOutput"]; - float_range: components["schemas"]["FloatCollectionOutput"]; - merge_metadata: components["schemas"]["MetadataOutput"]; - img_blur: components["schemas"]["ImageOutput"]; boolean: components["schemas"]["BooleanOutput"]; - tile_image_processor: components["schemas"]["ImageOutput"]; - mlsd_image_processor: components["schemas"]["ImageOutput"]; + tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"]; + rectangle_mask: components["schemas"]["MaskOutput"]; + lineart_image_processor: components["schemas"]["ImageOutput"]; + midas_depth_image_processor: components["schemas"]["ImageOutput"]; + img_nsfw: components["schemas"]["ImageOutput"]; infill_patchmatch: components["schemas"]["ImageOutput"]; - img_pad_crop: components["schemas"]["ImageOutput"]; - leres_image_processor: components["schemas"]["ImageOutput"]; - sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - dw_openpose_image_processor: components["schemas"]["ImageOutput"]; - img_scale: components["schemas"]["ImageOutput"]; - pair_tile_image: components["schemas"]["PairTileImageOutput"]; - lblend: components["schemas"]["LatentsOutput"]; - range_of_size: components["schemas"]["IntegerCollectionOutput"]; - image_collection: components["schemas"]["ImageCollectionOutput"]; - calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; - img_channel_offset: components["schemas"]["ImageOutput"]; - alpha_mask_to_tensor: components["schemas"]["MaskOutput"]; + infill_lama: components["schemas"]["ImageOutput"]; infill_cv2: components["schemas"]["ImageOutput"]; - mask_combine: components["schemas"]["ImageOutput"]; + float_to_int: components["schemas"]["IntegerOutput"]; + color: components["schemas"]["ColorOutput"]; + lora_collection_loader: components["schemas"]["LoRALoaderOutput"]; + vae_loader: components["schemas"]["VAEOutput"]; string_split_neg: components["schemas"]["StringPosNegOutput"]; - sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"]; - lineart_image_processor: components["schemas"]["ImageOutput"]; - img_nsfw: components["schemas"]["ImageOutput"]; - image: components["schemas"]["ImageOutput"]; - content_shuffle_image_processor: components["schemas"]["ImageOutput"]; - canvas_paste_back: components["schemas"]["ImageOutput"]; - iterate: components["schemas"]["IterateInvocationOutput"]; - div: components["schemas"]["IntegerOutput"]; - latents_collection: components["schemas"]["LatentsCollectionOutput"]; - img_conv: components["schemas"]["ImageOutput"]; + lresize: components["schemas"]["LatentsOutput"]; + string_collection: components["schemas"]["StringCollectionOutput"]; + invert_tensor_mask: components["schemas"]["MaskOutput"]; + depth_anything_image_processor: components["schemas"]["ImageOutput"]; + hed_image_processor: components["schemas"]["ImageOutput"]; + leres_image_processor: components["schemas"]["ImageOutput"]; + img_ilerp: components["schemas"]["ImageOutput"]; + freeu: components["schemas"]["UNetOutput"]; mask_edge: components["schemas"]["ImageOutput"]; - conditioning_collection: components["schemas"]["ConditioningCollectionOutput"]; + string_join: components["schemas"]["StringOutput"]; img_hue_adjust: components["schemas"]["ImageOutput"]; - depth_anything_image_processor: components["schemas"]["ImageOutput"]; - lora_loader: components["schemas"]["LoRALoaderOutput"]; - sdxl_compel_prompt: components["schemas"]["ConditioningOutput"]; - add: components["schemas"]["IntegerOutput"]; - controlnet: components["schemas"]["ControlOutput"]; color_correct: components["schemas"]["ImageOutput"]; - random_range: components["schemas"]["IntegerCollectionOutput"]; - denoise_latents: components["schemas"]["LatentsOutput"]; - metadata: components["schemas"]["MetadataOutput"]; - i2l: components["schemas"]["LatentsOutput"]; - show_image: components["schemas"]["ImageOutput"]; - img_paste: components["schemas"]["ImageOutput"]; - infill_tile: components["schemas"]["ImageOutput"]; + calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"]; + img_chan: components["schemas"]["ImageOutput"]; + calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"]; + create_denoise_mask: components["schemas"]["DenoiseMaskOutput"]; + lblend: components["schemas"]["LatentsOutput"]; + crop_latents: components["schemas"]["LatentsOutput"]; + string_replace: components["schemas"]["StringOutput"]; + range_of_size: components["schemas"]["IntegerCollectionOutput"]; + calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"]; + iterate: components["schemas"]["IterateInvocationOutput"]; + create_gradient_mask: components["schemas"]["GradientMaskOutput"]; + face_off: components["schemas"]["FaceOffOutput"]; + sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"]; + scheduler: components["schemas"]["SchedulerOutput"]; + float_collection: components["schemas"]["FloatCollectionOutput"]; + core_metadata: components["schemas"]["MetadataOutput"]; + segment_anything_processor: components["schemas"]["ImageOutput"]; }; /** * InvocationStartedEvent @@ -13206,6 +13218,19 @@ export type components = { */ type?: "url"; }; + /** UncategorizedImageCounts */ + UncategorizedImageCounts: { + /** + * Image Count + * @description The number of uncategorized images. + */ + image_count: number; + /** + * Asset Count + * @description The number of uncategorized assets. + */ + asset_count: number; + }; /** * Unsharp Mask * @description Applies an unsharp mask filter to an image @@ -15163,6 +15188,20 @@ export type operations = { }; }; }; + /** + * Get Uncategorized Image Counts + * @description Gets count of images and assets for uncategorized images (images with no board assocation) + */ + get_uncategorized_image_counts: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": components["schemas"]["UncategorizedImageCounts"]; + }; + }; + }; + }; /** * Add Image To Board * @description Creates a board_image From a6f114867652ed3c9cd09ad53dfb500ad72bfe9d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:04:59 +1000 Subject: [PATCH 06/12] feat(ui): use new uncategorized image counts & updated list boards queries --- .../Boards/BoardsList/BoardTotalsTooltip.tsx | 18 ++++-------------- .../Boards/BoardsList/GalleryBoard.tsx | 10 ++++++++-- .../Boards/BoardsList/NoBoardBoard.tsx | 18 ++++++++++-------- .../web/src/services/api/endpoints/boards.ts | 9 +++++++++ .../frontend/web/src/services/api/index.ts | 1 + 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTotalsTooltip.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTotalsTooltip.tsx index b4c89a002df..01d6c226dd1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTotalsTooltip.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTotalsTooltip.tsx @@ -1,22 +1,12 @@ import { useTranslation } from 'react-i18next'; -import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards'; type Props = { - board_id: string; + imageCount: number; + assetCount: number; isArchived: boolean; }; -export const BoardTotalsTooltip = ({ board_id, isArchived }: Props) => { +export const BoardTotalsTooltip = ({ imageCount, assetCount, isArchived }: Props) => { const { t } = useTranslation(); - const { imagesTotal } = useGetBoardImagesTotalQuery(board_id, { - selectFromResult: ({ data }) => { - return { imagesTotal: data?.total ?? 0 }; - }, - }); - const { assetsTotal } = useGetBoardAssetsTotalQuery(board_id, { - selectFromResult: ({ data }) => { - return { assetsTotal: data?.total ?? 0 }; - }, - }); - return `${t('boards.imagesWithCount', { count: imagesTotal })}, ${t('boards.assetsWithCount', { count: assetsTotal })}${isArchived ? ` (${t('boards.archived')})` : ''}`; + return `${t('boards.imagesWithCount', { count: imageCount })}, ${t('boards.assetsWithCount', { count: assetCount })}${isArchived ? ` (${t('boards.archived')})` : ''}`; }; diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx index 8f348b5c415..584372d70ad 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx @@ -116,7 +116,13 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps {(ref) => ( } + label={ + + } openDelay={1000} placement="left" closeOnScroll @@ -166,7 +172,7 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps {autoAddBoardId === board.board_id && !editingDisclosure.isOpen && } {board.archived && !editingDisclosure.isOpen && } - {!editingDisclosure.isOpen && {board.image_count}} + {!editingDisclosure.isOpen && {board.image_count + board.asset_count}} {t('unifiedCanvas.move')}} /> diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx index 14bf3d5742d..5ab53b78d58 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx @@ -9,7 +9,7 @@ import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardB import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards'; +import { useGetUncategorizedImageCountsQuery } from 'services/api/endpoints/boards'; import { useBoardName } from 'services/api/hooks/useBoardName'; interface Props { @@ -22,11 +22,7 @@ const _hover: SystemStyleObject = { const NoBoardBoard = memo(({ isSelected }: Props) => { const dispatch = useAppDispatch(); - const { imagesTotal } = useGetBoardImagesTotalQuery('none', { - selectFromResult: ({ data }) => { - return { imagesTotal: data?.total ?? 0 }; - }, - }); + const { data } = useGetUncategorizedImageCountsQuery(); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick); const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText); @@ -60,7 +56,13 @@ const NoBoardBoard = memo(({ isSelected }: Props) => { {(ref) => ( } + label={ + + } openDelay={1000} placement="left" closeOnScroll @@ -99,7 +101,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => { {boardName} {autoAddBoardId === 'none' && } - {imagesTotal} + {(data?.image_count ?? 0) + (data?.asset_count ?? 0)} {t('unifiedCanvas.move')}} /> diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts index 55ebeab3188..dfae9a871aa 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts @@ -4,6 +4,7 @@ import type { CreateBoardArg, ListBoardsArgs, OffsetPaginatedResults_ImageDTO_, + S, UpdateBoardArg, } from 'services/api/types'; import { getListImagesUrl } from 'services/api/util'; @@ -55,6 +56,13 @@ export const boardsApi = api.injectEndpoints({ keepUnusedDataFor: 0, }), + getUncategorizedImageCounts: build.query({ + query: () => ({ + url: buildBoardsUrl('uncategorized/counts'), + }), + providesTags: ['UncategorizedImageCounts', { type: 'Board', id: LIST_TAG }, { type: 'Board', id: 'none' }], + }), + getBoardImagesTotal: build.query<{ total: number }, string | undefined>({ query: (board_id) => ({ url: getListImagesUrl({ @@ -129,4 +137,5 @@ export const { useCreateBoardMutation, useUpdateBoardMutation, useListAllImageNamesForBoardQuery, + useGetUncategorizedImageCountsQuery, } = boardsApi; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 79ea662717b..e9db2b4caae 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -44,6 +44,7 @@ const tagTypes = [ // This is invalidated on reconnect. It should be used for queries that have changing data, // especially related to the queue and generation. 'FetchOnReconnect', + 'UncategorizedImageCounts', ] as const; export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>; export const LIST_TAG = 'LIST'; From a30d143c5ad997a9a8a514956a0513f40ce1a7fc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:31:18 +1000 Subject: [PATCH 07/12] fix(ui): remove unused queries and fix invocation complete listener --- .../socketio/socketInvocationComplete.ts | 9 ---- .../web/src/services/api/endpoints/boards.ts | 47 +------------------ .../frontend/web/src/services/api/types.ts | 1 - 3 files changed, 1 insertion(+), 56 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts index 2fc6210397b..9ddbb7ed373 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete.ts @@ -13,7 +13,6 @@ import { import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { zNodeStatus } from 'features/nodes/types/invocation'; import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants'; -import { boardsApi } from 'services/api/endpoints/boards'; import { imagesApi } from 'services/api/endpoints/images'; import { getCategories, getListImagesUrl } from 'services/api/util'; import { socketInvocationComplete } from 'services/events/actions'; @@ -52,14 +51,6 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi } if (!imageDTO.is_intermediate) { - // update the total images for the board - dispatch( - boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - draft.total += 1; - }) - ); - dispatch( imagesApi.util.invalidateTags([ { type: 'Board', id: imageDTO.board_id ?? 'none' }, diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts index dfae9a871aa..2b33a0a603f 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts @@ -1,13 +1,4 @@ -import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types'; -import type { - BoardDTO, - CreateBoardArg, - ListBoardsArgs, - OffsetPaginatedResults_ImageDTO_, - S, - UpdateBoardArg, -} from 'services/api/types'; -import { getListImagesUrl } from 'services/api/util'; +import type { BoardDTO, CreateBoardArg, ListBoardsArgs, S, UpdateBoardArg } from 'services/api/types'; import type { ApiTagDescription } from '..'; import { api, buildV1Url, LIST_TAG } from '..'; @@ -63,40 +54,6 @@ export const boardsApi = api.injectEndpoints({ providesTags: ['UncategorizedImageCounts', { type: 'Board', id: LIST_TAG }, { type: 'Board', id: 'none' }], }), - getBoardImagesTotal: build.query<{ total: number }, string | undefined>({ - query: (board_id) => ({ - url: getListImagesUrl({ - board_id: board_id ?? 'none', - categories: IMAGE_CATEGORIES, - is_intermediate: false, - limit: 0, - offset: 0, - }), - method: 'GET', - }), - providesTags: (result, error, arg) => [{ type: 'BoardImagesTotal', id: arg ?? 'none' }, 'FetchOnReconnect'], - transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => { - return { total: response.total }; - }, - }), - - getBoardAssetsTotal: build.query<{ total: number }, string | undefined>({ - query: (board_id) => ({ - url: getListImagesUrl({ - board_id: board_id ?? 'none', - categories: ASSETS_CATEGORIES, - is_intermediate: false, - limit: 0, - offset: 0, - }), - method: 'GET', - }), - providesTags: (result, error, arg) => [{ type: 'BoardAssetsTotal', id: arg ?? 'none' }, 'FetchOnReconnect'], - transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => { - return { total: response.total }; - }, - }), - /** * Boards Mutations */ @@ -132,8 +89,6 @@ export const boardsApi = api.injectEndpoints({ export const { useListAllBoardsQuery, - useGetBoardImagesTotalQuery, - useGetBoardAssetsTotalQuery, useCreateBoardMutation, useUpdateBoardMutation, useListAllImageNamesForBoardQuery, diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts index 5beb5cbbf5d..34855a9c82a 100644 --- a/invokeai/frontend/web/src/services/api/types.ts +++ b/invokeai/frontend/web/src/services/api/types.ts @@ -36,7 +36,6 @@ export type AppDependencyVersions = S['AppDependencyVersions']; export type ImageDTO = S['ImageDTO']; export type BoardDTO = S['BoardDTO']; export type ImageCategory = S['ImageCategory']; -export type OffsetPaginatedResults_ImageDTO_ = S['OffsetPaginatedResults_ImageDTO_']; // Models export type ModelType = S['ModelType']; From 25107e427c1931a6c3b92e9af209fbb8adeefcd3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:09:55 +1000 Subject: [PATCH 08/12] tidy(app): move sqlite-specific objects to sqlite file --- .../board_records/board_records_common.py | 92 ------------------ .../board_records/board_records_sqlite.py | 97 ++++++++++++++++++- 2 files changed, 93 insertions(+), 96 deletions(-) diff --git a/invokeai/app/services/board_records/board_records_common.py b/invokeai/app/services/board_records/board_records_common.py index 80a4c3313a7..3478746536f 100644 --- a/invokeai/app/services/board_records/board_records_common.py +++ b/invokeai/app/services/board_records/board_records_common.py @@ -1,103 +1,11 @@ from datetime import datetime from typing import Any, Optional, Union -from attr import dataclass from pydantic import BaseModel, Field from invokeai.app.util.misc import get_iso_timestamp from invokeai.app.util.model_exclude_null import BaseModelExcludeNull -# This query is missing a GROUP BY clause, which is required for the query to be valid. -BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY = """ - SELECT b.board_id, - b.board_name, - b.created_at, - b.updated_at, - b.archived, - COUNT( - CASE - WHEN i.image_category in ('general') - AND i.is_intermediate = 0 THEN 1 - END - ) AS image_count, - COUNT( - CASE - WHEN i.image_category in ('control', 'mask', 'user', 'other') - AND i.is_intermediate = 0 THEN 1 - END - ) AS asset_count, - ( - SELECT bi.image_name - FROM board_images bi - JOIN images i ON bi.image_name = i.image_name - WHERE bi.board_id = b.board_id - AND i.is_intermediate = 0 - ORDER BY i.created_at DESC - LIMIT 1 - ) AS cover_image_name - FROM boards b - LEFT JOIN board_images bi ON b.board_id = bi.board_id - LEFT JOIN images i ON bi.image_name = i.image_name - """ - - -@dataclass -class PaginatedBoardRecordsQueries: - main_query: str - total_count_query: str - - -def get_paginated_list_board_records_query(include_archived: bool) -> PaginatedBoardRecordsQueries: - """Gets a query to retrieve a paginated list of board records.""" - - archived_condition = "WHERE b.archived = 0" if not include_archived else "" - - # The GROUP BY must be added _after_ the WHERE clause! - main_query = f""" - {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} - {archived_condition} - GROUP BY b.board_id, - b.board_name, - b.created_at, - b.updated_at - ORDER BY b.created_at DESC - LIMIT ? OFFSET ?; - """ - - total_count_query = f""" - SELECT COUNT(*) - FROM boards b - {archived_condition}; - """ - - return PaginatedBoardRecordsQueries(main_query=main_query, total_count_query=total_count_query) - - -def get_list_all_board_records_query(include_archived: bool) -> str: - """Gets a query to retrieve all board records.""" - - archived_condition = "WHERE b.archived = 0" if not include_archived else "" - - # The GROUP BY must be added _after_ the WHERE clause! - return f""" - {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} - {archived_condition} - GROUP BY b.board_id, - b.board_name, - b.created_at, - b.updated_at - ORDER BY b.created_at DESC; - """ - - -def get_board_record_query() -> str: - """Gets a query to retrieve a board record.""" - - return f""" - {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} - WHERE b.board_id = ?; - """ - class BoardRecord(BaseModelExcludeNull): """Deserialized board record.""" diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index 27b47ea57dd..2a1fcd35d7f 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -1,5 +1,6 @@ import sqlite3 import threading +from dataclasses import dataclass from typing import Union, cast from invokeai.app.services.board_records.board_records_base import BoardRecordStorageBase @@ -11,14 +12,102 @@ BoardRecordSaveException, UncategorizedImageCounts, deserialize_board_record, - get_board_record_query, - get_list_all_board_records_query, - get_paginated_list_board_records_query, ) from invokeai.app.services.shared.pagination import OffsetPaginatedResults from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.app.util.misc import uuid_string +# This query is missing a GROUP BY clause, which is required for the query to be valid. +BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY = """ + SELECT b.board_id, + b.board_name, + b.created_at, + b.updated_at, + b.archived, + COUNT( + CASE + WHEN i.image_category in ('general') + AND i.is_intermediate = 0 THEN 1 + END + ) AS image_count, + COUNT( + CASE + WHEN i.image_category in ('control', 'mask', 'user', 'other') + AND i.is_intermediate = 0 THEN 1 + END + ) AS asset_count, + ( + SELECT bi.image_name + FROM board_images bi + JOIN images i ON bi.image_name = i.image_name + WHERE bi.board_id = b.board_id + AND i.is_intermediate = 0 + ORDER BY i.created_at DESC + LIMIT 1 + ) AS cover_image_name + FROM boards b + LEFT JOIN board_images bi ON b.board_id = bi.board_id + LEFT JOIN images i ON bi.image_name = i.image_name + """ + + +@dataclass +class PaginatedBoardRecordsQueries: + main_query: str + total_count_query: str + + +def get_paginated_list_board_records_queries(include_archived: bool) -> PaginatedBoardRecordsQueries: + """Gets a query to retrieve a paginated list of board records.""" + + archived_condition = "WHERE b.archived = 0" if not include_archived else "" + + # The GROUP BY must be added _after_ the WHERE clause! + main_query = f""" + {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + {archived_condition} + GROUP BY b.board_id, + b.board_name, + b.created_at, + b.updated_at + ORDER BY b.created_at DESC + LIMIT ? OFFSET ?; + """ + + total_count_query = f""" + SELECT COUNT(*) + FROM boards b + {archived_condition}; + """ + + return PaginatedBoardRecordsQueries(main_query=main_query, total_count_query=total_count_query) + + +def get_list_all_board_records_query(include_archived: bool) -> str: + """Gets a query to retrieve all board records.""" + + archived_condition = "WHERE b.archived = 0" if not include_archived else "" + + # The GROUP BY must be added _after_ the WHERE clause! + return f""" + {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + {archived_condition} + GROUP BY b.board_id, + b.board_name, + b.created_at, + b.updated_at + ORDER BY b.created_at DESC; + """ + + +def get_board_record_query() -> str: + """Gets a query to retrieve a board record.""" + + return f""" + {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + WHERE b.board_id = ?; + """ + class SqliteBoardRecordStorage(BoardRecordStorageBase): _conn: sqlite3.Connection @@ -149,7 +238,7 @@ def get_many( try: self._lock.acquire() - queries = get_paginated_list_board_records_query(include_archived=include_archived) + queries = get_paginated_list_board_records_queries(include_archived=include_archived) self._cursor.execute( queries.main_query, From c586d65a548d461cf245cc1cdde966710f580170 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:17:33 +1000 Subject: [PATCH 09/12] tidy(app): remove extraneous condition from query --- invokeai/app/services/board_records/board_records_sqlite.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index 2a1fcd35d7f..c78e6a62f86 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -285,8 +285,7 @@ def get_uncategorized_image_counts(self) -> UncategorizedImageCounts: COUNT(*) AS unassigned_count FROM images i LEFT JOIN board_images bi ON i.image_name = bi.image_name - WHERE i.image_category IN ('general', 'control', 'mask', 'user', 'other') - AND bi.board_id IS NULL + WHERE bi.board_id IS NULL AND i.is_intermediate = 0 GROUP BY category_type; """ From 9d3a72fff369798c1849c870f0f44721713c0120 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:25:12 +1000 Subject: [PATCH 10/12] docs(app): add comments to boards queries --- .../board_records/board_records_sqlite.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index c78e6a62f86..f7848c2195b 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -24,30 +24,34 @@ b.created_at, b.updated_at, b.archived, + -- Count the number of images in the board, alias image_count COUNT( CASE - WHEN i.image_category in ('general') - AND i.is_intermediate = 0 THEN 1 + WHEN i.image_category in ('general') -- Images (UI category) are in the 'general' category + AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted END ) AS image_count, + -- Count the number of assets in the board, alias asset_count COUNT( CASE - WHEN i.image_category in ('control', 'mask', 'user', 'other') - AND i.is_intermediate = 0 THEN 1 + WHEN i.image_category in ('control', 'mask', 'user', 'other') -- Assets (UI category) are in one of these categories + AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted END ) AS asset_count, + -- Get the name of the the most recent image in the board, alias cover_image_name ( SELECT bi.image_name FROM board_images bi JOIN images i ON bi.image_name = i.image_name WHERE bi.board_id = b.board_id - AND i.is_intermediate = 0 - ORDER BY i.created_at DESC + AND i.is_intermediate = 0 -- Intermediates cannot be cover images + ORDER BY i.created_at DESC -- Sort by created_at to get the most recent image LIMIT 1 ) AS cover_image_name FROM boards b LEFT JOIN board_images bi ON b.board_id = bi.board_id LEFT JOIN images i ON bi.image_name = i.image_name + -- This query is missing a GROUP BY clause! The utility functions using this query must add it """ @@ -279,15 +283,15 @@ def get_uncategorized_image_counts(self) -> UncategorizedImageCounts: query = """ SELECT CASE - WHEN i.image_category = 'general' THEN 'images' - ELSE 'assets' + WHEN i.image_category = 'general' THEN 'images' -- Images (UI category) includes images in the 'general' DB category + ELSE 'assets' -- Assets (UI category) includes all other DB categories: 'control', 'mask', 'user', 'other' END AS category_type, COUNT(*) AS unassigned_count FROM images i LEFT JOIN board_images bi ON i.image_name = bi.image_name - WHERE bi.board_id IS NULL - AND i.is_intermediate = 0 - GROUP BY category_type; + WHERE bi.board_id IS NULL -- Uncategorized images have no board + AND i.is_intermediate = 0 -- Omit intermediates from the counts + GROUP BY category_type; -- Group by category_type alias, as derived from the image_category column earlier """ self._cursor.execute(query) results = self._cursor.fetchall() From dca5a2ce261e4a0807581f6d2330c88555446604 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 20 Jul 2024 19:57:12 +1000 Subject: [PATCH 11/12] fix(app): fix swapped image counts for uncategorized --- invokeai/app/services/board_records/board_records_sqlite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index f7848c2195b..a9f5605beb4 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -295,8 +295,8 @@ def get_uncategorized_image_counts(self) -> UncategorizedImageCounts: """ self._cursor.execute(query) results = self._cursor.fetchall() - image_count = results[0][1] - asset_count = results[1][1] + image_count = dict(results)['images'] + asset_count = dict(results)['assets'] return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count) finally: self._lock.release() From 3cd836efdee4a4861784b652ce2ce6a3791167ab Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:35:20 +1000 Subject: [PATCH 12/12] feat(app): cleanup of board queries --- .../board_records/board_records_sqlite.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py index a9f5605beb4..10f0e283863 100644 --- a/invokeai/app/services/board_records/board_records_sqlite.py +++ b/invokeai/app/services/board_records/board_records_sqlite.py @@ -17,8 +17,9 @@ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase from invokeai.app.util.misc import uuid_string -# This query is missing a GROUP BY clause, which is required for the query to be valid. -BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY = """ +BASE_BOARD_RECORD_QUERY = """ + -- This query retrieves board records, joining with the board_images and images tables to get image counts and cover image names. + -- It is not a complete query, as it is missing a GROUP BY or WHERE clause (and is unterminated). SELECT b.board_id, b.board_name, b.created_at, @@ -27,14 +28,14 @@ -- Count the number of images in the board, alias image_count COUNT( CASE - WHEN i.image_category in ('general') -- Images (UI category) are in the 'general' category + WHEN i.image_category in ('general') -- "Images" are images in the 'general' category AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted END ) AS image_count, -- Count the number of assets in the board, alias asset_count COUNT( CASE - WHEN i.image_category in ('control', 'mask', 'user', 'other') -- Assets (UI category) are in one of these categories + WHEN i.image_category in ('control', 'mask', 'user', 'other') -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other') AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted END ) AS asset_count, @@ -51,7 +52,6 @@ FROM boards b LEFT JOIN board_images bi ON b.board_id = bi.board_id LEFT JOIN images i ON bi.image_name = i.image_name - -- This query is missing a GROUP BY clause! The utility functions using this query must add it """ @@ -68,7 +68,7 @@ def get_paginated_list_board_records_queries(include_archived: bool) -> Paginate # The GROUP BY must be added _after_ the WHERE clause! main_query = f""" - {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + {BASE_BOARD_RECORD_QUERY} {archived_condition} GROUP BY b.board_id, b.board_name, @@ -94,7 +94,7 @@ def get_list_all_board_records_query(include_archived: bool) -> str: # The GROUP BY must be added _after_ the WHERE clause! return f""" - {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + {BASE_BOARD_RECORD_QUERY} {archived_condition} GROUP BY b.board_id, b.board_name, @@ -108,7 +108,7 @@ def get_board_record_query() -> str: """Gets a query to retrieve a board record.""" return f""" - {BASE_UNTERMINATED_AND_MISSING_GROUP_BY_BOARD_RECORDS_QUERY} + {BASE_BOARD_RECORD_QUERY} WHERE b.board_id = ?; """ @@ -281,22 +281,23 @@ def get_uncategorized_image_counts(self) -> UncategorizedImageCounts: try: self._lock.acquire() query = """ + -- Get the count of uncategorized images and assets. SELECT CASE - WHEN i.image_category = 'general' THEN 'images' -- Images (UI category) includes images in the 'general' DB category - ELSE 'assets' -- Assets (UI category) includes all other DB categories: 'control', 'mask', 'user', 'other' + WHEN i.image_category = 'general' THEN 'image_count' -- "Images" are images in the 'general' category + ELSE 'asset_count' -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other') END AS category_type, COUNT(*) AS unassigned_count FROM images i LEFT JOIN board_images bi ON i.image_name = bi.image_name - WHERE bi.board_id IS NULL -- Uncategorized images have no board + WHERE bi.board_id IS NULL -- Uncategorized images have no board association AND i.is_intermediate = 0 -- Omit intermediates from the counts GROUP BY category_type; -- Group by category_type alias, as derived from the image_category column earlier """ self._cursor.execute(query) results = self._cursor.fetchall() - image_count = dict(results)['images'] - asset_count = dict(results)['assets'] + image_count = dict(results)["image_count"] + asset_count = dict(results)["asset_count"] return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count) finally: self._lock.release()