mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Compare commits
12 Commits
lstein/doc
...
psyche/exp
Author | SHA1 | Date | |
---|---|---|---|
a03c895668 | |||
dca5a2ce26 | |||
9d3a72fff3 | |||
c586d65a54 | |||
25107e427c | |||
a30d143c5a | |||
a6f1148676 | |||
2843a6a227 | |||
0484f458b6 | |||
c05f97d8ca | |||
a95aa6cc16 | |||
c74b9a40af |
@ -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,25 @@ 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()
|
||||
|
||||
|
||||
@boards_router.get(
|
||||
"/uncategorized/names",
|
||||
operation_id="get_uncategorized_image_names",
|
||||
response_model=list[str],
|
||||
)
|
||||
async def get_uncategorized_image_names() -> list[str]:
|
||||
"""Gets count of images and assets for uncategorized images (images with no board assocation)"""
|
||||
|
||||
return ApiDependencies.invoker.services.board_records.get_uncategorized_image_names()
|
||||
|
61
invokeai/app/api/routers/dupe images.ipynb
Normal file
61
invokeai/app/api/routers/dupe images.ipynb
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pathlib import Path\n",
|
||||
"import sqlite3\n",
|
||||
"from uuid import uuid4\n",
|
||||
"\n",
|
||||
"# duplicate _all_ images in gallery\n",
|
||||
"\n",
|
||||
"def duplicate_images(database_path: Path, num_copies: int):\n",
|
||||
" conn = sqlite3.connect(database_path)\n",
|
||||
" cursor = conn.cursor()\n",
|
||||
"\n",
|
||||
" cursor.execute(\"SELECT * FROM images\")\n",
|
||||
" rows = cursor.fetchall()\n",
|
||||
"\n",
|
||||
" for _ in range(num_copies):\n",
|
||||
" for row in rows:\n",
|
||||
" new_row = list(row)\n",
|
||||
" new_row[0] = str(uuid4()) # image_name is the first column\n",
|
||||
" placeholders = \", \".join(\"?\" for _ in new_row)\n",
|
||||
" cursor.execute(f\"INSERT INTO images VALUES ({placeholders})\", new_row)\n",
|
||||
"\n",
|
||||
" conn.commit()\n",
|
||||
" conn.close()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" database_path = Path(\"/home/bat/invokeai-4.0.0/databases/invokeai.db\")\n",
|
||||
" num_copies = 50\n",
|
||||
" duplicate_images(database_path, num_copies)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import io
|
||||
import traceback
|
||||
from typing import Optional
|
||||
from typing import Literal, Optional
|
||||
|
||||
from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, Response, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
@ -12,10 +12,11 @@ from invokeai.app.api.dependencies import ApiDependencies
|
||||
from invokeai.app.invocations.fields import MetadataField
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
ImageCategory,
|
||||
ImageRecord,
|
||||
ImageRecordChanges,
|
||||
ResourceOrigin,
|
||||
)
|
||||
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO
|
||||
from invokeai.app.services.images.images_common import ImageDTO, ImageUrlsDTO, image_record_to_dto
|
||||
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
|
||||
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
|
||||
|
||||
@ -450,3 +451,76 @@ async def get_bulk_download_item(
|
||||
return response
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
@images_router.get(
|
||||
"/image_names",
|
||||
operation_id="list_image_names",
|
||||
response_model=list[str],
|
||||
)
|
||||
async def list_image_names(
|
||||
board_id: str | None = Query(default=None),
|
||||
category: Literal["images", "assets"] = Query(default="images"),
|
||||
starred_first: bool = Query(default=True),
|
||||
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending),
|
||||
search_term: Optional[str] = Query(default=None),
|
||||
) -> list[str]:
|
||||
"""Gets a list of image names"""
|
||||
|
||||
return ApiDependencies.invoker.services.image_records.get_image_names(
|
||||
board_id,
|
||||
category,
|
||||
starred_first,
|
||||
order_dir,
|
||||
search_term,
|
||||
)
|
||||
|
||||
|
||||
@images_router.get(
|
||||
"/images",
|
||||
operation_id="list_images",
|
||||
response_model=list[ImageRecord],
|
||||
)
|
||||
async def images(
|
||||
board_id: str | None = Query(default=None),
|
||||
category: Literal["images", "assets"] = Query(default="images"),
|
||||
starred_first: bool = Query(default=True),
|
||||
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending),
|
||||
search_term: str | None = Query(default=None),
|
||||
from_image_name: str | None = Query(default=None),
|
||||
count: int = Query(default=10),
|
||||
) -> list[ImageRecord]:
|
||||
"""Gets a list of image names"""
|
||||
|
||||
return ApiDependencies.invoker.services.image_records.get_images(
|
||||
board_id,
|
||||
category,
|
||||
starred_first,
|
||||
order_dir,
|
||||
search_term,
|
||||
from_image_name,
|
||||
count,
|
||||
)
|
||||
|
||||
|
||||
@images_router.post(
|
||||
"/images/by_name",
|
||||
operation_id="get_images_by_name",
|
||||
response_model=list[ImageDTO],
|
||||
)
|
||||
async def get_images_by_name(image_names: list[str] = Body(embed=True)) -> list[ImageDTO]:
|
||||
"""Gets a list of image names"""
|
||||
|
||||
image_records = ApiDependencies.invoker.services.image_records.get_images_by_name(image_names)
|
||||
|
||||
image_dtos = [
|
||||
image_record_to_dto(
|
||||
image_record=r,
|
||||
image_url=ApiDependencies.invoker.services.urls.get_image_url(r.image_name),
|
||||
thumbnail_url=ApiDependencies.invoker.services.urls.get_image_url(r.image_name, True),
|
||||
board_id=ApiDependencies.invoker.services.board_image_records.get_board_for_image(r.image_name),
|
||||
)
|
||||
for r in image_records
|
||||
]
|
||||
|
||||
return image_dtos
|
||||
|
@ -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,13 @@ 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
|
||||
|
||||
@abstractmethod
|
||||
def get_uncategorized_image_names(self) -> list[str]:
|
||||
"""Gets names of uncategorized images."""
|
||||
pass
|
||||
|
@ -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.")
|
||||
|
@ -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
|
||||
|
||||
# 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 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
|
||||
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
|
||||
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
|
||||
-- This query is missing a GROUP BY clause! The utility functions using this query must add it
|
||||
"""
|
||||
|
||||
|
||||
@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
|
||||
@ -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,46 @@ class SqliteBoardRecordStorage(BoardRecordStorageBase):
|
||||
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' -- 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 -- 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()
|
||||
image_count = dict(results)['images']
|
||||
asset_count = dict(results)['assets']
|
||||
return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count)
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def get_uncategorized_image_names(self) -> list[str]:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
SELECT image_name
|
||||
FROM images
|
||||
WHERE image_name NOT IN (
|
||||
SELECT image_name
|
||||
FROM board_images
|
||||
);
|
||||
"""
|
||||
)
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
image_names = [r[0] for r in result]
|
||||
return image_names
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Literal, Optional
|
||||
|
||||
from invokeai.app.invocations.fields import MetadataField
|
||||
from invokeai.app.services.image_records.image_records_common import (
|
||||
@ -97,3 +97,32 @@ class ImageRecordStorageBase(ABC):
|
||||
def get_most_recent_image_for_board(self, board_id: str) -> Optional[ImageRecord]:
|
||||
"""Gets the most recent image for a board."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_image_names(
|
||||
self,
|
||||
board_id: str | None,
|
||||
category: Literal["images", "assets"],
|
||||
starred_first: bool = True,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
search_term: Optional[str] = None,
|
||||
) -> list[str]:
|
||||
"""Gets image names."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_images_by_name(self, image_names: list[str]) -> list[ImageRecord]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_images(
|
||||
self,
|
||||
board_id: str | None = None,
|
||||
category: Literal["images", "assets"] = "images",
|
||||
starred_first: bool = True,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
search_term: str | None = None,
|
||||
from_image_name: str | None = None, # omit for first page
|
||||
count: int = 10,
|
||||
) -> list[ImageRecord]:
|
||||
pass
|
||||
|
@ -1,7 +1,7 @@
|
||||
import sqlite3
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Optional, Union, cast
|
||||
from typing import Literal, Optional, Union, cast
|
||||
|
||||
from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
|
||||
from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
|
||||
@ -140,6 +140,264 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
# def get_image_names(
|
||||
# self,
|
||||
# board_id: str | None = None,
|
||||
# category: Literal["images", "assets"] = "images",
|
||||
# starred_first: bool = True,
|
||||
# order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
# search_term: Optional[str] = None,
|
||||
# ) -> list[str]:
|
||||
# try:
|
||||
# self._lock.acquire()
|
||||
|
||||
# query = """
|
||||
# SELECT images.image_name
|
||||
# FROM images
|
||||
# LEFT JOIN board_images ON board_images.image_name = images.image_name
|
||||
# WHERE images.is_intermediate = FALSE
|
||||
# """
|
||||
# params: list[int | str | bool] = []
|
||||
|
||||
# if board_id:
|
||||
# query += """
|
||||
# AND board_images.board_id = ?
|
||||
# """
|
||||
# params.append(board_id)
|
||||
# else:
|
||||
# query += """
|
||||
# AND board_images.board_id IS NULL
|
||||
# """
|
||||
|
||||
# if category == "images":
|
||||
# query += """
|
||||
# AND images.image_category = 'general'
|
||||
# """
|
||||
# elif category == "assets":
|
||||
# query += """
|
||||
# AND images.image_category IN ('control', 'mask', 'user', 'other')
|
||||
# """
|
||||
# else:
|
||||
# raise ValueError(f"Invalid category: {category}")
|
||||
|
||||
# if search_term:
|
||||
# query += """
|
||||
# AND images.metadata LIKE ?
|
||||
# """
|
||||
# params.append(f"%{search_term.lower()}%")
|
||||
|
||||
# if starred_first:
|
||||
# query += f"""
|
||||
# ORDER BY images.starred DESC, images.created_at {order_dir.value} -- cannot use parameter substitution here
|
||||
# """
|
||||
# else:
|
||||
# query += f"""
|
||||
# ORDER BY images.created_at {order_dir.value} -- cannot use parameter substitution here
|
||||
# """
|
||||
|
||||
# query += ";"
|
||||
# params_tuple = tuple(params)
|
||||
|
||||
# self._cursor.execute(query, params_tuple)
|
||||
# result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
# image_names = [str(r[0]) for r in result]
|
||||
# except Exception:
|
||||
# raise
|
||||
# finally:
|
||||
# self._lock.release()
|
||||
|
||||
# return image_names
|
||||
|
||||
def get_image_names(
|
||||
self,
|
||||
board_id: str | None = None,
|
||||
category: Literal["images", "assets"] = "images",
|
||||
starred_first: bool = True,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
search_term: str | None = None,
|
||||
) -> list[str]:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
|
||||
base_query = """
|
||||
SELECT images.image_name
|
||||
FROM images
|
||||
LEFT JOIN board_images ON board_images.image_name = images.image_name
|
||||
WHERE images.is_intermediate = FALSE
|
||||
"""
|
||||
params: list[int | str | bool] = []
|
||||
|
||||
if board_id:
|
||||
base_query += """
|
||||
AND board_images.board_id = ?
|
||||
"""
|
||||
params.append(board_id)
|
||||
else:
|
||||
base_query += """
|
||||
AND board_images.board_id IS NULL
|
||||
"""
|
||||
|
||||
if category == "images":
|
||||
base_query += """
|
||||
AND images.image_category = 'general'
|
||||
"""
|
||||
elif category == "assets":
|
||||
base_query += """
|
||||
AND images.image_category IN ('control', 'mask', 'user', 'other')
|
||||
"""
|
||||
else:
|
||||
raise ValueError(f"Invalid category: {category}")
|
||||
|
||||
if search_term:
|
||||
base_query += """
|
||||
AND images.metadata LIKE ?
|
||||
"""
|
||||
params.append(f"%{search_term.lower()}%")
|
||||
|
||||
if starred_first:
|
||||
base_query += f"""
|
||||
ORDER BY images.starred DESC, images.created_at {order_dir.value}, images.image_name {order_dir.value}
|
||||
"""
|
||||
else:
|
||||
base_query += f"""
|
||||
ORDER BY images.created_at {order_dir.value}, images.image_name {order_dir.value}
|
||||
"""
|
||||
|
||||
final_query = f"{base_query};"
|
||||
|
||||
self._cursor.execute(final_query, tuple(params))
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
images = [str(r[0]) for r in result]
|
||||
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
return images
|
||||
|
||||
def get_images_by_name(self, image_names: list[str]) -> list[ImageRecord]:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
|
||||
query = f"""
|
||||
SELECT {IMAGE_DTO_COLS}
|
||||
FROM images
|
||||
WHERE images.image_name in ({",".join("?" for _ in image_names)});
|
||||
"""
|
||||
params = tuple(image_names)
|
||||
|
||||
self._cursor.execute(query, tuple(params))
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
images = [deserialize_image_record(dict(r)) for r in result]
|
||||
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
return images
|
||||
|
||||
def get_images(
|
||||
self,
|
||||
board_id: str | None = None,
|
||||
category: Literal["images", "assets"] = "images",
|
||||
starred_first: bool = True,
|
||||
order_dir: SQLiteDirection = SQLiteDirection.Descending,
|
||||
search_term: str | None = None,
|
||||
from_image_name: str | None = None, # omit for first page
|
||||
count: int = 10,
|
||||
) -> list[ImageRecord]:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
|
||||
base_query = f"""
|
||||
SELECT {IMAGE_DTO_COLS}
|
||||
FROM images
|
||||
LEFT JOIN board_images ON board_images.image_name = images.image_name
|
||||
WHERE images.is_intermediate = FALSE
|
||||
"""
|
||||
params: list[int | str | bool] = []
|
||||
|
||||
if board_id:
|
||||
base_query += """
|
||||
AND board_images.board_id = ?
|
||||
"""
|
||||
params.append(board_id)
|
||||
else:
|
||||
base_query += """
|
||||
AND board_images.board_id IS NULL
|
||||
"""
|
||||
|
||||
if category == "images":
|
||||
base_query += """
|
||||
AND images.image_category = 'general'
|
||||
"""
|
||||
elif category == "assets":
|
||||
base_query += """
|
||||
AND images.image_category IN ('control', 'mask', 'user', 'other')
|
||||
"""
|
||||
else:
|
||||
raise ValueError(f"Invalid category: {category}")
|
||||
|
||||
if search_term:
|
||||
base_query += """
|
||||
AND images.metadata LIKE ?
|
||||
"""
|
||||
params.append(f"%{search_term.lower()}%")
|
||||
|
||||
if from_image_name:
|
||||
# Use keyset pagination to get the next page of results
|
||||
|
||||
keyset_query = f"""
|
||||
WITH image_keyset AS (
|
||||
SELECT created_at,
|
||||
image_name
|
||||
FROM images
|
||||
WHERE image_name = ?
|
||||
)
|
||||
{base_query}
|
||||
AND (images.created_at, images.image_name) < (
|
||||
(
|
||||
SELECT created_at
|
||||
FROM image_keyset
|
||||
),
|
||||
(
|
||||
SELECT image_name
|
||||
FROM image_keyset
|
||||
)
|
||||
)
|
||||
"""
|
||||
base_query = keyset_query
|
||||
params.append(from_image_name)
|
||||
|
||||
if starred_first:
|
||||
order_by_clause = f"""
|
||||
ORDER BY images.starred DESC, images.created_at {order_dir.value}, images.image_name {order_dir.value}
|
||||
"""
|
||||
else:
|
||||
order_by_clause = f"""
|
||||
ORDER BY images.created_at {order_dir.value}, images.image_name {order_dir.value}
|
||||
"""
|
||||
|
||||
final_query = f"""
|
||||
{base_query}
|
||||
{order_by_clause}
|
||||
LIMIT ?;
|
||||
"""
|
||||
params.append(count)
|
||||
|
||||
self._cursor.execute(final_query, tuple(params))
|
||||
result = cast(list[sqlite3.Row], self._cursor.fetchall())
|
||||
images = [deserialize_image_record(dict(r)) for r in result]
|
||||
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
return images
|
||||
|
||||
def get_many(
|
||||
self,
|
||||
offset: int = 0,
|
||||
|
59
invokeai/app/services/image_records/pagination notes.md
Normal file
59
invokeai/app/services/image_records/pagination notes.md
Normal file
@ -0,0 +1,59 @@
|
||||
these ideas are trying to figure out the macOS photos UX where you use scroll position instead of page number to go to a specific range of images.
|
||||
|
||||
### Brute Force
|
||||
|
||||
Two new methods/endpoints:
|
||||
|
||||
- `get_image_names`: gets a list of ordered image names for the query params (e.g. board, starred_first)
|
||||
- `get_images_by_name`: gets the dtos for a list of image names
|
||||
|
||||
Broad strokes of client handling:
|
||||
|
||||
- Fetch a list of all image names for a board.
|
||||
- Render a big scroll area, large enough to hold all images. The list of image names is passed to `react-virtuoso` (virtualized list lib).
|
||||
- As you scroll, we use the rangeChanged callback from `react-virtuoso`, which provides the indices of the currently-visible images in the list of all images. These indices map back to the list of image names from which we can derive the list of image names we need to fetch
|
||||
- Debounce the rnageChanged callback
|
||||
- Call the `get_images_by_name` endpoint with hte image names to fetch, use the result to update the `getImageDTO` query cache. De-duplicate the image_names against existing cache before fetching so we aren't requesting the smae data over and over
|
||||
- Each item/image in the virtualized list fetches its image DTO from the cache _without initiating a network request_. it just kinda waits until the image is in the cache and then displays it
|
||||
|
||||
this is roughed out in this branch
|
||||
|
||||
#### FATAL FLAW
|
||||
|
||||
Once you generate an image, you want to do an optimistic update and insert its name into the big ol' image_names list right? well, where do you insert it? depends on the query parms that can affect the sort order and which images are shown... we only have the image names at this point so we can't easily figure out where to insert
|
||||
|
||||
workarounds (?):
|
||||
|
||||
- along with the image names, we retrieve `starred_first` and `created_at`. then from the query params we can easily figure out where to insert the new image into the list to match the sort that he backend will be doing. eh
|
||||
- fetch `starred_first` images separately? so we don't have to worry about inserting the image into the right spot?
|
||||
|
||||
ahh but also metadata search... we won't know when to insert the image into the list if the user has a search term...
|
||||
|
||||
#### Sub-idea
|
||||
|
||||
Ok let's still use pagination but use virtuoso to tell us which page we are on.
|
||||
|
||||
virtuoso has an alternate mode where you just tell it how many items you have and it renders each item, passing only an index to it. Maybe we can derive the limit and offset from this information. here's an untested idea:
|
||||
|
||||
- pass virtuoso the board count
|
||||
- Instead of rendering individual images in the list, we render pages (ranges) of images. The list library’s rangeChanged indices now refer to pages or ranges. To the user, it still looks like a bunch of individual images, but internally we group it into pages/ranges of whatever size.
|
||||
- The page/range size is calculated via DOM, or we can rely on virtuoso to tell us how many items are to be rendered. only thing is it the number can different depending on scroll position, so we'd probably want to like take `endIndex - startIndex` as the limit, add 20% buffer to each end of the limit and round it to the nearest multiple of 5 or 10. that would give us a consistent limit
|
||||
- then we can derive offset from that value
|
||||
|
||||
still has the issue where we aren't sure if we should trigger a image list cache invalidation...
|
||||
|
||||
### More Efficient Pagination
|
||||
|
||||
sql OFFSET requires a scan thru the whole table upt othe offset. that means the higher the offset, the slower the query. unsure of the practical impact of this, probably negligible for us right now.
|
||||
|
||||
I did some quick experiments with cursor/keyset pagination, using an image name as the cursor. this doesn't have the perf issue w/ offset.
|
||||
|
||||
Also! This kind of pagination is unaffected by insertions and deletions, which is a problem for limit/offset pagination. When you insert or delete an image, it doesn't shift images at higher pages down. I think this pagination strategy suits our gallery better than limit/offset, given how volatile it is with adding and removing images regularly.
|
||||
|
||||
see the `test_keyset` notebook for implementation (also some scattered methods in services as I was fiddling withh it)
|
||||
|
||||
may be some way to use this pagination strat in combination with the above ideas to more elegantly handle inserting and deleting images...
|
||||
|
||||
### Alternative approach to the whole "how do we know when to insert new images in the list (or invalidate the list cache)" issue
|
||||
|
||||
What if we _always_ invalidate the cache when youa re at the top of the list ,but never invalidate it when you have scrolled down?
|
210
invokeai/app/services/image_records/test_keyset.ipynb
Normal file
210
invokeai/app/services/image_records/test_keyset.ipynb
Normal file
@ -0,0 +1,210 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"first query\n",
|
||||
"36e62fec-5c3a-4b28-867b-9029fb6d2319.png False\n",
|
||||
"c7f4f4b8-7ce6-4594-abf6-3f5e13fb7fe9.png False\n",
|
||||
"d8f57fda-5084-4d87-8668-06fb300282e4.png False\n",
|
||||
"a2fd7b8b-bbe5-4629-9d46-000f99b64931.png False\n",
|
||||
"c0880bc1-5f7a-452b-acea-53a261f4c0c4.png False\n",
|
||||
"0ad957df-c341-48e3-b384-f656985c2722.png False\n",
|
||||
"8c788d82-c81c-4ffe-bf6b-bdad601c5add.png False\n",
|
||||
"9b1179a0-09a0-4430-918d-60b618ff040c.png False\n",
|
||||
"c8ad6a32-75db-4d8b-a865-066365fa1563.png False\n",
|
||||
"e5eb1c19-8c69-4d29-a447-fbc2d649334a.png False\n",
|
||||
"\n",
|
||||
"next query, starting from the second image\n",
|
||||
"36e62fec-5c3a-4b28-867b-9029fb6d2319.png False\n",
|
||||
"c7f4f4b8-7ce6-4594-abf6-3f5e13fb7fe9.png False\n",
|
||||
"d8f57fda-5084-4d87-8668-06fb300282e4.png False\n",
|
||||
"a2fd7b8b-bbe5-4629-9d46-000f99b64931.png False\n",
|
||||
"c0880bc1-5f7a-452b-acea-53a261f4c0c4.png False\n",
|
||||
"0ad957df-c341-48e3-b384-f656985c2722.png False\n",
|
||||
"8c788d82-c81c-4ffe-bf6b-bdad601c5add.png False\n",
|
||||
"9b1179a0-09a0-4430-918d-60b618ff040c.png False\n",
|
||||
"c8ad6a32-75db-4d8b-a865-066365fa1563.png False\n",
|
||||
"e5eb1c19-8c69-4d29-a447-fbc2d649334a.png False\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import sqlite3\n",
|
||||
"from typing import Literal, cast\n",
|
||||
"from invokeai.app.services.image_records.image_records_common import (\n",
|
||||
" IMAGE_DTO_COLS,\n",
|
||||
" ImageRecord,\n",
|
||||
" deserialize_image_record,\n",
|
||||
")\n",
|
||||
"from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_images(\n",
|
||||
" from_image_name: str | None = None, # omit for first page\n",
|
||||
" count: int = 10,\n",
|
||||
" board_id: str | None = None,\n",
|
||||
" category: Literal[\"images\", \"assets\"] = \"images\",\n",
|
||||
" starred_first: bool = False,\n",
|
||||
" order_dir: SQLiteDirection = SQLiteDirection.Descending,\n",
|
||||
" search_term: str | None = None,\n",
|
||||
") -> list[ImageRecord]:\n",
|
||||
" conn = sqlite3.connect(\"/home/bat/invokeai-4.0.0/databases/invokeai.db\")\n",
|
||||
" conn.row_factory = sqlite3.Row\n",
|
||||
" cursor = conn.cursor()\n",
|
||||
"\n",
|
||||
" base_query = f\"\"\"\n",
|
||||
" SELECT {IMAGE_DTO_COLS}\n",
|
||||
" FROM images\n",
|
||||
" LEFT JOIN board_images ON board_images.image_name = images.image_name\n",
|
||||
" WHERE images.is_intermediate = FALSE\n",
|
||||
" \"\"\"\n",
|
||||
" params: list[int | str | bool] = []\n",
|
||||
"\n",
|
||||
" if board_id:\n",
|
||||
" base_query += \"\"\"\n",
|
||||
" AND board_images.board_id = ?\n",
|
||||
" \"\"\"\n",
|
||||
" params.append(board_id)\n",
|
||||
" else:\n",
|
||||
" base_query += \"\"\"\n",
|
||||
" AND board_images.board_id IS NULL\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" if category == \"images\":\n",
|
||||
" base_query += \"\"\"\n",
|
||||
" AND images.image_category = 'general'\n",
|
||||
" \"\"\"\n",
|
||||
" elif category == \"assets\":\n",
|
||||
" base_query += \"\"\"\n",
|
||||
" AND images.image_category IN ('control', 'mask', 'user', 'other')\n",
|
||||
" \"\"\"\n",
|
||||
" else:\n",
|
||||
" raise ValueError(f\"Invalid category: {category}\")\n",
|
||||
"\n",
|
||||
" if search_term:\n",
|
||||
" base_query += \"\"\"\n",
|
||||
" AND images.metadata LIKE ?\n",
|
||||
" \"\"\"\n",
|
||||
" params.append(f\"%{search_term.lower()}%\")\n",
|
||||
"\n",
|
||||
" if from_image_name:\n",
|
||||
" # Use keyset pagination to get the next page of results\n",
|
||||
"\n",
|
||||
" # This uses `<` so that the cursor image is NOT included in the results - only images after it\n",
|
||||
" if starred_first:\n",
|
||||
" keyset_query = f\"\"\"\n",
|
||||
" WITH image_keyset AS (\n",
|
||||
" SELECT created_at,\n",
|
||||
" image_name,\n",
|
||||
" starred\n",
|
||||
" FROM images\n",
|
||||
" WHERE image_name = ?\n",
|
||||
" )\n",
|
||||
" {base_query}\n",
|
||||
" AND (images.starred, images.created_at, images.image_name) < ((SELECT starred FROM image_keyset), (SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
|
||||
" \"\"\"\n",
|
||||
" else:\n",
|
||||
" keyset_query = f\"\"\"\n",
|
||||
" WITH image_keyset AS (\n",
|
||||
" SELECT created_at,\n",
|
||||
" image_name\n",
|
||||
" FROM images\n",
|
||||
" WHERE image_name = ?\n",
|
||||
" )\n",
|
||||
" {base_query}\n",
|
||||
" AND (images.created_at, images.image_name) < ((SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" # This uses `<=` so that the cursor image IS included in the results\n",
|
||||
" # if starred_first:\n",
|
||||
" # keyset_query = f\"\"\"\n",
|
||||
" # WITH image_keyset AS (\n",
|
||||
" # SELECT created_at,\n",
|
||||
" # image_name,\n",
|
||||
" # starred\n",
|
||||
" # FROM images\n",
|
||||
" # WHERE image_name = ?\n",
|
||||
" # )\n",
|
||||
" # {base_query}\n",
|
||||
" # AND (images.starred, images.created_at, images.image_name) <= ((SELECT starred FROM image_keyset), (SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
|
||||
" # \"\"\"\n",
|
||||
" # else:\n",
|
||||
" # keyset_query = f\"\"\"\n",
|
||||
" # WITH image_keyset AS (\n",
|
||||
" # SELECT created_at,\n",
|
||||
" # image_name\n",
|
||||
" # FROM images\n",
|
||||
" # WHERE image_name = ?\n",
|
||||
" # )\n",
|
||||
" # {base_query}\n",
|
||||
" # AND (images.created_at, images.image_name) <= ((SELECT created_at FROM image_keyset), (SELECT image_name FROM image_keyset))\n",
|
||||
" # \"\"\"\n",
|
||||
" base_query = keyset_query\n",
|
||||
" params.append(from_image_name)\n",
|
||||
"\n",
|
||||
" if starred_first:\n",
|
||||
" order_by_clause = f\"\"\"\n",
|
||||
" ORDER BY images.starred DESC, images.created_at {order_dir.value}, images.image_name {order_dir.value}\n",
|
||||
" \"\"\"\n",
|
||||
" else:\n",
|
||||
" order_by_clause = f\"\"\"\n",
|
||||
" ORDER BY images.created_at {order_dir.value}, images.image_name {order_dir.value}\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" final_query = f\"\"\"\n",
|
||||
" {base_query}\n",
|
||||
" {order_by_clause}\n",
|
||||
" LIMIT ?;\n",
|
||||
" \"\"\"\n",
|
||||
" params.append(count)\n",
|
||||
"\n",
|
||||
" cursor.execute(final_query, tuple(params))\n",
|
||||
" result = cast(list[sqlite3.Row], cursor.fetchall())\n",
|
||||
" images = [deserialize_image_record(dict(r)) for r in result]\n",
|
||||
"\n",
|
||||
" return images\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"kwargs = {\"starred_first\": False}\n",
|
||||
"\n",
|
||||
"images = get_images(**kwargs)\n",
|
||||
"print(\"first query\")\n",
|
||||
"for image in images:\n",
|
||||
" print(image.image_name, image.starred)\n",
|
||||
"\n",
|
||||
"print(\"\\nnext query, starting from the second image\")\n",
|
||||
"images_2 = get_images(from_image_name=images[0].image_name, **kwargs)\n",
|
||||
"for image in images_2:\n",
|
||||
" print(image.image_name, image.starred)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
@ -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,11 +51,13 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
|
||||
}
|
||||
|
||||
if (!imageDTO.is_intermediate) {
|
||||
// update the total images for the board
|
||||
console.log('maybe updating getImageNames');
|
||||
dispatch(
|
||||
boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
draft.total += 1;
|
||||
imagesApi.util.updateQueryData('getImageNames', { starred_first: false }, (draft) => {
|
||||
if (!draft.find((name) => name === imageDTO.image_name)) {
|
||||
console.log('image not found, adding');
|
||||
draft.unshift(imageDTO.image_name);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -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')})` : ''}`;
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
import { GalleryImageListExperiment } from 'features/gallery/components/ImageGrid/GalleryImageListExperiment';
|
||||
import { galleryViewChanged } from 'features/gallery/store/gallerySlice';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||
@ -26,7 +27,6 @@ import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
import BoardsList from './Boards/BoardsList/BoardsList';
|
||||
import BoardsSearch from './Boards/BoardsList/BoardsSearch';
|
||||
import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopover';
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
import { GalleryPagination } from './ImageGrid/GalleryPagination';
|
||||
import { GallerySearch } from './ImageGrid/GallerySearch';
|
||||
|
||||
@ -168,7 +168,7 @@ const ImageGalleryContent = () => {
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
<GalleryImageGrid />
|
||||
<GalleryImageListExperiment />
|
||||
<GalleryPagination />
|
||||
</Flex>
|
||||
</Panel>
|
||||
|
@ -0,0 +1,122 @@
|
||||
import { Box, Flex, Image, Skeleton, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppStore } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ListRange } from 'react-virtuoso';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { imagesApi, useGetImageNamesQuery, useLazyGetImagesByNameQuery } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type TableVirtuosoScrollerRef = (ref: HTMLElement | Window | null) => any;
|
||||
|
||||
export const GalleryImageListExperiment = memo(() => {
|
||||
const store = useAppStore();
|
||||
const { data } = useGetImageNamesQuery({ starred_first: false });
|
||||
const [getImagesByName] = useLazyGetImagesByNameQuery();
|
||||
|
||||
const itemContent = useCallback((index: number, data: string) => {
|
||||
return <ListItem index={index} data={data} />;
|
||||
}, []);
|
||||
|
||||
const onRangeChanged = useCallback(
|
||||
({ startIndex, endIndex }: ListRange) => {
|
||||
// user has scrolled to a new range, fetch images that are not already in the store
|
||||
console.log('rangeChanged', startIndex, endIndex);
|
||||
|
||||
// get the list of image names represented by this range
|
||||
// endIndex must be +1 bc else we miss the last image
|
||||
const imageNames = data?.slice(startIndex, endIndex + 1);
|
||||
|
||||
if (imageNames) {
|
||||
// optimisation: we may have already loaded some of these images, so filter out the ones we already have
|
||||
const imageNamesToFetch: string[] = [];
|
||||
for (const name of imageNames) {
|
||||
// check if we have this image cached already
|
||||
const { data } = imagesApi.endpoints.getImageDTO.select(name)(store.getState());
|
||||
if (!data) {
|
||||
// nope, we need to fetch it
|
||||
imageNamesToFetch.push(name);
|
||||
}
|
||||
}
|
||||
console.log('imageNamesToFetch', imageNamesToFetch);
|
||||
getImagesByName({ image_names: imageNamesToFetch });
|
||||
}
|
||||
},
|
||||
[data, getImagesByName, store]
|
||||
);
|
||||
|
||||
// debounce the onRangeChanged callback to avoid fetching images too frequently
|
||||
const debouncedOnRangeChanged = useMemo(() => debounce(onRangeChanged, 300), [onRangeChanged]);
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||
const [initialize, osInstance] = useOverlayScrollbars(overlayScrollbarsParams);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
if (scroller && root) {
|
||||
initialize({
|
||||
target: root,
|
||||
elements: {
|
||||
viewport: scroller,
|
||||
},
|
||||
});
|
||||
}
|
||||
return () => osInstance()?.destroy();
|
||||
}, [scroller, initialize, osInstance]);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={rootRef} position="relative" w="full" h="full" mt={2}>
|
||||
<Virtuoso
|
||||
data={data}
|
||||
itemContent={itemContent}
|
||||
rangeChanged={debouncedOnRangeChanged}
|
||||
// increases teh virual viewport by 200px in each direction, so we fetch a few more images than required
|
||||
increaseViewportBy={200}
|
||||
scrollerRef={setScroller as TableVirtuosoScrollerRef}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
GalleryImageListExperiment.displayName = 'GalleryImageListExperiment';
|
||||
|
||||
const useGetImageDTOCache = (imageName: string): ImageDTO | undefined => {
|
||||
// get the image data for this image - useQueryState does not trigger a fetch
|
||||
const { data, isUninitialized } = imagesApi.endpoints.getImageDTO.useQueryState(imageName);
|
||||
// but we want this component to be a subscriber of the cache! that way, when this component unmounts, the query cache is automatically cleared
|
||||
// useQuerySubscription allows us to subscribe, but by default it fetches the data immediately. using skip we can prevent that
|
||||
// the result is we never fetch data for this image from this component, it only subscribes to the cache
|
||||
// unfortunately this subcribe-to-cache-but-don't-fetch functionality is not built in to RTKQ.
|
||||
imagesApi.endpoints.getImageDTO.useQuerySubscription(imageName, { skip: isUninitialized });
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// the skeleton and real component need to be the same size else virtuoso will need to call rangeChanged multiples times to fill
|
||||
const HEIGHT = 24;
|
||||
|
||||
const ListItem = ({ index, data }: { index: number; data: string }) => {
|
||||
const imageDTO = useGetImageDTOCache(data);
|
||||
|
||||
if (!imageDTO) {
|
||||
return <Skeleton w="full" h={HEIGHT} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex h={HEIGHT}>
|
||||
<Image src={imageDTO.thumbnail_url} h="full" aspectRatio="1/1" />
|
||||
<Flex flexDir="column">
|
||||
<Text>{index}</Text>
|
||||
<Text>{imageDTO.image_name}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -498,6 +498,33 @@ export const imagesApi = api.injectEndpoints({
|
||||
},
|
||||
}),
|
||||
}),
|
||||
getImageNames: build.query<
|
||||
paths['/api/v1/images/image_names']['get']['responses']['200']['content']['application/json'],
|
||||
paths['/api/v1/images/image_names']['get']['parameters']['query']
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: buildImagesUrl('image_names'),
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
}),
|
||||
getImagesByName: build.query<
|
||||
paths['/api/v1/images/images/by_name']['post']['responses']['200']['content']['application/json'],
|
||||
paths['/api/v1/images/images/by_name']['post']['requestBody']['content']['application/json']
|
||||
>({
|
||||
query: (body) => ({
|
||||
url: buildImagesUrl('images/by_name'),
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
onQueryStarted: (_, { dispatch, queryFulfilled }) => {
|
||||
queryFulfilled.then(({ data }) => {
|
||||
for (const imageDTO of data) {
|
||||
dispatch(imagesApi.util.upsertQueryData('getImageDTO', imageDTO.image_name, imageDTO));
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -519,6 +546,9 @@ export const {
|
||||
useStarImagesMutation,
|
||||
useUnstarImagesMutation,
|
||||
useBulkDownloadImagesMutation,
|
||||
useGetImageNamesQuery,
|
||||
useLazyGetImagesByNameQuery,
|
||||
useLazyGetImageDTOQuery,
|
||||
} = imagesApi;
|
||||
|
||||
export const useGetImageDTOQuery = (...args: Parameters<typeof imagesApi.useGetImageDTOQuery>) => {
|
||||
|
@ -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';
|
||||
|
@ -297,6 +297,27 @@ export type paths = {
|
||||
*/
|
||||
get: operations["get_bulk_download_item"];
|
||||
};
|
||||
"/api/v1/images/image_names": {
|
||||
/**
|
||||
* List Image Names
|
||||
* @description Gets a list of image names
|
||||
*/
|
||||
get: operations["list_image_names"];
|
||||
};
|
||||
"/api/v1/images/images": {
|
||||
/**
|
||||
* Images
|
||||
* @description Gets a list of image names
|
||||
*/
|
||||
get: operations["list_images"];
|
||||
};
|
||||
"/api/v1/images/images/by_name": {
|
||||
/**
|
||||
* Get Images By Name
|
||||
* @description Gets a list of image names
|
||||
*/
|
||||
post: operations["get_images_by_name"];
|
||||
};
|
||||
"/api/v1/boards/": {
|
||||
/**
|
||||
* List Boards
|
||||
@ -333,6 +354,20 @@ 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/boards/uncategorized/names": {
|
||||
/**
|
||||
* Get Uncategorized Image Names
|
||||
* @description Gets count of images and assets for uncategorized images (images with no board assocation)
|
||||
*/
|
||||
get: operations["get_uncategorized_image_names"];
|
||||
};
|
||||
"/api/v1/board_images/": {
|
||||
/**
|
||||
* Add Image To Board
|
||||
@ -1020,7 +1055,7 @@ export type components = {
|
||||
};
|
||||
/**
|
||||
* BoardDTO
|
||||
* @description Deserialized board record with cover image URL and image count.
|
||||
* @description Deserialized board record.
|
||||
*/
|
||||
BoardDTO: {
|
||||
/**
|
||||
@ -1050,9 +1085,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 +1103,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
|
||||
@ -1176,6 +1216,11 @@ export type components = {
|
||||
*/
|
||||
prepend?: boolean;
|
||||
};
|
||||
/** Body_get_images_by_name */
|
||||
Body_get_images_by_name: {
|
||||
/** Image Names */
|
||||
image_names: string[];
|
||||
};
|
||||
/** Body_parse_dynamicprompts */
|
||||
Body_parse_dynamicprompts: {
|
||||
/**
|
||||
@ -6376,6 +6421,71 @@ export type components = {
|
||||
*/
|
||||
type: "img_paste";
|
||||
};
|
||||
/**
|
||||
* ImageRecord
|
||||
* @description Deserialized image record without metadata.
|
||||
*/
|
||||
ImageRecord: {
|
||||
/**
|
||||
* Image Name
|
||||
* @description The unique name of the image.
|
||||
*/
|
||||
image_name: string;
|
||||
/** @description The type of the image. */
|
||||
image_origin: components["schemas"]["ResourceOrigin"];
|
||||
/** @description The category of the image. */
|
||||
image_category: components["schemas"]["ImageCategory"];
|
||||
/**
|
||||
* Width
|
||||
* @description The width of the image in px.
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* Height
|
||||
* @description The height of the image in px.
|
||||
*/
|
||||
height: number;
|
||||
/**
|
||||
* Created At
|
||||
* @description The created timestamp of the image.
|
||||
*/
|
||||
created_at: string;
|
||||
/**
|
||||
* Updated At
|
||||
* @description The updated timestamp of the image.
|
||||
*/
|
||||
updated_at: string;
|
||||
/**
|
||||
* Deleted At
|
||||
* @description The deleted timestamp of the image.
|
||||
*/
|
||||
deleted_at?: string | null;
|
||||
/**
|
||||
* Is Intermediate
|
||||
* @description Whether this is an intermediate image.
|
||||
*/
|
||||
is_intermediate: boolean;
|
||||
/**
|
||||
* Session Id
|
||||
* @description The session ID that generated this image, if it is a generated image.
|
||||
*/
|
||||
session_id?: string | null;
|
||||
/**
|
||||
* Node Id
|
||||
* @description The node ID that generated this image, if it is a generated image.
|
||||
*/
|
||||
node_id?: string | null;
|
||||
/**
|
||||
* Starred
|
||||
* @description Whether this image is starred.
|
||||
*/
|
||||
starred: boolean;
|
||||
/**
|
||||
* Has Workflow
|
||||
* @description Whether this image has a workflow.
|
||||
*/
|
||||
has_workflow: boolean;
|
||||
};
|
||||
/**
|
||||
* ImageRecordChanges
|
||||
* @description A set of changes to apply to an image record.
|
||||
@ -6568,7 +6678,7 @@ export type components = {
|
||||
tiled?: boolean;
|
||||
/**
|
||||
* Tile Size
|
||||
* @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the
|
||||
* @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the model will be used. Larger tile sizes generally produce better results at the cost of higher memory usage.
|
||||
* @default 0
|
||||
*/
|
||||
tile_size?: number;
|
||||
@ -7304,145 +7414,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"];
|
||||
face_identifier: 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"];
|
||||
canny_image_processor: components["schemas"]["ImageOutput"];
|
||||
dynamic_prompt: components["schemas"]["StringCollectionOutput"];
|
||||
integer_math: components["schemas"]["IntegerOutput"];
|
||||
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"];
|
||||
t2i_adapter: components["schemas"]["T2IAdapterOutput"];
|
||||
infill_tile: components["schemas"]["ImageOutput"];
|
||||
img_resize: components["schemas"]["ImageOutput"];
|
||||
string: components["schemas"]["StringOutput"];
|
||||
img_channel_multiply: components["schemas"]["ImageOutput"];
|
||||
ip_adapter: components["schemas"]["IPAdapterOutput"];
|
||||
image: components["schemas"]["ImageOutput"];
|
||||
alpha_mask_to_tensor: components["schemas"]["MaskOutput"];
|
||||
round_float: components["schemas"]["FloatOutput"];
|
||||
img_blur: components["schemas"]["ImageOutput"];
|
||||
controlnet: components["schemas"]["ControlOutput"];
|
||||
ideal_size: components["schemas"]["IdealSizeOutput"];
|
||||
collect: components["schemas"]["CollectInvocationOutput"];
|
||||
lora_selector: components["schemas"]["LoRASelectorOutput"];
|
||||
l2i: components["schemas"]["ImageOutput"];
|
||||
tile_image_processor: components["schemas"]["ImageOutput"];
|
||||
merge_metadata: components["schemas"]["MetadataOutput"];
|
||||
img_scale: components["schemas"]["ImageOutput"];
|
||||
dw_openpose_image_processor: components["schemas"]["ImageOutput"];
|
||||
img_mul: components["schemas"]["ImageOutput"];
|
||||
img_paste: components["schemas"]["ImageOutput"];
|
||||
string_join_three: components["schemas"]["StringOutput"];
|
||||
img_crop: components["schemas"]["ImageOutput"];
|
||||
img_pad_crop: components["schemas"]["ImageOutput"];
|
||||
lora_collection_loader: components["schemas"]["LoRALoaderOutput"];
|
||||
vae_loader: components["schemas"]["VAEOutput"];
|
||||
lineart_anime_image_processor: components["schemas"]["ImageOutput"];
|
||||
tomask: components["schemas"]["ImageOutput"];
|
||||
add: components["schemas"]["IntegerOutput"];
|
||||
freeu: components["schemas"]["UNetOutput"];
|
||||
pidi_image_processor: components["schemas"]["ImageOutput"];
|
||||
color: components["schemas"]["ColorOutput"];
|
||||
content_shuffle_image_processor: components["schemas"]["ImageOutput"];
|
||||
heuristic_resize: components["schemas"]["ImageOutput"];
|
||||
mediapipe_face_processor: components["schemas"]["ImageOutput"];
|
||||
string_collection: components["schemas"]["StringCollectionOutput"];
|
||||
image_mask_to_tensor: components["schemas"]["MaskOutput"];
|
||||
show_image: components["schemas"]["ImageOutput"];
|
||||
pair_tile_image: components["schemas"]["PairTileImageOutput"];
|
||||
mul: components["schemas"]["IntegerOutput"];
|
||||
sub: components["schemas"]["IntegerOutput"];
|
||||
create_denoise_mask: components["schemas"]["DenoiseMaskOutput"];
|
||||
create_gradient_mask: components["schemas"]["GradientMaskOutput"];
|
||||
lineart_image_processor: components["schemas"]["ImageOutput"];
|
||||
midas_depth_image_processor: components["schemas"]["ImageOutput"];
|
||||
integer_collection: components["schemas"]["IntegerCollectionOutput"];
|
||||
depth_anything_image_processor: components["schemas"]["ImageOutput"];
|
||||
float_collection: components["schemas"]["FloatCollectionOutput"];
|
||||
mask_combine: components["schemas"]["ImageOutput"];
|
||||
sdxl_compel_prompt: components["schemas"]["ConditioningOutput"];
|
||||
sdxl_refiner_compel_prompt: components["schemas"]["ConditioningOutput"];
|
||||
sdxl_refiner_model_loader: components["schemas"]["SDXLRefinerModelLoaderOutput"];
|
||||
save_image: components["schemas"]["ImageOutput"];
|
||||
string_split: components["schemas"]["String2Output"];
|
||||
float_math: components["schemas"]["FloatOutput"];
|
||||
unsharp_mask: components["schemas"]["ImageOutput"];
|
||||
seamless: components["schemas"]["SeamlessModeOutput"];
|
||||
compel: components["schemas"]["ConditioningOutput"];
|
||||
calculate_image_tiles_min_overlap: components["schemas"]["CalculateImageTilesOutput"];
|
||||
scheduler: components["schemas"]["SchedulerOutput"];
|
||||
calculate_image_tiles_even_split: components["schemas"]["CalculateImageTilesOutput"];
|
||||
leres_image_processor: components["schemas"]["ImageOutput"];
|
||||
img_conv: components["schemas"]["ImageOutput"];
|
||||
metadata_item: components["schemas"]["MetadataItemOutput"];
|
||||
hed_image_processor: components["schemas"]["ImageOutput"];
|
||||
calculate_image_tiles: components["schemas"]["CalculateImageTilesOutput"];
|
||||
img_nsfw: components["schemas"]["ImageOutput"];
|
||||
face_off: components["schemas"]["FaceOffOutput"];
|
||||
div: components["schemas"]["IntegerOutput"];
|
||||
range: components["schemas"]["IntegerCollectionOutput"];
|
||||
infill_patchmatch: components["schemas"]["ImageOutput"];
|
||||
infill_lama: components["schemas"]["ImageOutput"];
|
||||
infill_cv2: components["schemas"]["ImageOutput"];
|
||||
latents_collection: components["schemas"]["LatentsCollectionOutput"];
|
||||
rand_int: components["schemas"]["IntegerOutput"];
|
||||
noise: components["schemas"]["NoiseOutput"];
|
||||
mask_edge: components["schemas"]["ImageOutput"];
|
||||
color_correct: components["schemas"]["ImageOutput"];
|
||||
img_hue_adjust: components["schemas"]["ImageOutput"];
|
||||
crop_latents: components["schemas"]["LatentsOutput"];
|
||||
segment_anything_processor: components["schemas"]["ImageOutput"];
|
||||
img_ilerp: components["schemas"]["ImageOutput"];
|
||||
conditioning_collection: components["schemas"]["ConditioningCollectionOutput"];
|
||||
lresize: components["schemas"]["LatentsOutput"];
|
||||
random_range: components["schemas"]["IntegerCollectionOutput"];
|
||||
conditioning: components["schemas"]["ConditioningOutput"];
|
||||
rectangle_mask: components["schemas"]["MaskOutput"];
|
||||
img_chan: components["schemas"]["ImageOutput"];
|
||||
prompt_from_file: components["schemas"]["StringCollectionOutput"];
|
||||
float_range: components["schemas"]["FloatCollectionOutput"];
|
||||
float_to_int: components["schemas"]["IntegerOutput"];
|
||||
invert_tensor_mask: components["schemas"]["MaskOutput"];
|
||||
img_channel_offset: components["schemas"]["ImageOutput"];
|
||||
string_split_neg: components["schemas"]["StringPosNegOutput"];
|
||||
normalbae_image_processor: components["schemas"]["ImageOutput"];
|
||||
image_collection: components["schemas"]["ImageCollectionOutput"];
|
||||
blank_image: components["schemas"]["ImageOutput"];
|
||||
string_join: components["schemas"]["StringOutput"];
|
||||
model_identifier: components["schemas"]["ModelIdentifierOutput"];
|
||||
canvas_paste_back: components["schemas"]["ImageOutput"];
|
||||
i2l: components["schemas"]["LatentsOutput"];
|
||||
tile_to_properties: components["schemas"]["TileToPropertiesOutput"];
|
||||
denoise_latents: components["schemas"]["LatentsOutput"];
|
||||
lora_loader: components["schemas"]["LoRALoaderOutput"];
|
||||
merge_tiles_to_image: components["schemas"]["ImageOutput"];
|
||||
mlsd_image_processor: components["schemas"]["ImageOutput"];
|
||||
integer: components["schemas"]["IntegerOutput"];
|
||||
cv_inpaint: components["schemas"]["ImageOutput"];
|
||||
string_replace: components["schemas"]["StringOutput"];
|
||||
range_of_size: components["schemas"]["IntegerCollectionOutput"];
|
||||
zoe_depth_image_processor: components["schemas"]["ImageOutput"];
|
||||
sdxl_model_loader: components["schemas"]["SDXLModelLoaderOutput"];
|
||||
color_map_image_processor: components["schemas"]["ImageOutput"];
|
||||
mask_from_id: components["schemas"]["ImageOutput"];
|
||||
infill_rgba: components["schemas"]["ImageOutput"];
|
||||
main_model_loader: components["schemas"]["ModelLoaderOutput"];
|
||||
float: components["schemas"]["FloatOutput"];
|
||||
tiled_multi_diffusion_denoise_latents: components["schemas"]["LatentsOutput"];
|
||||
core_metadata: components["schemas"]["MetadataOutput"];
|
||||
boolean_collection: components["schemas"]["BooleanCollectionOutput"];
|
||||
iterate: components["schemas"]["IterateInvocationOutput"];
|
||||
sdxl_lora_collection_loader: components["schemas"]["SDXLLoRALoaderOutput"];
|
||||
step_param_easing: components["schemas"]["FloatCollectionOutput"];
|
||||
img_lerp: components["schemas"]["ImageOutput"];
|
||||
clip_skip: components["schemas"]["CLIPSkipInvocationOutput"];
|
||||
boolean: components["schemas"]["BooleanOutput"];
|
||||
img_watermark: components["schemas"]["ImageOutput"];
|
||||
sdxl_lora_loader: components["schemas"]["SDXLLoRALoaderOutput"];
|
||||
face_mask_detection: components["schemas"]["FaceMaskOutput"];
|
||||
metadata: components["schemas"]["MetadataOutput"];
|
||||
};
|
||||
/**
|
||||
* InvocationStartedEvent
|
||||
@ -7782,7 +7892,7 @@ export type components = {
|
||||
tiled?: boolean;
|
||||
/**
|
||||
* Tile Size
|
||||
* @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the
|
||||
* @description The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the model will be used. Larger tile sizes generally produce better results at the cost of higher memory usage.
|
||||
* @default 0
|
||||
*/
|
||||
tile_size?: number;
|
||||
@ -13206,6 +13316,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
|
||||
@ -14990,6 +15113,91 @@ export type operations = {
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Image Names
|
||||
* @description Gets a list of image names
|
||||
*/
|
||||
list_image_names: {
|
||||
parameters: {
|
||||
query?: {
|
||||
board_id?: string | null;
|
||||
category?: "images" | "assets";
|
||||
starred_first?: boolean;
|
||||
order_dir?: components["schemas"]["SQLiteDirection"];
|
||||
search_term?: string | null;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": string[];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Images
|
||||
* @description Gets a list of image names
|
||||
*/
|
||||
list_images: {
|
||||
parameters: {
|
||||
query?: {
|
||||
board_id?: string | null;
|
||||
category?: "images" | "assets";
|
||||
starred_first?: boolean;
|
||||
order_dir?: components["schemas"]["SQLiteDirection"];
|
||||
search_term?: string | null;
|
||||
from_image_name?: string | null;
|
||||
count?: number;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ImageRecord"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get Images By Name
|
||||
* @description Gets a list of image names
|
||||
*/
|
||||
get_images_by_name: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Body_get_images_by_name"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ImageDTO"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* List Boards
|
||||
* @description Gets a list of boards
|
||||
@ -15163,6 +15371,34 @@ 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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get Uncategorized Image Names
|
||||
* @description Gets count of images and assets for uncategorized images (images with no board assocation)
|
||||
*/
|
||||
get_uncategorized_image_names: {
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Add Image To Board
|
||||
* @description Creates a board_image
|
||||
|
@ -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'];
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user