Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/src/content/docs/Users Guide/06.Gallery/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,15 @@ When image indexing is turned on, the Gallery can also search by what media *sho
* **Search by text.** Click the sparkle button at the right end of the search field (**Search semantically**). The field changes to **Describe what to find…**; describe what you are looking for. Any text already in the field is used straight away.
* **Search by image.** Drag an image onto the search field: a thumbnail from the Gallery, an image from the Preview, an image file from your computer, or an image from another browser tab. The Gallery then shows the media most similar to it, labelled **Similar to …**.

Results are ranked by similarity, most similar first, up to 500 items from the selected board. Videos are included. The Media and Uploads tabs, the sort order, the starred filter and date tokens do not apply to ranked results.
Results are ranked by similarity, most similar first, up to 500 items from the selected board. Videos are included unless the media type filter hides them. The Media and Uploads tabs, the sort order, the starred filter and date tokens do not apply to ranked results.

To leave semantic search, click the **✕** in the search field, click the sparkle button again (**Exit semantic search**), or switch tabs. If the index model is not installed, the Gallery says so when you try to search. Archived boards are not indexed.

### Sorting and the starred filter
### Sorting and filtering

The sort menu orders media by creation time, **Newest** first (the default) or **Oldest** first. The star button next to it (**Starred only**) shows only starred items. Images and videos are always shown together.
The sort menu orders media by creation time, **Newest** first (the default) or **Oldest** first. The star button (**Starred only**) shows only starred items.

The media type button between them chooses which kinds of media the Gallery shows: **Both** (the default), **Images only**, or **Videos only**. Its icon shows the current choice, and it is highlighted while it hides one kind. The choice is saved with the project and also applies to the [Image Map](/users-guide/image-map/introduction/). If a board has nothing of the chosen kind, the Gallery says so and offers **Show all media**. Revealing an item the filter hides, for example with **Find in Gallery** from the Preview, switches the filter back to **Both**. The Gallery picker that chooses media for other features always lists both kinds.

### Browsing large boards

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ you navigate the gallery.
You can show and hide the tags by clicking on the "label" icon in the
upper right-hand of the panel.

### Showing only images or only videos

The media type button, just left of the settings icon in the panel's
title bar, chooses whether the map plots **Both** images and videos,
**Images only**, or **Videos only**. It shares its setting with the
media type filter in the gallery, so changing it in either place changes
both. Clusters and their tags are recomputed for the media shown.

### Floating the Image Map Window

When you open the Image Map in the right rail, a floating window icon
Expand Down
15 changes: 15 additions & 0 deletions invokeai/app/api/routers/gallery.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.routers.images import _assert_board_read_access
from invokeai.app.services.gallery.gallery_base import GalleryMediaKind
from invokeai.app.services.gallery.gallery_common import (
GalleryItem,
GalleryItemKind,
Expand All @@ -19,6 +20,12 @@

gallery_router = APIRouter(prefix="/v1/gallery", tags=["gallery"])

MediaKindQuery = Query(
default=None,
description="List only images ('image') or only videos ('video'). Omit to include both. Totals, positions and "
"names cover only the listed kind.",
)


@gallery_router.get(
"/items/",
Expand Down Expand Up @@ -48,6 +55,7 @@ def list_gallery_items(
description="Filter by starred state: true for starred items only, false for unstarred only. Omit to include both.",
),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
media_kind: Optional[GalleryMediaKind] = MediaKindQuery,
created_from: Optional[date] = Query(
default=None, description="Inclusive start date (YYYY-MM-DD) to filter by created_at."
),
Expand All @@ -74,6 +82,7 @@ def list_gallery_items(
created_from=created_from.isoformat() if created_from else None,
created_to=created_to.isoformat() if created_to else None,
starred=starred,
media_kind=media_kind,
)


Expand Down Expand Up @@ -102,6 +111,7 @@ def get_gallery_item_location(
description="Filter by starred state: true for starred items only, false for unstarred only. Omit to include both.",
),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
media_kind: Optional[GalleryMediaKind] = MediaKindQuery,
created_from: Optional[date] = Query(
default=None, description="Inclusive start date (YYYY-MM-DD) to filter by created_at."
),
Expand All @@ -127,6 +137,7 @@ def get_gallery_item_location(
created_from=created_from.isoformat() if created_from else None,
created_to=created_to.isoformat() if created_to else None,
starred=starred,
media_kind=media_kind,
)
if location is None:
raise HTTPException(status_code=404, detail="Gallery item not found")
Expand Down Expand Up @@ -167,6 +178,7 @@ def list_gallery_item_names(
description="Filter by starred state: true for starred items only, false for unstarred only. Omit to include both.",
),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
media_kind: Optional[GalleryMediaKind] = MediaKindQuery,
) -> GalleryItemNames:
"""Returns the ordered flat list of item names — used to drive virtualized gallery selection.
Expand All @@ -191,6 +203,7 @@ def list_gallery_item_names(
created_from=created_from.isoformat() if created_from else None,
created_to=created_to.isoformat() if created_to else None,
starred=starred,
media_kind=media_kind,
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to get gallery item names")
Expand Down Expand Up @@ -221,6 +234,7 @@ def get_gallery_item_names(
description="Filter by starred state: true for starred items only, false for unstarred only. Omit to include both.",
),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
media_kind: Optional[GalleryMediaKind] = MediaKindQuery,
created_from: Optional[date] = Query(
default=None, description="Inclusive start date (YYYY-MM-DD) to filter by created_at."
),
Expand Down Expand Up @@ -251,6 +265,7 @@ def get_gallery_item_names(
created_from=created_from.isoformat() if created_from else None,
created_to=created_to.isoformat() if created_to else None,
starred=starred,
media_kind=media_kind,
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to get gallery item names")
29 changes: 22 additions & 7 deletions invokeai/app/api/routers/image_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,16 @@ def _with_model(indexer: ImageIndexServiceBase, model_id: str, operation: Callab
)


def _served_kinds(include_videos: bool) -> tuple[MediaKind, ...]:
MediaKindQuery = Query(
default=None,
description="Serve only images ('image') or only videos ('video'). When set it alone decides the served kinds, "
"overriding include_videos; omit to serve what include_videos selects.",
)


def _served_kinds(include_videos: bool, media_kind: Optional[MediaKind]) -> tuple[MediaKind, ...]:
if media_kind is not None:
return (media_kind,)
return ("image", "video") if include_videos else ("image",)


Expand Down Expand Up @@ -409,6 +418,7 @@ async def get_image_map_points(
eps: Optional[float] = ClusterEpsQuery,
min_samples: int = ClusterMinSamplesQuery,
include_videos: bool = IncludeVideosQuery,
media_kind: Optional[MediaKind] = MediaKindQuery,
) -> ImageMapPointsResponse:
"""Gets the current user's semantic image map.
Expand All @@ -433,7 +443,7 @@ async def get_image_map_points(
)

user_id, is_admin = _scope(current_user)
kinds = _served_kinds(include_videos)
kinds = _served_kinds(include_videos, media_kind)
# Both reads are database work (on SQLite under the process-wide lock), and on a large gallery
# the accessible listing is the expensive one — so they run off the event loop, like the
# clustering below.
Expand Down Expand Up @@ -603,6 +613,7 @@ async def search_image_map(
video_name: Optional[str] = Query(default=None, description="Reference video for similarity search"),
limit: int = Query(default=100, ge=1, le=500, description="Maximum number of results"),
include_videos: bool = IncludeVideosQuery,
media_kind: Optional[MediaKind] = MediaKindQuery,
board_id: Optional[str] = SearchBoardQuery,
created_date: Optional[date] = SearchCreatedDateQuery,
) -> ImageMapSearchResponse:
Expand Down Expand Up @@ -700,7 +711,7 @@ def embed_from_file() -> np.ndarray:
scope_user,
query_embedding,
limit,
_served_kinds(include_videos),
_served_kinds(include_videos, media_kind),
within,
)
return ImageMapSearchResponse(
Expand Down Expand Up @@ -833,6 +844,7 @@ async def search_image_map_by_image(
image_url: Optional[str] = Query(default=None, max_length=2000, description="URL of a reference image"),
limit: int = Query(default=100, ge=1, le=500, description="Maximum number of results"),
include_videos: bool = IncludeVideosQuery,
media_kind: Optional[MediaKind] = MediaKindQuery,
board_id: Optional[str] = SearchBoardQuery,
created_date: Optional[date] = SearchCreatedDateQuery,
) -> ImageMapSearchResponse:
Expand Down Expand Up @@ -900,7 +912,7 @@ def embed_bytes() -> np.ndarray:
scope_user,
query_embedding,
limit,
_served_kinds(include_videos),
_served_kinds(include_videos, media_kind),
within,
)
return ImageMapSearchResponse(
Expand Down Expand Up @@ -940,6 +952,7 @@ async def get_image_map_cluster_labels(
min_samples: int = ClusterMinSamplesQuery,
top_k: int = Query(default=3, ge=1, le=10, description="Candidate labels per cluster"),
include_videos: bool = IncludeVideosQuery,
media_kind: Optional[MediaKind] = MediaKindQuery,
) -> ImageMapClusterLabelsResponse:
"""Labels the user's visible clusters with the most similar vocabulary phrases.
Expand Down Expand Up @@ -977,7 +990,7 @@ def build() -> tuple[dict[int, dict], str]:
accessible = set(current_items)
# Exactly the mask /points applies, kind filter included: the two responses' visible
# hashes are compared by the client, so they must cover the same set.
kinds = _served_kinds(include_videos)
kinds = _served_kinds(include_videos, media_kind)
mask = np.fromiter(
(item in accessible and item.kind in kinds for item in record.items),
dtype=bool,
Expand Down Expand Up @@ -1251,7 +1264,9 @@ def update_image_map_vocab(update: ImageMapVocabUpdate, current_user: AdminUserO

@image_map_router.get("/status", operation_id="get_image_map_status", response_model=ImageMapStatusResponse)
def get_image_map_status(
current_user: CurrentUserOrDefault, include_videos: bool = IncludeVideosQuery
current_user: CurrentUserOrDefault,
include_videos: bool = IncludeVideosQuery,
media_kind: Optional[MediaKind] = MediaKindQuery,
) -> ImageMapStatusResponse:
"""Gets embedding index progress and the user's projection cache status."""
services = ApiDependencies.invoker.services
Expand All @@ -1275,7 +1290,7 @@ def get_image_map_status(
# Count only currently-accessible points; a stale record's raw count
# would reveal the size of a since-revoked scope. Kinds the caller did not ask for are
# excluded too, so this count matches the points /points would serve it.
kinds = _served_kinds(include_videos)
kinds = _served_kinds(include_videos, media_kind)
visible_count = len({item for item in set(record.items) & set(current_items) if item.kind in kinds})
projection = ImageMapProjectionStatus(
state="ready" if visible_count else "empty",
Expand Down
4 changes: 4 additions & 0 deletions invokeai/app/api/routers/virtual_boards.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.routers.gallery import MediaKindQuery
from invokeai.app.services.gallery.gallery_base import GalleryMediaKind
from invokeai.app.services.gallery.gallery_common import GalleryItemNamesResult
from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageNamesResult
from invokeai.app.services.shared.pagination import SQLiteDirection
Expand Down Expand Up @@ -79,6 +81,7 @@ def list_virtual_board_item_names_by_date(
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The sort direction"),
categories: list[ImageCategory] | None = Query(default=None, description="The categories of items to include"),
search_term: str | None = Query(default=None, description="Search term to filter items"),
media_kind: GalleryMediaKind | None = MediaKindQuery,
) -> GalleryItemNamesResult:
"""Gets ordered polymorphic (image + video) item refs for a specific date.
Expand All @@ -96,6 +99,7 @@ def list_virtual_board_item_names_by_date(
is_admin=current_user.is_admin,
created_date=date,
starred=starred,
media_kind=media_kind,
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to get gallery item names for date")
11 changes: 10 additions & 1 deletion invokeai/app/services/gallery/gallery_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Optional
from typing import Literal, Optional

from invokeai.app.services.gallery.gallery_common import (
BoardMediaSummary,
Expand All @@ -13,6 +13,9 @@
from invokeai.app.services.shared.pagination import OffsetPaginatedResults, SQLiteDirection
from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO

# A listing's media filter: one kind's items only. None (the default everywhere) lists both.
GalleryMediaKind = Literal["image", "video"]


class GalleryServiceABC(ABC):
"""High-level service producing a polymorphic stream of images and videos."""
Expand All @@ -34,6 +37,7 @@ def list_items(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> OffsetPaginatedResults[GalleryItem]:
"""Lists a paginated, time-sorted stream of image + video items."""
pass
Expand All @@ -54,6 +58,7 @@ def get_item_location(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> Optional[GalleryItemLocation]:
"""Finds an item's exact zero-based position in an ordinary item listing."""
pass
Expand All @@ -74,6 +79,7 @@ def list_item_names(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> GalleryItemNamesResult:
"""Returns ordered (kind, name) refs for optimistic UI / virtualized lists.
Expand All @@ -98,6 +104,7 @@ def get_item_names(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> GalleryItemNames:
"""Returns the ordered flat name list for optimistic UI / virtualized lists.
Expand All @@ -107,6 +114,8 @@ def get_item_names(
`created_from` and `created_to` are inclusive YYYY-MM-DD bounds on created_at (UTC days).
`starred` restricts to starred (`True`) or unstarred (`False`) items; `None` applies no filter.
`media_kind` restricts to images or videos; `None` lists both.
"""
pass

Expand Down
10 changes: 9 additions & 1 deletion invokeai/app/services/gallery/gallery_default.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional, Sequence

from invokeai.app.services.gallery.gallery_base import GalleryServiceABC
from invokeai.app.services.gallery.gallery_base import GalleryMediaKind, GalleryServiceABC
from invokeai.app.services.gallery.gallery_common import (
BoardMediaSummary,
GalleryItem,
Expand Down Expand Up @@ -47,6 +47,7 @@ def list_items(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> OffsetPaginatedResults[GalleryItem]:
filters = Filters(
origin=origin,
Expand All @@ -59,6 +60,7 @@ def list_items(
created_from=created_from,
created_to=created_to,
starred=starred,
media_kind=media_kind,
)
rows, total = self._queries.gallery.page(
filters,
Expand All @@ -85,6 +87,7 @@ def get_item_location(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> Optional[GalleryItemLocation]:
"""Finds item's exact rank without building or returning the full name list."""
filters = Filters(
Expand All @@ -98,6 +101,7 @@ def get_item_location(
created_from=created_from,
created_to=created_to,
starred=starred,
media_kind=media_kind,
)
row = self._queries.gallery.location(
filters, kind=kind.value, name=name, descending=order_dir == SQLiteDirection.Descending
Expand Down Expand Up @@ -132,6 +136,7 @@ def list_item_names(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> GalleryItemNamesResult:
filters = Filters(
origin=origin,
Expand All @@ -145,6 +150,7 @@ def list_item_names(
created_from=created_from,
created_to=created_to,
starred=starred,
media_kind=media_kind,
)
rows, starred_count = self._names(filters, starred_first, order_dir)
refs = [GalleryItemRef(kind=GalleryItemKind(kind), name=name) for kind, name, _ in rows]
Expand All @@ -165,6 +171,7 @@ def get_item_names(
created_from: Optional[str] = None,
created_to: Optional[str] = None,
starred: Optional[bool] = None,
media_kind: Optional[GalleryMediaKind] = None,
) -> GalleryItemNames:
filters = Filters(
origin=origin,
Expand All @@ -178,6 +185,7 @@ def get_item_names(
created_from=created_from,
created_to=created_to,
starred=starred,
media_kind=media_kind,
)
rows, starred_count = self._names(filters, starred_first, order_dir)
# The raw names, deliberately: building one model per row is what made the deprecated variant expensive.
Expand Down
Loading
Loading