Compare commits

...

12 Commits

Author SHA1 Message Date
psychedelicious
3cd836efde feat(app): cleanup of board queries 2024-09-24 15:35:20 +10:00
psychedelicious
dca5a2ce26 fix(app): fix swapped image counts for uncategorized 2024-07-20 19:57:12 +10:00
psychedelicious
9d3a72fff3 docs(app): add comments to boards queries 2024-07-15 17:25:47 +10:00
psychedelicious
c586d65a54 tidy(app): remove extraneous condition from query 2024-07-15 17:25:47 +10:00
psychedelicious
25107e427c tidy(app): move sqlite-specific objects to sqlite file 2024-07-15 17:25:47 +10:00
psychedelicious
a30d143c5a fix(ui): remove unused queries and fix invocation complete listener 2024-07-15 17:25:47 +10:00
psychedelicious
a6f1148676 feat(ui): use new uncategorized image counts & updated list boards queries 2024-07-15 17:25:47 +10:00
psychedelicious
2843a6a227 chore(ui): typegen
chore(ui): typegen
2024-07-15 17:25:47 +10:00
psychedelicious
0484f458b6 tests(app): fix bulk downloads test 2024-07-15 17:25:47 +10:00
psychedelicious
c05f97d8ca 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.
2024-07-15 17:25:47 +10:00
psychedelicious
a95aa6cc16 feat(api): add get_uncategorized_image_counts endpoint 2024-07-15 17:25:47 +10:00
psychedelicious
c74b9a40af feat(app): add get_uncategorized_image_counts method on board_records service 2024-07-15 17:25:47 +10:00
15 changed files with 402 additions and 340 deletions

View File

@@ -5,7 +5,7 @@ from fastapi.routing import APIRouter
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()

View File

@@ -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 @@ class BoardRecordStorageBase(ABC):
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

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Optional, Union
from typing import Any, Optional, Union
from pydantic import BaseModel, Field
@@ -26,21 +26,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 +55,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,19 +69,24 @@ 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)
class UncategorizedImageCounts(BaseModel):
image_count: int = Field(description="The number of uncategorized images.")
asset_count: int = Field(description="The number of uncategorized assets.")

View File

@@ -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
@@ -9,12 +10,108 @@ from invokeai.app.services.board_records.board_records_common import (
BoardRecordDeleteException,
BoardRecordNotFoundException,
BoardRecordSaveException,
UncategorizedImageCounts,
deserialize_board_record,
)
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
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,
b.updated_at,
b.archived,
-- Count the number of images in the board, alias image_count
COUNT(
CASE
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" 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,
-- 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 -- 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
"""
@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_BOARD_RECORD_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_BOARD_RECORD_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_BOARD_RECORD_QUERY}
WHERE b.board_id = ?;
"""
class SqliteBoardRecordStorage(BoardRecordStorageBase):
_conn: sqlite3.Connection
@@ -76,11 +173,7 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
try:
self._lock.acquire()
self._cursor.execute(
"""--sql
SELECT *
FROM boards
WHERE board_id = ?;
""",
get_board_record_query(),
(board_id,),
)
@@ -92,7 +185,7 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
self._lock.release()
if result is None:
raise BoardRecordNotFoundException
return BoardRecord(**dict(result))
return deserialize_board_record(dict(result))
def update(
self,
@@ -149,45 +242,17 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
try:
self._lock.acquire()
# Build base query
base_query = """
SELECT *
FROM boards
{archived_filter}
ORDER BY created_at DESC
LIMIT ? OFFSET ?;
"""
queries = get_paginated_list_board_records_queries(include_archived=include_archived)
# Determine archived filter condition
if include_archived:
archived_filter = ""
else:
archived_filter = "WHERE archived = 0"
final_query = base_query.format(archived_filter=archived_filter)
# 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)
@@ -201,26 +266,9 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
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:
@@ -228,3 +276,28 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
raise e
finally:
self._lock.release()
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 '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 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)["image_count"]
asset_count = dict(results)["asset_count"]
return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count)
finally:
self._lock.release()

View File

@@ -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."""
"""Deserialized board record."""
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."""
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

View File

@@ -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 @@ class BoardService(BoardServiceABC):
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 @@ class BoardService(BoardServiceABC):
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 @@ class BoardService(BoardServiceABC):
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

View File

@@ -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' },

View File

@@ -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')})` : ''}`;
};

View File

@@ -116,7 +116,13 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
<BoardContextMenu board={board} setBoardToDelete={setBoardToDelete}>
{(ref) => (
<Tooltip
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
label={
<BoardTotalsTooltip
imageCount={board.image_count}
assetCount={board.asset_count}
isArchived={Boolean(board.archived)}
/>
}
openDelay={1000}
placement="left"
closeOnScroll
@@ -166,7 +172,7 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
</Editable>
{autoAddBoardId === board.board_id && !editingDisclosure.isOpen && <AutoAddBadge />}
{board.archived && !editingDisclosure.isOpen && <Icon as={PiArchiveBold} fill="base.300" />}
{!editingDisclosure.isOpen && <Text variant="subtext">{board.image_count}</Text>}
{!editingDisclosure.isOpen && <Text variant="subtext">{board.image_count + board.asset_count}</Text>}
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
</Flex>

View File

@@ -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) => {
<NoBoardBoardContextMenu>
{(ref) => (
<Tooltip
label={<BoardTotalsTooltip board_id="none" isArchived={false} />}
label={
<BoardTotalsTooltip
imageCount={data?.image_count ?? 0}
assetCount={data?.asset_count ?? 0}
isArchived={false}
/>
}
openDelay={1000}
placement="left"
closeOnScroll
@@ -99,7 +101,7 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
{boardName}
</Text>
{autoAddBoardId === 'none' && <AutoAddBadge />}
<Text variant="subtext">{imagesTotal}</Text>
<Text variant="subtext">{(data?.image_count ?? 0) + (data?.asset_count ?? 0)}</Text>
<IAIDroppable data={droppableData} dropLabel={<Text fontSize="md">{t('unifiedCanvas.move')}</Text>} />
</Flex>
</Tooltip>

View File

@@ -1,12 +1,4 @@
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
import type {
BoardDTO,
CreateBoardArg,
ListBoardsArgs,
OffsetPaginatedResults_ImageDTO_,
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 '..';
@@ -55,38 +47,11 @@ export const boardsApi = api.injectEndpoints({
keepUnusedDataFor: 0,
}),
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',
getUncategorizedImageCounts: build.query<S['UncategorizedImageCounts'], void>({
query: () => ({
url: buildBoardsUrl('uncategorized/counts'),
}),
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 };
},
providesTags: ['UncategorizedImageCounts', { type: 'Board', id: LIST_TAG }, { type: 'Board', id: 'none' }],
}),
/**
@@ -124,9 +89,8 @@ export const boardsApi = api.injectEndpoints({
export const {
useListAllBoardsQuery,
useGetBoardImagesTotalQuery,
useGetBoardAssetsTotalQuery,
useCreateBoardMutation,
useUpdateBoardMutation,
useListAllImageNamesForBoardQuery,
useGetUncategorizedImageCountsQuery,
} = boardsApi;

View File

@@ -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';

View File

@@ -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"];
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"];
rand_float: components["schemas"]["FloatOutput"];
zoe_depth_image_processor: components["schemas"]["ImageOutput"];
face_off: components["schemas"]["FaceOffOutput"];
tile_to_properties: components["schemas"]["TileToPropertiesOutput"];
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"];
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"];
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"];
l2i: components["schemas"]["ImageOutput"];
img_lerp: components["schemas"]["ImageOutput"];
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_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"];
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"];
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_cv2: components["schemas"]["ImageOutput"];
mask_combine: components["schemas"]["ImageOutput"];
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"];
mask_edge: components["schemas"]["ImageOutput"];
conditioning_collection: components["schemas"]["ConditioningCollectionOutput"];
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"];
clip_skip: components["schemas"]["CLIPSkipInvocationOutput"];
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"];
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"];
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"];
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"];
face_identifier: components["schemas"]["ImageOutput"];
noise: components["schemas"]["NoiseOutput"];
l2i: 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"];
lscale: components["schemas"]["LatentsOutput"];
sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"];
latents: components["schemas"]["LatentsOutput"];
string_split: components["schemas"]["String2Output"];
sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"];
esrgan: components["schemas"]["ImageOutput"];
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"];
boolean: components["schemas"]["BooleanOutput"];
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"];
infill_lama: components["schemas"]["ImageOutput"];
infill_cv2: 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"];
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"];
string_join: components["schemas"]["StringOutput"];
img_hue_adjust: components["schemas"]["ImageOutput"];
color_correct: 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

View File

@@ -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'];

View File

@@ -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)