Compare commits

..

14 Commits

Author SHA1 Message Date
psychedelicious
e048ff1746 fix(ui): ImageDTO mocks 2025-08-14 22:13:41 +10:00
psychedelicious
ecbe948f0c feat(ui): resource id helper 2025-08-14 22:13:41 +10:00
psychedelicious
440a8bdc5f chore(ui): lint 2025-08-14 22:13:41 +10:00
psychedelicious
1eae96675b chore: ruff 2025-08-14 22:13:41 +10:00
psychedelicious
8b6cbb714e feat: add type discriminator field to ImageDTO and VideoDTO 2025-08-14 22:13:41 +10:00
Mary Hipp
5fff9d3245 cleanup GalleryItem rendering to branch on either Images or Videos, more cleanup 2025-08-14 22:13:41 +10:00
Mary Hipp
a0f5189d61 POC for unifying videos and images for board and bulk operations and gallery display 2025-08-14 22:13:40 +10:00
Mary Hipp
6afe995dbe combine nodes that generate and save videos 2025-08-14 22:13:40 +10:00
Mary Hipp
d8fcf18b6c build out adhoc video saving graph 2025-08-14 22:13:40 +10:00
Mary Hipp
b3dbf5b914 push up updates for VideoField 2025-08-14 22:13:40 +10:00
Mary Hipp
d81716f0b6 update VideoField 2025-08-14 22:13:40 +10:00
Mary Hipp
d86617241d split out RunwayVideoOutput from VideoOutput 2025-08-14 22:13:40 +10:00
Mary Hipp
b47544968e rough rough POC of video tab 2025-08-14 22:13:40 +10:00
Mary Hipp
4d5604df48 video_output support 2025-08-14 22:13:40 +10:00
523 changed files with 10242 additions and 20859 deletions

View File

@@ -18,6 +18,5 @@
- [ ] _The PR has a short but descriptive title, suitable for a changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _❗Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_

View File

@@ -1,30 +0,0 @@
# Checks that large files and LFS-tracked files are properly checked in with pointer format.
# Uses https://github.com/ppremk/lfs-warning to detect LFS issues.
name: 'lfs checks'
on:
push:
branches:
- 'main'
pull_request:
types:
- 'ready_for_review'
- 'opened'
- 'synchronize'
merge_group:
workflow_dispatch:
jobs:
lfs-check:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
# Required to label and comment on the PRs
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v4
- name: check lfs files
uses: ppremk/lfs-warning@v3.3

View File

@@ -33,45 +33,30 @@ Hardware requirements vary significantly depending on model and image output siz
More detail on system requirements can be found [here](./requirements.md).
## Step 2: Download and Set Up the Launcher
## Step 2: Download
The Launcher manages your Invoke install. Follow these instructions to download and set up the Launcher.
Download the most recent launcher for your operating system:
!!! info "Instructions for each OS"
- [Download for Windows](https://download.invoke.ai/Invoke%20Community%20Edition.exe)
- [Download for macOS](https://download.invoke.ai/Invoke%20Community%20Edition.dmg)
- [Download for Linux](https://download.invoke.ai/Invoke%20Community%20Edition.AppImage)
=== "Windows"
## Step 3: Install or Update
- [Download for Windows](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition.Setup.latest.exe)
- Run the `EXE` to install the Launcher and start it.
- A desktop shortcut will be created; use this to run the Launcher in the future.
- You can delete the `EXE` file you downloaded.
=== "macOS"
- [Download for macOS](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest-arm64.dmg)
- Open the `DMG` and drag the app into `Applications`.
- Run the Launcher using its entry in `Applications`.
- You can delete the `DMG` file you downloaded.
=== "Linux"
- [Download for Linux](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest.AppImage)
- You may need to edit the `AppImage` file properties and make it executable.
- Optionally move the file to a location that does not require admin privileges and add a desktop shortcut for it.
- Run the Launcher by double-clicking the `AppImage` or the shortcut you made.
## Step 3: Install Invoke
Run the Launcher you just set up if you haven't already. Click **Install** and follow the instructions to install (or update) Invoke.
Run the launcher you just downloaded, click **Install** and follow the instructions to get set up.
If you have an existing Invoke installation, you can select it and let the launcher manage the install. You'll be able to update or launch the installation.
!!! tip "Updating"
!!! warning "Problem running the launcher on macOS"
The Launcher will check for updates for itself _and_ Invoke.
macOS may not allow you to run the launcher. We are working to resolve this by signing the launcher executable. Until that is done, you can manually flag the launcher as safe:
- When the Launcher detects an update is available for itself, you'll get a small popup window. Click through this and the Launcher will update itself.
- When the Launcher detects an update for Invoke, you'll see a small green alert in the Launcher. Click that and follow the instructions to update Invoke.
- Open the **Invoke Community Edition.dmg** file.
- Drag the launcher to **Applications**.
- Open a terminal.
- Run `xattr -d 'com.apple.quarantine' /Applications/Invoke\ Community\ Edition.app`.
You should now be able to run the launcher.
## Step 4: Launch

View File

@@ -5,9 +5,11 @@ from logging import Logger
import torch
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_images.board_images_default import BoardImagesService
# from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
# from invokeai.app.services.board_images.board_images_default import BoardImagesService
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
from invokeai.app.services.board_resource_records.board_resource_records_sqlite import SqliteBoardResourceRecordStorage
from invokeai.app.services.board_resources.board_resources_default import BoardResourcesService
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
@@ -40,6 +42,8 @@ from invokeai.app.services.shared.sqlite.sqlite_util import init_db
from invokeai.app.services.style_preset_images.style_preset_images_disk import StylePresetImageFileStorageDisk
from invokeai.app.services.style_preset_records.style_preset_records_sqlite import SqliteStylePresetRecordsStorage
from invokeai.app.services.urls.urls_default import LocalUrlService
from invokeai.app.services.video_records.video_records_sqlite import SqliteVideoRecordStorage
from invokeai.app.services.videos.videos_default import VideoService
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_disk import WorkflowThumbnailFileStorageDisk
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
@@ -103,14 +107,18 @@ class ApiDependencies:
configuration = config
logger = logger
board_image_records = SqliteBoardImageRecordStorage(db=db)
board_images = BoardImagesService()
# board_image_records = SqliteBoardImageRecordStorage(db=db)
# board_images = BoardImagesService()
board_resource_records = SqliteBoardResourceRecordStorage(db=db)
board_resources = BoardResourcesService()
board_records = SqliteBoardRecordStorage(db=db)
boards = BoardService()
events = FastAPIEventService(event_handler_id, loop=loop)
bulk_download = BulkDownloadService()
image_records = SqliteImageRecordStorage(db=db)
images = ImageService()
video_records = SqliteVideoRecordStorage(db=db)
videos = VideoService()
invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size)
tensors = ObjectSerializerForwardCache(
ObjectSerializerDisk[torch.Tensor](
@@ -155,8 +163,10 @@ class ApiDependencies:
client_state_persistence = ClientStatePersistenceSqlite(db=db)
services = InvocationServices(
board_image_records=board_image_records,
board_images=board_images,
# board_image_records=board_image_records,
# board_images=board_images,
board_resource_records=board_resource_records,
board_resources=board_resources,
board_records=board_records,
boards=boards,
bulk_download=bulk_download,
@@ -177,6 +187,8 @@ class ApiDependencies:
session_processor=session_processor,
session_queue=session_queue,
urls=urls,
videos=videos,
video_records=video_records,
workflow_records=workflow_records,
tensors=tensors,
conditioning=conditioning,

View File

@@ -0,0 +1,93 @@
from fastapi import Body, HTTPException
from fastapi.routing import APIRouter
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.board_resources.board_resources_common import ResourceType
from invokeai.app.services.resources.resources_common import AddResourcesToBoardResult, RemoveResourcesFromBoardResult
board_resources_router = APIRouter(prefix="/v1/board_resources", tags=["boards"])
@board_resources_router.post(
"/batch",
operation_id="add_resources_to_board",
responses={
201: {"description": "Resources were added to board successfully"},
},
status_code=201,
response_model=AddResourcesToBoardResult, # For now, using same response model
)
async def add_resources_to_board(
board_id: str = Body(description="The id of the board to add to"),
resource_ids: list[str] = Body(description="The ids of the resources to add", embed=True),
resource_type: ResourceType = Body(description="The type of resources"),
) -> AddResourcesToBoardResult:
"""Adds a list of resources to a board"""
try:
added_resources: set[str] = set()
affected_boards: set[str] = set()
for resource_id in resource_ids:
try:
if resource_type == ResourceType.IMAGE:
old_board_id = ApiDependencies.invoker.services.images.get_dto(resource_id).board_id or "none"
else:
old_board_id = ApiDependencies.invoker.services.videos.get_dto(resource_id).board_id or "none"
ApiDependencies.invoker.services.board_resources.add_resource_to_board(
board_id=board_id,
resource_id=resource_id,
resource_type=resource_type,
)
added_resources.add(resource_id)
affected_boards.add(board_id)
affected_boards.add(old_board_id)
except Exception:
pass
return AddResourcesToBoardResult(
added_resources=list(added_resources),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to add resources to board")
@board_resources_router.post(
"/batch/delete",
operation_id="remove_resources_from_board",
responses={
201: {"description": "Resources were removed from board successfully"},
},
status_code=201,
response_model=RemoveResourcesFromBoardResult, # For now, using same response model
)
async def remove_resources_from_board(
resource_ids: list[str] = Body(description="The ids of the resources to remove", embed=True),
resource_type: ResourceType = Body(description="The type of resources", embed=True),
) -> RemoveResourcesFromBoardResult:
"""Removes a list of resources from their board, if they had one"""
try:
removed_resources: set[str] = set()
affected_boards: set[str] = set()
for resource_id in resource_ids:
try:
if resource_type == ResourceType.IMAGE:
old_board_id = ApiDependencies.invoker.services.images.get_dto(resource_id).board_id or "none"
else:
# For videos, we'll need to implement this once video service exists
old_board_id = ApiDependencies.invoker.services.videos.get_dto(resource_id).board_id or "none"
ApiDependencies.invoker.services.board_resources.remove_resource_from_board(
resource_id=resource_id, resource_type=resource_type
)
removed_resources.add(resource_id)
affected_boards.add("none")
affected_boards.add(old_board_id)
except Exception:
pass
return RemoveResourcesFromBoardResult(
removed_resources=list(removed_resources),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to remove resources from board")

View File

@@ -1,39 +0,0 @@
from fastapi import Body, HTTPException
from fastapi.routing import APIRouter
from invokeai.app.services.videos_common import AddVideosToBoardResult, RemoveVideosFromBoardResult
board_videos_router = APIRouter(prefix="/v1/board_videos", tags=["boards"])
@board_videos_router.post(
"/batch",
operation_id="add_videos_to_board",
responses={
201: {"description": "Videos were added to board successfully"},
},
status_code=201,
response_model=AddVideosToBoardResult,
)
async def add_videos_to_board(
board_id: str = Body(description="The id of the board to add to"),
video_ids: list[str] = Body(description="The ids of the videos to add", embed=True),
) -> AddVideosToBoardResult:
"""Adds a list of videos to a board"""
raise HTTPException(status_code=501, detail="Not implemented")
@board_videos_router.post(
"/batch/delete",
operation_id="remove_videos_from_board",
responses={
201: {"description": "Videos were removed from board successfully"},
},
status_code=201,
response_model=RemoveVideosFromBoardResult,
)
async def remove_videos_from_board(
video_ids: list[str] = Body(description="The ids of the videos to remove", embed=True),
) -> RemoveVideosFromBoardResult:
"""Removes a list of videos from their board, if they had one"""
raise HTTPException(status_code=501, detail="Not implemented")

View File

@@ -8,6 +8,7 @@ from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy
from invokeai.app.services.boards.boards_common import BoardDTO
from invokeai.app.services.image_records.image_records_common import ImageCategory
from invokeai.app.services.resources.resources_common import ResourceIdentifier, ResourceType
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
@@ -16,10 +17,10 @@ boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
class DeleteBoardResult(BaseModel):
board_id: str = Field(description="The id of the board that was deleted.")
deleted_board_images: list[str] = Field(
description="The image names of the board-images relationships that were deleted."
deleted_board_resources: list[str] = Field(
description="The resource ids of the board-resources relationships that were deleted."
)
deleted_images: list[str] = Field(description="The names of the images that were deleted.")
deleted_resources: list[str] = Field(description="The names of the resources that were deleted.")
@boards_router.post(
@@ -82,34 +83,34 @@ async def update_board(
@boards_router.delete("/{board_id}", operation_id="delete_board", response_model=DeleteBoardResult)
async def delete_board(
board_id: str = Path(description="The id of board to delete"),
include_images: Optional[bool] = Query(description="Permanently delete all images on the board", default=False),
include_resources: Optional[bool] = Query(
description="Permanently delete all resources on the board", default=False
),
) -> DeleteBoardResult:
"""Deletes a board"""
try:
if include_images is True:
deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
if include_resources is True:
deleted_resources = ApiDependencies.invoker.services.board_resources.get_all_board_resource_ids_for_board(
board_id=board_id,
categories=None,
is_intermediate=None,
)
ApiDependencies.invoker.services.images.delete_images_on_board(board_id=board_id)
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
return DeleteBoardResult(
board_id=board_id,
deleted_board_images=[],
deleted_images=deleted_images,
deleted_board_resources=[],
deleted_resources=deleted_resources,
)
else:
deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
board_id=board_id,
categories=None,
is_intermediate=None,
deleted_board_resources = (
ApiDependencies.invoker.services.board_resources.get_all_board_resource_ids_for_board(
board_id=board_id,
)
)
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
return DeleteBoardResult(
board_id=board_id,
deleted_board_images=deleted_board_images,
deleted_images=[],
deleted_board_resources=deleted_board_resources,
deleted_resources=[],
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to delete board")
@@ -141,20 +142,22 @@ async def list_boards(
@boards_router.get(
"/{board_id}/image_names",
operation_id="list_all_board_image_names",
response_model=list[str],
"/{board_id}/resource_ids",
operation_id="list_all_board_resource_ids",
response_model=list[ResourceIdentifier],
)
async def list_all_board_image_names(
async def list_all_board_resource_ids(
board_id: str = Path(description="The id of the board or 'none' for uncategorized images"),
resource_type: ResourceType = Query(default=ResourceType.IMAGE, description="The type of resource to include."),
categories: list[ImageCategory] | None = Query(default=None, description="The categories of image to include."),
is_intermediate: bool | None = Query(default=None, description="Whether to list intermediate images."),
) -> list[str]:
) -> list[ResourceIdentifier]:
"""Gets a list of images for a board"""
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
resources = ApiDependencies.invoker.services.board_resources.get_all_board_resource_ids_for_board(
board_id,
resource_type,
categories,
is_intermediate,
)
return image_names
return resources

View File

@@ -19,13 +19,9 @@ from invokeai.app.services.image_records.image_records_common import (
ResourceOrigin,
)
from invokeai.app.services.images.images_common import (
DeleteImagesResult,
ImageDTO,
ImageUrlsDTO,
StarredImagesResult,
UnstarredImagesResult,
)
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.util.controlnet_utils import heuristic_resize_fast
from invokeai.backend.image_util.util import np_to_pil, pil_to_np
@@ -160,31 +156,6 @@ async def create_image_upload_entry(
raise HTTPException(status_code=501, detail="Not implemented")
@images_router.delete("/i/{image_name}", operation_id="delete_image", response_model=DeleteImagesResult)
async def delete_image(
image_name: str = Path(description="The name of the image to delete"),
) -> DeleteImagesResult:
"""Deletes an image"""
deleted_images: set[str] = set()
affected_boards: set[str] = set()
try:
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
board_id = image_dto.board_id or "none"
ApiDependencies.invoker.services.images.delete(image_name)
deleted_images.add(image_name)
affected_boards.add(board_id)
except Exception:
# TODO: Does this need any exception handling at all?
pass
return DeleteImagesResult(
deleted_images=list(deleted_images),
affected_boards=list(affected_boards),
)
@images_router.delete("/intermediates", operation_id="clear_intermediates")
async def clear_intermediates() -> int:
"""Clears all intermediates"""
@@ -367,136 +338,6 @@ async def get_image_urls(
raise HTTPException(status_code=404)
@images_router.get(
"/",
operation_id="list_image_dtos",
response_model=OffsetPaginatedResults[ImageDTO],
)
async def list_image_dtos(
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find images without a board.",
),
offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of images per page"),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred_first: bool = Query(default=True, description="Whether to sort by starred images first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> OffsetPaginatedResults[ImageDTO]:
"""Gets a list of image DTOs"""
image_dtos = ApiDependencies.invoker.services.images.get_many(
offset, limit, starred_first, order_dir, image_origin, categories, is_intermediate, board_id, search_term
)
return image_dtos
@images_router.post("/delete", operation_id="delete_images_from_list", response_model=DeleteImagesResult)
async def delete_images_from_list(
image_names: list[str] = Body(description="The list of names of images to delete", embed=True),
) -> DeleteImagesResult:
try:
deleted_images: set[str] = set()
affected_boards: set[str] = set()
for image_name in image_names:
try:
image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
board_id = image_dto.board_id or "none"
ApiDependencies.invoker.services.images.delete(image_name)
deleted_images.add(image_name)
affected_boards.add(board_id)
except Exception:
pass
return DeleteImagesResult(
deleted_images=list(deleted_images),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to delete images")
@images_router.delete("/uncategorized", operation_id="delete_uncategorized_images", response_model=DeleteImagesResult)
async def delete_uncategorized_images() -> DeleteImagesResult:
"""Deletes all images that are uncategorized"""
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
board_id="none", categories=None, is_intermediate=None
)
try:
deleted_images: set[str] = set()
affected_boards: set[str] = set()
for image_name in image_names:
try:
ApiDependencies.invoker.services.images.delete(image_name)
deleted_images.add(image_name)
affected_boards.add("none")
except Exception:
pass
return DeleteImagesResult(
deleted_images=list(deleted_images),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to delete images")
class ImagesUpdatedFromListResult(BaseModel):
updated_image_names: list[str] = Field(description="The image names that were updated")
@images_router.post("/star", operation_id="star_images_in_list", response_model=StarredImagesResult)
async def star_images_in_list(
image_names: list[str] = Body(description="The list of names of images to star", embed=True),
) -> StarredImagesResult:
try:
starred_images: set[str] = set()
affected_boards: set[str] = set()
for image_name in image_names:
try:
updated_image_dto = ApiDependencies.invoker.services.images.update(
image_name, changes=ImageRecordChanges(starred=True)
)
starred_images.add(image_name)
affected_boards.add(updated_image_dto.board_id or "none")
except Exception:
pass
return StarredImagesResult(
starred_images=list(starred_images),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to star images")
@images_router.post("/unstar", operation_id="unstar_images_in_list", response_model=UnstarredImagesResult)
async def unstar_images_in_list(
image_names: list[str] = Body(description="The list of names of images to unstar", embed=True),
) -> UnstarredImagesResult:
try:
unstarred_images: set[str] = set()
affected_boards: set[str] = set()
for image_name in image_names:
try:
updated_image_dto = ApiDependencies.invoker.services.images.update(
image_name, changes=ImageRecordChanges(starred=False)
)
unstarred_images.add(image_name)
affected_boards.add(updated_image_dto.board_id or "none")
except Exception:
pass
return UnstarredImagesResult(
unstarred_images=list(unstarred_images),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to unstar images")
class ImagesDownloaded(BaseModel):
response: Optional[str] = Field(
default=None, description="The message to display to the user when images begin downloading"

View File

@@ -0,0 +1,159 @@
from fastapi import Body, HTTPException
from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.image_records.image_records_common import (
ImageRecordChanges,
)
from invokeai.app.services.resources.resources_common import (
DeleteResourcesResult,
ResourceIdentifier,
ResourceType,
StarredResourcesResult,
UnstarredResourcesResult,
)
from invokeai.app.services.video_records.video_records_common import VideoRecordChanges
# routes that act on both images and videos, possibly together
resources_router = APIRouter(prefix="/v1/resources", tags=["resources"])
@resources_router.post("/delete", operation_id="delete_resources_from_list", response_model=DeleteResourcesResult)
async def delete_resources_from_list(
resources: list[ResourceIdentifier] = Body(description="The list of resources to delete", embed=True),
) -> DeleteResourcesResult:
try:
deleted_resources: set[ResourceIdentifier] = set()
affected_boards: set[str] = set()
for resource in resources:
if resource.resource_type == ResourceType.IMAGE:
try:
image_dto = ApiDependencies.invoker.services.images.get_dto(resource.resource_id)
board_id = image_dto.board_id or "none"
ApiDependencies.invoker.services.images.delete(resource.resource_id)
deleted_resources.add(resource)
affected_boards.add(board_id)
except Exception:
pass
elif resource.resource_type == ResourceType.VIDEO:
try:
video_dto = ApiDependencies.invoker.services.videos.get_dto(resource.resource_id)
board_id = video_dto.board_id or "none"
ApiDependencies.invoker.services.videos.delete(resource.resource_id)
deleted_resources.add(resource)
affected_boards.add(board_id)
except Exception:
pass
return DeleteResourcesResult(
deleted_resources=list(deleted_resources),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to delete images")
@resources_router.delete(
"/uncategorized", operation_id="delete_uncategorized_resources", response_model=DeleteResourcesResult
)
async def delete_uncategorized_resources() -> DeleteResourcesResult:
"""Deletes all resources that are uncategorized"""
resources = ApiDependencies.invoker.services.board_resources.get_all_board_resource_ids_for_board(board_id="none")
try:
deleted_resources: set[ResourceIdentifier] = set()
affected_boards: set[str] = set()
for resource in resources:
if resource.resource_type == ResourceType.IMAGE:
try:
ApiDependencies.invoker.services.images.delete(resource.resource_id)
deleted_resources.add(resource)
affected_boards.add("none")
except Exception:
pass
elif resource.resource_type == ResourceType.VIDEO:
try:
ApiDependencies.invoker.services.videos.delete(resource.resource_id)
deleted_resources.add(resource)
affected_boards.add("none")
except Exception:
pass
return DeleteResourcesResult(
deleted_resources=list(deleted_resources),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to delete images")
class ResourcesUpdatedFromListResult(BaseModel):
updated_resource: list[ResourceIdentifier] = Field(description="The resource ids that were updated")
@resources_router.post("/star", operation_id="star_resources_in_list", response_model=StarredResourcesResult)
async def star_resources_in_list(
resources: list[ResourceIdentifier] = Body(description="The list of resources to star", embed=True),
) -> StarredResourcesResult:
try:
starred_resources: set[ResourceIdentifier] = set()
affected_boards: set[str] = set()
for resource in resources:
if resource.resource_type == ResourceType.IMAGE:
try:
updated_resource_dto = ApiDependencies.invoker.services.images.update(
resource.resource_id, changes=ImageRecordChanges(starred=True)
)
starred_resources.add(resource)
affected_boards.add(updated_resource_dto.board_id or "none")
except Exception:
pass
elif resource.resource_type == ResourceType.VIDEO:
try:
updated_resource_dto = ApiDependencies.invoker.services.videos.update(
resource.resource_id, changes=VideoRecordChanges(starred=True)
)
starred_resources.add(resource)
affected_boards.add(updated_resource_dto.board_id or "none")
except Exception:
pass
return StarredResourcesResult(
starred_resources=list(starred_resources),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to star images")
@resources_router.post("/unstar", operation_id="unstar_resources_in_list", response_model=UnstarredResourcesResult)
async def unstar_resources_in_list(
resources: list[ResourceIdentifier] = Body(description="The list of resources to unstar", embed=True),
) -> UnstarredResourcesResult:
try:
unstarred_resources: set[ResourceIdentifier] = set()
affected_boards: set[str] = set()
for resource in resources:
if resource.resource_type == ResourceType.IMAGE:
try:
updated_resource_dto = ApiDependencies.invoker.services.images.update(
resource.resource_id, changes=ImageRecordChanges(starred=False)
)
unstarred_resources.add(resource)
affected_boards.add(updated_resource_dto.board_id or "none")
except Exception:
pass
elif resource.resource_type == ResourceType.VIDEO:
try:
updated_resource_dto = ApiDependencies.invoker.services.videos.update(
resource.resource_id, changes=VideoRecordChanges(starred=False)
)
unstarred_resources.add(resource)
affected_boards.add(updated_resource_dto.board_id or "none")
except Exception:
pass
return UnstarredResourcesResult(
unstarred_resources=list(unstarred_resources),
affected_boards=list(affected_boards),
)
except Exception:
raise HTTPException(status_code=500, detail="Failed to unstar images")

View File

@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus
from invokeai.app.services.session_queue.session_queue_common import (
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
CancelAllExceptCurrentResult,
@@ -17,7 +18,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
DeleteByDestinationResult,
EnqueueBatchResult,
FieldIdentifier,
ItemIdsResult,
PruneResult,
RetryItemsResult,
SessionQueueCountsByDestination,
@@ -25,7 +25,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
SessionQueueItemNotFoundError,
SessionQueueStatus,
)
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.shared.pagination import CursorPaginatedResults
session_queue_router = APIRouter(prefix="/v1/queue", tags=["queue"])
@@ -68,6 +68,36 @@ async def enqueue_batch(
raise HTTPException(status_code=500, detail=f"Unexpected error while enqueuing batch: {e}")
@session_queue_router.get(
"/{queue_id}/list",
operation_id="list_queue_items",
responses={
200: {"model": CursorPaginatedResults[SessionQueueItem]},
},
)
async def list_queue_items(
queue_id: str = Path(description="The queue id to perform this operation on"),
limit: int = Query(default=50, description="The number of items to fetch"),
status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"),
cursor: Optional[int] = Query(default=None, description="The pagination cursor"),
priority: int = Query(default=0, description="The pagination cursor priority"),
destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
) -> CursorPaginatedResults[SessionQueueItem]:
"""Gets cursor-paginated queue items"""
try:
return ApiDependencies.invoker.services.session_queue.list_queue_items(
queue_id=queue_id,
limit=limit,
status=status,
cursor=cursor,
priority=priority,
destination=destination,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while listing all items: {e}")
@session_queue_router.get(
"/{queue_id}/list_all",
operation_id="list_all_queue_items",
@@ -89,56 +119,6 @@ async def list_all_queue_items(
raise HTTPException(status_code=500, detail=f"Unexpected error while listing all queue items: {e}")
@session_queue_router.get(
"/{queue_id}/item_ids",
operation_id="get_queue_item_ids",
responses={
200: {"model": ItemIdsResult},
},
)
async def get_queue_item_ids(
queue_id: str = Path(description="The queue id to perform this operation on"),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
) -> ItemIdsResult:
"""Gets all queue item ids that match the given parameters"""
try:
return ApiDependencies.invoker.services.session_queue.get_queue_item_ids(queue_id=queue_id, order_dir=order_dir)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while listing all queue item ids: {e}")
@session_queue_router.post(
"/{queue_id}/items_by_ids",
operation_id="get_queue_items_by_item_ids",
responses={200: {"model": list[SessionQueueItem]}},
)
async def get_queue_items_by_item_ids(
queue_id: str = Path(description="The queue id to perform this operation on"),
item_ids: list[int] = Body(
embed=True, description="Object containing list of queue item ids to fetch queue items for"
),
) -> list[SessionQueueItem]:
"""Gets queue items for the specified queue item ids. Maintains order of item ids."""
try:
session_queue_service = ApiDependencies.invoker.services.session_queue
# Fetch queue items preserving the order of requested item ids
queue_items: list[SessionQueueItem] = []
for item_id in item_ids:
try:
queue_item = session_queue_service.get_queue_item(item_id=item_id)
if queue_item.queue_id != queue_id: # Auth protection for items from other queues
continue
queue_items.append(queue_item)
except Exception:
# Skip missing queue items - they may have been deleted between item id fetch and queue item fetch
continue
return queue_items
except Exception:
raise HTTPException(status_code=500, detail="Failed to get queue items")
@session_queue_router.put(
"/{queue_id}/processor/resume",
operation_id="resume",
@@ -374,10 +354,7 @@ async def get_queue_item(
) -> SessionQueueItem:
"""Gets a queue item"""
try:
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id=item_id)
if queue_item.queue_id != queue_id:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
return queue_item
return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
except SessionQueueItemNotFoundError:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
except Exception as e:

View File

@@ -1,22 +1,87 @@
from typing import Optional
from fastapi import Body, HTTPException, Path, Query
from fastapi import Body, Path, Query, Response
from fastapi.routing import APIRouter
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.videos_common import (
DeleteVideosResult,
StarredVideosResult,
UnstarredVideosResult,
VideoDTO,
VideoIdsResult,
from invokeai.app.services.video_records.video_records_common import (
VideoNamesResult,
VideoRecordChanges,
)
from invokeai.app.services.videos.videos_common import (
VideoDTO,
)
videos_router = APIRouter(prefix="/v1/videos", tags=["videos"])
# videos are immutable; set a high max-age
VIDEO_MAX_AGE = 31536000
@videos_router.get(
"/i/{video_id}",
operation_id="get_video_dto",
response_model=VideoDTO,
)
async def get_video_dto(
video_id: str = Path(description="The id of the video to get"),
) -> VideoDTO:
"""Gets a video's DTO"""
raise NotImplementedError("Not implemented")
@videos_router.get(
"/",
operation_id="list_video_dtos",
response_model=OffsetPaginatedResults[VideoDTO],
)
async def list_video_dtos(
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find videos without a board.",
),
offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of videos per page"),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> OffsetPaginatedResults[VideoDTO]:
"""Gets a list of video DTOs"""
raise NotImplementedError("Not implemented")
@videos_router.get("/ids", operation_id="get_video_ids")
async def get_video_ids(
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find videos without a board.",
),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> VideoNamesResult:
"""Gets ordered list of video names with metadata for optimistic updates"""
raise NotImplementedError("Not implemented")
@videos_router.post(
"/videos_by_ids",
operation_id="get_videos_by_ids",
responses={200: {"model": list[VideoDTO]}},
)
async def get_videos_by_ids(
video_ids: list[str] = Body(embed=True, description="Object containing list of video ids to fetch DTOs for"),
) -> list[VideoDTO]:
"""Gets video DTOs for the specified video ids. Maintains order of input ids."""
raise NotImplementedError("Not implemented")
@videos_router.patch(
"/i/{video_id}",
operation_id="update_video",
@@ -28,92 +93,24 @@ async def update_video(
) -> VideoDTO:
"""Updates a video"""
raise HTTPException(status_code=501, detail="Not implemented")
raise NotImplementedError("Not implemented")
@videos_router.get(
"/i/{video_id}",
operation_id="get_video_dto",
response_model=VideoDTO,
"/i/{video_id}/thumbnail",
operation_id="get_video_thumbnail",
response_class=Response,
responses={
200: {
"description": "Return the video thumbnail",
"content": {"image/webp": {}},
},
404: {"description": "Video not found"},
},
)
async def get_video_dto(
video_id: str = Path(description="The id of the video to get"),
) -> VideoDTO:
"""Gets a video's DTO"""
async def get_video_thumbnail(
video_id: str = Path(description="The id of video to get thumbnail for"),
) -> Response:
"""Gets a video thumbnail file"""
raise HTTPException(status_code=501, detail="Not implemented")
@videos_router.post("/delete", operation_id="delete_videos_from_list", response_model=DeleteVideosResult)
async def delete_videos_from_list(
video_ids: list[str] = Body(description="The list of ids of videos to delete", embed=True),
) -> DeleteVideosResult:
raise HTTPException(status_code=501, detail="Not implemented")
@videos_router.post("/star", operation_id="star_videos_in_list", response_model=StarredVideosResult)
async def star_videos_in_list(
video_ids: list[str] = Body(description="The list of ids of videos to star", embed=True),
) -> StarredVideosResult:
raise HTTPException(status_code=501, detail="Not implemented")
@videos_router.post("/unstar", operation_id="unstar_videos_in_list", response_model=UnstarredVideosResult)
async def unstar_videos_in_list(
video_ids: list[str] = Body(description="The list of ids of videos to unstar", embed=True),
) -> UnstarredVideosResult:
raise HTTPException(status_code=501, detail="Not implemented")
@videos_router.delete("/uncategorized", operation_id="delete_uncategorized_videos", response_model=DeleteVideosResult)
async def delete_uncategorized_videos() -> DeleteVideosResult:
"""Deletes all videos that are uncategorized"""
raise HTTPException(status_code=501, detail="Not implemented")
@videos_router.get("/", operation_id="list_video_dtos", response_model=OffsetPaginatedResults[VideoDTO])
async def list_video_dtos(
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find videos without a board.",
),
offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of videos per page"),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> OffsetPaginatedResults[VideoDTO]:
"""Lists video DTOs"""
raise HTTPException(status_code=501, detail="Not implemented")
@videos_router.get("/ids", operation_id="get_video_ids")
async def get_video_ids(
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find videos without a board.",
),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> VideoIdsResult:
"""Gets ordered list of video ids with metadata for optimistic updates"""
raise HTTPException(status_code=501, detail="Not implemented")
@videos_router.post(
"/videos_by_ids",
operation_id="get_videos_by_ids",
responses={200: {"model": list[VideoDTO]}},
)
async def get_videos_by_ids(
video_ids: list[str] = Body(embed=True, description="Object containing list of video ids to fetch DTOs for"),
) -> list[VideoDTO]:
"""Gets video DTOs for the specified video ids. Maintains order of input ids."""
raise HTTPException(status_code=501, detail="Not implemented")
raise NotImplementedError("Not implemented")

View File

@@ -17,14 +17,15 @@ from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
from invokeai.app.api.routers import (
app_info,
board_images,
board_videos,
# board_images,
board_resources,
boards,
client_state,
download_queue,
images,
model_manager,
model_relationships,
resources,
session_queue,
style_presets,
utilities,
@@ -128,9 +129,9 @@ app.include_router(model_manager.model_manager_router, prefix="/api")
app.include_router(download_queue.download_queue_router, prefix="/api")
app.include_router(images.images_router, prefix="/api")
app.include_router(videos.videos_router, prefix="/api")
app.include_router(resources.resources_router, prefix="/api")
app.include_router(boards.boards_router, prefix="/api")
app.include_router(board_images.board_images_router, prefix="/api")
app.include_router(board_videos.board_videos_router, prefix="/api")
app.include_router(board_resources.board_resources_router, prefix="/api")
app.include_router(model_relationships.model_relationships_router, prefix="/api")
app.include_router(app_info.app_router, prefix="/api")
app.include_router(session_queue.session_queue_router, prefix="/api")

View File

@@ -36,9 +36,6 @@ from pydantic_core import PydanticUndefined
from invokeai.app.invocations.fields import (
FieldKind,
Input,
InputFieldJSONSchemaExtra,
UIType,
migrate_model_ui_type,
)
from invokeai.app.services.config.config_default import get_config
from invokeai.app.services.shared.invocation_context import InvocationContext
@@ -259,9 +256,7 @@ class BaseInvocation(ABC, BaseModel):
is_intermediate: bool = Field(
default=False,
description="Whether or not this is an intermediate invocation.",
json_schema_extra=InputFieldJSONSchemaExtra(
input=Input.Direct, field_kind=FieldKind.NodeAttribute, ui_type=UIType._IsIntermediate
).model_dump(exclude_none=True),
json_schema_extra={"ui_type": "IsIntermediate", "field_kind": FieldKind.NodeAttribute},
)
use_cache: bool = Field(
default=True,
@@ -450,15 +445,6 @@ with warnings.catch_warnings():
RESERVED_PYDANTIC_FIELD_NAMES = {m[0] for m in inspect.getmembers(_Model())}
def is_enum_member(value: Any, enum_class: type[Enum]) -> bool:
"""Checks if a value is a member of an enum class."""
try:
enum_class(value)
return True
except ValueError:
return False
def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None:
"""
Validates the fields of an invocation or invocation output:
@@ -470,99 +456,51 @@ def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None
"""
for name, field in model_fields.items():
if name in RESERVED_PYDANTIC_FIELD_NAMES:
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved by pydantic)")
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved by pydantic)')
if not field.annotation:
raise InvalidFieldError(f"{model_type}.{name}: Invalid field type (missing annotation)")
raise InvalidFieldError(f'Invalid field type "{name}" on "{model_type}" (missing annotation)')
if not isinstance(field.json_schema_extra, dict):
raise InvalidFieldError(f"{model_type}.{name}: Invalid field definition (missing json_schema_extra dict)")
raise InvalidFieldError(
f'Invalid field definition for "{name}" on "{model_type}" (missing json_schema_extra dict)'
)
field_kind = field.json_schema_extra.get("field_kind", None)
# must have a field_kind
if not is_enum_member(field_kind, FieldKind):
if not isinstance(field_kind, FieldKind):
raise InvalidFieldError(
f"{model_type}.{name}: Invalid field definition for (maybe it's not an InputField or OutputField?)"
f'Invalid field definition for "{name}" on "{model_type}" (maybe it\'s not an InputField or OutputField?)'
)
if field_kind == FieldKind.Input.value and (
if field_kind is FieldKind.Input and (
name in RESERVED_NODE_ATTRIBUTE_FIELD_NAMES or name in RESERVED_INPUT_FIELD_NAMES
):
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved input field name)")
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved input field name)')
if field_kind == FieldKind.Output.value and name in RESERVED_OUTPUT_FIELD_NAMES:
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved output field name)")
if field_kind is FieldKind.Output and name in RESERVED_OUTPUT_FIELD_NAMES:
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved output field name)')
if field_kind == FieldKind.Internal.value and name not in RESERVED_INPUT_FIELD_NAMES:
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (internal field without reserved name)")
if (field_kind is FieldKind.Internal) and name not in RESERVED_INPUT_FIELD_NAMES:
raise InvalidFieldError(
f'Invalid field name "{name}" on "{model_type}" (internal field without reserved name)'
)
# node attribute fields *must* be in the reserved list
if (
field_kind == FieldKind.NodeAttribute.value
field_kind is FieldKind.NodeAttribute
and name not in RESERVED_NODE_ATTRIBUTE_FIELD_NAMES
and name not in RESERVED_OUTPUT_FIELD_NAMES
):
raise InvalidFieldError(
f"{model_type}.{name}: Invalid field name (node attribute field without reserved name)"
f'Invalid field name "{name}" on "{model_type}" (node attribute field without reserved name)'
)
ui_type = field.json_schema_extra.get("ui_type", None)
ui_model_base = field.json_schema_extra.get("ui_model_base", None)
ui_model_type = field.json_schema_extra.get("ui_model_type", None)
ui_model_variant = field.json_schema_extra.get("ui_model_variant", None)
ui_model_format = field.json_schema_extra.get("ui_model_format", None)
if ui_type is not None:
# There are 3 cases where we may need to take action:
#
# 1. The ui_type is a migratable, deprecated value. For example, ui_type=UIType.MainModel value is
# deprecated and should be migrated to:
# - ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]
# - ui_model_type=[ModelType.Main]
#
# 2. ui_type was set in conjunction with any of the new ui_model_[base|type|variant|format] fields, which
# is not allowed (they are mutually exclusive). In this case, we ignore ui_type and log a warning.
#
# 3. ui_type is a deprecated value that is not migratable. For example, ui_type=UIType.Image is deprecated;
# Image fields are now automatically detected based on the field's type annotation. In this case, we
# ignore ui_type and log a warning.
#
# The cases must be checked in this order to ensure proper handling.
# Easier to work with as an enum
ui_type = UIType(ui_type)
# The enum member values are not always the same as their names - we want to log the name so the user can
# easily review their code and see where the deprecated enum member is used.
human_readable_name = f"UIType.{ui_type.name}"
# Case 1: migratable deprecated value
did_migrate = migrate_model_ui_type(ui_type, field.json_schema_extra)
if did_migrate:
logger.warning(
f'{model_type}.{name}: Migrated deprecated "ui_type" "{human_readable_name}" to new ui_model_[base|type|variant|format] fields'
)
field.json_schema_extra.pop("ui_type")
# Case 2: mutually exclusive with new fields
elif (
ui_model_base is not None
or ui_model_type is not None
or ui_model_variant is not None
or ui_model_format is not None
):
logger.warning(
f'{model_type}.{name}: "ui_type" is mutually exclusive with "ui_model_[base|type|format|variant]", ignoring "ui_type"'
)
field.json_schema_extra.pop("ui_type")
# Case 3: deprecated value that is not migratable
elif ui_type.startswith("DEPRECATED_"):
logger.warning(f'{model_type}.{name}: Deprecated "ui_type" "{human_readable_name}", ignoring')
field.json_schema_extra.pop("ui_type")
if isinstance(ui_type, str) and ui_type.startswith("DEPRECATED_"):
logger.warning(f'"UIType.{ui_type.split("_")[-1]}" is deprecated, ignoring')
field.json_schema_extra.pop("ui_type")
return None

View File

@@ -17,7 +17,6 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_cogview4
# TODO(ryand): This is effectively a copy of SD3ImageToLatentsInvocation and a subset of ImageToLatentsInvocation. We
# should refactor to avoid this duplication.
@@ -37,12 +36,18 @@ class CogView4ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
image: ImageField = InputField(description="The image to encode.")
vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
def _estimate_working_memory(self, image_tensor: torch.Tensor, vae: AutoencoderKL) -> int:
"""Estimate the working memory required by the invocation in bytes."""
# Encode operations use approximately 50% of the memory required for decode operations
h = image_tensor.shape[-2]
w = image_tensor.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 1100 # 50% of decode scaling constant (2200)
working_memory = h * w * element_size * scaling_constant
return int(working_memory)
@staticmethod
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
assert isinstance(vae_info.model, AutoencoderKL)
estimated_working_memory = estimate_vae_working_memory_cogview4(
operation="encode", image_tensor=image_tensor, vae=vae_info.model
)
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor, estimated_working_memory: int) -> torch.Tensor:
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, AutoencoderKL)
@@ -69,7 +74,10 @@ class CogView4ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, AutoencoderKL)
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
estimated_working_memory = self._estimate_working_memory(image_tensor, vae_info.model)
latents = self.vae_encode(
vae_info=vae_info, image_tensor=image_tensor, estimated_working_memory=estimated_working_memory
)
latents = latents.to("cpu")
name = context.tensors.save(tensor=latents)

View File

@@ -6,6 +6,7 @@ from einops import rearrange
from PIL import Image
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.app.invocations.fields import (
FieldDescriptions,
Input,
@@ -19,7 +20,6 @@ from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_cogview4
# TODO(ryand): This is effectively a copy of SD3LatentsToImageInvocation and a subset of LatentsToImageInvocation. We
# should refactor to avoid this duplication.
@@ -39,15 +39,22 @@ class CogView4LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
def _estimate_working_memory(self, latents: torch.Tensor, vae: AutoencoderKL) -> int:
"""Estimate the working memory required by the invocation in bytes."""
out_h = LATENT_SCALE_FACTOR * latents.shape[-2]
out_w = LATENT_SCALE_FACTOR * latents.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 2200 # Determined experimentally.
working_memory = out_h * out_w * element_size * scaling_constant
return int(working_memory)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
latents = context.tensors.load(self.latents.latents_name)
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, (AutoencoderKL))
estimated_working_memory = estimate_vae_working_memory_cogview4(
operation="decode", image_tensor=latents, vae=vae_info.model
)
estimated_working_memory = self._estimate_working_memory(latents, vae_info.model)
with (
SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),

View File

@@ -5,7 +5,7 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.model import (
GlmEncoderField,
ModelIdentifierField,
@@ -14,7 +14,6 @@ from invokeai.app.invocations.model import (
)
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.config import SubModelType
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
@invocation_output("cogview4_model_loader_output")
@@ -39,9 +38,8 @@ class CogView4ModelLoaderInvocation(BaseInvocation):
model: ModelIdentifierField = InputField(
description=FieldDescriptions.cogview4_model,
ui_type=UIType.CogView4MainModel,
input=Input.Direct,
ui_model_base=BaseModelType.CogView4,
ui_model_type=ModelType.Main,
)
def invoke(self, context: InvocationContext) -> CogView4ModelLoaderOutput:

View File

@@ -16,6 +16,7 @@ from invokeai.app.invocations.fields import (
ImageField,
InputField,
OutputField,
UIType,
)
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageOutput
@@ -27,7 +28,6 @@ from invokeai.app.util.controlnet_utils import (
heuristic_resize_fast,
)
from invokeai.backend.image_util.util import np_to_pil, pil_to_np
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
class ControlField(BaseModel):
@@ -63,17 +63,13 @@ class ControlOutput(BaseInvocationOutput):
control: ControlField = OutputField(description=FieldDescriptions.control)
@invocation(
"controlnet", title="ControlNet - SD1.5, SD2, SDXL", tags=["controlnet"], category="controlnet", version="1.1.3"
)
@invocation("controlnet", title="ControlNet - SD1.5, SDXL", tags=["controlnet"], category="controlnet", version="1.1.3")
class ControlNetInvocation(BaseInvocation):
"""Collects ControlNet info to pass to other nodes"""
image: ImageField = InputField(description="The control image")
control_model: ModelIdentifierField = InputField(
description=FieldDescriptions.controlnet_model,
ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2, BaseModelType.StableDiffusionXL],
ui_model_type=ModelType.ControlNet,
description=FieldDescriptions.controlnet_model, ui_type=UIType.ControlNetModel
)
control_weight: Union[float, List[float]] = InputField(
default=1.0, ge=-1, le=2, description="The weight given to the ControlNet"

View File

@@ -1,19 +1,11 @@
from enum import Enum
from typing import Any, Callable, Optional, Tuple
from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter
from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter, model_validator
from pydantic.fields import _Unset
from pydantic_core import PydanticUndefined
from invokeai.app.util.metaenum import MetaEnum
from invokeai.backend.image_util.segment_anything.shared import BoundingBox
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
ClipVariantType,
ModelFormat,
ModelType,
ModelVariantType,
)
from invokeai.backend.util.logging import InvokeAILogger
logger = InvokeAILogger.get_logger()
@@ -46,6 +38,35 @@ class UIType(str, Enum, metaclass=MetaEnum):
used, and the type will be ignored. They are included here for backwards compatibility.
"""
# region Model Field Types
MainModel = "MainModelField"
CogView4MainModel = "CogView4MainModelField"
FluxMainModel = "FluxMainModelField"
SD3MainModel = "SD3MainModelField"
SDXLMainModel = "SDXLMainModelField"
SDXLRefinerModel = "SDXLRefinerModelField"
ONNXModel = "ONNXModelField"
VAEModel = "VAEModelField"
FluxVAEModel = "FluxVAEModelField"
LoRAModel = "LoRAModelField"
ControlNetModel = "ControlNetModelField"
IPAdapterModel = "IPAdapterModelField"
T2IAdapterModel = "T2IAdapterModelField"
T5EncoderModel = "T5EncoderModelField"
CLIPEmbedModel = "CLIPEmbedModelField"
CLIPLEmbedModel = "CLIPLEmbedModelField"
CLIPGEmbedModel = "CLIPGEmbedModelField"
SpandrelImageToImageModel = "SpandrelImageToImageModelField"
ControlLoRAModel = "ControlLoRAModelField"
SigLipModel = "SigLipModelField"
FluxReduxModel = "FluxReduxModelField"
LlavaOnevisionModel = "LLaVAModelField"
Imagen3Model = "Imagen3ModelField"
Imagen4Model = "Imagen4ModelField"
ChatGPT4oModel = "ChatGPT4oModelField"
FluxKontextModel = "FluxKontextModelField"
# endregion
# region Misc Field Types
Scheduler = "SchedulerField"
Any = "AnyField"
@@ -54,7 +75,6 @@ class UIType(str, Enum, metaclass=MetaEnum):
# region Internal Field Types
_Collection = "CollectionField"
_CollectionItem = "CollectionItemField"
_IsIntermediate = "IsIntermediate"
# endregion
# region DEPRECATED
@@ -92,44 +112,13 @@ class UIType(str, Enum, metaclass=MetaEnum):
CollectionItem = "DEPRECATED_CollectionItem"
Enum = "DEPRECATED_Enum"
WorkflowField = "DEPRECATED_WorkflowField"
IsIntermediate = "DEPRECATED_IsIntermediate"
BoardField = "DEPRECATED_BoardField"
MetadataItem = "DEPRECATED_MetadataItem"
MetadataItemCollection = "DEPRECATED_MetadataItemCollection"
MetadataItemPolymorphic = "DEPRECATED_MetadataItemPolymorphic"
MetadataDict = "DEPRECATED_MetadataDict"
# Deprecated Model Field Types - use ui_model_[base|type|variant|format] instead
MainModel = "DEPRECATED_MainModelField"
CogView4MainModel = "DEPRECATED_CogView4MainModelField"
FluxMainModel = "DEPRECATED_FluxMainModelField"
SD3MainModel = "DEPRECATED_SD3MainModelField"
SDXLMainModel = "DEPRECATED_SDXLMainModelField"
SDXLRefinerModel = "DEPRECATED_SDXLRefinerModelField"
ONNXModel = "DEPRECATED_ONNXModelField"
VAEModel = "DEPRECATED_VAEModelField"
FluxVAEModel = "DEPRECATED_FluxVAEModelField"
LoRAModel = "DEPRECATED_LoRAModelField"
ControlNetModel = "DEPRECATED_ControlNetModelField"
IPAdapterModel = "DEPRECATED_IPAdapterModelField"
T2IAdapterModel = "DEPRECATED_T2IAdapterModelField"
T5EncoderModel = "DEPRECATED_T5EncoderModelField"
CLIPEmbedModel = "DEPRECATED_CLIPEmbedModelField"
CLIPLEmbedModel = "DEPRECATED_CLIPLEmbedModelField"
CLIPGEmbedModel = "DEPRECATED_CLIPGEmbedModelField"
SpandrelImageToImageModel = "DEPRECATED_SpandrelImageToImageModelField"
ControlLoRAModel = "DEPRECATED_ControlLoRAModelField"
SigLipModel = "DEPRECATED_SigLipModelField"
FluxReduxModel = "DEPRECATED_FluxReduxModelField"
LlavaOnevisionModel = "DEPRECATED_LLaVAModelField"
Imagen3Model = "DEPRECATED_Imagen3ModelField"
Imagen4Model = "DEPRECATED_Imagen4ModelField"
ChatGPT4oModel = "DEPRECATED_ChatGPT4oModelField"
Gemini2_5Model = "DEPRECATED_Gemini2_5ModelField"
FluxKontextModel = "DEPRECATED_FluxKontextModelField"
Veo3Model = "DEPRECATED_Veo3ModelField"
RunwayModel = "DEPRECATED_RunwayModelField"
# endregion
class UIComponent(str, Enum, metaclass=MetaEnum):
"""
@@ -235,12 +224,6 @@ class ImageField(BaseModel):
image_name: str = Field(description="The name of the image")
class VideoField(BaseModel):
"""A video primitive field"""
video_id: str = Field(description="The id of the video")
class BoardField(BaseModel):
"""A board primitive field"""
@@ -338,9 +321,14 @@ class ConditioningField(BaseModel):
)
class BoundingBoxField(BoundingBox):
class BoundingBoxField(BaseModel):
"""A bounding box primitive value."""
x_min: int = Field(ge=0, description="The minimum x-coordinate of the bounding box (inclusive).")
x_max: int = Field(ge=0, description="The maximum x-coordinate of the bounding box (exclusive).")
y_min: int = Field(ge=0, description="The minimum y-coordinate of the bounding box (inclusive).")
y_max: int = Field(ge=0, description="The maximum y-coordinate of the bounding box (exclusive).")
score: Optional[float] = Field(
default=None,
ge=0.0,
@@ -349,6 +337,21 @@ class BoundingBoxField(BoundingBox):
"when the bounding box was produced by a detector and has an associated confidence score.",
)
@model_validator(mode="after")
def check_coords(self):
if self.x_min > self.x_max:
raise ValueError(f"x_min ({self.x_min}) is greater than x_max ({self.x_max}).")
if self.y_min > self.y_max:
raise ValueError(f"y_min ({self.y_min}) is greater than y_max ({self.y_max}).")
return self
def tuple(self) -> Tuple[int, int, int, int]:
"""
Returns the bounding box as a tuple suitable for use with PIL's `Image.crop()` method.
This method returns a tuple of the form (left, upper, right, lower) == (x_min, y_min, x_max, y_max).
"""
return (self.x_min, self.y_min, self.x_max, self.y_max)
class MetadataField(RootModel[dict[str, Any]]):
"""
@@ -415,15 +418,10 @@ class InputFieldJSONSchemaExtra(BaseModel):
ui_component: Optional[UIComponent] = None
ui_order: Optional[int] = None
ui_choice_labels: Optional[dict[str, str]] = None
ui_model_base: Optional[list[BaseModelType]] = None
ui_model_type: Optional[list[ModelType]] = None
ui_model_variant: Optional[list[ClipVariantType | ModelVariantType]] = None
ui_model_format: Optional[list[ModelFormat]] = None
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
use_enum_values=True,
)
@@ -476,121 +474,16 @@ class OutputFieldJSONSchemaExtra(BaseModel):
"""
field_kind: FieldKind
ui_hidden: bool = False
ui_order: Optional[int] = None
ui_type: Optional[UIType] = None
ui_hidden: bool
ui_type: Optional[UIType]
ui_order: Optional[int]
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
use_enum_values=True,
)
def migrate_model_ui_type(ui_type: UIType | str, json_schema_extra: dict[str, Any]) -> bool:
"""Migrate deprecated model-specifier ui_type values to new-style ui_model_[base|type|variant|format] in json_schema_extra."""
if not isinstance(ui_type, UIType):
ui_type = UIType(ui_type)
ui_model_type: list[ModelType] | None = None
ui_model_base: list[BaseModelType] | None = None
ui_model_format: list[ModelFormat] | None = None
ui_model_variant: list[ClipVariantType | ModelVariantType] | None = None
match ui_type:
case UIType.MainModel:
ui_model_base = [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]
ui_model_type = [ModelType.Main]
case UIType.CogView4MainModel:
ui_model_base = [BaseModelType.CogView4]
ui_model_type = [ModelType.Main]
case UIType.FluxMainModel:
ui_model_base = [BaseModelType.Flux]
ui_model_type = [ModelType.Main]
case UIType.SD3MainModel:
ui_model_base = [BaseModelType.StableDiffusion3]
ui_model_type = [ModelType.Main]
case UIType.SDXLMainModel:
ui_model_base = [BaseModelType.StableDiffusionXL]
ui_model_type = [ModelType.Main]
case UIType.SDXLRefinerModel:
ui_model_base = [BaseModelType.StableDiffusionXLRefiner]
ui_model_type = [ModelType.Main]
case UIType.VAEModel:
ui_model_type = [ModelType.VAE]
case UIType.FluxVAEModel:
ui_model_base = [BaseModelType.Flux]
ui_model_type = [ModelType.VAE]
case UIType.LoRAModel:
ui_model_type = [ModelType.LoRA]
case UIType.ControlNetModel:
ui_model_type = [ModelType.ControlNet]
case UIType.IPAdapterModel:
ui_model_type = [ModelType.IPAdapter]
case UIType.T2IAdapterModel:
ui_model_type = [ModelType.T2IAdapter]
case UIType.T5EncoderModel:
ui_model_type = [ModelType.T5Encoder]
case UIType.CLIPEmbedModel:
ui_model_type = [ModelType.CLIPEmbed]
case UIType.CLIPLEmbedModel:
ui_model_type = [ModelType.CLIPEmbed]
ui_model_variant = [ClipVariantType.L]
case UIType.CLIPGEmbedModel:
ui_model_type = [ModelType.CLIPEmbed]
ui_model_variant = [ClipVariantType.G]
case UIType.SpandrelImageToImageModel:
ui_model_type = [ModelType.SpandrelImageToImage]
case UIType.ControlLoRAModel:
ui_model_type = [ModelType.ControlLoRa]
case UIType.SigLipModel:
ui_model_type = [ModelType.SigLIP]
case UIType.FluxReduxModel:
ui_model_type = [ModelType.FluxRedux]
case UIType.LlavaOnevisionModel:
ui_model_type = [ModelType.LlavaOnevision]
case UIType.Imagen3Model:
ui_model_base = [BaseModelType.Imagen3]
ui_model_type = [ModelType.Main]
case UIType.Imagen4Model:
ui_model_base = [BaseModelType.Imagen4]
ui_model_type = [ModelType.Main]
case UIType.ChatGPT4oModel:
ui_model_base = [BaseModelType.ChatGPT4o]
ui_model_type = [ModelType.Main]
case UIType.Gemini2_5Model:
ui_model_base = [BaseModelType.Gemini2_5]
ui_model_type = [ModelType.Main]
case UIType.FluxKontextModel:
ui_model_base = [BaseModelType.FluxKontext]
ui_model_type = [ModelType.Main]
case UIType.Veo3Model:
ui_model_base = [BaseModelType.Veo3]
ui_model_type = [ModelType.Video]
case UIType.RunwayModel:
ui_model_base = [BaseModelType.Runway]
ui_model_type = [ModelType.Video]
case _:
pass
did_migrate = False
if ui_model_type is not None:
json_schema_extra["ui_model_type"] = [m.value for m in ui_model_type]
did_migrate = True
if ui_model_base is not None:
json_schema_extra["ui_model_base"] = [m.value for m in ui_model_base]
did_migrate = True
if ui_model_format is not None:
json_schema_extra["ui_model_format"] = [m.value for m in ui_model_format]
did_migrate = True
if ui_model_variant is not None:
json_schema_extra["ui_model_variant"] = [m.value for m in ui_model_variant]
did_migrate = True
return did_migrate
def InputField(
# copied from pydantic's Field
# TODO: Can we support default_factory?
@@ -617,63 +510,35 @@ def InputField(
ui_hidden: Optional[bool] = None,
ui_order: Optional[int] = None,
ui_choice_labels: Optional[dict[str, str]] = None,
ui_model_base: Optional[BaseModelType | list[BaseModelType]] = None,
ui_model_type: Optional[ModelType | list[ModelType]] = None,
ui_model_variant: Optional[ClipVariantType | ModelVariantType | list[ClipVariantType | ModelVariantType]] = None,
ui_model_format: Optional[ModelFormat | list[ModelFormat]] = None,
) -> Any:
"""
Creates an input field for an invocation.
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field)
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field) \
that adds a few extra parameters to support graph execution and the node editor UI.
If the field is a `ModelIdentifierField`, use the `ui_model_[base|type|variant|format]` args to filter the model list
in the Workflow Editor. Otherwise, use `ui_type` to provide extra type hints for the UI.
:param Input input: [Input.Any] The kind of input this field requires. \
`Input.Direct` means a value must be provided on instantiation. \
`Input.Connection` means the value must be provided by a connection. \
`Input.Any` means either will do.
Don't use both `ui_type` and `ui_model_[base|type|variant|format]` - if both are provided, a warning will be
logged and `ui_type` will be ignored.
:param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \
In some situations, the field's type is not enough to infer the correct UI type. \
For example, model selection fields should render a dropdown UI component to select a model. \
Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \
`MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \
`UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field.
Args:
input: The kind of input this field requires.
- `Input.Direct` means a value must be provided on instantiation.
- `Input.Connection` means the value must be provided by a connection.
- `Input.Any` means either will do.
:param UIComponent ui_component: [None] Optionally specifies a specific component to use in the UI. \
The UI will always render a suitable component, but sometimes you want something different than the default. \
For example, a `string` field will default to a single-line input, but you may want a multi-line textarea instead. \
For this case, you could provide `UIComponent.Textarea`.
ui_type: Optionally provides an extra type hint for the UI. In some situations, the field's type is not enough
to infer the correct UI type. For example, Scheduler fields are enums, but we want to render a special scheduler
dropdown in the UI. Use `UIType.Scheduler` to indicate this.
:param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI.
ui_component: Optionally specifies a specific component to use in the UI. The UI will always render a suitable
component, but sometimes you want something different than the default. For example, a `string` field will
default to a single-line input, but you may want a multi-line textarea instead. In this case, you could use
`UIComponent.Textarea`.
:param int ui_order: [None] Specifies the order in which this field should be rendered in the UI.
ui_hidden: Specifies whether or not this field should be hidden in the UI.
ui_order: Specifies the order in which this field should be rendered in the UI. If omitted, the field will be
rendered after all fields with an explicit order, in the order they are defined in the Invocation class.
ui_model_base: Specifies the base model architectures to filter the model list by in the Workflow Editor. For
example, `ui_model_base=BaseModelType.StableDiffusionXL` will show only SDXL architecture models. This arg is
only valid if this Input field is annotated as a `ModelIdentifierField`.
ui_model_type: Specifies the model type(s) to filter the model list by in the Workflow Editor. For example,
`ui_model_type=ModelType.VAE` will show only VAE models. This arg is only valid if this Input field is
annotated as a `ModelIdentifierField`.
ui_model_variant: Specifies the model variant(s) to filter the model list by in the Workflow Editor. For example,
`ui_model_variant=ModelVariantType.Inpainting` will show only inpainting models. This arg is only valid if this
Input field is annotated as a `ModelIdentifierField`.
ui_model_format: Specifies the model format(s) to filter the model list by in the Workflow Editor. For example,
`ui_model_format=ModelFormat.Diffusers` will show only models in the diffusers format. This arg is only valid
if this Input field is annotated as a `ModelIdentifierField`.
ui_choice_labels: Specifies the labels to use for the choices in an enum field. If omitted, the enum values
will be used. This arg is only valid if the field is annotated with as a `Literal`. For example,
`Literal["choice1", "choice2", "choice3"]` with `ui_choice_labels={"choice1": "Choice 1", "choice2": "Choice 2",
"choice3": "Choice 3"}` will render a dropdown with the labels "Choice 1", "Choice 2" and "Choice 3".
:param dict[str, str] ui_choice_labels: [None] Specifies the labels to use for the choices in an enum field.
"""
json_schema_extra_ = InputFieldJSONSchemaExtra(
@@ -681,6 +546,8 @@ def InputField(
field_kind=FieldKind.Input,
)
if ui_type is not None:
json_schema_extra_.ui_type = ui_type
if ui_component is not None:
json_schema_extra_.ui_component = ui_component
if ui_hidden is not None:
@@ -689,28 +556,6 @@ def InputField(
json_schema_extra_.ui_order = ui_order
if ui_choice_labels is not None:
json_schema_extra_.ui_choice_labels = ui_choice_labels
if ui_model_base is not None:
if isinstance(ui_model_base, list):
json_schema_extra_.ui_model_base = ui_model_base
else:
json_schema_extra_.ui_model_base = [ui_model_base]
if ui_model_type is not None:
if isinstance(ui_model_type, list):
json_schema_extra_.ui_model_type = ui_model_type
else:
json_schema_extra_.ui_model_type = [ui_model_type]
if ui_model_variant is not None:
if isinstance(ui_model_variant, list):
json_schema_extra_.ui_model_variant = ui_model_variant
else:
json_schema_extra_.ui_model_variant = [ui_model_variant]
if ui_model_format is not None:
if isinstance(ui_model_format, list):
json_schema_extra_.ui_model_format = ui_model_format
else:
json_schema_extra_.ui_model_format = [ui_model_format]
if ui_type is not None:
json_schema_extra_.ui_type = ui_type
"""
There is a conflict between the typing of invocation definitions and the typing of an invocation's
@@ -812,20 +657,20 @@ def OutputField(
"""
Creates an output field for an invocation output.
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/1.10/usage/schema/#field-customization)
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/1.10/usage/schema/#field-customization) \
that adds a few extra parameters to support graph execution and the node editor UI.
Args:
ui_type: Optionally provides an extra type hint for the UI. In some situations, the field's type is not enough
to infer the correct UI type. For example, Scheduler fields are enums, but we want to render a special scheduler
dropdown in the UI. Use `UIType.Scheduler` to indicate this.
:param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \
In some situations, the field's type is not enough to infer the correct UI type. \
For example, model selection fields should render a dropdown UI component to select a model. \
Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \
`MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \
`UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field.
ui_hidden: Specifies whether or not this field should be hidden in the UI.
:param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. \
ui_order: Specifies the order in which this field should be rendered in the UI. If omitted, the field will be
rendered after all fields with an explicit order, in the order they are defined in the Invocation class.
:param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \
"""
return Field(
default=default,
title=title,
@@ -843,9 +688,9 @@ def OutputField(
min_length=min_length,
max_length=max_length,
json_schema_extra=OutputFieldJSONSchemaExtra(
ui_type=ui_type,
ui_hidden=ui_hidden,
ui_order=ui_order,
ui_type=ui_type,
field_kind=FieldKind.Output,
).model_dump(exclude_none=True),
)

View File

@@ -4,10 +4,9 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType
from invokeai.app.invocations.model import ControlLoRAField, ModelIdentifierField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
@invocation_output("flux_control_lora_loader_output")
@@ -30,10 +29,7 @@ class FluxControlLoRALoaderInvocation(BaseInvocation):
"""LoRA model and Image to use with FLUX transformer generation."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.control_lora_model,
title="Control LoRA",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.ControlLoRa,
description=FieldDescriptions.control_lora_model, title="Control LoRA", ui_type=UIType.ControlLoRAModel
)
image: ImageField = InputField(description="The image to encode.")
weight: float = InputField(description="The weight of the LoRA.", default=1.0)

View File

@@ -6,12 +6,11 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
class FluxControlNetField(BaseModel):
@@ -58,9 +57,7 @@ class FluxControlNetInvocation(BaseInvocation):
image: ImageField = InputField(description="The control image")
control_model: ModelIdentifierField = InputField(
description=FieldDescriptions.controlnet_model,
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.ControlNet,
description=FieldDescriptions.controlnet_model, ui_type=UIType.ControlNetModel
)
control_weight: float | list[float] = InputField(
default=1.0, ge=-1, le=2, description="The weight given to the ControlNet"

View File

@@ -5,7 +5,7 @@ from pydantic import field_validator, model_validator
from typing_extensions import Self
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.fields import InputField
from invokeai.app.invocations.fields import InputField, UIType
from invokeai.app.invocations.ip_adapter import (
CLIP_VISION_MODEL_MAP,
IPAdapterField,
@@ -20,7 +20,6 @@ from invokeai.backend.model_manager.config import (
IPAdapterCheckpointConfig,
IPAdapterInvokeAIConfig,
)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
@invocation(
@@ -37,10 +36,7 @@ class FluxIPAdapterInvocation(BaseInvocation):
image: ImageField = InputField(description="The IP-Adapter image prompt(s).")
ip_adapter_model: ModelIdentifierField = InputField(
description="The IP-Adapter model.",
title="IP-Adapter Model",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.IPAdapter,
description="The IP-Adapter model.", title="IP-Adapter Model", ui_type=UIType.IPAdapterModel
)
# Currently, the only known ViT model used by FLUX IP-Adapters is ViT-L.
clip_vision_model: Literal["ViT-L"] = InputField(description="CLIP Vision model to use.", default="ViT-L")

View File

@@ -6,10 +6,10 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.model import CLIPField, LoRAField, ModelIdentifierField, T5EncoderField, TransformerField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
from invokeai.backend.model_manager.taxonomy import BaseModelType
@invocation_output("flux_lora_loader_output")
@@ -36,10 +36,7 @@ class FluxLoRALoaderInvocation(BaseInvocation):
"""Apply a LoRA model to a FLUX transformer and/or text encoder."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.LoRA,
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
transformer: TransformerField | None = InputField(

View File

@@ -6,7 +6,7 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, T5EncoderField, TransformerField, VAEField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.t5_model_identifier import (
@@ -17,7 +17,7 @@ from invokeai.backend.flux.util import max_seq_lengths
from invokeai.backend.model_manager.config import (
CheckpointConfigBase,
)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
from invokeai.backend.model_manager.taxonomy import SubModelType
@invocation_output("flux_model_loader_output")
@@ -46,30 +46,23 @@ class FluxModelLoaderInvocation(BaseInvocation):
model: ModelIdentifierField = InputField(
description=FieldDescriptions.flux_model,
ui_type=UIType.FluxMainModel,
input=Input.Direct,
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.Main,
)
t5_encoder_model: ModelIdentifierField = InputField(
description=FieldDescriptions.t5_encoder,
input=Input.Direct,
title="T5 Encoder",
ui_model_type=ModelType.T5Encoder,
description=FieldDescriptions.t5_encoder, ui_type=UIType.T5EncoderModel, input=Input.Direct, title="T5 Encoder"
)
clip_embed_model: ModelIdentifierField = InputField(
description=FieldDescriptions.clip_embed_model,
ui_type=UIType.CLIPEmbedModel,
input=Input.Direct,
title="CLIP Embed",
ui_model_type=ModelType.CLIPEmbed,
)
vae_model: ModelIdentifierField = InputField(
description=FieldDescriptions.vae_model,
title="VAE",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.VAE,
description=FieldDescriptions.vae_model, ui_type=UIType.FluxVAEModel, title="VAE"
)
def invoke(self, context: InvocationContext) -> FluxModelLoaderOutput:

View File

@@ -18,6 +18,7 @@ from invokeai.app.invocations.fields import (
InputField,
OutputField,
TensorField,
UIType,
)
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageField
@@ -63,8 +64,7 @@ class FluxReduxInvocation(BaseInvocation):
redux_model: ModelIdentifierField = InputField(
description="The FLUX Redux model to use.",
title="FLUX Redux Model",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.FluxRedux,
ui_type=UIType.FluxReduxModel,
)
downsampling_factor: int = InputField(
ge=1,

View File

@@ -3,6 +3,7 @@ from einops import rearrange
from PIL import Image
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.app.invocations.fields import (
FieldDescriptions,
Input,
@@ -17,7 +18,6 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.modules.autoencoder import AutoEncoder
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
@invocation(
@@ -39,11 +39,17 @@ class FluxVaeDecodeInvocation(BaseInvocation, WithMetadata, WithBoard):
input=Input.Connection,
)
def _estimate_working_memory(self, latents: torch.Tensor, vae: AutoEncoder) -> int:
"""Estimate the working memory required by the invocation in bytes."""
out_h = LATENT_SCALE_FACTOR * latents.shape[-2]
out_w = LATENT_SCALE_FACTOR * latents.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 2200 # Determined experimentally.
working_memory = out_h * out_w * element_size * scaling_constant
return int(working_memory)
def _vae_decode(self, vae_info: LoadedModel, latents: torch.Tensor) -> Image.Image:
assert isinstance(vae_info.model, AutoEncoder)
estimated_working_memory = estimate_vae_working_memory_flux(
operation="decode", image_tensor=latents, vae=vae_info.model
)
estimated_working_memory = self._estimate_working_memory(latents, vae_info.model)
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, AutoEncoder)
vae_dtype = next(iter(vae.parameters())).dtype

View File

@@ -15,7 +15,6 @@ from invokeai.backend.flux.modules.autoencoder import AutoEncoder
from invokeai.backend.model_manager import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
@invocation(
@@ -36,16 +35,22 @@ class FluxVaeEncodeInvocation(BaseInvocation):
input=Input.Connection,
)
def _estimate_working_memory(self, image_tensor: torch.Tensor, vae: AutoEncoder) -> int:
"""Estimate the working memory required by the invocation in bytes."""
# Encode operations use approximately 50% of the memory required for decode operations
h = image_tensor.shape[-2]
w = image_tensor.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 1100 # 50% of decode scaling constant (2200)
working_memory = h * w * element_size * scaling_constant
return int(working_memory)
@staticmethod
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor, estimated_working_memory: int) -> torch.Tensor:
# TODO(ryand): Expose seed parameter at the invocation level.
# TODO(ryand): Write a util function for generating random tensors that is consistent across devices / dtypes.
# There's a starting point in get_noise(...), but it needs to be extracted and generalized. This function
# should be used for VAE encode sampling.
assert isinstance(vae_info.model, AutoEncoder)
estimated_working_memory = estimate_vae_working_memory_flux(
operation="encode", image_tensor=image_tensor, vae=vae_info.model
)
generator = torch.Generator(device=TorchDevice.choose_torch_device()).manual_seed(0)
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, AutoEncoder)
@@ -65,7 +70,10 @@ class FluxVaeEncodeInvocation(BaseInvocation):
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
context.util.signal_progress("Running VAE")
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
estimated_working_memory = self._estimate_working_memory(image_tensor, vae_info.model)
latents = self.vae_encode(
vae_info=vae_info, image_tensor=image_tensor, estimated_working_memory=estimated_working_memory
)
latents = latents.to("cpu")
name = context.tensors.save(tensor=latents)

View File

@@ -27,7 +27,6 @@ from invokeai.backend.model_manager import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.stable_diffusion.vae_tiling import patch_vae_tiling_params
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd15_sdxl
@invocation(
@@ -53,23 +52,47 @@ class ImageToLatentsInvocation(BaseInvocation):
tile_size: int = InputField(default=0, multiple_of=8, description=FieldDescriptions.vae_tile_size)
fp32: bool = InputField(default=False, description=FieldDescriptions.fp32)
@classmethod
def _estimate_working_memory(
self, image_tensor: torch.Tensor, use_tiling: bool, vae: AutoencoderKL | AutoencoderTiny
) -> int:
"""Estimate the working memory required by the invocation in bytes."""
# Encode operations use approximately 50% of the memory required for decode operations
element_size = 4 if self.fp32 else 2
scaling_constant = 1100 # 50% of decode scaling constant (2200)
if use_tiling:
tile_size = self.tile_size
if tile_size == 0:
tile_size = vae.tile_sample_min_size
assert isinstance(tile_size, int)
h = tile_size
w = tile_size
working_memory = h * w * element_size * scaling_constant
# We add 25% to the working memory estimate when tiling is enabled to account for factors like tile overlap
# and number of tiles. We could make this more precise in the future, but this should be good enough for
# most use cases.
working_memory = working_memory * 1.25
else:
h = image_tensor.shape[-2]
w = image_tensor.shape[-1]
working_memory = h * w * element_size * scaling_constant
if self.fp32:
# If we are running in FP32, then we should account for the likely increase in model size (~250MB).
working_memory += 250 * 2**20
return int(working_memory)
@staticmethod
def vae_encode(
cls,
vae_info: LoadedModel,
upcast: bool,
tiled: bool,
image_tensor: torch.Tensor,
tile_size: int = 0,
estimated_working_memory: int = 0,
) -> torch.Tensor:
assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny))
estimated_working_memory = estimate_vae_working_memory_sd15_sdxl(
operation="encode",
image_tensor=image_tensor,
vae=vae_info.model,
tile_size=tile_size if tiled else None,
fp32=upcast,
)
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, (AutoencoderKL, AutoencoderTiny))
orig_dtype = vae.dtype
@@ -133,13 +156,17 @@ class ImageToLatentsInvocation(BaseInvocation):
if image_tensor.dim() == 3:
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
use_tiling = self.tiled or context.config.get().force_tiled_decode
estimated_working_memory = self._estimate_working_memory(image_tensor, use_tiling, vae_info.model)
context.util.signal_progress("Running VAE encoder")
latents = self.vae_encode(
vae_info=vae_info,
upcast=self.fp32,
tiled=self.tiled or context.config.get().force_tiled_decode,
tiled=self.tiled,
image_tensor=image_tensor,
tile_size=self.tile_size,
estimated_working_memory=estimated_working_memory,
)
latents = latents.to("cpu")

View File

@@ -5,7 +5,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator
from typing_extensions import Self
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, TensorField
from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, TensorField, UIType
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageField
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
@@ -85,8 +85,7 @@ class IPAdapterInvocation(BaseInvocation):
description="The IP-Adapter model.",
title="IP-Adapter Model",
ui_order=-1,
ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusionXL],
ui_model_type=ModelType.IPAdapter,
ui_type=UIType.IPAdapterModel,
)
clip_vision_model: Literal["ViT-H", "ViT-G", "ViT-L"] = InputField(
description="CLIP Vision model to use. Overrides model settings. Mandatory for checkpoint models.",

View File

@@ -27,7 +27,6 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
from invokeai.backend.stable_diffusion.vae_tiling import patch_vae_tiling_params
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd15_sdxl
@invocation(
@@ -54,6 +53,39 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
tile_size: int = InputField(default=0, multiple_of=8, description=FieldDescriptions.vae_tile_size)
fp32: bool = InputField(default=False, description=FieldDescriptions.fp32)
def _estimate_working_memory(
self, latents: torch.Tensor, use_tiling: bool, vae: AutoencoderKL | AutoencoderTiny
) -> int:
"""Estimate the working memory required by the invocation in bytes."""
# It was found experimentally that the peak working memory scales linearly with the number of pixels and the
# element size (precision). This estimate is accurate for both SD1 and SDXL.
element_size = 4 if self.fp32 else 2
scaling_constant = 2200 # Determined experimentally.
if use_tiling:
tile_size = self.tile_size
if tile_size == 0:
tile_size = vae.tile_sample_min_size
assert isinstance(tile_size, int)
out_h = tile_size
out_w = tile_size
working_memory = out_h * out_w * element_size * scaling_constant
# We add 25% to the working memory estimate when tiling is enabled to account for factors like tile overlap
# and number of tiles. We could make this more precise in the future, but this should be good enough for
# most use cases.
working_memory = working_memory * 1.25
else:
out_h = LATENT_SCALE_FACTOR * latents.shape[-2]
out_w = LATENT_SCALE_FACTOR * latents.shape[-1]
working_memory = out_h * out_w * element_size * scaling_constant
if self.fp32:
# If we are running in FP32, then we should account for the likely increase in model size (~250MB).
working_memory += 250 * 2**20
return int(working_memory)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
latents = context.tensors.load(self.latents.latents_name)
@@ -62,13 +94,8 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny))
estimated_working_memory = estimate_vae_working_memory_sd15_sdxl(
operation="decode",
image_tensor=latents,
vae=vae_info.model,
tile_size=self.tile_size if use_tiling else None,
fp32=self.fp32,
)
estimated_working_memory = self._estimate_working_memory(latents, use_tiling, vae_info.model)
with (
SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),

View File

@@ -6,12 +6,11 @@ from pydantic import field_validator
from transformers import AutoProcessor, LlavaOnevisionForConditionalGeneration, LlavaOnevisionProcessor
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, UIComponent
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, UIComponent, UIType
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import StringOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.llava_onevision_pipeline import LlavaOnevisionPipeline
from invokeai.backend.model_manager.taxonomy import ModelType
from invokeai.backend.util.devices import TorchDevice
@@ -35,7 +34,7 @@ class LlavaOnevisionVllmInvocation(BaseInvocation):
vllm_model: ModelIdentifierField = InputField(
title="LLaVA Model Type",
description=FieldDescriptions.vllm_model,
ui_model_type=ModelType.LlavaOnevision,
ui_type=UIType.LlavaOnevisionModel,
)
@field_validator("images", mode="before")

View File

@@ -53,7 +53,7 @@ from invokeai.app.invocations.primitives import (
from invokeai.app.invocations.scheduler import SchedulerOutput
from invokeai.app.invocations.t2i_adapter import T2IAdapterField, T2IAdapterInvocation
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
from invokeai.backend.model_manager.taxonomy import ModelType, SubModelType
from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
from invokeai.version import __version__
@@ -473,6 +473,7 @@ class MetadataToModelOutput(BaseInvocationOutput):
model: ModelIdentifierField = OutputField(
description=FieldDescriptions.main_model,
title="Model",
ui_type=UIType.MainModel,
)
name: str = OutputField(description="Model Name", title="Name")
unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
@@ -487,6 +488,7 @@ class MetadataToSDXLModelOutput(BaseInvocationOutput):
model: ModelIdentifierField = OutputField(
description=FieldDescriptions.main_model,
title="Model",
ui_type=UIType.SDXLMainModel,
)
name: str = OutputField(description="Model Name", title="Name")
unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
@@ -517,7 +519,8 @@ class MetadataToModelInvocation(BaseInvocation, WithMetadata):
input=Input.Direct,
)
default_value: ModelIdentifierField = InputField(
description="The default model to use if not found in the metadata", ui_model_type=ModelType.Main
description="The default model to use if not found in the metadata",
ui_type=UIType.MainModel,
)
_validate_custom_label = model_validator(mode="after")(validate_custom_label)
@@ -572,8 +575,7 @@ class MetadataToSDXLModelInvocation(BaseInvocation, WithMetadata):
)
default_value: ModelIdentifierField = InputField(
description="The default SDXL Model to use if not found in the metadata",
ui_model_type=ModelType.Main,
ui_model_base=BaseModelType.StableDiffusionXL,
ui_type=UIType.SDXLMainModel,
)
_validate_custom_label = model_validator(mode="after")(validate_custom_label)

View File

@@ -9,7 +9,7 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, UIType
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.shared.models import FreeUConfig
from invokeai.backend.model_manager.config import (
@@ -145,7 +145,7 @@ class ModelIdentifierInvocation(BaseInvocation):
@invocation(
"main_model_loader",
title="Main Model - SD1.5, SD2",
title="Main Model - SD1.5",
tags=["model"],
category="model",
version="1.0.4",
@@ -153,11 +153,7 @@ class ModelIdentifierInvocation(BaseInvocation):
class MainModelLoaderInvocation(BaseInvocation):
"""Loads a main model, outputting its submodels."""
model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2],
ui_model_type=ModelType.Main,
)
model: ModelIdentifierField = InputField(description=FieldDescriptions.main_model, ui_type=UIType.MainModel)
# TODO: precision?
def invoke(self, context: InvocationContext) -> ModelLoaderOutput:
@@ -191,10 +187,7 @@ class LoRALoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_base=BaseModelType.StableDiffusion1,
ui_model_type=ModelType.LoRA,
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
unet: Optional[UNetField] = InputField(
@@ -257,9 +250,7 @@ class LoRASelectorInvocation(BaseInvocation):
"""Selects a LoRA model and weight."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_type=ModelType.LoRA,
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
@@ -341,10 +332,7 @@ class SDXLLoRALoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_base=BaseModelType.StableDiffusionXL,
ui_model_type=ModelType.LoRA,
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
unet: Optional[UNetField] = InputField(
@@ -485,26 +473,13 @@ class SDXLLoRACollectionLoader(BaseInvocation):
@invocation(
"vae_loader",
title="VAE Model - SD1.5, SD2, SDXL, SD3, FLUX",
tags=["vae", "model"],
category="model",
version="1.0.4",
"vae_loader", title="VAE Model - SD1.5, SDXL, SD3, FLUX", tags=["vae", "model"], category="model", version="1.0.4"
)
class VAELoaderInvocation(BaseInvocation):
"""Loads a VAE model, outputting a VaeLoaderOutput"""
vae_model: ModelIdentifierField = InputField(
description=FieldDescriptions.vae_model,
title="VAE",
ui_model_base=[
BaseModelType.StableDiffusion1,
BaseModelType.StableDiffusion2,
BaseModelType.StableDiffusionXL,
BaseModelType.StableDiffusion3,
BaseModelType.Flux,
],
ui_model_type=ModelType.VAE,
description=FieldDescriptions.vae_model, title="VAE", ui_type=UIType.VAEModel
)
def invoke(self, context: InvocationContext) -> VAEOutput:

View File

@@ -27,7 +27,6 @@ from invokeai.app.invocations.fields import (
SD3ConditioningField,
TensorField,
UIComponent,
VideoField,
)
from invokeai.app.services.images.images_common import ImageDTO
from invokeai.app.services.shared.invocation_context import InvocationContext
@@ -288,30 +287,6 @@ class ImageCollectionInvocation(BaseInvocation):
return ImageCollectionOutput(collection=self.collection)
# endregion
# region Video
@invocation_output("video_output")
class VideoOutput(BaseInvocationOutput):
"""Base class for nodes that output a video"""
video: VideoField = OutputField(description="The output video")
width: int = OutputField(description="The width of the video in pixels")
height: int = OutputField(description="The height of the video in pixels")
duration_seconds: float = OutputField(description="The duration of the video in seconds")
@classmethod
def build(cls, video_id: str, width: int, height: int, duration_seconds: float) -> "VideoOutput":
return cls(
video=VideoField(video_id=video_id),
width=width,
height=height,
duration_seconds=duration_seconds,
)
# endregion
# region DenoiseMask

View File

@@ -17,7 +17,6 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd3
@invocation(
@@ -33,12 +32,18 @@ class SD3ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
image: ImageField = InputField(description="The image to encode")
vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
def _estimate_working_memory(self, image_tensor: torch.Tensor, vae: AutoencoderKL) -> int:
"""Estimate the working memory required by the invocation in bytes."""
# Encode operations use approximately 50% of the memory required for decode operations
h = image_tensor.shape[-2]
w = image_tensor.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 1100 # 50% of decode scaling constant (2200)
working_memory = h * w * element_size * scaling_constant
return int(working_memory)
@staticmethod
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
assert isinstance(vae_info.model, AutoencoderKL)
estimated_working_memory = estimate_vae_working_memory_sd3(
operation="encode", image_tensor=image_tensor, vae=vae_info.model
)
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor, estimated_working_memory: int) -> torch.Tensor:
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, AutoencoderKL)
@@ -65,7 +70,10 @@ class SD3ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, AutoencoderKL)
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
estimated_working_memory = self._estimate_working_memory(image_tensor, vae_info.model)
latents = self.vae_encode(
vae_info=vae_info, image_tensor=image_tensor, estimated_working_memory=estimated_working_memory
)
latents = latents.to("cpu")
name = context.tensors.save(tensor=latents)

View File

@@ -6,6 +6,7 @@ from einops import rearrange
from PIL import Image
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.app.invocations.fields import (
FieldDescriptions,
Input,
@@ -19,7 +20,6 @@ from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd3
@invocation(
@@ -41,15 +41,22 @@ class SD3LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
input=Input.Connection,
)
def _estimate_working_memory(self, latents: torch.Tensor, vae: AutoencoderKL) -> int:
"""Estimate the working memory required by the invocation in bytes."""
out_h = LATENT_SCALE_FACTOR * latents.shape[-2]
out_w = LATENT_SCALE_FACTOR * latents.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 2200 # Determined experimentally.
working_memory = out_h * out_w * element_size * scaling_constant
return int(working_memory)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
latents = context.tensors.load(self.latents.latents_name)
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, (AutoencoderKL))
estimated_working_memory = estimate_vae_working_memory_sd3(
operation="decode", image_tensor=latents, vae=vae_info.model
)
estimated_working_memory = self._estimate_working_memory(latents, vae_info.model)
with (
SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),

View File

@@ -6,14 +6,14 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, T5EncoderField, TransformerField, VAEField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.t5_model_identifier import (
preprocess_t5_encoder_model_identifier,
preprocess_t5_tokenizer_model_identifier,
)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ClipVariantType, ModelType, SubModelType
from invokeai.backend.model_manager.taxonomy import SubModelType
@invocation_output("sd3_model_loader_output")
@@ -39,43 +39,36 @@ class Sd3ModelLoaderInvocation(BaseInvocation):
model: ModelIdentifierField = InputField(
description=FieldDescriptions.sd3_model,
ui_type=UIType.SD3MainModel,
input=Input.Direct,
ui_model_base=BaseModelType.StableDiffusion3,
ui_model_type=ModelType.Main,
)
t5_encoder_model: Optional[ModelIdentifierField] = InputField(
description=FieldDescriptions.t5_encoder,
ui_type=UIType.T5EncoderModel,
input=Input.Direct,
title="T5 Encoder",
default=None,
ui_model_type=ModelType.T5Encoder,
)
clip_l_model: Optional[ModelIdentifierField] = InputField(
description=FieldDescriptions.clip_embed_model,
ui_type=UIType.CLIPLEmbedModel,
input=Input.Direct,
title="CLIP L Encoder",
default=None,
ui_model_type=ModelType.CLIPEmbed,
ui_model_variant=ClipVariantType.L,
)
clip_g_model: Optional[ModelIdentifierField] = InputField(
description=FieldDescriptions.clip_g_model,
ui_type=UIType.CLIPGEmbedModel,
input=Input.Direct,
title="CLIP G Encoder",
default=None,
ui_model_type=ModelType.CLIPEmbed,
ui_model_variant=ClipVariantType.G,
)
vae_model: Optional[ModelIdentifierField] = InputField(
description=FieldDescriptions.vae_model,
title="VAE",
default=None,
ui_model_base=BaseModelType.StableDiffusion3,
ui_model_type=ModelType.VAE,
description=FieldDescriptions.vae_model, ui_type=UIType.VAEModel, title="VAE", default=None
)
def invoke(self, context: InvocationContext) -> Sd3ModelLoaderOutput:

View File

@@ -1,8 +1,8 @@
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, UIType
from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, UNetField, VAEField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
from invokeai.backend.model_manager.taxonomy import SubModelType
@invocation_output("sdxl_model_loader_output")
@@ -29,9 +29,7 @@ class SDXLModelLoaderInvocation(BaseInvocation):
"""Loads an sdxl base model, outputting its submodels."""
model: ModelIdentifierField = InputField(
description=FieldDescriptions.sdxl_main_model,
ui_model_base=BaseModelType.StableDiffusionXL,
ui_model_type=ModelType.Main,
description=FieldDescriptions.sdxl_main_model, ui_type=UIType.SDXLMainModel
)
# TODO: precision?
@@ -69,9 +67,7 @@ class SDXLRefinerModelLoaderInvocation(BaseInvocation):
"""Loads an sdxl refiner model, outputting its submodels."""
model: ModelIdentifierField = InputField(
description=FieldDescriptions.sdxl_refiner_model,
ui_model_base=BaseModelType.StableDiffusionXLRefiner,
ui_model_type=ModelType.Main,
description=FieldDescriptions.sdxl_refiner_model, ui_type=UIType.SDXLRefinerModel
)
# TODO: precision?

View File

@@ -1,75 +1,72 @@
from itertools import zip_longest
from enum import Enum
from pathlib import Path
from typing import Literal
import numpy as np
import torch
from PIL import Image
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, Field
from transformers import AutoProcessor
from transformers.models.sam import SamModel
from transformers.models.sam.processing_sam import SamProcessor
from transformers.models.sam2 import Sam2Model
from transformers.models.sam2.processing_sam2 import Sam2Processor
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.fields import BoundingBoxField, ImageField, InputField, TensorField
from invokeai.app.invocations.primitives import MaskOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.image_util.segment_anything.mask_refinement import mask_to_polygon, polygon_to_mask
from invokeai.backend.image_util.segment_anything.segment_anything_2_pipeline import SegmentAnything2Pipeline
from invokeai.backend.image_util.segment_anything.segment_anything_pipeline import SegmentAnythingPipeline
from invokeai.backend.image_util.segment_anything.shared import SAMInput, SAMPoint
SegmentAnythingModelKey = Literal[
"segment-anything-base",
"segment-anything-large",
"segment-anything-huge",
"segment-anything-2-tiny",
"segment-anything-2-small",
"segment-anything-2-base",
"segment-anything-2-large",
]
SegmentAnythingModelKey = Literal["segment-anything-base", "segment-anything-large", "segment-anything-huge"]
SEGMENT_ANYTHING_MODEL_IDS: dict[SegmentAnythingModelKey, str] = {
"segment-anything-base": "facebook/sam-vit-base",
"segment-anything-large": "facebook/sam-vit-large",
"segment-anything-huge": "facebook/sam-vit-huge",
"segment-anything-2-tiny": "facebook/sam2.1-hiera-tiny",
"segment-anything-2-small": "facebook/sam2.1-hiera-small",
"segment-anything-2-base": "facebook/sam2.1-hiera-base-plus",
"segment-anything-2-large": "facebook/sam2.1-hiera-large",
}
class SAMPointsField(BaseModel):
points: list[SAMPoint] = Field(..., description="The points of the object", min_length=1)
class SAMPointLabel(Enum):
negative = -1
neutral = 0
positive = 1
def to_list(self) -> list[list[float]]:
class SAMPoint(BaseModel):
x: int = Field(..., description="The x-coordinate of the point")
y: int = Field(..., description="The y-coordinate of the point")
label: SAMPointLabel = Field(..., description="The label of the point")
class SAMPointsField(BaseModel):
points: list[SAMPoint] = Field(..., description="The points of the object")
def to_list(self) -> list[list[int]]:
return [[point.x, point.y, point.label.value] for point in self.points]
@invocation(
"segment_anything",
title="Segment Anything",
tags=["prompt", "segmentation", "sam", "sam2"],
tags=["prompt", "segmentation"],
category="segmentation",
version="1.3.0",
version="1.2.0",
)
class SegmentAnythingInvocation(BaseInvocation):
"""Runs a Segment Anything Model (SAM or SAM2)."""
"""Runs a Segment Anything Model."""
# Reference:
# - https://arxiv.org/pdf/2304.02643
# - https://huggingface.co/docs/transformers/v4.43.3/en/model_doc/grounding-dino#grounded-sam
# - https://github.com/NielsRogge/Transformers-Tutorials/blob/a39f33ac1557b02ebfb191ea7753e332b5ca933f/Grounding%20DINO/GroundingDINO_with_Segment_Anything.ipynb
model: SegmentAnythingModelKey = InputField(description="The Segment Anything model to use (SAM or SAM2).")
model: SegmentAnythingModelKey = InputField(description="The Segment Anything model to use.")
image: ImageField = InputField(description="The image to segment.")
bounding_boxes: list[BoundingBoxField] | None = InputField(
default=None, description="The bounding boxes to prompt the model with."
default=None, description="The bounding boxes to prompt the SAM model with."
)
point_lists: list[SAMPointsField] | None = InputField(
default=None,
description="The list of point lists to prompt the model with. Each list of points represents a single object.",
description="The list of point lists to prompt the SAM model with. Each list of points represents a single object.",
)
apply_polygon_refinement: bool = InputField(
description="Whether to apply polygon refinement to the masks. This will smooth the edges of the masks slightly and ensure that each mask consists of a single closed polygon (before merging).",
@@ -80,18 +77,14 @@ class SegmentAnythingInvocation(BaseInvocation):
default="all",
)
@model_validator(mode="after")
def validate_points_and_boxes_len(self):
if self.point_lists is not None and self.bounding_boxes is not None:
if len(self.point_lists) != len(self.bounding_boxes):
raise ValueError("If both point_lists and bounding_boxes are provided, they must have the same length.")
return self
@torch.no_grad()
def invoke(self, context: InvocationContext) -> MaskOutput:
# The models expect a 3-channel RGB image.
image_pil = context.images.get_pil(self.image.image_name, mode="RGB")
if self.point_lists is not None and self.bounding_boxes is not None:
raise ValueError("Only one of point_lists or bounding_box can be provided.")
if (not self.bounding_boxes or len(self.bounding_boxes) == 0) and (
not self.point_lists or len(self.point_lists) == 0
):
@@ -118,38 +111,26 @@ class SegmentAnythingInvocation(BaseInvocation):
# model, and figure out how to make it work in the pipeline.
# torch_dtype=TorchDevice.choose_torch_dtype(),
)
sam_processor = SamProcessor.from_pretrained(model_path, local_files_only=True)
sam_processor = AutoProcessor.from_pretrained(model_path, local_files_only=True)
assert isinstance(sam_processor, SamProcessor)
return SegmentAnythingPipeline(sam_model=sam_model, sam_processor=sam_processor)
@staticmethod
def _load_sam_2_model(model_path: Path):
sam2_model = Sam2Model.from_pretrained(model_path, local_files_only=True)
sam2_processor = Sam2Processor.from_pretrained(model_path, local_files_only=True)
return SegmentAnything2Pipeline(sam2_model=sam2_model, sam2_processor=sam2_processor)
def _segment(self, context: InvocationContext, image: Image.Image) -> list[torch.Tensor]:
"""Use Segment Anything (SAM or SAM2) to generate masks given an image + a set of bounding boxes."""
"""Use Segment Anything (SAM) to generate masks given an image + a set of bounding boxes."""
# Convert the bounding boxes to the SAM input format.
sam_bounding_boxes = (
[[bb.x_min, bb.y_min, bb.x_max, bb.y_max] for bb in self.bounding_boxes] if self.bounding_boxes else None
)
sam_points = [p.to_list() for p in self.point_lists] if self.point_lists else None
source = SEGMENT_ANYTHING_MODEL_IDS[self.model]
inputs: list[SAMInput] = []
for bbox_field, point_field in zip_longest(self.bounding_boxes or [], self.point_lists or [], fillvalue=None):
inputs.append(
SAMInput(
bounding_box=bbox_field,
points=point_field.points if point_field else None,
)
)
if "sam2" in source:
loader = SegmentAnythingInvocation._load_sam_2_model
with context.models.load_remote_model(source=source, loader=loader) as pipeline:
assert isinstance(pipeline, SegmentAnything2Pipeline)
masks = pipeline.segment(image=image, inputs=inputs)
else:
loader = SegmentAnythingInvocation._load_sam_model
with context.models.load_remote_model(source=source, loader=loader) as pipeline:
assert isinstance(pipeline, SegmentAnythingPipeline)
masks = pipeline.segment(image=image, inputs=inputs)
with (
context.models.load_remote_model(
source=SEGMENT_ANYTHING_MODEL_IDS[self.model], loader=SegmentAnythingInvocation._load_sam_model
) as sam_pipeline,
):
assert isinstance(sam_pipeline, SegmentAnythingPipeline)
masks = sam_pipeline.segment(image=image, bounding_boxes=sam_bounding_boxes, point_lists=sam_points)
masks = self._process_masks(masks)
if self.apply_polygon_refinement:

View File

@@ -11,6 +11,7 @@ from invokeai.app.invocations.fields import (
FieldDescriptions,
ImageField,
InputField,
UIType,
WithBoard,
WithMetadata,
)
@@ -18,7 +19,6 @@ from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.session_processor.session_processor_common import CanceledException
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import ModelType
from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel
from invokeai.backend.tiles.tiles import calc_tiles_min_overlap
from invokeai.backend.tiles.utils import TBLR, Tile
@@ -33,7 +33,7 @@ class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
image_to_image_model: ModelIdentifierField = InputField(
title="Image-to-Image Model",
description=FieldDescriptions.spandrel_image_to_image_model,
ui_model_type=ModelType.SpandrelImageToImage,
ui_type=UIType.SpandrelImageToImageModel,
)
tile_size: int = InputField(
default=512, description="The tile size for tiled image-to-image. Set to 0 to disable tiling."

View File

@@ -8,12 +8,11 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
class T2IAdapterField(BaseModel):
@@ -61,8 +60,7 @@ class T2IAdapterInvocation(BaseInvocation):
description="The T2I-Adapter model.",
title="T2I-Adapter Model",
ui_order=-1,
ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusionXL],
ui_model_type=ModelType.T2IAdapter,
ui_type=UIType.T2IAdapterModel,
)
weight: Union[float, list[float]] = InputField(
default=1, ge=0, description="The weight given to the T2I-Adapter", title="Weight"

View File

@@ -49,11 +49,3 @@ class BoardImageRecordStorageBase(ABC):
) -> int:
"""Gets the number of images for a board."""
pass
@abstractmethod
def get_asset_count_for_board(
self,
board_id: str,
) -> int:
"""Gets the number of assets for a board."""
pass

View File

@@ -3,8 +3,6 @@ from typing import Optional, cast
from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
from invokeai.app.services.image_records.image_records_common import (
ASSETS_CATEGORIES,
IMAGE_CATEGORIES,
ImageCategory,
ImageRecord,
deserialize_image_record,
@@ -153,38 +151,15 @@ class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
def get_image_count_for_board(self, board_id: str) -> int:
with self._db.transaction() as cursor:
# Convert the enum values to unique list of strings
category_strings = [c.value for c in set(IMAGE_CATEGORIES)]
# Create the correct length of placeholders
placeholders = ",".join("?" * len(category_strings))
cursor.execute(
f"""--sql
"""--sql
SELECT COUNT(*)
FROM board_images
INNER JOIN images ON board_images.image_name = images.image_name
WHERE images.is_intermediate = FALSE AND images.image_category IN ( {placeholders} )
WHERE images.is_intermediate = FALSE
AND board_images.board_id = ?;
""",
(*category_strings, board_id),
)
count = cast(int, cursor.fetchone()[0])
return count
def get_asset_count_for_board(self, board_id: str) -> int:
with self._db.transaction() as cursor:
# Convert the enum values to unique list of strings
category_strings = [c.value for c in set(ASSETS_CATEGORIES)]
# Create the correct length of placeholders
placeholders = ",".join("?" * len(category_strings))
cursor.execute(
f"""--sql
SELECT COUNT(*)
FROM board_images
INNER JOIN images ON board_images.image_name = images.image_name
WHERE images.is_intermediate = FALSE AND images.image_category IN ( {placeholders} )
AND board_images.board_id = ?;
""",
(*category_strings, board_id),
(board_id,),
)
count = cast(int, cursor.fetchone()[0])
return count

View File

@@ -0,0 +1,57 @@
from abc import ABC, abstractmethod
from typing import Optional
from invokeai.app.services.image_records.image_records_common import ImageCategory
from invokeai.app.services.resources.resources_common import ResourceIdentifier, ResourceType
class BoardResourceRecordStorageBase(ABC):
"""Abstract base class for the one-to-many board-resource relationship record storage."""
@abstractmethod
def add_resource_to_board(
self,
board_id: str,
resource_id: str,
resource_type: ResourceType,
) -> None:
"""Adds a resource to a board."""
pass
@abstractmethod
def remove_resource_from_board(
self,
resource_id: str,
resource_type: ResourceType,
) -> None:
"""Removes a resource from a board."""
pass
@abstractmethod
def get_all_board_resource_ids_for_board(
self,
board_id: str,
resource_type: Optional[ResourceType] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
) -> list[ResourceIdentifier]:
"""Gets all board resources for a board, as a list of resource IDs."""
pass
@abstractmethod
def get_board_for_resource(
self,
resource_id: str,
resource_type: ResourceType,
) -> Optional[str]:
"""Gets a resource's board id, if it has one."""
pass
@abstractmethod
def get_resource_count_for_board(
self,
board_id: str,
resource_type: Optional[ResourceType] = None,
) -> int:
"""Gets the number of resources for a board."""
pass

View File

@@ -0,0 +1,153 @@
import sqlite3
from typing import Optional, cast
from invokeai.app.services.board_resource_records.board_resource_records_base import BoardResourceRecordStorageBase
from invokeai.app.services.image_records.image_records_common import ImageCategory
from invokeai.app.services.resources.resources_common import ResourceIdentifier, ResourceType
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
class SqliteBoardResourceRecordStorage(BoardResourceRecordStorageBase):
def __init__(self, db: SqliteDatabase) -> None:
super().__init__()
self._db = db
def add_resource_to_board(
self,
board_id: str,
resource_id: str,
resource_type: ResourceType,
) -> None:
with self._db.transaction() as cursor:
if resource_type == ResourceType.IMAGE:
cursor.execute(
"""--sql
INSERT INTO board_images (board_id, image_name)
VALUES (?, ?)
ON CONFLICT (image_name) DO UPDATE SET board_id = ?;
""",
(board_id, resource_id, board_id),
)
elif resource_type == ResourceType.VIDEO:
raise NotImplementedError("Video resource type is not supported in OSS")
def remove_resource_from_board(
self,
resource_id: str,
resource_type: ResourceType,
) -> None:
with self._db.transaction() as cursor:
if resource_type == ResourceType.IMAGE:
cursor.execute(
"""--sql
DELETE FROM board_images
WHERE image_name = ?;
""",
(resource_id,),
)
elif resource_type == ResourceType.VIDEO:
raise NotImplementedError("Video resource type is not supported in OSS")
def get_all_board_resource_ids_for_board(
self,
board_id: str,
resource_type: Optional[ResourceType] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
) -> list[ResourceIdentifier]:
image_name_results = []
with self._db.transaction() as cursor:
if resource_type == ResourceType.IMAGE or resource_type is None:
params: list[str | bool] = []
# Base query is a join between images and board_images
stmt = """
SELECT images.image_name
FROM images
LEFT JOIN board_images ON board_images.image_name = images.image_name
WHERE 1=1
"""
# Handle board_id filter
if board_id == "none":
stmt += """--sql
AND board_images.board_id IS NULL
"""
else:
stmt += """--sql
AND board_images.board_id = ?
"""
params.append(board_id)
# Add the category filter
if categories is not None:
# Convert the enum values to unique list of strings
category_strings = [c.value for c in set(categories)]
# Create the correct length of placeholders
placeholders = ",".join("?" * len(category_strings))
stmt += f"""--sql
AND images.image_category IN ( {placeholders} )
"""
# Unpack the included categories into the query params
for c in category_strings:
params.append(c)
# Add the is_intermediate filter
if is_intermediate is not None:
stmt += """--sql
AND images.is_intermediate = ?
"""
params.append(is_intermediate)
# Put a ring on it
stmt += ";"
cursor.execute(stmt, params)
image_name_results = cast(list[sqlite3.Row], cursor.fetchall())
if resource_type == ResourceType.VIDEO or resource_type is None:
# this is not actually a valid code path for OSS, just demonstrating that it could be
raise NotImplementedError("Video resource type is not supported in OSS")
return [
ResourceIdentifier(resource_id=image_name, resource_type=ResourceType.IMAGE)
for image_name in image_name_results
]
def get_board_for_resource(
self,
resource_id: str,
resource_type: ResourceType,
) -> Optional[str]:
with self._db.transaction() as cursor:
cursor.execute(
"""--sql
SELECT board_id
FROM board_images
WHERE image_name = ?;
""",
(resource_id,),
)
result = cursor.fetchone()
if result is None:
return None
return cast(str, result[0])
def get_resource_count_for_board(self, board_id: str, resource_type: Optional[ResourceType] = None) -> int:
with self._db.transaction() as cursor:
# only images are supported in OSS
cursor.execute(
"""--sql
SELECT COUNT(*)
FROM board_images
INNER JOIN images ON board_images.image_name = images.image_name
WHERE images.is_intermediate = FALSE
AND board_images.board_id = ?;
""",
(board_id,),
)
count = cast(int, cursor.fetchone()[0])
return count

View File

@@ -0,0 +1,48 @@
from abc import ABC, abstractmethod
from typing import Optional
from invokeai.app.services.image_records.image_records_common import ImageCategory
from invokeai.app.services.resources.resources_common import ResourceIdentifier, ResourceType
class BoardResourcesServiceABC(ABC):
"""High-level service for board-resource relationship management."""
@abstractmethod
def add_resource_to_board(
self,
board_id: str,
resource_id: str,
resource_type: ResourceType,
) -> None:
"""Adds a resource (image or video) to a board."""
pass
@abstractmethod
def remove_resource_from_board(
self,
resource_id: str,
resource_type: ResourceType,
) -> None:
"""Removes a resource (image or video) from a board."""
pass
@abstractmethod
def get_all_board_resource_ids_for_board(
self,
board_id: str,
resource_type: Optional[ResourceType] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
) -> list[ResourceIdentifier]:
"""Gets all board resources for a board, as a list of resource IDs."""
pass
@abstractmethod
def get_board_for_resource(
self,
resource_id: str,
resource_type: ResourceType,
) -> Optional[str]:
"""Gets a resource's board id, if it has one."""
pass

View File

@@ -0,0 +1,12 @@
from pydantic import Field
from invokeai.app.services.resources.resources_common import ResourceType
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
class BoardResource(BaseModelExcludeNull):
"""Represents a resource (image or video) associated with a board."""
board_id: str = Field(description="The id of the board")
resource_id: str = Field(description="The id of the resource (image_name or video_id)")
resource_type: ResourceType = Field(description="The type of resource")

View File

@@ -0,0 +1,49 @@
from typing import Optional
from invokeai.app.services.board_resources.board_resources_base import BoardResourcesServiceABC
from invokeai.app.services.image_records.image_records_common import ImageCategory
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.resources.resources_common import ResourceIdentifier, ResourceType
class BoardResourcesService(BoardResourcesServiceABC):
__invoker: Invoker
def start(self, invoker: Invoker) -> None:
self.__invoker = invoker
def add_resource_to_board(
self,
board_id: str,
resource_id: str,
resource_type: ResourceType,
) -> None:
self.__invoker.services.board_resource_records.add_resource_to_board(board_id, resource_id, resource_type)
def remove_resource_from_board(
self,
resource_id: str,
resource_type: ResourceType,
) -> None:
self.__invoker.services.board_resource_records.remove_resource_from_board(resource_id, resource_type)
def get_all_board_resource_ids_for_board(
self,
board_id: str,
resource_type: Optional[ResourceType] = None,
categories: Optional[list[ImageCategory]] = None,
is_intermediate: Optional[bool] = None,
) -> list[ResourceIdentifier]:
return self.__invoker.services.board_resource_records.get_all_board_resource_ids_for_board(
board_id,
resource_type,
categories,
is_intermediate,
)
def get_board_for_resource(
self,
resource_id: str,
resource_type: ResourceType,
) -> Optional[str]:
return self.__invoker.services.board_resource_records.get_board_for_resource(resource_id, resource_type)

View File

@@ -12,20 +12,17 @@ class BoardDTO(BoardRecord):
"""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."""
asset_count: int = Field(description="The number of assets in the board.")
"""The number of assets in the board."""
video_count: int = Field(description="The number of videos in the board.")
"""The number of videos in the board."""
def board_record_to_dto(
board_record: BoardRecord, cover_image_name: Optional[str], image_count: int, asset_count: int, video_count: int
board_record: BoardRecord, cover_image_name: Optional[str], image_count: int, video_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,
asset_count=asset_count,
video_count=video_count,
)

View File

@@ -2,6 +2,7 @@ from invokeai.app.services.board_records.board_records_common import BoardChange
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.invoker import Invoker
from invokeai.app.services.resources.resources_common import ResourceType
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
@@ -17,7 +18,7 @@ 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, 0, 0)
return board_record_to_dto(board_record, None, 0, 0)
def get_dto(self, board_id: str) -> BoardDTO:
board_record = self.__invoker.services.board_records.get(board_id)
@@ -26,10 +27,13 @@ class BoardService(BoardServiceABC):
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)
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
video_count = 0 # noop for OSS
return board_record_to_dto(board_record, cover_image_name, image_count, asset_count, video_count)
image_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
board_id, ResourceType.IMAGE
)
video_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
board_id, ResourceType.VIDEO
)
return board_record_to_dto(board_record, cover_image_name, image_count, video_count)
def update(
self,
@@ -43,10 +47,13 @@ class BoardService(BoardServiceABC):
else:
cover_image_name = None
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
video_count = 0 # noop for OSS
return board_record_to_dto(board_record, cover_image_name, image_count, asset_count, video_count)
image_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
board_id, ResourceType.IMAGE
)
video_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
board_id, ResourceType.VIDEO
)
return board_record_to_dto(board_record, cover_image_name, image_count, video_count)
def delete(self, board_id: str) -> None:
self.__invoker.services.board_records.delete(board_id)
@@ -62,7 +69,7 @@ class BoardService(BoardServiceABC):
board_records = self.__invoker.services.board_records.get_many(
order_by, direction, offset, limit, include_archived
)
board_dtos = []
board_dtos: list[BoardDTO] = []
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:
@@ -70,10 +77,13 @@ class BoardService(BoardServiceABC):
else:
cover_image_name = None
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
video_count = 0 # noop for OSS
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, video_count))
image_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
r.board_id, ResourceType.IMAGE
)
video_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
r.board_id, ResourceType.VIDEO
)
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, video_count))
return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos))
@@ -81,7 +91,7 @@ class BoardService(BoardServiceABC):
self, order_by: BoardRecordOrderBy, direction: SQLiteDirection, include_archived: bool = False
) -> list[BoardDTO]:
board_records = self.__invoker.services.board_records.get_all(order_by, direction, include_archived)
board_dtos = []
board_dtos: list[BoardDTO] = []
for r in board_records:
cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
if cover_image:
@@ -89,9 +99,12 @@ class BoardService(BoardServiceABC):
else:
cover_image_name = None
image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
video_count = 0 # noop for OSS
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, video_count))
image_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
r.board_id, ResourceType.IMAGE
)
video_count = self.__invoker.services.board_resource_records.get_resource_count_for_board(
r.board_id, ResourceType.VIDEO
)
board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, video_count))
return board_dtos

View File

@@ -14,6 +14,7 @@ from invokeai.app.services.bulk_download.bulk_download_common import (
from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException
from invokeai.app.services.images.images_common import ImageDTO
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.resources.resources_common import ResourceType
from invokeai.app.util.misc import uuid_string
@@ -63,12 +64,11 @@ class BulkDownloadService(BulkDownloadBase):
return [self._invoker.services.images.get_dto(image_name) for image_name in image_names]
def _board_handler(self, board_id: str) -> list[ImageDTO]:
image_names = self._invoker.services.board_image_records.get_all_board_image_names_for_board(
resource_ids = self._invoker.services.board_resource_records.get_all_board_resource_ids_for_board(
board_id,
categories=None,
is_intermediate=None,
resource_type=ResourceType.IMAGE,
)
return self._image_handler(image_names)
return self._image_handler([resource_id.resource_id for resource_id in resource_ids])
def generate_item_id(self, board_id: Optional[str]) -> str:
return uuid_string() if board_id is None else self._get_clean_board_name(board_id) + "_" + uuid_string()
@@ -150,15 +150,4 @@ class BulkDownloadService(BulkDownloadBase):
def _is_valid_path(self, path: Union[str, Path]) -> bool:
"""Validates the path given for a bulk download."""
path = path if isinstance(path, Path) else Path(path)
# Resolve the path to handle any path traversal attempts (e.g., ../)
resolved_path = path.resolve()
# The path may not traverse out of the bulk downloads folder or its subfolders
does_not_traverse = resolved_path.parent == self._bulk_downloads_folder.resolve()
# The path must exist and be a .zip file
does_exist = resolved_path.exists()
is_zip_file = resolved_path.suffix == ".zip"
return does_exist and is_zip_file and does_not_traverse
return path.exists()

View File

@@ -234,8 +234,8 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
error_type: Optional[str] = Field(default=None, description="The error type, if any")
error_message: Optional[str] = Field(default=None, description="The error message, if any")
error_traceback: Optional[str] = Field(default=None, description="The error traceback, if any")
created_at: str = Field(description="The timestamp when the queue item was created")
updated_at: str = Field(description="The timestamp when the queue item was last updated")
created_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was created")
updated_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was last updated")
started_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was started")
completed_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was completed")
batch_status: BatchStatus = Field(description="The status of the batch")
@@ -258,8 +258,8 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
error_type=queue_item.error_type,
error_message=queue_item.error_message,
error_traceback=queue_item.error_traceback,
created_at=str(queue_item.created_at),
updated_at=str(queue_item.updated_at),
created_at=str(queue_item.created_at) if queue_item.created_at else None,
updated_at=str(queue_item.updated_at) if queue_item.updated_at else None,
started_at=str(queue_item.started_at) if queue_item.started_at else None,
completed_at=str(queue_item.completed_at) if queue_item.completed_at else None,
batch_status=batch_status,

View File

@@ -58,15 +58,6 @@ class ImageCategory(str, Enum, metaclass=MetaEnum):
"""OTHER: The image is some other type of image with a specialized purpose. To be used by external nodes."""
IMAGE_CATEGORIES: list[ImageCategory] = [ImageCategory.GENERAL]
ASSETS_CATEGORIES: list[ImageCategory] = [
ImageCategory.CONTROL,
ImageCategory.MASK,
ImageCategory.USER,
ImageCategory.OTHER,
]
class InvalidImageCategoryException(ValueError):
"""Raised when a provided value is not a valid ImageCategory.

View File

@@ -1,6 +1,6 @@
from typing import Optional
from typing import Any, Literal, Optional
from pydantic import BaseModel, Field
from pydantic import ConfigDict, Field
from invokeai.app.services.image_records.image_records_common import ImageRecord
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
@@ -17,14 +17,24 @@ class ImageUrlsDTO(BaseModelExcludeNull):
"""The URL of the image's thumbnail."""
def make_type_required(s: dict[str, Any]):
if "required" in s:
s["required"].append("type")
else:
s["required"] = ["type"]
class ImageDTO(ImageRecord, ImageUrlsDTO):
"""Deserialized image record, enriched for the frontend."""
type: Literal["image"] = Field(default="image")
board_id: Optional[str] = Field(
default=None, description="The id of the board the image belongs to, if one exists."
)
"""The id of the board the image belongs to, if one exists."""
model_config = ConfigDict(json_schema_extra=make_type_required)
def image_record_to_dto(
image_record: ImageRecord,
@@ -39,27 +49,3 @@ def image_record_to_dto(
thumbnail_url=thumbnail_url,
board_id=board_id,
)
class ResultWithAffectedBoards(BaseModel):
affected_boards: list[str] = Field(description="The ids of boards affected by the delete operation")
class DeleteImagesResult(ResultWithAffectedBoards):
deleted_images: list[str] = Field(description="The names of the images that were deleted")
class StarredImagesResult(ResultWithAffectedBoards):
starred_images: list[str] = Field(description="The names of the images that were starred")
class UnstarredImagesResult(ResultWithAffectedBoards):
unstarred_images: list[str] = Field(description="The names of the images that were unstarred")
class AddImagesToBoardResult(ResultWithAffectedBoards):
added_images: list[str] = Field(description="The image names that were added to the board")
class RemoveImagesFromBoardResult(ResultWithAffectedBoards):
removed_images: list[str] = Field(description="The image names that were removed from their board")

View File

@@ -23,6 +23,7 @@ from invokeai.app.services.image_records.image_records_common import (
from invokeai.app.services.images.images_base import ImageServiceABC
from invokeai.app.services.images.images_common import ImageDTO, image_record_to_dto
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.resources.resources_common import ResourceType
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
@@ -75,8 +76,8 @@ class ImageService(ImageServiceABC):
)
if board_id is not None:
try:
self.__invoker.services.board_image_records.add_image_to_board(
board_id=board_id, image_name=image_name
self.__invoker.services.board_resource_records.add_resource_to_board(
board_id=board_id, resource_id=image_name, resource_type=ResourceType.IMAGE
)
except Exception as e:
self.__invoker.services.logger.warning(f"Failed to add image to board {board_id}: {str(e)}")
@@ -142,7 +143,9 @@ class ImageService(ImageServiceABC):
image_record=image_record,
image_url=self.__invoker.services.urls.get_image_url(image_name),
thumbnail_url=self.__invoker.services.urls.get_image_url(image_name, True),
board_id=self.__invoker.services.board_image_records.get_board_for_image(image_name),
board_id=self.__invoker.services.board_resource_records.get_board_for_resource(
image_name, ResourceType.IMAGE
),
)
return image_dto
@@ -234,7 +237,9 @@ class ImageService(ImageServiceABC):
image_record=r,
image_url=self.__invoker.services.urls.get_image_url(r.image_name),
thumbnail_url=self.__invoker.services.urls.get_image_url(r.image_name, True),
board_id=self.__invoker.services.board_image_records.get_board_for_image(r.image_name),
board_id=self.__invoker.services.board_resource_records.get_board_for_resource(
r.image_name, ResourceType.IMAGE
),
)
for r in results.items
]
@@ -266,10 +271,9 @@ class ImageService(ImageServiceABC):
def delete_images_on_board(self, board_id: str):
try:
image_names = self.__invoker.services.board_image_records.get_all_board_image_names_for_board(
image_names = self.__invoker.services.board_resource_records.get_all_board_resource_ids_for_board(
board_id,
categories=None,
is_intermediate=None,
resource_type=ResourceType.IMAGE,
)
for image_name in image_names:
self.__invoker.services.image_files.delete(image_name)

View File

@@ -12,9 +12,11 @@ if TYPE_CHECKING:
import torch
from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
from invokeai.app.services.board_images.board_images_base import BoardImagesServiceABC
# from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
# from invokeai.app.services.board_images.board_images_base import BoardImagesServiceABC
from invokeai.app.services.board_records.board_records_base import BoardRecordStorageBase
from invokeai.app.services.board_resource_records.board_resource_records_base import BoardResourceRecordStorageBase
from invokeai.app.services.board_resources.board_resources_base import BoardResourcesServiceABC
from invokeai.app.services.boards.boards_base import BoardServiceABC
from invokeai.app.services.bulk_download.bulk_download_base import BulkDownloadBase
from invokeai.app.services.client_state_persistence.client_state_persistence_base import ClientStatePersistenceABC
@@ -36,6 +38,8 @@ if TYPE_CHECKING:
from invokeai.app.services.session_processor.session_processor_base import SessionProcessorBase
from invokeai.app.services.session_queue.session_queue_base import SessionQueueBase
from invokeai.app.services.urls.urls_base import UrlServiceBase
from invokeai.app.services.video_records.video_records_base import VideoRecordStorageBase
from invokeai.app.services.videos.videos_base import VideoServiceABC
from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase
from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_base import WorkflowThumbnailServiceBase
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
@@ -46,8 +50,10 @@ class InvocationServices:
def __init__(
self,
board_images: "BoardImagesServiceABC",
board_image_records: "BoardImageRecordStorageBase",
# board_images: "BoardImagesServiceABC",
# board_image_records: "BoardImageRecordStorageBase",
board_resources: "BoardResourcesServiceABC",
board_resource_records: "BoardResourceRecordStorageBase",
boards: "BoardServiceABC",
board_records: "BoardRecordStorageBase",
bulk_download: "BulkDownloadBase",
@@ -68,6 +74,8 @@ class InvocationServices:
invocation_cache: "InvocationCacheBase",
names: "NameServiceBase",
urls: "UrlServiceBase",
videos: "VideoServiceABC",
video_records: "VideoRecordStorageBase",
workflow_records: "WorkflowRecordsStorageBase",
tensors: "ObjectSerializerBase[torch.Tensor]",
conditioning: "ObjectSerializerBase[ConditioningFieldData]",
@@ -76,8 +84,10 @@ class InvocationServices:
workflow_thumbnails: "WorkflowThumbnailServiceBase",
client_state_persistence: "ClientStatePersistenceABC",
):
self.board_images = board_images
self.board_image_records = board_image_records
# self.board_images = board_images
# self.board_image_records = board_image_records
self.board_resources = board_resources
self.board_resource_records = board_resource_records
self.boards = boards
self.board_records = board_records
self.bulk_download = bulk_download
@@ -98,6 +108,8 @@ class InvocationServices:
self.invocation_cache = invocation_cache
self.names = names
self.urls = urls
self.videos = videos
self.video_records = video_records
self.workflow_records = workflow_records
self.tensors = tensors
self.conditioning = conditioning

View File

@@ -15,7 +15,6 @@ from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
from invokeai.backend.model_manager.config import (
AnyModelConfig,
ControlAdapterDefaultSettings,
LoraModelDefaultSettings,
MainModelDefaultSettings,
)
from invokeai.backend.model_manager.taxonomy import (
@@ -84,8 +83,8 @@ class ModelRecordChanges(BaseModelExcludeNull):
file_size: Optional[int] = Field(description="Size of model file", default=None)
format: Optional[str] = Field(description="format of model file", default=None)
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
default_settings: Optional[MainModelDefaultSettings | LoraModelDefaultSettings | ControlAdapterDefaultSettings] = (
Field(description="Default settings for this model", default=None)
default_settings: Optional[MainModelDefaultSettings | ControlAdapterDefaultSettings] = Field(
description="Default settings for this model", default=None
)
# Checkpoint-specific changes

View File

@@ -1,8 +0,0 @@
from enum import Enum, EnumMeta
class ResourceType(str, Enum, metaclass=EnumMeta):
"""Enum for resource types."""
IMAGE = "image"
LATENT = "latent"

View File

@@ -0,0 +1,39 @@
from enum import Enum
from pydantic import BaseModel, Field
class ResourceType(str, Enum):
"""The type of resource that can be associated with a board."""
IMAGE = "image"
VIDEO = "video"
class ResourceIdentifier(BaseModel):
resource_id: str = Field(description="The id of the resource to delete")
resource_type: ResourceType = Field(description="The type of the resource to delete")
class ResultWithAffectedBoards(BaseModel):
affected_boards: list[str] = Field(description="The ids of boards affected by the delete operation")
class DeleteResourcesResult(ResultWithAffectedBoards):
deleted_resources: list[ResourceIdentifier] = Field(description="The ids of the resources that were deleted")
class StarredResourcesResult(ResultWithAffectedBoards):
starred_resources: list[ResourceIdentifier] = Field(description="The resources that were starred")
class UnstarredResourcesResult(ResultWithAffectedBoards):
unstarred_resources: list[ResourceIdentifier] = Field(description="The resources that were unstarred")
class AddResourcesToBoardResult(ResultWithAffectedBoards):
added_resources: list[ResourceIdentifier] = Field(description="The resources that were added to the board")
class RemoveResourcesFromBoardResult(ResultWithAffectedBoards):
removed_resources: list[ResourceIdentifier] = Field(description="The resources that were removed from their board")

View File

@@ -15,7 +15,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
EnqueueBatchResult,
IsEmptyResult,
IsFullResult,
ItemIdsResult,
PruneResult,
RetryItemsResult,
SessionQueueCountsByDestination,
@@ -24,7 +23,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
)
from invokeai.app.services.shared.graph import GraphExecutionState
from invokeai.app.services.shared.pagination import CursorPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
class SessionQueueBase(ABC):
@@ -147,7 +145,7 @@ class SessionQueueBase(ABC):
status: Optional[QUEUE_ITEM_STATUS] = None,
destination: Optional[str] = None,
) -> CursorPaginatedResults[SessionQueueItem]:
"""Gets a page of session queue items. Do not remove."""
"""Gets a page of session queue items"""
pass
@abstractmethod
@@ -159,18 +157,9 @@ class SessionQueueBase(ABC):
"""Gets all queue items that match the given parameters"""
pass
@abstractmethod
def get_queue_item_ids(
self,
queue_id: str,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
) -> ItemIdsResult:
"""Gets all queue item ids that match the given parameters"""
pass
@abstractmethod
def get_queue_item(self, item_id: int) -> SessionQueueItem:
"""Gets a session queue item by ID for a given queue"""
"""Gets a session queue item by ID"""
pass
@abstractmethod

View File

@@ -176,14 +176,6 @@ DEFAULT_QUEUE_ID = "default"
QUEUE_ITEM_STATUS = Literal["pending", "in_progress", "completed", "failed", "canceled"]
class ItemIdsResult(BaseModel):
"""Response containing ordered item ids with metadata for optimistic updates."""
item_ids: list[int] = Field(description="Ordered list of item ids")
total_count: int = Field(description="Total number of queue items matching the query")
NodeFieldValueValidator = TypeAdapter(list[NodeFieldValue])

View File

@@ -22,7 +22,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
EnqueueBatchResult,
IsEmptyResult,
IsFullResult,
ItemIdsResult,
PruneResult,
RetryItemsResult,
SessionQueueCountsByDestination,
@@ -35,7 +34,6 @@ from invokeai.app.services.session_queue.session_queue_common import (
)
from invokeai.app.services.shared.graph import GraphExecutionState
from invokeai.app.services.shared.pagination import CursorPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
@@ -673,26 +671,6 @@ class SqliteSessionQueue(SessionQueueBase):
items = [SessionQueueItem.queue_item_from_dict(dict(result)) for result in results]
return items
def get_queue_item_ids(
self,
queue_id: str,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
) -> ItemIdsResult:
with self._db.transaction() as cursor_:
query = f"""--sql
SELECT item_id
FROM session_queue
WHERE queue_id = ?
ORDER BY created_at {order_dir.value}
"""
query_params = [queue_id]
cursor_.execute(query, query_params)
result = cast(list[sqlite3.Row], cursor_.fetchall())
item_ids = [row[0] for row in result]
return ItemIdsResult(item_ids=item_ids, total_count=len(item_ids))
def get_queue_status(self, queue_id: str) -> SessionQueueStatus:
with self._db.transaction() as cursor:
cursor.execute(

View File

@@ -16,6 +16,7 @@ from invokeai.app.services.image_records.image_records_common import ImageCatego
from invokeai.app.services.images.images_common import ImageDTO
from invokeai.app.services.invocation_services import InvocationServices
from invokeai.app.services.model_records.model_records_base import UnknownModelException
from invokeai.app.services.resources.resources_common import ResourceType
from invokeai.app.services.session_processor.session_processor_common import ProgressImage
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.util.step_callback import diffusion_step_callback
@@ -112,7 +113,7 @@ class BoardsInterface(InvocationContextInterface):
board_id: The ID of the board to add the image to.
image_name: The name of the image to add to the board.
"""
return self._services.board_images.add_image_to_board(board_id, image_name)
return self._services.board_resources.add_resource_to_board(board_id, image_name, ResourceType.IMAGE)
def get_all_image_names_for_board(self, board_id: str) -> list[str]:
"""Gets all image names for a board.
@@ -123,11 +124,11 @@ class BoardsInterface(InvocationContextInterface):
Returns:
A list of all image names for the board.
"""
return self._services.board_images.get_all_board_image_names_for_board(
resource_ids = self._services.board_resources.get_all_board_resource_ids_for_board(
board_id,
categories=None,
is_intermediate=None,
resource_type=ResourceType.IMAGE,
)
return [resource_id.resource_id for resource_id in resource_ids]
class LoggerInterface(InvocationContextInterface):

View File

@@ -0,0 +1,105 @@
from abc import ABC, abstractmethod
from typing import Optional
from invokeai.app.invocations.fields import MetadataField
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.video_records.video_records_common import (
VideoNamesResult,
VideoRecord,
VideoRecordChanges,
)
class VideoRecordStorageBase(ABC):
"""Low-level service responsible for interfacing with the video record store."""
@abstractmethod
def get(self, video_id: str) -> VideoRecord:
"""Gets a video record."""
pass
@abstractmethod
def get_metadata(self, video_id: str) -> Optional[MetadataField]:
"""Gets a video's metadata."""
pass
@abstractmethod
def update(
self,
video_id: str,
changes: VideoRecordChanges,
) -> None:
"""Updates a video record."""
pass
@abstractmethod
def get_many(
self,
offset: int = 0,
limit: int = 10,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> OffsetPaginatedResults[VideoRecord]:
"""Gets a page of video records."""
pass
@abstractmethod
def delete(self, video_id: str) -> None:
"""Deletes a video record."""
pass
@abstractmethod
def delete_many(self, video_ids: list[str]) -> None:
"""Deletes many video records."""
pass
@abstractmethod
def save(
self,
video_id: str,
width: int,
height: int,
duration: Optional[float] = None,
frame_rate: Optional[float] = None,
node_id: Optional[str] = None,
session_id: Optional[str] = None,
metadata: Optional[str] = None,
workflow: Optional[str] = None,
graph: Optional[str] = None,
) -> VideoRecord:
"""Saves a video record."""
pass
@abstractmethod
def get_workflow(self, video_id: str) -> Optional[str]:
"""Gets a video's workflow."""
pass
@abstractmethod
def get_graph(self, video_id: str) -> Optional[str]:
"""Gets a video's graph."""
pass
@abstractmethod
def get_video_names(
self,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> VideoNamesResult:
"""Gets video names with metadata for optimistic updates."""
pass
@abstractmethod
def get_intermediates_count(self) -> int:
"""Gets the count of intermediate videos."""
pass
@abstractmethod
def delete_intermediates(self) -> int:
"""Deletes all intermediate videos and returns the count of deleted videos."""
pass

View File

@@ -0,0 +1,52 @@
import datetime
from typing import Optional, Union
from pydantic import BaseModel, Field
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
VIDEO_DTO_COLS = ", ".join(
[
"videos." + c
for c in [
"id",
"width",
"height",
"created_at",
"updated_at",
]
]
)
class VideoRecord(BaseModelExcludeNull):
"""Deserialized video record without metadata."""
id: str = Field(description="The unique id of the video.")
"""The unique id of the video."""
width: int = Field(description="The width of the video in px.")
"""The actual width of the video in px."""
height: int = Field(description="The height of the video in px.")
"""The actual height of the video in px."""
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the video.")
"""The created timestamp of the video."""
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the video.")
"""The updated timestamp of the video."""
class VideoRecordChanges(BaseModelExcludeNull):
"""
A set of changes to apply to a video record.
Only limited changes are allowed:
- `starred` - Whether the video is starred.
"""
starred: Optional[bool] = Field(default=None, description="Whether the video is starred.")
"""The video's new `starred` state."""
class VideoNamesResult(BaseModel):
"""Result of fetching video names."""
video_ids: list[str] = Field(description="The video IDs")

View File

@@ -0,0 +1,86 @@
from typing import Optional
from invokeai.app.invocations.fields import MetadataField
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.app.services.video_records.video_records_base import VideoRecordStorageBase
from invokeai.app.services.video_records.video_records_common import (
VideoNamesResult,
VideoRecord,
VideoRecordChanges,
)
class SqliteVideoRecordStorage(VideoRecordStorageBase):
def __init__(self, db: SqliteDatabase) -> None:
super().__init__()
self._db = db
def get(self, video_id: str) -> VideoRecord:
# For now, this is a placeholder that raises NotImplementedError
# In a real implementation, this would query the videos table
raise NotImplementedError("Video record storage not yet implemented")
def get_metadata(self, video_id: str) -> Optional[MetadataField]:
raise NotImplementedError("Video record storage not yet implemented")
def update(
self,
video_id: str,
changes: VideoRecordChanges,
) -> None:
raise NotImplementedError("Video record storage not yet implemented")
def get_many(
self,
offset: int = 0,
limit: int = 10,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> OffsetPaginatedResults[VideoRecord]:
raise NotImplementedError("Video record storage not yet implemented")
def delete(self, video_id: str) -> None:
raise NotImplementedError("Video record storage not yet implemented")
def delete_many(self, video_ids: list[str]) -> None:
raise NotImplementedError("Video record storage not yet implemented")
def save(
self,
video_id: str,
width: int,
height: int,
duration: Optional[float] = None,
frame_rate: Optional[float] = None,
node_id: Optional[str] = None,
session_id: Optional[str] = None,
metadata: Optional[str] = None,
workflow: Optional[str] = None,
graph: Optional[str] = None,
) -> VideoRecord:
raise NotImplementedError("Video record storage not yet implemented")
def get_workflow(self, video_id: str) -> Optional[str]:
raise NotImplementedError("Video record storage not yet implemented")
def get_graph(self, video_id: str) -> Optional[str]:
raise NotImplementedError("Video record storage not yet implemented")
def get_video_names(
self,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> VideoNamesResult:
raise NotImplementedError("Video record storage not yet implemented")
def get_intermediates_count(self) -> int:
return 0 # Placeholder implementation
def delete_intermediates(self) -> int:
return 0 # Placeholder implementation

View File

View File

@@ -0,0 +1,151 @@
from abc import ABC, abstractmethod
from typing import Callable, Optional
from invokeai.app.invocations.fields import MetadataField
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.video_records.video_records_common import (
VideoNamesResult,
VideoRecord,
VideoRecordChanges,
)
from invokeai.app.services.videos.videos_common import VideoDTO
class VideoServiceABC(ABC):
"""High-level service for video management."""
_on_changed_callbacks: list[Callable[[VideoDTO], None]]
_on_deleted_callbacks: list[Callable[[str], None]]
def __init__(self) -> None:
self._on_changed_callbacks = []
self._on_deleted_callbacks = []
def on_changed(self, on_changed: Callable[[VideoDTO], None]) -> None:
"""Register a callback for when a video is changed."""
self._on_changed_callbacks.append(on_changed)
def on_deleted(self, on_deleted: Callable[[str], None]) -> None:
"""Register a callback for when a video is deleted."""
self._on_deleted_callbacks.append(on_deleted)
def _on_changed(self, item: VideoDTO) -> None:
for callback in self._on_changed_callbacks:
callback(item)
def _on_deleted(self, item_id: str) -> None:
for callback in self._on_deleted_callbacks:
callback(item_id)
@abstractmethod
def create(
self,
video_id: str,
width: int,
height: int,
duration: Optional[float] = None,
frame_rate: Optional[float] = None,
node_id: Optional[str] = None,
session_id: Optional[str] = None,
board_id: Optional[str] = None,
is_intermediate: Optional[bool] = False,
metadata: Optional[str] = None,
workflow: Optional[str] = None,
graph: Optional[str] = None,
) -> VideoDTO:
"""Creates a video record and returns its DTO."""
pass
@abstractmethod
def update(
self,
video_id: str,
changes: VideoRecordChanges,
) -> VideoDTO:
"""Updates a video record and returns its DTO."""
pass
@abstractmethod
def get_record(self, video_id: str) -> VideoRecord:
"""Gets a video record."""
pass
@abstractmethod
def get_dto(self, video_id: str) -> VideoDTO:
"""Gets a video DTO."""
pass
@abstractmethod
def get_metadata(self, video_id: str) -> Optional[MetadataField]:
"""Gets a video's metadata."""
pass
@abstractmethod
def get_workflow(self, video_id: str) -> Optional[str]:
"""Gets a video's workflow."""
pass
@abstractmethod
def get_graph(self, video_id: str) -> Optional[str]:
"""Gets a video's graph."""
pass
@abstractmethod
def get_path(self, video_id: str, thumbnail: bool = False) -> str:
"""Gets a video's path on disk."""
pass
@abstractmethod
def validate_path(self, path: str) -> bool:
"""Validates a video path."""
pass
@abstractmethod
def get_url(self, video_id: str, thumbnail: bool = False) -> str:
"""Gets a video's URL."""
pass
@abstractmethod
def get_many(
self,
offset: int = 0,
limit: int = 10,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> OffsetPaginatedResults[VideoDTO]:
"""Gets a page of video DTOs."""
pass
@abstractmethod
def delete(self, video_id: str):
"""Deletes a video."""
pass
@abstractmethod
def delete_intermediates(self) -> int:
"""Deletes all intermediate videos and returns the count."""
pass
@abstractmethod
def get_intermediates_count(self) -> int:
"""Gets the count of intermediate videos."""
pass
@abstractmethod
def delete_videos_on_board(self, board_id: str):
"""Deletes all videos on a board."""
pass
@abstractmethod
def get_video_names(
self,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> VideoNamesResult:
"""Gets video names with metadata for optimistic updates."""
pass

View File

@@ -0,0 +1,38 @@
from typing import Any, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from invokeai.app.services.video_records.video_records_common import VideoRecord
def make_type_required(s: dict[str, Any]):
if "required" in s:
s["required"].append("type")
else:
s["required"] = ["type"]
class VideoDTO(BaseModel):
"""Deserialized video record, enriched for the frontend."""
type: Literal["video"] = Field(default="video")
video_id: str = Field(description="The id of the board the video belongs to, if one exists.")
"""The id of the board the video belongs to, if one exists."""
width: int = Field(description="The width of the video.")
height: int = Field(description="The height of the video.")
board_id: Optional[str] = Field(
default=None, description="The id of the board the video belongs to, if one exists."
)
model_config = ConfigDict(json_schema_extra=make_type_required)
def video_record_to_dto(
video_record: VideoRecord,
board_id: Optional[str],
) -> VideoDTO:
"""Converts a VideoRecord to a VideoDTO."""
return VideoDTO(
**video_record.model_dump(),
board_id=board_id,
)

View File

@@ -0,0 +1,103 @@
from typing import Optional
from invokeai.app.invocations.fields import MetadataField
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.video_records.video_records_common import (
VideoNamesResult,
VideoRecord,
VideoRecordChanges,
)
from invokeai.app.services.videos.videos_base import VideoServiceABC
from invokeai.app.services.videos.videos_common import VideoDTO
class VideoService(VideoServiceABC):
__invoker: Invoker
def start(self, invoker: Invoker) -> None:
self.__invoker = invoker
def create(
self,
video_id: str,
width: int,
height: int,
duration: Optional[float] = None,
frame_rate: Optional[float] = None,
node_id: Optional[str] = None,
session_id: Optional[str] = None,
board_id: Optional[str] = None,
is_intermediate: Optional[bool] = False,
metadata: Optional[str] = None,
workflow: Optional[str] = None,
graph: Optional[str] = None,
) -> VideoDTO:
# For now, this is a placeholder implementation
raise NotImplementedError("Video service not yet implemented")
def update(
self,
video_id: str,
changes: VideoRecordChanges,
) -> VideoDTO:
raise NotImplementedError("Video service not yet implemented")
def get_record(self, video_id: str) -> VideoRecord:
raise NotImplementedError("Video service not yet implemented")
def get_dto(self, video_id: str) -> VideoDTO:
raise NotImplementedError("Video service not yet implemented")
def get_metadata(self, video_id: str) -> Optional[MetadataField]:
raise NotImplementedError("Video service not yet implemented")
def get_workflow(self, video_id: str) -> Optional[str]:
raise NotImplementedError("Video service not yet implemented")
def get_graph(self, video_id: str) -> Optional[str]:
raise NotImplementedError("Video service not yet implemented")
def get_path(self, video_id: str, thumbnail: bool = False) -> str:
raise NotImplementedError("Video service not yet implemented")
def validate_path(self, path: str) -> bool:
raise NotImplementedError("Video service not yet implemented")
def get_url(self, video_id: str, thumbnail: bool = False) -> str:
raise NotImplementedError("Video service not yet implemented")
def get_many(
self,
offset: int = 0,
limit: int = 10,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> OffsetPaginatedResults[VideoDTO]:
# Return empty results for now
return OffsetPaginatedResults(items=[], offset=offset, limit=limit, total=0, has_more=False)
def delete(self, video_id: str):
raise NotImplementedError("Video service not yet implemented")
def delete_intermediates(self) -> int:
return 0 # Placeholder
def get_intermediates_count(self) -> int:
return 0 # Placeholder
def delete_videos_on_board(self, board_id: str):
raise NotImplementedError("Video service not yet implemented")
def get_video_names(
self,
starred_first: bool = True,
order_dir: SQLiteDirection = SQLiteDirection.Descending,
board_id: Optional[str] = None,
search_term: Optional[str] = None,
) -> VideoNamesResult:
# Return empty results for now
return VideoNamesResult(video_ids=[])

View File

@@ -1,179 +0,0 @@
import datetime
from typing import Optional, Union
from pydantic import BaseModel, Field, StrictBool, StrictStr
from invokeai.app.util.misc import get_iso_timestamp
from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
VIDEO_DTO_COLS = ", ".join(
[
"videos." + c
for c in [
"video_id",
"width",
"height",
"session_id",
"node_id",
"is_intermediate",
"created_at",
"updated_at",
"deleted_at",
"starred",
]
]
)
class VideoRecord(BaseModelExcludeNull):
"""Deserialized video record without metadata."""
video_id: str = Field(description="The unique id of the video.")
"""The unique id of the video."""
width: int = Field(description="The width of the video in px.")
"""The actual width of the video in px. This may be different from the width in metadata."""
height: int = Field(description="The height of the video in px.")
"""The actual height of the video in px. This may be different from the height in metadata."""
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the video.")
"""The created timestamp of the video."""
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the video.")
"""The updated timestamp of the video."""
deleted_at: Optional[Union[datetime.datetime, str]] = Field(
default=None, description="The deleted timestamp of the video."
)
"""The deleted timestamp of the video."""
is_intermediate: bool = Field(description="Whether this is an intermediate video.")
"""Whether this is an intermediate video."""
session_id: Optional[str] = Field(
default=None,
description="The session ID that generated this video, if it is a generated video.",
)
"""The session ID that generated this video, if it is a generated video."""
node_id: Optional[str] = Field(
default=None,
description="The node ID that generated this video, if it is a generated video.",
)
"""The node ID that generated this video, if it is a generated video."""
starred: bool = Field(description="Whether this video is starred.")
"""Whether this video is starred."""
class VideoRecordChanges(BaseModelExcludeNull):
"""A set of changes to apply to a video record.
Only limited changes are valid:
- `session_id`: change the session associated with a video
- `is_intermediate`: change the video's `is_intermediate` flag
- `starred`: change whether the video is starred
"""
session_id: Optional[StrictStr] = Field(
default=None,
description="The video's new session ID.",
)
"""The video's new session ID."""
is_intermediate: Optional[StrictBool] = Field(default=None, description="The video's new `is_intermediate` flag.")
"""The video's new `is_intermediate` flag."""
starred: Optional[StrictBool] = Field(default=None, description="The video's new `starred` state")
"""The video's new `starred` state."""
def deserialize_video_record(video_dict: dict) -> VideoRecord:
"""Deserializes a video record."""
# Retrieve all the values, setting "reasonable" defaults if they are not present.
video_id = video_dict.get("video_id", "unknown")
width = video_dict.get("width", 0)
height = video_dict.get("height", 0)
session_id = video_dict.get("session_id", None)
node_id = video_dict.get("node_id", None)
created_at = video_dict.get("created_at", get_iso_timestamp())
updated_at = video_dict.get("updated_at", get_iso_timestamp())
deleted_at = video_dict.get("deleted_at", get_iso_timestamp())
is_intermediate = video_dict.get("is_intermediate", False)
starred = video_dict.get("starred", False)
return VideoRecord(
video_id=video_id,
width=width,
height=height,
session_id=session_id,
node_id=node_id,
created_at=created_at,
updated_at=updated_at,
deleted_at=deleted_at,
is_intermediate=is_intermediate,
starred=starred,
)
class VideoCollectionCounts(BaseModel):
starred_count: int = Field(description="The number of starred videos in the collection.")
unstarred_count: int = Field(description="The number of unstarred videos in the collection.")
class VideoIdsResult(BaseModel):
"""Response containing ordered video ids with metadata for optimistic updates."""
video_ids: list[str] = Field(description="Ordered list of video ids")
starred_count: int = Field(description="Number of starred videos (when starred_first=True)")
total_count: int = Field(description="Total number of videos matching the query")
class VideoUrlsDTO(BaseModelExcludeNull):
"""The URLs for an image and its thumbnail."""
video_id: str = Field(description="The unique id of the video.")
"""The unique id of the video."""
video_url: str = Field(description="The URL of the video.")
"""The URL of the video."""
thumbnail_url: str = Field(description="The URL of the video's thumbnail.")
"""The URL of the video's thumbnail."""
class VideoDTO(VideoRecord, VideoUrlsDTO):
"""Deserialized video record, enriched for the frontend."""
board_id: Optional[str] = Field(
default=None, description="The id of the board the image belongs to, if one exists."
)
"""The id of the board the image belongs to, if one exists."""
def video_record_to_dto(
video_record: VideoRecord,
video_url: str,
thumbnail_url: str,
board_id: Optional[str],
) -> VideoDTO:
"""Converts a video record to a video DTO."""
return VideoDTO(
**video_record.model_dump(),
video_url=video_url,
thumbnail_url=thumbnail_url,
board_id=board_id,
)
class ResultWithAffectedBoards(BaseModel):
affected_boards: list[str] = Field(description="The ids of boards affected by the delete operation")
class DeleteVideosResult(ResultWithAffectedBoards):
deleted_videos: list[str] = Field(description="The ids of the videos that were deleted")
class StarredVideosResult(ResultWithAffectedBoards):
starred_videos: list[str] = Field(description="The ids of the videos that were starred")
class UnstarredVideosResult(ResultWithAffectedBoards):
unstarred_videos: list[str] = Field(description="The ids of the videos that were unstarred")
class AddVideosToBoardResult(ResultWithAffectedBoards):
added_videos: list[str] = Field(description="The video ids that were added to the board")
class RemoveVideosFromBoardResult(ResultWithAffectedBoards):
removed_videos: list[str] = Field(description="The video ids that were removed from their board")

View File

@@ -1,304 +0,0 @@
# This file is vendored from https://github.com/ShieldMnt/invisible-watermark
#
# `invisible-watermark` is MIT licensed as of August 23, 2025, when the code was copied into this repo.
#
# Why we vendored it in:
# `invisible-watermark` has a dependency on `opencv-python`, which conflicts with Invoke's dependency on
# `opencv-contrib-python`. It's easier to copy the code over than complicate the installation process by
# requiring an extra post-install step of removing `opencv-python` and installing `opencv-contrib-python`.
import struct
import uuid
import base64
import cv2
import numpy as np
import pywt
class WatermarkEncoder(object):
def __init__(self, content=b""):
seq = np.array([n for n in content], dtype=np.uint8)
self._watermarks = list(np.unpackbits(seq))
self._wmLen = len(self._watermarks)
self._wmType = "bytes"
def set_by_ipv4(self, addr):
bits = []
ips = addr.split(".")
for ip in ips:
bits += list(np.unpackbits(np.array([ip % 255], dtype=np.uint8)))
self._watermarks = bits
self._wmLen = len(self._watermarks)
self._wmType = "ipv4"
assert self._wmLen == 32
def set_by_uuid(self, uid):
u = uuid.UUID(uid)
self._wmType = "uuid"
seq = np.array([n for n in u.bytes], dtype=np.uint8)
self._watermarks = list(np.unpackbits(seq))
self._wmLen = len(self._watermarks)
def set_by_bytes(self, content):
self._wmType = "bytes"
seq = np.array([n for n in content], dtype=np.uint8)
self._watermarks = list(np.unpackbits(seq))
self._wmLen = len(self._watermarks)
def set_by_b16(self, b16):
content = base64.b16decode(b16)
self.set_by_bytes(content)
self._wmType = "b16"
def set_by_bits(self, bits=[]):
self._watermarks = [int(bit) % 2 for bit in bits]
self._wmLen = len(self._watermarks)
self._wmType = "bits"
def set_watermark(self, wmType="bytes", content=""):
if wmType == "ipv4":
self.set_by_ipv4(content)
elif wmType == "uuid":
self.set_by_uuid(content)
elif wmType == "bits":
self.set_by_bits(content)
elif wmType == "bytes":
self.set_by_bytes(content)
elif wmType == "b16":
self.set_by_b16(content)
else:
raise NameError("%s is not supported" % wmType)
def get_length(self):
return self._wmLen
# @classmethod
# def loadModel(cls):
# RivaWatermark.loadModel()
def encode(self, cv2Image, method="dwtDct", **configs):
(r, c, channels) = cv2Image.shape
if r * c < 256 * 256:
raise RuntimeError("image too small, should be larger than 256x256")
if method == "dwtDct":
embed = EmbedMaxDct(self._watermarks, wmLen=self._wmLen, **configs)
return embed.encode(cv2Image)
# elif method == 'dwtDctSvd':
# embed = EmbedDwtDctSvd(self._watermarks, wmLen=self._wmLen, **configs)
# return embed.encode(cv2Image)
# elif method == 'rivaGan':
# embed = RivaWatermark(self._watermarks, self._wmLen)
# return embed.encode(cv2Image)
else:
raise NameError("%s is not supported" % method)
class WatermarkDecoder(object):
def __init__(self, wm_type="bytes", length=0):
self._wmType = wm_type
if wm_type == "ipv4":
self._wmLen = 32
elif wm_type == "uuid":
self._wmLen = 128
elif wm_type == "bytes":
self._wmLen = length
elif wm_type == "bits":
self._wmLen = length
elif wm_type == "b16":
self._wmLen = length
else:
raise NameError("%s is unsupported" % wm_type)
def reconstruct_ipv4(self, bits):
ips = [str(ip) for ip in list(np.packbits(bits))]
return ".".join(ips)
def reconstruct_uuid(self, bits):
nums = np.packbits(bits)
bstr = b""
for i in range(16):
bstr += struct.pack(">B", nums[i])
return str(uuid.UUID(bytes=bstr))
def reconstruct_bits(self, bits):
# return ''.join([str(b) for b in bits])
return bits
def reconstruct_b16(self, bits):
bstr = self.reconstruct_bytes(bits)
return base64.b16encode(bstr)
def reconstruct_bytes(self, bits):
nums = np.packbits(bits)
bstr = b""
for i in range(self._wmLen // 8):
bstr += struct.pack(">B", nums[i])
return bstr
def reconstruct(self, bits):
if len(bits) != self._wmLen:
raise RuntimeError("bits are not matched with watermark length")
if self._wmType == "ipv4":
return self.reconstruct_ipv4(bits)
elif self._wmType == "uuid":
return self.reconstruct_uuid(bits)
elif self._wmType == "bits":
return self.reconstruct_bits(bits)
elif self._wmType == "b16":
return self.reconstruct_b16(bits)
else:
return self.reconstruct_bytes(bits)
def decode(self, cv2Image, method="dwtDct", **configs):
(r, c, channels) = cv2Image.shape
if r * c < 256 * 256:
raise RuntimeError("image too small, should be larger than 256x256")
bits = []
if method == "dwtDct":
embed = EmbedMaxDct(watermarks=[], wmLen=self._wmLen, **configs)
bits = embed.decode(cv2Image)
# elif method == 'dwtDctSvd':
# embed = EmbedDwtDctSvd(watermarks=[], wmLen=self._wmLen, **configs)
# bits = embed.decode(cv2Image)
# elif method == 'rivaGan':
# embed = RivaWatermark(watermarks=[], wmLen=self._wmLen, **configs)
# bits = embed.decode(cv2Image)
else:
raise NameError("%s is not supported" % method)
return self.reconstruct(bits)
# @classmethod
# def loadModel(cls):
# RivaWatermark.loadModel()
class EmbedMaxDct(object):
def __init__(self, watermarks=[], wmLen=8, scales=[0, 36, 36], block=4):
self._watermarks = watermarks
self._wmLen = wmLen
self._scales = scales
self._block = block
def encode(self, bgr):
(row, col, channels) = bgr.shape
yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV)
for channel in range(2):
if self._scales[channel] <= 0:
continue
ca1, (h1, v1, d1) = pywt.dwt2(yuv[: row // 4 * 4, : col // 4 * 4, channel], "haar")
self.encode_frame(ca1, self._scales[channel])
yuv[: row // 4 * 4, : col // 4 * 4, channel] = pywt.idwt2((ca1, (v1, h1, d1)), "haar")
bgr_encoded = cv2.cvtColor(yuv, cv2.COLOR_YUV2BGR)
return bgr_encoded
def decode(self, bgr):
(row, col, channels) = bgr.shape
yuv = cv2.cvtColor(bgr, cv2.COLOR_BGR2YUV)
scores = [[] for i in range(self._wmLen)]
for channel in range(2):
if self._scales[channel] <= 0:
continue
ca1, (h1, v1, d1) = pywt.dwt2(yuv[: row // 4 * 4, : col // 4 * 4, channel], "haar")
scores = self.decode_frame(ca1, self._scales[channel], scores)
avgScores = list(map(lambda l: np.array(l).mean(), scores))
bits = np.array(avgScores) * 255 > 127
return bits
def decode_frame(self, frame, scale, scores):
(row, col) = frame.shape
num = 0
for i in range(row // self._block):
for j in range(col // self._block):
block = frame[
i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block
]
score = self.infer_dct_matrix(block, scale)
# score = self.infer_dct_svd(block, scale)
wmBit = num % self._wmLen
scores[wmBit].append(score)
num = num + 1
return scores
def diffuse_dct_svd(self, block, wmBit, scale):
u, s, v = np.linalg.svd(cv2.dct(block))
s[0] = (s[0] // scale + 0.25 + 0.5 * wmBit) * scale
return cv2.idct(np.dot(u, np.dot(np.diag(s), v)))
def infer_dct_svd(self, block, scale):
u, s, v = np.linalg.svd(cv2.dct(block))
score = 0
score = int((s[0] % scale) > scale * 0.5)
return score
if score >= 0.5:
return 1.0
else:
return 0.0
def diffuse_dct_matrix(self, block, wmBit, scale):
pos = np.argmax(abs(block.flatten()[1:])) + 1
i, j = pos // self._block, pos % self._block
val = block[i][j]
if val >= 0.0:
block[i][j] = (val // scale + 0.25 + 0.5 * wmBit) * scale
else:
val = abs(val)
block[i][j] = -1.0 * (val // scale + 0.25 + 0.5 * wmBit) * scale
return block
def infer_dct_matrix(self, block, scale):
pos = np.argmax(abs(block.flatten()[1:])) + 1
i, j = pos // self._block, pos % self._block
val = block[i][j]
if val < 0:
val = abs(val)
if (val % scale) > 0.5 * scale:
return 1
else:
return 0
def encode_frame(self, frame, scale):
"""
frame is a matrix (M, N)
we get K (watermark bits size) blocks (self._block x self._block)
For i-th block, we encode watermark[i] bit into it
"""
(row, col) = frame.shape
num = 0
for i in range(row // self._block):
for j in range(col // self._block):
block = frame[
i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block
]
wmBit = self._watermarks[(num % self._wmLen)]
diffusedBlock = self.diffuse_dct_matrix(block, wmBit, scale)
# diffusedBlock = self.diffuse_dct_svd(block, wmBit, scale)
frame[
i * self._block : i * self._block + self._block, j * self._block : j * self._block + self._block
] = diffusedBlock
num = num + 1

View File

@@ -6,10 +6,13 @@ configuration variable, that allows the watermarking to be supressed.
import cv2
import numpy as np
from imwatermark import WatermarkEncoder
from PIL import Image
import invokeai.backend.util.logging as logger
from invokeai.backend.image_util.imwatermark.vendor import WatermarkEncoder
from invokeai.app.services.config.config_default import get_config
config = get_config()
class InvisibleWatermark:

View File

@@ -1,109 +0,0 @@
from typing import Optional
import torch
from PIL import Image
# Import SAM2 components - these should be available in transformers 4.56.0+
from transformers.models.sam2 import Sam2Model
from transformers.models.sam2.processing_sam2 import Sam2Processor
from invokeai.backend.image_util.segment_anything.shared import SAMInput
from invokeai.backend.raw_model import RawModel
class SegmentAnything2Pipeline(RawModel):
"""A wrapper class for the transformers SAM2 model and processor that makes it compatible with the model manager."""
def __init__(self, sam2_model: Sam2Model, sam2_processor: Sam2Processor):
"""Initialize the SAM2 pipeline.
Args:
sam2_model: The SAM2 model
sam2_processor: The SAM2 processor (can be Sam2Processor or Sam2VideoProcessor)
"""
self._sam2_model = sam2_model
self._sam2_processor = sam2_processor
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None):
# HACK: The SAM2 pipeline may not work on MPS devices. We only allow it to be moved to CPU or CUDA.
if device is not None and device.type not in {"cpu", "cuda"}:
device = None
self._sam2_model.to(device=device, dtype=dtype)
def calc_size(self) -> int:
# HACK: Fix the circular import issue.
from invokeai.backend.model_manager.load.model_util import calc_module_size
return calc_module_size(self._sam2_model)
def segment(
self,
image: Image.Image,
inputs: list[SAMInput],
) -> torch.Tensor:
"""Segment the image using the provided inputs.
Args:
image: The image to segment.
inputs: A list of SAMInput objects containing bounding boxes and/or point lists.
Returns:
torch.Tensor: The segmentation masks. dtype: torch.bool. shape: [num_masks, channels, height, width].
"""
input_boxes: list[list[float]] = []
input_points: list[list[list[float]]] = []
input_labels: list[list[int]] = []
for i in inputs:
box: list[float] | None = None
points: list[list[float]] | None = None
labels: list[int] | None = None
if i.bounding_box is not None:
box: list[float] | None = [
i.bounding_box.x_min,
i.bounding_box.y_min,
i.bounding_box.x_max,
i.bounding_box.y_max,
]
if i.points is not None:
points = []
labels = []
for point in i.points:
points.append([point.x, point.y])
labels.append(point.label.value)
if box is not None:
input_boxes.append(box)
if points is not None:
input_points.append(points)
if labels is not None:
input_labels.append(labels)
batched_input_boxes = [input_boxes] if input_boxes else None
batched_input_points = [input_points] if input_points else None
batched_input_labels = [input_labels] if input_labels else None
processed_inputs = self._sam2_processor(
images=image,
input_boxes=batched_input_boxes,
input_points=batched_input_points,
input_labels=batched_input_labels,
return_tensors="pt",
).to(self._sam2_model.device)
# Generate masks using the SAM2 model
outputs = self._sam2_model(**processed_inputs)
# Post-process the masks to get the final segmentation
masks = self._sam2_processor.post_process_masks(
masks=outputs.pred_masks,
original_sizes=processed_inputs.original_sizes,
reshaped_input_sizes=processed_inputs.reshaped_input_sizes,
)
# There should be only one batch.
assert len(masks) == 1
return masks[0]

View File

@@ -1,13 +1,20 @@
from typing import Optional
from typing import Optional, TypeAlias
import torch
from PIL import Image
from transformers.models.sam import SamModel
from transformers.models.sam.processing_sam import SamProcessor
from invokeai.backend.image_util.segment_anything.shared import SAMInput
from invokeai.backend.raw_model import RawModel
# Type aliases for the inputs to the SAM model.
ListOfBoundingBoxes: TypeAlias = list[list[int]]
"""A list of bounding boxes. Each bounding box is in the format [xmin, ymin, xmax, ymax]."""
ListOfPoints: TypeAlias = list[list[int]]
"""A list of points. Each point is in the format [x, y]."""
ListOfPointLabels: TypeAlias = list[int]
"""A list of SAM point labels. Each label is an integer where -1 is background, 0 is neutral, and 1 is foreground."""
class SegmentAnythingPipeline(RawModel):
"""A wrapper class for the transformers SAM model and processor that makes it compatible with the model manager."""
@@ -31,65 +38,55 @@ class SegmentAnythingPipeline(RawModel):
def segment(
self,
image: Image.Image,
inputs: list[SAMInput],
bounding_boxes: list[list[int]] | None = None,
point_lists: list[list[list[int]]] | None = None,
) -> torch.Tensor:
"""Segment the image using the provided inputs.
"""Run the SAM model.
Either bounding_boxes or point_lists must be provided. If both are provided, bounding_boxes will be used and
point_lists will be ignored.
Args:
image: The image to segment.
inputs: A list of SAMInput objects containing bounding boxes and/or point lists.
image (Image.Image): The image to segment.
bounding_boxes (list[list[int]]): The bounding box prompts. Each bounding box is in the format
[xmin, ymin, xmax, ymax].
point_lists (list[list[list[int]]]): The points prompts. Each point is in the format [x, y, label].
`label` is an integer where -1 is background, 0 is neutral, and 1 is foreground.
Returns:
torch.Tensor: The segmentation masks. dtype: torch.bool. shape: [num_masks, channels, height, width].
"""
input_boxes: list[list[float]] = []
input_points: list[list[list[float]]] = []
input_labels: list[list[int]] = []
# Prep the inputs:
# - Create a list of bounding boxes or points and labels.
# - Add a batch dimension of 1 to the inputs.
if bounding_boxes:
input_boxes: list[ListOfBoundingBoxes] | None = [bounding_boxes]
input_points: list[ListOfPoints] | None = None
input_labels: list[ListOfPointLabels] | None = None
elif point_lists:
input_boxes: list[ListOfBoundingBoxes] | None = None
input_points: list[ListOfPoints] | None = []
input_labels: list[ListOfPointLabels] | None = []
for point_list in point_lists:
input_points.append([[p[0], p[1]] for p in point_list])
input_labels.append([p[2] for p in point_list])
for i in inputs:
box: list[float] | None = None
points: list[list[float]] | None = None
labels: list[int] | None = None
else:
raise ValueError("Either bounding_boxes or points and labels must be provided.")
if i.bounding_box is not None:
box: list[float] | None = [
i.bounding_box.x_min,
i.bounding_box.y_min,
i.bounding_box.x_max,
i.bounding_box.y_max,
]
if i.points is not None:
points = []
labels = []
for point in i.points:
points.append([point.x, point.y])
labels.append(point.label.value)
if box is not None:
input_boxes.append(box)
if points is not None:
input_points.append(points)
if labels is not None:
input_labels.append(labels)
batched_input_boxes = [input_boxes] if input_boxes else None
batched_input_points = input_points if input_points else None
batched_input_labels = input_labels if input_labels else None
processed_inputs = self._sam_processor(
inputs = self._sam_processor(
images=image,
input_boxes=batched_input_boxes,
input_points=batched_input_points,
input_labels=batched_input_labels,
input_boxes=input_boxes,
input_points=input_points,
input_labels=input_labels,
return_tensors="pt",
).to(self._sam_model.device)
outputs = self._sam_model(**processed_inputs)
outputs = self._sam_model(**inputs)
masks = self._sam_processor.post_process_masks(
masks=outputs.pred_masks,
original_sizes=processed_inputs.original_sizes,
reshaped_input_sizes=processed_inputs.reshaped_input_sizes,
original_sizes=inputs.original_sizes,
reshaped_input_sizes=inputs.reshaped_input_sizes,
)
# There should be only one batch.

View File

@@ -1,49 +0,0 @@
from enum import Enum
from pydantic import BaseModel, model_validator
from pydantic.fields import Field
class BoundingBox(BaseModel):
x_min: int = Field(..., description="The minimum x-coordinate of the bounding box (inclusive).")
x_max: int = Field(..., description="The maximum x-coordinate of the bounding box (exclusive).")
y_min: int = Field(..., description="The minimum y-coordinate of the bounding box (inclusive).")
y_max: int = Field(..., description="The maximum y-coordinate of the bounding box (exclusive).")
@model_validator(mode="after")
def check_coords(self):
if self.x_min > self.x_max:
raise ValueError(f"x_min ({self.x_min}) is greater than x_max ({self.x_max}).")
if self.y_min > self.y_max:
raise ValueError(f"y_min ({self.y_min}) is greater than y_max ({self.y_max}).")
return self
def tuple(self) -> tuple[int, int, int, int]:
"""
Returns the bounding box as a tuple suitable for use with PIL's `Image.crop()` method.
This method returns a tuple of the form (left, upper, right, lower) == (x_min, y_min, x_max, y_max).
"""
return (self.x_min, self.y_min, self.x_max, self.y_max)
class SAMPointLabel(Enum):
negative = -1
neutral = 0
positive = 1
class SAMPoint(BaseModel):
x: int = Field(..., description="The x-coordinate of the point")
y: int = Field(..., description="The y-coordinate of the point")
label: SAMPointLabel = Field(..., description="The label of the point")
class SAMInput(BaseModel):
bounding_box: BoundingBox | None = Field(None, description="The bounding box to use for segmentation")
points: list[SAMPoint] | None = Field(None, description="The points to use for segmentation")
@model_validator(mode="after")
def check_input(self):
if not self.bounding_box and not self.points:
raise ValueError("Either bounding_box or points must be provided")
return self

View File

@@ -207,24 +207,15 @@ class IPAdapterPlusXL(IPAdapterPlus):
def load_ip_adapter_tensors(ip_adapter_ckpt_path: pathlib.Path, device: str) -> IPAdapterStateDict:
state_dict: IPAdapterStateDict = {
"ip_adapter": {},
"image_proj": {},
"adapter_modules": {}, # added for noobai-mark-ipa
"image_proj_model": {}, # added for noobai-mark-ipa
}
state_dict: IPAdapterStateDict = {"ip_adapter": {}, "image_proj": {}}
if ip_adapter_ckpt_path.suffix == ".safetensors":
model = safetensors.torch.load_file(ip_adapter_ckpt_path, device=device)
for key in model.keys():
if key.startswith("ip_adapter."):
state_dict["ip_adapter"][key.replace("ip_adapter.", "")] = model[key]
elif key.startswith("image_proj_model."):
state_dict["image_proj_model"][key.replace("image_proj_model.", "")] = model[key]
elif key.startswith("image_proj."):
if key.startswith("image_proj."):
state_dict["image_proj"][key.replace("image_proj.", "")] = model[key]
elif key.startswith("adapter_modules."):
state_dict["adapter_modules"][key.replace("adapter_modules.", "")] = model[key]
elif key.startswith("ip_adapter."):
state_dict["ip_adapter"][key.replace("ip_adapter.", "")] = model[key]
else:
raise RuntimeError(f"Encountered unexpected IP Adapter state dict key: '{key}'.")
else:

View File

@@ -90,11 +90,6 @@ class MainModelDefaultSettings(BaseModel):
model_config = ConfigDict(extra="forbid")
class LoraModelDefaultSettings(BaseModel):
weight: float | None = Field(default=None, ge=-1, le=2, description="Default weight for this model")
model_config = ConfigDict(extra="forbid")
class ControlAdapterDefaultSettings(BaseModel):
# This could be narrowed to controlnet processor nodes, but they change. Leaving this a string is safer.
preprocessor: str | None
@@ -292,9 +287,6 @@ class LoRAConfigBase(ABC, BaseModel):
type: Literal[ModelType.LoRA] = ModelType.LoRA
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
default_settings: Optional[LoraModelDefaultSettings] = Field(
description="Default settings for this model", default=None
)
@classmethod
def flux_lora_format(cls, mod: ModelOnDisk):
@@ -500,15 +492,6 @@ class MainConfigBase(ABC, BaseModel):
variant: AnyVariant = ModelVariantType.Normal
class VideoConfigBase(ABC, BaseModel):
type: Literal[ModelType.Video] = ModelType.Video
trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
default_settings: Optional[MainModelDefaultSettings] = Field(
description="Default settings for this model", default=None
)
variant: AnyVariant = ModelVariantType.Normal
class MainCheckpointConfig(CheckpointConfigBase, MainConfigBase, LegacyProbeMixin, ModelConfigBase):
"""Model config for main checkpoint models."""
@@ -666,21 +649,6 @@ class ApiModelConfig(MainConfigBase, ModelConfigBase):
raise NotImplementedError("API models are not parsed from disk.")
class VideoApiModelConfig(VideoConfigBase, ModelConfigBase):
"""Model config for API-based video models."""
format: Literal[ModelFormat.Api] = ModelFormat.Api
@classmethod
def matches(cls, mod: ModelOnDisk) -> bool:
# API models are not stored on disk, so we can't match them.
return False
@classmethod
def parse(cls, mod: ModelOnDisk) -> dict[str, Any]:
raise NotImplementedError("API models are not parsed from disk.")
def get_model_discriminator_value(v: Any) -> str:
"""
Computes the discriminator value for a model config.
@@ -750,13 +718,12 @@ AnyModelConfig = Annotated[
Annotated[FluxReduxConfig, FluxReduxConfig.get_tag()],
Annotated[LlavaOnevisionConfig, LlavaOnevisionConfig.get_tag()],
Annotated[ApiModelConfig, ApiModelConfig.get_tag()],
Annotated[VideoApiModelConfig, VideoApiModelConfig.get_tag()],
],
Discriminator(get_model_discriminator_value),
]
AnyModelConfigValidator = TypeAdapter(AnyModelConfig)
AnyDefaultSettings: TypeAlias = Union[MainModelDefaultSettings, LoraModelDefaultSettings, ControlAdapterDefaultSettings]
AnyDefaultSettings: TypeAlias = Union[MainModelDefaultSettings, ControlAdapterDefaultSettings]
class ModelConfigFactory:

View File

@@ -23,7 +23,6 @@ from invokeai.backend.model_manager.config import (
AnyModelConfig,
ControlAdapterDefaultSettings,
InvalidModelConfigException,
LoraModelDefaultSettings,
MainModelDefaultSettings,
ModelConfigFactory,
SubmodelDefinition,
@@ -218,8 +217,6 @@ class ModelProbe(object):
if not fields["default_settings"]:
if fields["type"] in {ModelType.ControlNet, ModelType.T2IAdapter, ModelType.ControlLoRa}:
fields["default_settings"] = get_default_settings_control_adapters(fields["name"])
if fields["type"] in {ModelType.LoRA}:
fields["default_settings"] = get_default_settings_lora()
elif fields["type"] is ModelType.Main:
fields["default_settings"] = get_default_settings_main(fields["base"])
@@ -546,10 +543,6 @@ def get_default_settings_control_adapters(model_name: str) -> Optional[ControlAd
return None
def get_default_settings_lora() -> LoraModelDefaultSettings:
return LoraModelDefaultSettings()
def get_default_settings_main(model_base: BaseModelType) -> Optional[MainModelDefaultSettings]:
if model_base is BaseModelType.StableDiffusion1 or model_base is BaseModelType.StableDiffusion2:
return MainModelDefaultSettings(width=512, height=512)

View File

@@ -28,11 +28,8 @@ class BaseModelType(str, Enum):
CogView4 = "cogview4"
Imagen3 = "imagen3"
Imagen4 = "imagen4"
Gemini2_5 = "gemini-2.5"
ChatGPT4o = "chatgpt-4o"
FluxKontext = "flux-kontext"
Veo3 = "veo3"
Runway = "runway"
class ModelType(str, Enum):
@@ -54,7 +51,6 @@ class ModelType(str, Enum):
SigLIP = "siglip"
FluxRedux = "flux_redux"
LlavaOnevision = "llava_onevision"
Video = "video"
class SubModelType(str, Enum):

View File

@@ -18,25 +18,16 @@ def is_state_dict_likely_in_flux_diffusers_format(state_dict: Dict[str, torch.Te
# First, check that all keys end in "lora_A.weight" or "lora_B.weight" (i.e. are in PEFT format).
all_keys_in_peft_format = all(k.endswith(("lora_A.weight", "lora_B.weight")) for k in state_dict.keys())
# Check if keys use transformer prefix
transformer_prefix_keys = [
# Next, check that this is likely a FLUX model by spot-checking a few keys.
expected_keys = [
"transformer.single_transformer_blocks.0.attn.to_q.lora_A.weight",
"transformer.single_transformer_blocks.0.attn.to_q.lora_B.weight",
"transformer.transformer_blocks.0.attn.add_q_proj.lora_A.weight",
"transformer.transformer_blocks.0.attn.add_q_proj.lora_B.weight",
]
transformer_keys_present = all(k in state_dict for k in transformer_prefix_keys)
all_expected_keys_present = all(k in state_dict for k in expected_keys)
# Check if keys use base_model.model prefix
base_model_prefix_keys = [
"base_model.model.single_transformer_blocks.0.attn.to_q.lora_A.weight",
"base_model.model.single_transformer_blocks.0.attn.to_q.lora_B.weight",
"base_model.model.transformer_blocks.0.attn.add_q_proj.lora_A.weight",
"base_model.model.transformer_blocks.0.attn.add_q_proj.lora_B.weight",
]
base_model_keys_present = all(k in state_dict for k in base_model_prefix_keys)
return all_keys_in_peft_format and (transformer_keys_present or base_model_keys_present)
return all_keys_in_peft_format and all_expected_keys_present
def lora_model_from_flux_diffusers_state_dict(
@@ -58,16 +49,8 @@ def lora_layers_from_flux_diffusers_grouped_state_dict(
https://github.com/huggingface/diffusers/blob/55ac421f7bb12fd00ccbef727be4dc2f3f920abb/scripts/convert_flux_to_diffusers.py
"""
# Determine which prefix is used and remove it from all keys.
# Check if any key starts with "base_model.model." prefix
has_base_model_prefix = any(k.startswith("base_model.model.") for k in grouped_state_dict.keys())
if has_base_model_prefix:
# Remove the "base_model.model." prefix from all keys.
grouped_state_dict = {k.replace("base_model.model.", ""): v for k, v in grouped_state_dict.items()}
else:
# Remove the "transformer." prefix from all keys.
grouped_state_dict = {k.replace("transformer.", ""): v for k, v in grouped_state_dict.items()}
# Remove the "transformer." prefix from all keys.
grouped_state_dict = {k.replace("transformer.", ""): v for k, v in grouped_state_dict.items()}
# Constants for FLUX.1
num_double_layers = 19

View File

@@ -1,117 +0,0 @@
from typing import Literal
import torch
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.backend.flux.modules.autoencoder import AutoEncoder
def estimate_vae_working_memory_sd15_sdxl(
operation: Literal["encode", "decode"],
image_tensor: torch.Tensor,
vae: AutoencoderKL | AutoencoderTiny,
tile_size: int | None,
fp32: bool,
) -> int:
"""Estimate the working memory required to encode or decode the given tensor."""
# It was found experimentally that the peak working memory scales linearly with the number of pixels and the
# element size (precision). This estimate is accurate for both SD1 and SDXL.
element_size = 4 if fp32 else 2
# This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414
# Encoding uses ~45% the working memory as decoding.
scaling_constant = 2200 if operation == "decode" else 1100
latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1
if tile_size is not None:
if tile_size == 0:
tile_size = vae.tile_sample_min_size
assert isinstance(tile_size, int)
h = tile_size
w = tile_size
working_memory = h * w * element_size * scaling_constant
# We add 25% to the working memory estimate when tiling is enabled to account for factors like tile overlap
# and number of tiles. We could make this more precise in the future, but this should be good enough for
# most use cases.
working_memory = working_memory * 1.25
else:
h = latent_scale_factor_for_operation * image_tensor.shape[-2]
w = latent_scale_factor_for_operation * image_tensor.shape[-1]
working_memory = h * w * element_size * scaling_constant
if fp32:
# If we are running in FP32, then we should account for the likely increase in model size (~250MB).
working_memory += 250 * 2**20
print(f"estimate_vae_working_memory_sd15_sdxl: {int(working_memory)}")
return int(working_memory)
def estimate_vae_working_memory_cogview4(
operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoencoderKL
) -> int:
"""Estimate the working memory required by the invocation in bytes."""
latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1
h = latent_scale_factor_for_operation * image_tensor.shape[-2]
w = latent_scale_factor_for_operation * image_tensor.shape[-1]
element_size = next(vae.parameters()).element_size()
# This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414
# Encoding uses ~45% the working memory as decoding.
scaling_constant = 2200 if operation == "decode" else 1100
working_memory = h * w * element_size * scaling_constant
print(f"estimate_vae_working_memory_cogview4: {int(working_memory)}")
return int(working_memory)
def estimate_vae_working_memory_flux(
operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoEncoder
) -> int:
"""Estimate the working memory required by the invocation in bytes."""
latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1
out_h = latent_scale_factor_for_operation * image_tensor.shape[-2]
out_w = latent_scale_factor_for_operation * image_tensor.shape[-1]
element_size = next(vae.parameters()).element_size()
# This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414
# Encoding uses ~45% the working memory as decoding.
scaling_constant = 2200 if operation == "decode" else 1100
working_memory = out_h * out_w * element_size * scaling_constant
print(f"estimate_vae_working_memory_flux: {int(working_memory)}")
return int(working_memory)
def estimate_vae_working_memory_sd3(
operation: Literal["encode", "decode"], image_tensor: torch.Tensor, vae: AutoencoderKL
) -> int:
"""Estimate the working memory required by the invocation in bytes."""
# Encode operations use approximately 50% of the memory required for decode operations
latent_scale_factor_for_operation = LATENT_SCALE_FACTOR if operation == "decode" else 1
h = latent_scale_factor_for_operation * image_tensor.shape[-2]
w = latent_scale_factor_for_operation * image_tensor.shape[-1]
element_size = next(vae.parameters()).element_size()
# This constant is determined experimentally and takes into consideration both allocated and reserved memory. See #8414
# Encoding uses ~45% the working memory as decoding.
scaling_constant = 2200 if operation == "decode" else 1100
working_memory = h * w * element_size * scaling_constant
print(f"estimate_vae_working_memory_sd3: {int(working_memory)}")
return int(working_memory)

View File

@@ -1,39 +0,0 @@
# Bash commands
All commands should be run from `<REPO_ROOT>/invokeai/frontend/web/`.
- `pnpm lint:prettier`: check formatting
- `pnpm lint:eslint`: check for linting issues
- `pnpm lint:knip`: check for unused dependencies
- `pnpm lint:dpdm`: check for dependency cycles
- `pnpm lint:tsc`: check for TypeScript issues
- `pnpm lint`: run all checks
- `pnpm fix`: automatically fix issues where possible
- `pnpm test:no-watch`: run the test suite
# Writing Tests
This repo uses `vitest` for unit tests.
Tests should be colocated with the code they test, and should use the `.test.ts` suffix.
Tests do not need to be written for code that is trivial or has no logic (e.g. simple type definitions, re-exports, etc.). We currently do not do UI tests.
# Agents
- Use @agent-javascript-pro and @agent-typescript-pro for JavaScript and TypeScript code generation and assistance.
- Use @frontend-developer for general frontend development tasks.
## Workflow
Split up tasks into smaller subtasks and handle them one at a time using an agent. Ensure each subtask is completed before moving on to the next.
Each agent should maintain a work log in a markdown file.
When an agent completes a task, it should:
1. Summarize the changes made.
2. List any files that were added, modified, or deleted.
3. Commit the changes with a descriptive commit message.
DO NOT PUSH ANY CHANGES TO THE REMOTE REPOSITORY.

View File

@@ -45,7 +45,7 @@
"@dagrejs/dagre": "^1.1.5",
"@dagrejs/graphlib": "^2.2.4",
"@fontsource-variable/inter": "^5.2.6",
"@invoke-ai/ui-library": "^0.0.47",
"@invoke-ai/ui-library": "^0.0.46",
"@nanostores/react": "^1.0.0",
"@observ33r/object-equals": "^1.1.5",
"@reduxjs/toolkit": "2.8.2",
@@ -56,7 +56,7 @@
"chakra-react-select": "^4.9.2",
"cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
"dockview": "^4.7.1",
"dockview": "^4.4.1",
"es-toolkit": "^1.39.7",
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.1.0",
@@ -69,7 +69,6 @@
"linkify-react": "^4.3.1",
"linkifyjs": "^4.3.1",
"lru-cache": "^11.1.0",
"media-chrome": "^4.13.0",
"mtwist": "^1.0.2",
"nanoid": "^5.1.5",
"nanostores": "^1.0.1",
@@ -88,7 +87,6 @@
"react-hotkeys-hook": "4.5.0",
"react-i18next": "^15.5.3",
"react-icons": "^5.5.0",
"react-player": "^3.3.1",
"react-redux": "9.2.0",
"react-resizable-panels": "^3.0.3",
"react-textarea-autosize": "^8.5.9",

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,8 @@
"gallery": {
"galleryImageSize": "حجم الصورة",
"gallerySettings": "إعدادات المعرض",
"autoSwitchNewImages": "التبديل التلقائي إلى الصور الجديدة"
"autoSwitchNewImages": "التبديل التلقائي إلى الصور الجديدة",
"noImagesInGallery": "لا توجد صور في المعرض"
},
"modelManager": {
"modelManager": "مدير النموذج",
@@ -61,10 +62,12 @@
"infillMethod": "طريقة التعبئة",
"tileSize": "حجم البلاطة",
"copyImage": "نسخ الصورة",
"downloadImage": "تحميل الصورة",
"usePrompt": "استخدم المحث",
"useSeed": "استخدام البذور",
"useAll": "استخدام الكل",
"info": "معلومات"
"info": "معلومات",
"showOptionsPanel": "إظهار لوحة الخيارات"
},
"settings": {
"models": "موديلات",

View File

@@ -24,6 +24,7 @@
"ipAdapter": "IP Adapter",
"auto": "Auto",
"controlNet": "ControlNet",
"imageFailedToLoad": "Kann Bild nicht laden",
"modelManager": "Model Manager",
"learnMore": "Mehr erfahren",
"loading": "Lade",
@@ -51,6 +52,7 @@
"somethingWentWrong": "Etwas ist schief gelaufen",
"copyError": "$t(gallery.copy) Fehler",
"input": "Eingabe",
"notInstalled": "Nicht $t(common.installed)",
"alpha": "Alpha",
"red": "Rot",
"green": "Grün",
@@ -60,8 +62,11 @@
"direction": "Richtung",
"save": "Speichern",
"created": "Erstellt",
"prevPage": "Vorherige Seite",
"nextPage": "Nächste Seite",
"unknownError": "Unbekannter Fehler",
"aboutDesc": "Verwenden Sie Invoke für die Arbeit? Siehe hier:",
"localSystem": "Lokales System",
"orderBy": "Ordnen nach",
"saveAs": "Speichern als",
"updated": "Aktualisiert",
@@ -72,6 +77,7 @@
"selected": "Ausgewählt",
"beta": "Beta",
"editor": "Editor",
"goTo": "Gehe zu",
"positivePrompt": "Positiv-Prompt",
"negativePrompt": "Negativ-Prompt",
"tab": "Tabulator",
@@ -100,6 +106,7 @@
"values": "Werte",
"min": "Min",
"max": "Max",
"resetToDefaults": "Auf Standard zurücksetzen",
"seed": "Seed",
"row": "Reihe",
"column": "Spalte",
@@ -128,12 +135,14 @@
"galleryImageSize": "Bildgröße",
"gallerySettings": "Galerie-Einstellungen",
"autoSwitchNewImages": "Auto-Wechsel zu neuen Bildern",
"noImagesInGallery": "Keine Bilder in der Galerie",
"loading": "Lade",
"deleteImage_one": "Lösche Bild",
"deleteImage_other": "Lösche {{count}} Bilder",
"copy": "Kopieren",
"download": "Runterladen",
"featuresWillReset": "Wenn Sie dieses Bild löschen, werden diese Funktionen sofort zurückgesetzt.",
"unableToLoad": "Galerie kann nicht geladen werden",
"downloadSelection": "Auswahl herunterladen",
"currentlyInUse": "Dieses Bild wird derzeit in den folgenden Funktionen verwendet:",
"deleteImagePermanent": "Gelöschte Bilder können nicht wiederhergestellt werden.",
@@ -173,12 +182,16 @@
"gallery": "Galerie",
"sortDirection": "Sortierreihenfolge",
"sideBySide": "Nebeneinander",
"openViewer": "Viewer öffnen",
"viewerImage": "Viewer-Bild",
"exitCompare": "Vergleichen beenden",
"closeViewer": "Viewer schließen",
"selectAnImageToCompare": "Wählen Sie ein Bild zum Vergleichen",
"stretchToFit": "Strecken bis es passt",
"displayBoardSearch": "Board durchsuchen",
"displaySearch": "Bild suchen",
"go": "Los",
"jump": "Springen",
"assetsTab": "Dateien, die Sie zur Verwendung in Ihren Projekten hochgeladen haben.",
"imagesTab": "Bilder, die Sie in Invoke erstellt und gespeichert haben.",
"boardsSettings": "Ordnereinstellungen",
@@ -197,6 +210,10 @@
"title": "Bbox Werkzeug",
"desc": "Bbox Werkzeug auswählen."
},
"setFillToWhite": {
"title": "Farbe auf Weiß einstellen",
"desc": "Setzt die aktuelle Werkzeugfarbe auf weiß."
},
"title": "Leinwand",
"selectBrushTool": {
"title": "Pinselwerkzeug",
@@ -561,6 +578,7 @@
"urlOrLocalPath": "URL oder lokaler Pfad",
"install": "Installieren",
"textualInversions": "Textuelle Inversionen",
"ipAdapters": "IP-Adapter",
"modelImageUpdated": "Modellbild aktualisiert",
"path": "Pfad",
"pathToConfig": "Pfad zur Konfiguration",
@@ -583,6 +601,7 @@
"repoVariant": "Repo Variante",
"learnMoreAboutSupportedModels": "Erfahren Sie mehr über die Modelle, die wir unterstützen",
"clipEmbed": "CLIP einbetten",
"starterModelsInModelManager": "Modelle für Ihren Start finden Sie im Modell-Manager",
"noModelsInstalledDesc1": "Installiere Modelle mit dem",
"modelImageUpdateFailed": "Modellbild-Update fehlgeschlagen",
"prune": "Bereinigen",
@@ -642,9 +661,11 @@
"scaledHeight": "Skaliert H",
"infillMethod": "Infill-Methode",
"tileSize": "Kachelgröße",
"downloadImage": "Bild herunterladen",
"usePrompt": "Prompt verwenden",
"useSeed": "Seed verwenden",
"useAll": "Alle verwenden",
"showOptionsPanel": "Optionsleiste zeigen",
"copyImage": "Bild kopieren",
"denoisingStrength": "Stärke der Entrauschung",
"symmetry": "Symmetrie",
@@ -660,6 +681,10 @@
"remixImage": "Remix des Bilds erstellen",
"imageActions": "Weitere Bildaktionen",
"invoke": {
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Skalierte Bbox-Breite ist {{width}}",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Skalierte Bbox-Höhe ist {{height}}",
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Bbox-Breite ist {{width}}",
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), Bbox-Höhe ist {{height}}",
"noNodesInGraph": "Keine Knoten im Graphen",
"canvasIsTransforming": "Leinwand ist beschäftigt (wird transformiert)",
"canvasIsRasterizing": "Leinwand ist beschäftigt (wird gerastert)",
@@ -725,6 +750,7 @@
"parametersNotSet": "Parameter nicht zurückgerufen",
"addedToBoard": "Dem Board hinzugefügt",
"loadedWithWarnings": "Workflow mit Warnungen geladen",
"imageSaved": "Bild gespeichert",
"linkCopied": "Link kopiert",
"problemCopyingLayer": "Ebene kann nicht kopiert werden",
"problemSavingLayer": "Ebene kann nicht gespeichert werden",
@@ -735,6 +761,8 @@
"prunedQueue": "Warteschlange bereinigt",
"modelAddedSimple": "Modell zur Warteschlange hinzugefügt",
"parametersSet": "Parameter zurückgerufen",
"imageNotLoadedDesc": "Bild konnte nicht gefunden werden",
"setControlImage": "Als Kontrollbild festlegen",
"sentToUpscale": "An Vergrößerung gesendet",
"parameterNotSetDescWithMessage": "{{parameter}} kann nicht zurückgerufen werden: {{message}}",
"unableToLoadImageMetadata": "Bildmetadaten können nicht geladen werden",
@@ -747,6 +775,7 @@
"parameterSet": "Parameter zurückgerufen",
"importFailed": "Import fehlgeschlagen",
"importSuccessful": "Import erfolgreich",
"setNodeField": "Als Knotenfeld festlegen",
"somethingWentWrong": "Etwas ist schief gelaufen",
"workflowLoaded": "Arbeitsablauf geladen",
"workflowDeleted": "Arbeitsablauf gelöscht",
@@ -754,12 +783,16 @@
"layerCopiedToClipboard": "Ebene in die Zwischenablage kopiert",
"sentToCanvas": "An Leinwand gesendet",
"problemDeletingWorkflow": "Problem beim Löschen des Arbeitsablaufs",
"uploadFailedInvalidUploadDesc_withCount_one": "Darf maximal 1 PNG-, JPEG- oder WEBP-Bild sein.",
"uploadFailedInvalidUploadDesc_withCount_other": "Dürfen maximal {{count}} PNG-, JPEG- oder WEBP-Bild sein.",
"problemRetrievingWorkflow": "Problem beim Abrufen des Arbeitsablaufs",
"uploadFailedInvalidUploadDesc": "Müssen PNG-, JPEG- oder WEBP-Bilder sein.",
"pasteSuccess": "Eingefügt in {{destination}}",
"pasteFailed": "Einfügen fehlgeschlagen",
"unableToCopy": "Kopieren nicht möglich",
"unableToCopyDesc_theseSteps": "diese Schritte",
"noRasterLayers": "Keine Rasterebenen gefunden",
"noActiveRasterLayers": "Keine aktiven Rasterebenen",
"noVisibleRasterLayers": "Keine sichtbaren Rasterebenen"
},
"accessibility": {
@@ -812,13 +845,16 @@
"archiveBoard": "Ordner archivieren",
"archived": "Archiviert",
"noBoards": "Kein {{boardType}} Ordner",
"hideBoards": "Ordner verstecken",
"viewBoards": "Ordner ansehen",
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt.",
"assetsWithCount_one": "{{count}} in der Sammlung",
"assetsWithCount_other": "{{count}} in der Sammlung",
"deletedBoardsCannotbeRestored": "Gelöschte Ordner können nicht wiederhergestellt werden. Die Auswahl von \"Nur Ordner löschen\" verschiebt Bilder in einen unkategorisierten Zustand.",
"updateBoardError": "Fehler beim Aktualisieren des Ordners",
"uncategorizedImages": "Nicht kategorisierte Bilder",
"deleteAllUncategorizedImages": "Alle nicht kategorisierten Bilder löschen"
"deleteAllUncategorizedImages": "Alle nicht kategorisierten Bilder löschen",
"deletedImagesCannotBeRestored": "Gelöschte Bilder können nicht wiederhergestellt werden."
},
"queue": {
"status": "Status",
@@ -873,6 +909,7 @@
"batchQueuedDesc_other": "{{count}} Einträge an {{direction}} der Wartschlange hinzugefügt",
"openQueue": "Warteschlange öffnen",
"batchFailedToQueue": "Fehler beim Einreihen in die Stapelverarbeitung",
"batchFieldValues": "Stapelverarbeitungswerte",
"batchQueued": "Stapelverarbeitung eingereiht",
"graphQueued": "Graph eingereiht",
"graphFailedToQueue": "Fehler beim Einreihen des Graphen",
@@ -919,6 +956,8 @@
"allPrompts": "Alle Prompts",
"imageDimensions": "Bilder Auslösungen",
"parameterSet": "Parameter {{parameter}} setzen",
"recallParameter": "{{label}} Abrufen",
"parsingFailed": "Parsing Fehlgeschlagen",
"canvasV2Metadata": "Leinwand",
"guidance": "Führung",
"seamlessXAxis": "Nahtlose X Achse",
@@ -1201,7 +1240,9 @@
"collectionFieldType": "{{name}} (Sammlung)",
"connectionWouldCreateCycle": "Verbindung würde einen Kreislauf/cycle schaffen",
"inputMayOnlyHaveOneConnection": "Eingang darf nur eine Verbindung haben",
"hideLegendNodes": "Feldtyp-Legende ausblenden",
"integer": "Ganze Zahl",
"addLinearView": "Zur linearen Ansicht hinzufügen",
"currentImageDescription": "Zeigt das aktuelle Bild im Node-Editor an",
"ipAdapter": "IP-Adapter",
"hideMinimapnodes": "Miniatur-Kartenansicht ausblenden",
@@ -1210,6 +1251,7 @@
"reloadNodeTemplates": "Knoten-Vorlagen neu laden",
"newWorkflow": "Neuer Arbeitsablauf / Workflow",
"newWorkflowDesc": "Einen neuen Arbeitsablauf erstellen?",
"noFieldsLinearview": "Keine Felder zur linearen Ansicht hinzugefügt",
"clearWorkflow": "Workflow löschen",
"clearWorkflowDesc": "Diesen Arbeitsablauf löschen und neu starten?",
"noConnectionInProgress": "Es besteht keine Verbindung",
@@ -1217,6 +1259,7 @@
"nodeVersion": "Knoten Version",
"node": "Knoten",
"nodeSearch": "Knoten suchen",
"removeLinearView": "Entfernen aus Linear View",
"nodeOutputs": "Knoten-Ausgänge",
"nodeTemplate": "Knoten-Vorlage",
"nodeType": "Knotentyp",
@@ -1227,6 +1270,7 @@
"clearWorkflowDesc2": "Ihr aktueller Arbeitsablauf hat ungespeicherte Änderungen.",
"scheduler": "Planer",
"showMinimapnodes": "MiniMap anzeigen",
"showLegendNodes": "Feldtyp-Legende anzeigen",
"executionStateCompleted": "Erledigt",
"downloadWorkflow": "Workflow JSON herunterladen",
"executionStateInProgress": "In Bearbeitung",
@@ -1236,6 +1280,7 @@
"fieldTypesMustMatch": "Feldtypen müssen übereinstimmen",
"fitViewportNodes": "An Ansichtsgröße anpassen",
"loadingNodes": "Lade Nodes...",
"mismatchedVersion": "Ungültiger Knoten: Knoten {{node}} vom Typ {{type}} hat keine passende Version (Update versuchen?)",
"fullyContainNodesHelp": "Nodes müssen vollständig innerhalb der Auswahlbox sein, um ausgewählt werden zu können",
"noWorkflow": "Kein Workflow",
"executionStateError": "Fehler",
@@ -1247,7 +1292,9 @@
"sourceNodeDoesNotExist": "Ungültiger Rand: Quell- / Ausgabe-Knoten {{node}} existiert nicht",
"updateAllNodes": "Update Knoten",
"allNodesUpdated": "Alle Knoten aktualisiert",
"unknownTemplate": "Unbekannte Vorlage",
"updateApp": "Update App",
"unknownInput": "Unbekannte Eingabe: {{name}}",
"unknownNodeType": "Unbekannter Knotentyp",
"float": "Kommazahlen",
"enum": "Aufzählung",
@@ -1263,6 +1310,7 @@
"workflowAuthor": "Autor",
"graph": "Graph",
"workflowDescription": "Kurze Beschreibung",
"versionUnknown": " Version unbekannt",
"workflow": "Arbeitsablauf",
"noGraph": "Kein Graph",
"version": "Version",
@@ -1280,6 +1328,7 @@
"unknownErrorValidatingWorkflow": "Unbekannter Fehler beim Validieren des Arbeitsablaufes",
"inputFieldTypeParseError": "Typ des Eingabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})",
"workflowSettings": "Arbeitsablauf Editor Einstellungen",
"unableToLoadWorkflow": "Arbeitsablauf kann nicht geladen werden",
"viewMode": "In linearen Ansicht verwenden",
"unableToValidateWorkflow": "Arbeitsablauf kann nicht validiert werden",
"outputFieldTypeParseError": "Typ des Ausgabefelds {{node}}.{{field}} kann nicht analysiert werden ({{message}})",
@@ -1295,6 +1344,7 @@
"arithmeticSequence": "Arithmetische Folge",
"noBatchGroup": "keine Gruppe",
"generatorNoValues": "leer",
"generatorLoading": "wird geladen",
"generatorLoadFromFile": "Aus Datei laden",
"showEdgeLabels": "Kantenbeschriftungen anzeigen",
"downloadWorkflowError": "Fehler beim Herunterladen des Arbeitsablaufs",
@@ -1302,11 +1352,14 @@
"description": "Beschreibung",
"loadWorkflowDesc": "Arbeitsablauf laden?",
"loadWorkflowDesc2": "Ihr aktueller Arbeitsablauf enthält nicht gespeicherte Änderungen.",
"loadingTemplates": "Lade {{name}}",
"missingSourceOrTargetHandle": "Fehlender Quell- oder Zielgriff",
"missingSourceOrTargetNode": "Fehlender Quell- oder Zielknoten",
"showEdgeLabelsHelp": "Beschriftungen an Kanten anzeigen, um die verknüpften Knoten zu kennzeichnen"
},
"hrf": {
"enableHrf": "Korrektur für hohe Auflösungen",
"upscaleMethod": "Vergrößerungsmethode",
"metadata": {
"strength": "Auflösungs-Fix Stärke",
"enabled": "Auflösungs-Fix aktiviert",
@@ -1317,9 +1370,11 @@
"models": {
"noMatchingModels": "Keine passenden Modelle",
"loading": "lade",
"noMatchingLoRAs": "Keine passenden LoRAs",
"noModelsAvailable": "Keine Modelle verfügbar",
"selectModel": "Wählen ein Modell aus",
"noRefinerModelsInstalled": "Keine SDXL Refiner-Modelle installiert",
"noLoRAsInstalled": "Keine LoRAs installiert",
"addLora": "LoRA hinzufügen",
"defaultVAE": "Standard VAE",
"lora": "LoRA",
@@ -1349,23 +1404,31 @@
"workflows": "Arbeitsabläufe",
"workflowName": "Arbeitsablauf-Name",
"saveWorkflowAs": "Arbeitsablauf speichern als",
"searchWorkflows": "Suche Arbeitsabläufe",
"newWorkflowCreated": "Neuer Arbeitsablauf erstellt",
"problemSavingWorkflow": "Problem beim Speichern des Arbeitsablaufs",
"problemLoading": "Problem beim Laden von Arbeitsabläufen",
"downloadWorkflow": "Speichern als",
"savingWorkflow": "Speichere Arbeitsablauf...",
"saveWorkflow": "Arbeitsablauf speichern",
"noWorkflows": "Keine Arbeitsabläufe",
"workflowLibrary": "Bibliothek",
"unnamedWorkflow": "Unbenannter Arbeitsablauf",
"noDescription": "Keine Beschreibung",
"clearWorkflowSearchFilter": "Suchfilter zurücksetzen",
"workflowEditorMenu": "Arbeitsablauf-Editor Menü",
"deleteWorkflow": "Arbeitsablauf löschen",
"workflowSaved": "Arbeitsablauf gespeichert",
"uploadWorkflow": "Aus Datei laden",
"openWorkflow": "Arbeitsablauf öffnen",
"saveWorkflowToProject": "Arbeitsablauf in Projekt speichern",
"workflowCleared": "Arbeitsablauf gelöscht",
"loading": "Lade Arbeitsabläufe",
"name": "Name",
"ascending": "Aufsteigend",
"defaultWorkflows": "Standard Arbeitsabläufe",
"userWorkflows": "Benutzer Arbeitsabläufe",
"projectWorkflows": "Projekt Arbeitsabläufe",
"opened": "Geöffnet",
"loadWorkflow": "Arbeitsablauf $t(common.load)",
"updated": "Aktualisiert",
@@ -1379,10 +1442,12 @@
"copyShareLink": "Teilen-Link kopieren",
"download": "Herunterladen",
"convertGraph": "Graph konvertieren",
"filterByTags": "Nach Tags filtern",
"yourWorkflows": "Ihre Arbeitsabläufe",
"recentlyOpened": "Kürzlich geöffnet"
},
"sdxl": {
"concatPromptStyle": "Verknüpfen von Prompt & Stil",
"scheduler": "Planer",
"steps": "Schritte"
},
@@ -1394,11 +1459,13 @@
"addPromptTrigger": "Prompt-Trigger hinzufügen",
"compatibleEmbeddings": "Kompatible Einbettungen",
"replace": "Ersetzen",
"insert": "Einfügen",
"discard": "Verwerfen",
"generateFromImage": "Prompt aus Bild generieren",
"expandCurrentPrompt": "Aktuelle Prompt erweitern",
"uploadImageForPromptGeneration": "Bild zur Prompt-Generierung hochladen",
"expandingPrompt": "Prompt wird erweitert..."
"expandingPrompt": "Prompt wird erweitert...",
"resultTitle": "Prompt-Erweiterung abgeschlossen"
},
"ui": {
"tabs": {
@@ -1537,6 +1604,8 @@
"opacity": "Opazität",
"removeBookmark": "Lesezeichen entfernen",
"rasterLayer": "Rasterebene",
"rasterLayers_withCount_visible": "Rasterebenen ({{count}})",
"controlLayers_withCount_visible": "Kontroll-Ebenen ({{count}})",
"deleteSelected": "Ausgewählte löschen",
"newRegionalReferenceImageError": "Problem beim Erstellen eines regionalen Referenzbilds",
"newControlLayerOk": "Kontroll-Ebene erstellt",
@@ -1544,8 +1613,10 @@
"newRasterLayerOk": "Rasterebene erstellt",
"moveToFront": "Nach vorne bringen",
"copyToClipboard": "In die Zwischenablage kopieren",
"controlLayers_withCount_hidden": "Kontroll-Ebenen ({{count}} ausgeblendet)",
"clearCaches": "Cache leeren",
"controlLayer": "Kontroll-Ebene",
"rasterLayers_withCount_hidden": "Rasterebenen ({{count}} ausgeblendet)",
"transparency": "Transparenz",
"canvas": "Leinwand",
"global": "Global",
@@ -1568,7 +1639,9 @@
"weight": "Gewichtung",
"addReferenceImage": "$t(controlLayers.referenceImage) hinzufügen",
"addInpaintMask": "$t(controlLayers.inpaintMask) hinzufügen",
"addGlobalReferenceImage": "$t(controlLayers.globalReferenceImage) hinzufügen",
"regionalGuidance": "Regionale Führung",
"globalReferenceImages_withCount_visible": "Globale Referenzbilder ({{count}})",
"addPositivePrompt": "$t(controlLayers.prompt) hinzufügen",
"locked": "Gesperrt",
"showHUD": "HUD anzeigen",
@@ -1576,12 +1649,16 @@
"addRasterLayer": "$t(controlLayers.rasterLayer) hinzufügen",
"addRegionalGuidance": "$t(controlLayers.regionalGuidance) hinzufügen",
"addControlLayer": "$t(controlLayers.controlLayer) hinzufügen",
"newCanvasSession": "Neue Leinwand-Sitzung",
"replaceLayer": "Ebene ersetzen",
"newGallerySession": "Neue Galerie-Sitzung",
"unlocked": "Entsperrt",
"showProgressOnCanvas": "Fortschritt auf Leinwand anzeigen",
"controlMode": {
"balanced": "Ausgewogen"
},
"globalReferenceImages_withCount_hidden": "Globale Referenzbilder ({{count}} ausgeblendet)",
"sendToGallery": "An Galerie senden",
"stagingArea": {
"accept": "Annehmen",
"next": "Nächste",
@@ -1589,6 +1666,8 @@
"discard": "Verwerfen",
"previous": "Vorherige"
},
"regionalGuidance_withCount_visible": "Regionale Führung ({{count}})",
"regionalGuidance_withCount_hidden": "Regionale Führung ({{count}} ausgeblendet)",
"settings": {
"snapToGrid": {
"on": "Ein",
@@ -1598,6 +1677,8 @@
},
"layer_one": "Ebene",
"layer_other": "Ebenen",
"layer_withCount_one": "Ebene ({{count}})",
"layer_withCount_other": "Ebenen ({{count}})",
"fill": {
"fillStyle": "Füllstil",
"diagonal": "Diagonal",

View File

@@ -38,13 +38,10 @@
"deletedImagesCannotBeRestored": "Deleted images cannot be restored.",
"hideBoards": "Hide Boards",
"loading": "Loading...",
"locateInGalery": "Locate in Gallery",
"menuItemAutoAdd": "Auto-add to this Board",
"move": "Move",
"movingImagesToBoard_one": "Moving {{count}} image to board:",
"movingImagesToBoard_other": "Moving {{count}} images to board:",
"movingVideosToBoard_one": "Moving {{count}} video to board:",
"movingVideosToBoard_other": "Moving {{count}} videos to board:",
"myBoard": "My Board",
"noBoards": "No {{boardType}} Boards",
"noMatching": "No matching Boards",
@@ -61,8 +58,6 @@
"imagesWithCount_other": "{{count}} images",
"assetsWithCount_one": "{{count}} asset",
"assetsWithCount_other": "{{count}} assets",
"videosWithCount_one": "{{count}} video",
"videosWithCount_other": "{{count}} videos",
"updateBoardError": "Error updating board"
},
"accordions": {
@@ -104,7 +99,6 @@
"copy": "Copy",
"copyError": "$t(gallery.copy) Error",
"clipboard": "Clipboard",
"crop": "Crop",
"on": "On",
"off": "Off",
"or": "or",
@@ -243,10 +237,7 @@
"resultSubtitle": "Choose how to handle the expanded prompt:",
"replace": "Replace",
"insert": "Insert",
"discard": "Discard",
"noPromptHistory": "No prompt history recorded.",
"noMatchingPrompts": "No matching prompts in history.",
"toSwitchBetweenPrompts": "to switch between prompts."
"discard": "Discard"
},
"queue": {
"queue": "Queue",
@@ -302,7 +293,7 @@
"completedIn": "Completed in",
"batch": "Batch",
"origin": "Origin",
"destination": "Dest",
"destination": "Destination",
"upscaling": "Upscaling",
"canvas": "Canvas",
"generation": "Generation",
@@ -328,13 +319,7 @@
"iterations_other": "Iterations",
"generations_one": "Generation",
"generations_other": "Generations",
"batchSize": "Batch Size",
"createdAt": "Created At",
"completedAt": "Completed At",
"sortColumn": "Sort Column",
"sortBy": "Sort by {{column}}",
"sortOrderAscending": "Ascending",
"sortOrderDescending": "Descending"
"batchSize": "Batch Size"
},
"invocationCache": {
"invocationCache": "Invocation Cache",
@@ -375,9 +360,6 @@
"deleteImage_one": "Delete Image",
"deleteImage_other": "Delete {{count}} Images",
"deleteImagePermanent": "Deleted images cannot be restored.",
"deleteVideo_one": "Delete Video",
"deleteVideo_other": "Delete {{count}} Videos",
"deleteVideoPermanent": "Deleted videos cannot be restored.",
"displayBoardSearch": "Board Search",
"displaySearch": "Image Search",
"download": "Download",
@@ -397,10 +379,9 @@
"sortDirection": "Sort Direction",
"showStarredImagesFirst": "Show Starred Images First",
"noImageSelected": "No Image Selected",
"noVideoSelected": "No Video Selected",
"noImagesInGallery": "No Images to Display",
"starImage": "Star",
"unstarImage": "Unstar",
"starImage": "Star Image",
"unstarImage": "Unstar Image",
"unableToLoad": "Unable to load Gallery",
"deleteSelection": "Delete Selection",
"downloadSelection": "Download Selection",
@@ -429,9 +410,7 @@
"openViewer": "Open Viewer",
"closeViewer": "Close Viewer",
"move": "Move",
"useForPromptGeneration": "Use for Prompt Generation",
"videos": "Videos",
"videosTab": "Videos you've created and saved within Invoke."
"useForPromptGeneration": "Use for Prompt Generation"
},
"hotkeys": {
"hotkeys": "Hotkeys",
@@ -476,22 +455,10 @@
"title": "Select the Queue Tab",
"desc": "Selects the Queue tab."
},
"selectVideoTab": {
"title": "Select the Video Tab",
"desc": "Selects the Video tab."
},
"focusPrompt": {
"title": "Focus Prompt",
"desc": "Move cursor focus to the positive prompt."
},
"promptHistoryPrev": {
"title": "Previous Prompt in History",
"desc": "When the prompt is focused, move to the previous (older) prompt in your history."
},
"promptHistoryNext": {
"title": "Next Prompt in History",
"desc": "When the prompt is focused, move to the next (newer) prompt in your history."
},
"toggleLeftPanel": {
"title": "Toggle Left Panel",
"desc": "Show or hide the left panel."
@@ -514,9 +481,6 @@
"key": "1"
}
},
"video": {
"title": "Video"
},
"canvas": {
"title": "Canvas",
"selectBrushTool": {
@@ -607,13 +571,9 @@
"title": "Prev Layer",
"desc": "Select the previous layer in the list."
},
"setFillColorsToDefault": {
"title": "Set Colors to Default",
"desc": "Set the current tool colors to default."
},
"toggleFillColor": {
"title": "Toggle Fill Color",
"desc": "Toggle the current tool fill color."
"setFillToWhite": {
"title": "Set Color to White",
"desc": "Set the current tool color to white."
},
"filterSelected": {
"title": "Filter",
@@ -661,10 +621,6 @@
"title": "Fit Bbox To Masks",
"desc": "Automatically adjust the generation bounding box to fit visible inpaint masks"
},
"toggleBbox": {
"title": "Toggle Bbox Visibility",
"desc": "Hide or show the generation bounding box"
},
"applySegmentAnything": {
"title": "Apply Segment Anything",
"desc": "Apply the current Segment Anything mask.",
@@ -810,9 +766,6 @@
}
}
},
"lora": {
"weight": "Weight"
},
"metadata": {
"allPrompts": "All Prompts",
"cfgScale": "CFG scale",
@@ -823,13 +776,11 @@
"guidance": "Guidance",
"height": "Height",
"imageDetails": "Image Details",
"videoDetails": "Video Details",
"imageDimensions": "Image Dimensions",
"metadata": "Metadata",
"model": "Model",
"negativePrompt": "Negative Prompt",
"noImageDetails": "No image details found",
"noVideoDetails": "No video details found",
"noMetaData": "No metadata found",
"noRecallParameters": "No parameters to recall found",
"parameterSet": "Parameter {{parameter}} set",
@@ -847,11 +798,7 @@
"vae": "VAE",
"width": "Width",
"workflow": "Workflow",
"canvasV2Metadata": "Canvas Layers",
"videoModel": "Model",
"videoDuration": "Duration",
"videoAspectRatio": "Aspect Ratio",
"videoResolution": "Resolution"
"canvasV2Metadata": "Canvas Layers"
},
"modelManager": {
"active": "active",
@@ -926,9 +873,6 @@
"install": "Install",
"installAll": "Install All",
"installRepo": "Install Repo",
"installBundle": "Install Bundle",
"installBundleMsg1": "Are you sure you want to install the {{bundleName}} bundle?",
"installBundleMsg2": "This bundle will install the following {{count}} models:",
"ipAdapters": "IP Adapters",
"learnMoreAboutSupportedModels": "Learn more about the models we support",
"load": "Load",
@@ -1237,7 +1181,6 @@
},
"parameters": {
"aspect": "Aspect",
"duration": "Duration",
"lockAspectRatio": "Lock Aspect Ratio",
"swapDimensions": "Swap Dimensions",
"setToOptimalSize": "Optimize size for model",
@@ -1262,15 +1205,9 @@
"height": "Height",
"imageFit": "Fit Initial Image To Output Size",
"images": "Images",
"images_withCount_one": "Image",
"images_withCount_other": "Images",
"videos_withCount_one": "Video",
"videos_withCount_other": "Videos",
"infillMethod": "Infill Method",
"infillColorValue": "Fill Color",
"info": "Info",
"startingFrameImage": "Start Frame",
"startingFrameImageAspectRatioWarning": "Image aspect ratio does not match the video aspect ratio ({{videoAspectRatio}}). This could lead to unexpected cropping during video generation.",
"invoke": {
"addingImagesTo": "Adding images to",
"modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your account settings to upgrade.",
@@ -1294,7 +1231,6 @@
"batchNodeCollectionSizeMismatchNoGroupId": "Batch group collection size mismatch",
"batchNodeCollectionSizeMismatch": "Collection size mismatch on Batch {{batchGroupId}}",
"noModelSelected": "No model selected",
"noStartingFrameImage": "No starting frame image",
"noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation",
"noFLUXVAEModelSelected": "No VAE model selected for FLUX generation",
"noCLIPEmbedModelSelected": "No CLIP Embed model selected for FLUX generation",
@@ -1307,7 +1243,6 @@
"modelIncompatibleScaledBboxWidth": "Scaled bbox width is {{width}} but {{model}} requires multiple of {{multiple}}",
"modelIncompatibleScaledBboxHeight": "Scaled bbox height is {{height}} but {{model}} requires multiple of {{multiple}}",
"fluxModelMultipleControlLoRAs": "Can only use 1 Control LoRA at a time",
"incompatibleLoRAs": "Incompatible LoRA(s) added",
"canvasIsFiltering": "Canvas is busy (filtering)",
"canvasIsTransforming": "Canvas is busy (transforming)",
"canvasIsRasterizing": "Canvas is busy (rasterizing)",
@@ -1317,8 +1252,7 @@
"noNodesInGraph": "No nodes in graph",
"systemDisconnected": "System disconnected",
"promptExpansionPending": "Prompt expansion in progress",
"promptExpansionResultPending": "Please accept or discard your prompt expansion result",
"videoIsDisabled": "Video generation is not enabled for {{accountType}} accounts."
"promptExpansionResultPending": "Please accept or discard your prompt expansion result"
},
"maskBlur": "Mask Blur",
"negativePromptPlaceholder": "Negative Prompt",
@@ -1336,11 +1270,9 @@
"seamlessXAxis": "Seamless X Axis",
"seamlessYAxis": "Seamless Y Axis",
"seed": "Seed",
"videoActions": "Video Actions",
"imageActions": "Image Actions",
"sendToCanvas": "Send To Canvas",
"sendToUpscale": "Send To Upscale",
"sendToVideo": "Send To Video",
"showOptionsPanel": "Show Side Panel (O or T)",
"shuffle": "Shuffle Seed",
"steps": "Steps",
@@ -1352,7 +1284,6 @@
"postProcessing": "Post-Processing (Shift + U)",
"processImage": "Process Image",
"upscaling": "Upscaling",
"video": "Video",
"useAll": "Use All",
"useSize": "Use Size",
"useCpuNoise": "Use CPU Noise",
@@ -1364,7 +1295,6 @@
"gaussianBlur": "Gaussian Blur",
"boxBlur": "Box Blur",
"staged": "Staged",
"resolution": "Resolution",
"modelDisabledForTrial": "Generating with {{modelName}} is not available on trial accounts. Visit your <LinkComponent>account settings</LinkComponent> to upgrade."
},
"dynamicPrompts": {
@@ -1442,8 +1372,8 @@
"addedToBoard": "Added to board {{name}}'s assets",
"addedToUncategorized": "Added to board $t(boards.uncategorized)'s assets",
"baseModelChanged": "Base Model Changed",
"baseModelChangedCleared_one": "Updated, cleared or disabled {{count}} incompatible submodel",
"baseModelChangedCleared_other": "Updated, cleared or disabled {{count}} incompatible submodels",
"baseModelChangedCleared_one": "Cleared or disabled {{count}} incompatible submodel",
"baseModelChangedCleared_other": "Cleared or disabled {{count}} incompatible submodels",
"canceled": "Processing Canceled",
"connected": "Connected to Server",
"imageCopied": "Image Copied",
@@ -2011,11 +1941,8 @@
"zoomToNode": "Zoom to Node",
"nodeFieldTooltip": "To add a node field, click the small plus sign button on the field in the Workflow Editor, or drag the field by its name into the form.",
"addToForm": "Add to Form",
"removeFromForm": "Remove from Form",
"label": "Label",
"showDescription": "Show Description",
"showShuffle": "Show Shuffle",
"shuffle": "Shuffle",
"component": "Component",
"numberInput": "Number Input",
"singleLine": "Single Line",
@@ -2096,24 +2023,6 @@
"pullBboxIntoLayerError": "Problem Pulling BBox Into Layer",
"pullBboxIntoReferenceImageOk": "Bbox Pulled Into ReferenceImage",
"pullBboxIntoReferenceImageError": "Problem Pulling BBox Into ReferenceImage",
"addAdjustments": "Add Adjustments",
"removeAdjustments": "Remove Adjustments",
"adjustments": {
"simple": "Simple",
"curves": "Curves",
"heading": "Adjustments",
"expand": "Expand adjustments",
"collapse": "Collapse adjustments",
"brightness": "Brightness",
"contrast": "Contrast",
"saturation": "Saturation",
"temperature": "Temperature",
"tint": "Tint",
"sharpness": "Sharpness",
"finish": "Finish",
"reset": "Reset",
"master": "Master"
},
"regionIsEmpty": "Selected region is empty",
"mergeVisible": "Merge Visible",
"mergeDown": "Merge Down",
@@ -2312,8 +2221,6 @@
},
"fill": {
"fillColor": "Fill Color",
"bgFillColor": "Background Color",
"fgFillColor": "Foreground Color",
"fillStyle": "Fill Style",
"solid": "Solid",
"grid": "Grid",
@@ -2485,21 +2392,12 @@
"saveAs": "Save As",
"cancel": "Cancel",
"process": "Process",
"desc": "Select a single target object. After selection is complete, click <Bold>Apply</Bold> to discard everything outside the selected area, or save the selection as a new layer.",
"visualModeDesc": "Visual mode uses box and point inputs to select an object.",
"visualMode1": "Click and drag to draw a box around the object you want to select. You may get better results by drawing the box a bit larger or smaller than the object.",
"visualMode2": "Click to add a green <Bold>include</Bold> point, or shift-click to add a red <Bold>exclude</Bold> point to tell the model what to include or exclude.",
"visualMode3": "Points can be used to refine a box selection or used independently.",
"promptModeDesc": "Prompt mode uses text input to select an object.",
"promptMode1": "Type a brief description of the object you want to select.",
"promptMode2": "Use simple language, avoiding complex descriptions or multiple objects.",
"help1": "Select a single target object. Add <Bold>Include</Bold> and <Bold>Exclude</Bold> points to indicate which parts of the layer are part of the target object.",
"help2": "Start with one <Bold>Include</Bold> point within the target object. Add more points to refine the selection. Fewer points typically produce better results.",
"help3": "Invert the selection to select everything except the target object.",
"clickToAdd": "Click on the layer to add a point",
"dragToMove": "Drag a point to move it",
"clickToRemove": "Click on a point to remove it",
"model": "Model",
"segmentAnything1": "Segment Anything 1",
"segmentAnything2": "Segment Anything 2",
"prompt": "Selection Prompt"
"clickToRemove": "Click on a point to remove it"
},
"settings": {
"snapToGrid": {
@@ -2661,7 +2559,7 @@
"panels": {
"launchpad": "Launchpad",
"workflowEditor": "Workflow Editor",
"imageViewer": "Viewer",
"imageViewer": "Image Viewer",
"canvas": "Canvas",
"video": "Video"
},
@@ -2670,15 +2568,6 @@
"upscalingTitle": "Upscale and add detail.",
"canvasTitle": "Edit and refine on Canvas.",
"generateTitle": "Generate images from text prompts.",
"videoTitle": "Generate videos from text prompts.",
"video": {
"startingFrameCalloutTitle": "Add a Starting Frame",
"startingFrameCalloutDesc": "Add an image to control the first frame of your video."
},
"addStartingFrame": {
"title": "Add a Starting Frame",
"description": "Add an image to control the first frame of your video."
},
"modelGuideText": "Want to learn what prompts work best for each model?",
"modelGuideLink": "Check out our Model Guide.",
"createNewWorkflowFromScratch": "Create a new Workflow from scratch",
@@ -2753,10 +2642,6 @@
}
}
},
"video": {
"noVideoSelected": "No video selected",
"selectFromGallery": "Select a video from the gallery to play"
},
"system": {
"enableLogging": "Enable Logging",
"logLevel": {
@@ -2794,9 +2679,8 @@
"whatsNew": {
"whatsNewInInvoke": "What's New in Invoke",
"items": [
"Select Object v2: Improved object selection with point and box inputs or text prompts.",
"Raster Layer Adjustments: Easily adjust layer brightness, contrast, saturation, curves and more.",
"Prompt History: Review and quickly recall your last 100 prompts."
"Studio state is saved to the server, allowing you to continue your work on any device.",
"Support for multiple reference images for FLUX Kontext (local model only)."
],
"readReleaseNotes": "Read Release Notes",
"watchRecentReleaseVideos": "Watch Recent Release Videos",

View File

@@ -47,8 +47,11 @@
"editor": "Editor",
"orderBy": "Ordenar por",
"file": "Archivo",
"goTo": "Ir a",
"imageFailedToLoad": "No se puede cargar la imagen",
"saveAs": "Guardar Como",
"somethingWentWrong": "Algo salió mal",
"nextPage": "Página Siguiente",
"selected": "Seleccionado",
"tab": "Tabulador",
"positivePrompt": "Prompt Positivo",
@@ -58,6 +61,7 @@
"unknown": "Desconocido",
"input": "Entrada",
"template": "Plantilla",
"prevPage": "Página Anterior",
"red": "Rojo",
"alpha": "Transparencia",
"outputs": "Resultados",
@@ -90,6 +94,8 @@
"edit": "Editar",
"safetensors": "Safetensors",
"toResolve": "Para resolver",
"localSystem": "Sistema local",
"notInstalled": "No $t(common.installed)",
"outpaint": "outpaint",
"simple": "Sencillo",
"close": "Cerrar"
@@ -98,6 +104,7 @@
"galleryImageSize": "Tamaño de la imagen",
"gallerySettings": "Ajustes de la galería",
"autoSwitchNewImages": "Auto seleccionar Imágenes nuevas",
"noImagesInGallery": "No hay imágenes para mostrar",
"deleteImage_one": "Eliminar Imagen",
"deleteImage_many": "Eliminar {{count}} Imágenes",
"deleteImage_other": "Eliminar {{count}} Imágenes",
@@ -111,7 +118,9 @@
"selectForCompare": "Seleccionar para comparar",
"alwaysShowImageSizeBadge": "Mostrar siempre las dimensiones de la imagen",
"currentlyInUse": "Esta imagen se utiliza actualmente con las siguientes funciones:",
"unableToLoad": "No se puede cargar la galería",
"selectAllOnPage": "Seleccionar todo en la página",
"selectAnImageToCompare": "Seleccione una imagen para comparar",
"bulkDownloadFailed": "Error en la descarga",
"compareHelp2": "Presione <Kbd> M </Kbd> para recorrer los modos de comparación.",
"move": "Mover",
@@ -136,6 +145,7 @@
"exitBoardSearch": "Finalizar búsqueda",
"exitSearch": "Salir de la búsqueda de imágenes",
"featuresWillReset": "Si elimina esta imagen, dichas funciones se restablecerán inmediatamente.",
"jump": "Omitir",
"loading": "Cargando",
"newestFirst": "La más nueva primero",
"unstarImage": "Dejar de ser favorita",
@@ -153,7 +163,9 @@
"boardsSettings": "Ajustes de los tableros",
"imagesSettings": "Configuración de imágenes de la galería",
"compareHelp3": "Presione <Kbd> C </Kbd> para intercambiar las imágenes comparadas.",
"showArchivedBoards": "Mostrar paneles archivados"
"showArchivedBoards": "Mostrar paneles archivados",
"closeViewer": "Cerrar visor",
"openViewer": "Abrir visor"
},
"modelManager": {
"modelManager": "Gestor de Modelos",
@@ -227,10 +239,12 @@
"scaledHeight": "Alto escalado",
"infillMethod": "Método de relleno",
"tileSize": "Tamaño del mosaico",
"downloadImage": "Descargar imagen",
"usePrompt": "Usar Entrada",
"useSeed": "Usar Semilla",
"useAll": "Usar Todo",
"info": "Información",
"showOptionsPanel": "Mostrar panel lateral (O o T)",
"symmetry": "Simetría",
"copyImage": "Copiar la imagen",
"general": "General",
@@ -309,6 +323,8 @@
"hideMinimapnodes": "Ocultar el minimapa",
"fitViewportNodes": "Ajustar la vista",
"zoomOutNodes": "Alejar",
"hideLegendNodes": "Ocultar la leyenda del tipo de campo",
"showLegendNodes": "Mostrar la leyenda del tipo de campo",
"showMinimapnodes": "Mostrar el minimapa",
"reloadNodeTemplates": "Recargar las plantillas de nodos",
"loadWorkflow": "Cargar el flujo de trabajo",
@@ -345,6 +361,7 @@
"assetsWithCount_one": "{{count}} activo",
"assetsWithCount_many": "{{count}} activos",
"assetsWithCount_other": "{{count}} activos",
"hideBoards": "Ocultar paneles",
"addPrivateBoard": "Agregar un panel privado",
"addSharedBoard": "Añadir panel compartido",
"boards": "Paneles",
@@ -355,6 +372,7 @@
"noBoards": "No hay paneles {{boardType}}",
"shared": "Paneles compartidos",
"deletedPrivateBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al elegir \"Eliminar solo el panel\", las imágenes se colocan en un estado privado y sin categoría para el creador de la imagen.",
"viewBoards": "Ver paneles",
"private": "Paneles privados",
"updateBoardError": "No se pudo actualizar el panel"
},
@@ -443,6 +461,7 @@
"other": "Otro",
"queueFront": "Añadir al principio de la cola",
"gallery": "Galería",
"batchFieldValues": "Valores de procesamiento por lotes",
"session": "Sesión",
"notReady": "La cola aún no está lista",
"graphQueued": "Gráfico en cola",
@@ -475,11 +494,15 @@
"layer_one": "Capa",
"layer_many": "Capas",
"layer_other": "Capas",
"layer_withCount_one": "({{count}}) capa",
"layer_withCount_many": "({{count}}) capas",
"layer_withCount_other": "({{count}}) capas",
"copyToClipboard": "Copiar al portapapeles"
},
"whatsNew": {
"readReleaseNotes": "Leer las notas de la versión",
"watchRecentReleaseVideos": "Ver videos de versiones recientes",
"watchUiUpdatesOverview": "Descripción general de las actualizaciones de la interfaz de usuario de Watch",
"whatsNewInInvoke": "Novedades en Invoke",
"items": [
"<StrongComponent>SD 3.5</StrongComponent>: compatibilidad con SD 3.5 Medium y Large."
@@ -504,11 +527,13 @@
},
"hrf": {
"hrf": "Solución de alta resolución",
"enableHrf": "Activar corrección de alta resolución",
"metadata": {
"enabled": "Corrección de alta resolución activada",
"strength": "Forzar la corrección de alta resolución",
"method": "Método de corrección de alta resolución"
}
},
"upscaleMethod": "Método de expansión"
},
"prompt": {
"addPromptTrigger": "Añadir activador de los avisos",
@@ -566,6 +591,10 @@
"title": "Ajustar capas al lienzo",
"desc": "Escala y posiciona la vista para que se ajuste a todas las capas visibles."
},
"setFillToWhite": {
"title": "Establecer color en blanco",
"desc": "Establece el color actual de la herramienta en blanco."
},
"resetSelected": {
"title": "Restablecer capa",
"desc": "Restablecer la capa seleccionada. Solo se aplica a Máscara de retoque y Guía regional."
@@ -839,8 +868,10 @@
"seed": "Semilla",
"strength": "Forzar imagen a imagen",
"recallParameters": "Parámetros de recuperación",
"recallParameter": "Recuperar {{label}}",
"steps": "Pasos",
"noRecallParameters": "Sin parámetros para recuperar"
"noRecallParameters": "Sin parámetros para recuperar",
"parsingFailed": "Error al analizar"
},
"system": {
"logLevel": {

View File

@@ -28,6 +28,7 @@
"gallery": {
"galleryImageSize": "Kuvan koko",
"gallerySettings": "Gallerian asetukset",
"autoSwitchNewImages": "Vaihda uusiin kuviin automaattisesti"
"autoSwitchNewImages": "Vaihda uusiin kuviin automaattisesti",
"noImagesInGallery": "Ei kuvia galleriassa"
}
}

View File

@@ -27,15 +27,21 @@
"error": "Erreur",
"installed": "Installé",
"format": "format",
"goTo": "Aller à",
"input": "Entrée",
"linear": "Linéaire",
"localSystem": "Système local",
"learnMore": "En savoir plus",
"modelManager": "Gestionnaire de modèle",
"notInstalled": "Non $t(common.installed)",
"openInNewTab": "Ouvrir dans un nouvel onglet",
"somethingWentWrong": "Une erreur s'est produite",
"created": "Créé",
"tab": "Onglet",
"folder": "Dossier",
"imageFailedToLoad": "Impossible de charger l'Image",
"prevPage": "Page précédente",
"nextPage": "Page suivante",
"selected": "Sélectionné",
"save": "Enregistrer",
"updated": "Mis à jour",
@@ -105,6 +111,7 @@
"min": "Min",
"max": "Max",
"values": "Valeurs",
"resetToDefaults": "Réinitialiser par défaut",
"seed": "Graine",
"combinatorial": "Combinatoire"
},
@@ -112,9 +119,11 @@
"galleryImageSize": "Taille de l'image",
"gallerySettings": "Paramètres de la galerie",
"autoSwitchNewImages": "Basculer automatiquement vers de nouvelles images",
"noImagesInGallery": "Aucune image à afficher",
"bulkDownloadRequestedDesc": "Votre demande de téléchargement est en cours de traitement. Cela peut prendre quelques instants.",
"deleteSelection": "Supprimer la sélection",
"selectAllOnPage": "Séléctionner toute la page",
"unableToLoad": "Impossible de charger la Galerie",
"featuresWillReset": "Si vous supprimez cette image, ces fonctionnalités vont être réinitialisés.",
"loading": "Chargement",
"sortDirection": "Direction de tri",
@@ -140,6 +149,7 @@
"openInViewer": "Ouvrir dans le Visualiseur",
"showArchivedBoards": "Montrer les Planches archivées",
"selectForCompare": "Séléctionner pour comparaison",
"selectAnImageToCompare": "Séléctionner une Image à comparer",
"exitCompare": "Sortir de la comparaison",
"compareHelp2": "Appuyez sur <Kbd>M</Kbd> pour faire défiler les modes de comparaison.",
"swapImages": "Échanger les Images",
@@ -147,7 +157,10 @@
"compareHelp1": "Maintenir <Kbd>Alt</Kbd> lors du clic d'une image dans la galerie ou en utilisant les flèches du clavier pour changer l'Image à comparer.",
"compareHelp3": "Appuyer sur <Kbd>C</Kbd> pour échanger les images à comparer.",
"image": "image",
"openViewer": "Ouvrir le Visualisateur",
"closeViewer": "Fermer le Visualisateur",
"currentlyInUse": "Cette image est actuellement utilisée dans ces fonctionalités :",
"jump": "Sauter",
"starImage": "Marquer l'Image",
"download": "Téléchargement",
"deleteImage_one": "Supprimer l'Image",
@@ -234,6 +247,7 @@
"metadata": "Métadonnées",
"scanFolder": "Scanner le dossier",
"inplaceInstallDesc": "Installez les modèles sans copier les fichiers. Lors de l'utilisation du modèle, il sera chargé depuis cet emplacement. Si cette option est désactivée, le(s) fichier(s) du modèle seront copiés dans le répertoire des modèles géré par Invoke lors de l'installation.",
"ipAdapters": "Adaptateurs IP",
"installQueue": "File d'attente d'installation",
"modelImageDeleteFailed": "Échec de la suppression de l'image du modèle",
"modelName": "Nom du modèle",
@@ -274,6 +288,7 @@
"scanFolderHelper": "Le dossier sera analysé de manière récursive à la recherche de modèles. Cela peut prendre quelques instants pour des dossiers très volumineux.",
"clipEmbed": "Intégration CLIP",
"spandrelImageToImage": "Image vers Image (Spandrel)",
"starterModelsInModelManager": "Les modèles de démarrage peuvent être trouvés dans le gestionnaire de modèles",
"t5Encoder": "Encodeur T5",
"learnMoreAboutSupportedModels": "En savoir plus sur les modèles que nous prenons en charge",
"includesNModels": "Contient {{n}} modèles et leurs dépendances",
@@ -331,10 +346,12 @@
"infillMethod": "Méthode de Remplissage",
"tileSize": "Taille des Tuiles",
"copyImage": "Copier Image",
"downloadImage": "Télécharger Image",
"usePrompt": "Utiliser la suggestion",
"useSeed": "Utiliser la graine",
"useAll": "Tout utiliser",
"info": "Info",
"showOptionsPanel": "Afficher le panneau latéral (O ou T)",
"invoke": {
"noPrompts": "Aucun prompts généré",
"missingInputForField": "entrée manquante",
@@ -345,16 +362,21 @@
"noModelSelected": "Aucun modèle sélectionné",
"noNodesInGraph": "Aucun nœud dans le graphique",
"systemDisconnected": "Système déconnecté",
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la hauteur de la bounding box est {{height}}",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la hauteur de la bounding box est {{height}}",
"noFLUXVAEModelSelected": "Aucun modèle VAE sélectionné pour la génération FLUX",
"canvasIsTransforming": "La Toile est occupée (en transformation)",
"canvasIsRasterizing": "La Toile est occupée (en rastérisation)",
"noCLIPEmbedModelSelected": "Aucun modèle CLIP Embed sélectionné pour la génération FLUX",
"canvasIsFiltering": "La Toile est occupée (en filtration)",
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la largeur de la bounding box est {{width}}",
"noT5EncoderModelSelected": "Aucun modèle T5 Encoder sélectionné pour la génération FLUX",
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), la largeur de la bounding box mise à l'échelle est {{width}}",
"canvasIsCompositing": "La Toile est occupée (en composition)",
"collectionTooFewItems": "trop peu d'éléments, minimum {{minItems}}",
"collectionTooManyItems": "trop d'éléments, maximum {{maxItems}}",
"canvasIsSelectingObject": "La toile est occupée (sélection d'objet)",
"emptyBatches": "lots vides",
"batchNodeNotConnected": "Noeud de lots non connecté : {{label}}",
"fluxModelMultipleControlLoRAs": "Vous ne pouvez utiliser qu'un seul Control LoRA à la fois",
"collectionNumberLTMin": "{{value}} < {{minimum}} (incl. min)",
@@ -446,7 +468,9 @@
"informationalPopoversDisabled": "Pop-ups d'information désactivés",
"informationalPopoversDisabledDesc": "Les pop-ups d'information ont été désactivés. Activez-les dans les paramètres.",
"confirmOnNewSession": "Confirmer lors d'une nouvelle session",
"modelDescriptionsDisabledDesc": "Les descriptions des modèles dans les menus déroulants ont été désactivées. Activez-les dans les paramètres.",
"enableModelDescriptions": "Activer les descriptions de modèle dans les menus déroulants",
"modelDescriptionsDisabled": "Descriptions de modèle dans les menus déroulants désactivés",
"showDetailedInvocationProgress": "Afficher les détails de progression"
},
"toast": {
@@ -462,14 +486,17 @@
"addedToBoard": "Ajouté aux ressources de la planche {{name}}",
"workflowLoaded": "Workflow chargé",
"connected": "Connecté au serveur",
"setNodeField": "Définir comme champ de nœud",
"imageUploadFailed": "Échec de l'importation de l'image",
"loadedWithWarnings": "Workflow chargé avec des avertissements",
"imageUploaded": "Image importée",
"modelAddedSimple": "Modèle ajouté à la file d'attente",
"setControlImage": "Définir comme image de contrôle",
"workflowDeleted": "Workflow supprimé",
"baseModelChangedCleared_one": "Effacé ou désactivé {{count}} sous-modèle incompatible",
"baseModelChangedCleared_many": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"baseModelChangedCleared_other": "Effacé ou désactivé {{count}} sous-modèles incompatibles",
"invalidUpload": "Importation invalide",
"problemDownloadingImage": "Impossible de télécharger l'image",
"problemRetrievingWorkflow": "Problème de récupération du Workflow",
"problemDeletingWorkflow": "Problème de suppression du Workflow",
@@ -483,10 +510,12 @@
"errorCopied": "Erreur copiée",
"parametersSet": "Paramètres rappelés",
"somethingWentWrong": "Quelque chose a échoué",
"imageSaved": "Image enregistrée",
"unableToLoadStylePreset": "Impossible de charger le préréglage de style",
"stylePresetLoaded": "Préréglage de style chargé",
"parameterNotSetDescWithMessage": "Impossible de rappeler {{parameter}} : {{message}}",
"importFailed": "Importation échouée",
"imageSavingFailed": "Échec de l'enregistrement de l'image",
"importSuccessful": "Importation réussie",
"outOfMemoryError": "Erreur de mémoire insuffisante",
"sessionRef": "Session : {{sessionId}}",
@@ -494,11 +523,16 @@
"parameterSetDesc": "Rappelé {{parameter}}",
"parameterNotSetDesc": "Impossible de rappeler {{parameter}}",
"layerCopiedToClipboard": "Calque copié dans le presse-papiers",
"layerSavedToAssets": "Calque enregistré dans les ressources",
"problemCopyingLayer": "Impossible de copier la couche",
"baseModelChanged": "Modèle de base changé",
"problemSavingLayer": "Impossible d'enregistrer la couche",
"imageNotLoadedDesc": "Image introuvable",
"linkCopied": "Lien copié",
"imagesWillBeAddedTo": "Les images Importées seront ajoutées au ressources de la Planche {{boardName}}.",
"uploadFailedInvalidUploadDesc_withCount_one": "Doit être au maximum une image PNG ou JPEG.",
"uploadFailedInvalidUploadDesc_withCount_many": "Doit être au maximum {{count}} images PNG ou JPEG.",
"uploadFailedInvalidUploadDesc_withCount_other": "Doit être au maximum {{count}} images PNG ou JPEG.",
"addedToUncategorized": "Ajouté aux ressources de la planche $t(boards.uncategorized)",
"pasteSuccess": "Collé à {{destination}}",
"pasteFailed": "Échec du collage",
@@ -546,6 +580,8 @@
"movingImagesToBoard_one": "Déplacer {{count}} image à cette planche :",
"movingImagesToBoard_many": "Déplacer {{count}} images à cette planche :",
"movingImagesToBoard_other": "Déplacer {{count}} image à cette planche :",
"viewBoards": "Voir les Planches",
"hideBoards": "Cacher les Planches",
"noBoards": "Pas de Planches {{boardType}}",
"shared": "Planches Partagées",
"searchBoard": "Chercher les Planches...",
@@ -645,6 +681,7 @@
"batchQueued": "Lot ajouté à la file d'attente",
"gallery": "Galerie",
"notReady": "Impossible d'ajouter à la file d'attente",
"batchFieldValues": "Valeurs Champ Lot",
"front": "début",
"graphQueued": "Graph ajouté à la file d'attente",
"other": "Autre",
@@ -675,11 +712,13 @@
"compatibleEmbeddings": "Embeddings Compatibles"
},
"hrf": {
"upscaleMethod": "Méthode d'Agrandissement",
"metadata": {
"enabled": "Correction Haute Résolution Activée",
"strength": "Force de la Correction Haute Résolution",
"method": "Méthode de la Correction Haute Résolution"
},
"enableHrf": "Activer la Correction Haute Résolution",
"hrf": "Correction Haute Résolution"
},
"invocationCache": {
@@ -862,6 +901,10 @@
"desc": "Définit le zoom de la toile à 400 %.",
"title": "Zoomer à 400 %"
},
"setFillToWhite": {
"title": "Définir la couleur sur blanc",
"desc": "Définir la couleur de l'outil actuel sur blanc."
},
"transformSelected": {
"title": "Transformer",
"desc": "Transforme la couche sélectionnée."
@@ -1447,7 +1490,8 @@
"showDynamicPrompts": "Afficher les Prompts dynamiques",
"dynamicPrompts": "Prompts Dynamiques",
"promptsPreview": "Prévisualisation des Prompts",
"loading": "Génération des Pompts Dynamiques..."
"loading": "Génération des Pompts Dynamiques...",
"promptsToGenerate": "Prompts à générer"
},
"metadata": {
"positivePrompt": "Prompt Positif",
@@ -1475,12 +1519,18 @@
"recallParameters": "Rappeler les paramètres",
"imageDimensions": "Dimensions de l'image",
"parameterSet": "Paramètre {{parameter}} défini",
"parsingFailed": "L'analyse a échoué",
"recallParameter": "Rappeler {{label}}",
"canvasV2Metadata": "Toile",
"guidance": "Guide",
"seamlessXAxis": "Axe X sans bords",
"seamlessYAxis": "Axe Y sans bords"
},
"sdxl": {
"freePromptStyle": "Écriture de Prompt manuelle",
"concatPromptStyle": "Lier Prompt & Style",
"negStylePrompt": "Style Prompt Négatif",
"posStylePrompt": "Style Prompt Positif",
"refinerStart": "Démarrer le Refiner",
"denoisingStrength": "Force de débruitage",
"steps": "Étapes",
@@ -1497,6 +1547,8 @@
"nodes": {
"showMinimapnodes": "Afficher la MiniCarte",
"fitViewportNodes": "Ajuster la Vue",
"hideLegendNodes": "Masquer la légende du type de champ",
"showLegendNodes": "Afficher la légende du type de champ",
"hideMinimapnodes": "Masquer MiniCarte",
"zoomOutNodes": "Dézoomer",
"zoomInNodes": "Zoomer",
@@ -1520,7 +1572,9 @@
"colorCodeEdges": "Code de couleur des connexions",
"colorCodeEdgesHelp": "Code couleur des connexions en fonction de leurs champs connectés",
"currentImage": "Image actuelle",
"noFieldsLinearview": "Aucun champ ajouté à la vue linéaire",
"float": "Flottant",
"mismatchedVersion": "Nœud invalide : le nœud {{node}} de type {{type}} a une version incompatible (essayez de mettre à jour?)",
"missingTemplate": "Nœud invalide : le nœud {{node}} de type {{type}} modèle manquant (non installé?)",
"noWorkflow": "Pas de Workflow",
"validateConnectionsHelp": "Prévenir la création de connexions invalides et l'invocation de graphes invalides",
@@ -1531,10 +1585,12 @@
"scheduler": "Planificateur",
"notes": "Notes",
"notesDescription": "Ajouter des notes sur votre workflow",
"unableToLoadWorkflow": "Impossible de charger le Workflow",
"addNode": "Ajouter un nœud",
"problemSettingTitle": "Problème lors de définition du Titre",
"connectionWouldCreateCycle": "La connexion créerait un cycle",
"currentImageDescription": "Affiche l'image actuelle dans l'éditeur de nœuds",
"versionUnknown": " Version inconnue",
"cannotConnectInputToInput": "Impossible de connecter l'entrée à l'entrée",
"addNodeToolTip": "Ajouter un nœud (Shift+A, Espace)",
"fullyContainNodesHelp": "Les nœuds doivent être entièrement à l'intérieur de la zone de sélection pour être sélectionnés",
@@ -1550,6 +1606,7 @@
"nodeSearch": "Rechercher des nœuds",
"collection": "Collection",
"noOutputRecorded": "Aucun résultat enregistré",
"removeLinearView": "Retirer de la vue linéaire",
"snapToGrid": "Aligner sur la grille",
"workflow": "Workflow",
"updateApp": "Mettre à jour l'application",
@@ -1558,6 +1615,7 @@
"noConnectionInProgress": "Aucune connexion en cours",
"nodeType": "Type de nœud",
"workflowContact": "Contact",
"unknownTemplate": "Modèle inconnu",
"unknownNode": "Nœud inconnu",
"workflowVersion": "Version",
"string": "Chaîne de caractères",
@@ -1571,6 +1629,7 @@
"cannotDuplicateConnection": "Impossible de créer des connexions en double",
"resetToDefaultValue": "Réinitialiser à la valeur par défaut",
"unknownNodeType": "Type de nœud inconnu",
"unknownInput": "Entrée inconnue : {{name}}",
"prototypeDesc": "Cette invocation est un prototype. Elle peut subir des modifications majeures lors des mises à jour de l'application et peut être supprimée à tout moment.",
"nodePack": "Paquet de nœuds",
"sourceNodeDoesNotExist": "Connexion invalide : le nœud source/de sortie {{node}} n'existe pas",
@@ -1585,6 +1644,7 @@
"clearWorkflow": "Effacer le Workflow",
"clearWorkflowDesc": "Effacer ce workflow et en commencer un nouveau?",
"unsupportedArrayItemType": "type d'élément de tableau non pris en charge \"{{type}}\"",
"addLinearView": "Ajouter à la vue linéaire",
"collectionOrScalarFieldType": "{{name}} (Unique ou Collection)",
"unableToExtractEnumOptions": "impossible d'extraire les options d'énumération",
"unsupportedAnyOfLength": "trop de membres dans l'union ({{count}})",
@@ -1592,6 +1652,7 @@
"viewMode": "Utiliser en vue linéaire",
"collectionFieldType": "{{name}} (Collection)",
"newWorkflow": "Nouveau Workflow",
"reorderLinearView": "Réorganiser la vue linéaire",
"outputFieldTypeParseError": "Impossible d'analyser le type du champ de sortie {{node}}.{{field}} ({{message}})",
"unsupportedMismatchedUnion": "type CollectionOrScalar non concordant avec les types de base {{firstType}} et {{secondType}}",
"unableToParseFieldType": "impossible d'analyser le type de champ",
@@ -1625,9 +1686,13 @@
"arithmeticSequence": "Séquence Arithmétique",
"uniformRandomDistribution": "Distribution Aléatoire Uniforme",
"noBatchGroup": "aucun groupe",
"generatorLoading": "chargement",
"generatorLoadFromFile": "Charger depuis un Fichier",
"dynamicPromptsRandom": "Prompts Dynamiques (Aléatoire)",
"integerRangeGenerator": "Générateur d'interval d'entiers",
"generateValues": "Générer Valeurs",
"linearDistribution": "Distribution Linéaire",
"floatRangeGenerator": "Générateur d'interval de nombres décimaux",
"generatorNRandomValues_one": "{{count}} valeur aléatoire",
"generatorNRandomValues_many": "{{count}} valeurs aléatoires",
"generatorNRandomValues_other": "{{count}} valeurs aléatoires",
@@ -1647,6 +1712,7 @@
"generatorImagesCategory": "Catégorie",
"generatorImagesFromBoard": "Images de la Planche",
"missingSourceOrTargetHandle": "Manque de gestionnaire source ou cible",
"loadingTemplates": "Chargement de {{name}}",
"loadWorkflowDesc2": "Votre workflow actuel contient des modifications non enregistrées.",
"generatorImages_one": "{{count}} image",
"generatorImages_many": "{{count}} images",
@@ -1657,8 +1723,10 @@
"noModelsAvailable": "Aucun modèle disponible",
"loading": "chargement",
"selectModel": "Sélectionner un modèle",
"noMatchingLoRAs": "Aucun LoRA correspondant",
"lora": "LoRA",
"noRefinerModelsInstalled": "Aucun modèle SDXL Refiner installé",
"noLoRAsInstalled": "Aucun LoRA installé",
"addLora": "Ajouter LoRA",
"defaultVAE": "VAE par défaut",
"concepts": "Concepts"
@@ -1666,8 +1734,11 @@
"workflows": {
"workflowLibrary": "Bibliothèque",
"loading": "Chargement des Workflows",
"searchWorkflows": "Chercher des Workflows",
"workflowCleared": "Workflow effacé",
"noDescription": "Aucune description",
"deleteWorkflow": "Supprimer le Workflow",
"openWorkflow": "Ouvrir le Workflow",
"uploadWorkflow": "Charger à partir d'un fichier",
"workflowName": "Nom du Workflow",
"unnamedWorkflow": "Workflow sans nom",
@@ -1680,6 +1751,8 @@
"problemSavingWorkflow": "Problème de sauvegarde du Workflow",
"workflowEditorMenu": "Menu de l'Éditeur de Workflow",
"newWorkflowCreated": "Nouveau Workflow créé",
"clearWorkflowSearchFilter": "Réinitialiser le filtre de recherche de Workflow",
"problemLoading": "Problème de chargement des Workflows",
"workflowSaved": "Workflow enregistré",
"noWorkflows": "Pas de Workflows",
"ascending": "Ascendant",
@@ -1692,6 +1765,9 @@
"opened": "Ouvert",
"name": "Nom",
"autoLayout": "Mise en page automatique",
"defaultWorkflows": "Workflows par défaut",
"userWorkflows": "Workflows de l'utilisateur",
"projectWorkflows": "Workflows du projet",
"copyShareLink": "Copier le lien de partage",
"chooseWorkflowFromLibrary": "Choisir le Workflow dans la Bibliothèque",
"edit": "Modifer",
@@ -1708,6 +1784,7 @@
"multiLine": "Multi Ligne",
"headingPlaceholder": "En-tête vide",
"emptyRootPlaceholderEditMode": "Faites glisser un élément de formulaire ou un champ de nœud ici pour commencer.",
"emptyRootPlaceholderViewMode": "Cliquez sur Modifier pour commencer à créer un formulaire pour ce workflow.",
"containerPlaceholder": "Conteneur Vide",
"row": "Ligne",
"column": "Colonne",
@@ -1721,8 +1798,10 @@
"builder": "Constructeur de Formulaire",
"resetAllNodeFields": "Réinitialiser tous les champs de nœud",
"deleteAllElements": "Supprimer tous les éléments de formulaire",
"workflowBuilderAlphaWarning": "Le constructeur de workflow est actuellement en version alpha. Il peut y avoir des changements majeurs avant la version stable.",
"showDescription": "Afficher la description"
}
},
"openLibrary": "Ouvrir la Bibliothèque"
},
"whatsNew": {
"whatsNewInInvoke": "Quoi de neuf dans Invoke",
@@ -1731,7 +1810,8 @@
"<StrongComponent>FLUX Guidage Régional (bêta)</StrongComponent> : Notre version bêta de FLUX Guidage Régional est en ligne pour le contrôle des prompt régionaux.",
"Autres améliorations : mise en file d'attente par lots plus rapide, meilleur redimensionnement, sélecteur de couleurs amélioré et nœuds de métadonnées."
],
"readReleaseNotes": "Notes de version"
"readReleaseNotes": "Notes de version",
"watchUiUpdatesOverview": "Aperçu des mises à jour de l'interface utilisateur"
},
"ui": {
"tabs": {
@@ -1748,6 +1828,7 @@
},
"controlLayers": {
"newLayerFromImage": "Nouvelle couche à partir de l'image",
"sendToGalleryDesc": "Appuyer sur Invoker génère et enregistre une image unique dans votre galerie.",
"sendToCanvas": "Envoyer vers la Toile",
"globalReferenceImage": "Image de référence globale",
"newCanvasFromImage": "Nouvelle Toile à partir de l'image",
@@ -1903,6 +1984,7 @@
},
"bookmark": "Marque-page pour Changement Rapide",
"saveLayerToAssets": "Enregistrer la couche dans les ressources",
"stagingOnCanvas": "Mise en attente des images sur",
"enableTransparencyEffect": "Activer l'effet de transparence",
"hidingType": "Masquer {{type}}",
"settings": {
@@ -1933,6 +2015,11 @@
"disableAutoNegative": "Désactiver l'Auto Négatif",
"addNegativePrompt": "Ajouter $t(controlLayers.negativePrompt)",
"addRegionalGuidance": "Ajouter $t(controlLayers.regionalGuidance)",
"controlLayers_withCount_hidden": "Control Layers ({{count}} cachées)",
"rasterLayers_withCount_hidden": "Couche de Rastérisation ({{count}} cachées)",
"regionalGuidance_withCount_hidden": "Guidage Régional ({{count}} caché)",
"rasterLayers_withCount_visible": "Couche de Rastérisation ({{count}})",
"inpaintMasks_withCount_visible": "Masques de remplissage ({{count}})",
"layer_one": "Couche",
"layer_many": "Couches",
"layer_other": "Couches",
@@ -1982,6 +2069,8 @@
"next": "Suivant",
"saveToGallery": "Enregistrer dans la galerie"
},
"viewProgressOnCanvas": "Voir les progrès et les sorties de la scène sur la <Btn>Toile</Btn>.",
"sendToCanvasDesc": "Appuyer sur Invoker met en attente votre travail en cours sur la toile.",
"mergeVisibleError": "Erreur lors de la fusion des calques visibles",
"mergeVisibleOk": "Couches visibles fusionnées",
"clearHistory": "Effacer l'historique",
@@ -1990,6 +2079,8 @@
"duplicate": "Dupliquer",
"enableAutoNegative": "Activer l'Auto Négatif",
"showHUD": "Afficher HUD",
"sendToGallery": "Envoyer à la galerie",
"sendingToGallery": "Envoi des générations à la galerie",
"disableTransparencyEffect": "Désactiver l'effet de transparence",
"HUD": {
"entityStatus": {
@@ -2006,11 +2097,16 @@
"opacity": "Opacité",
"savedToGalleryError": "Erreur lors de l'enregistrement dans la galerie",
"addInpaintMask": "Ajouter $t(controlLayers.inpaintMask)",
"newCanvasSessionDesc": "Cela effacera la toile et tous les paramètres, sauf votre sélection de modèle. Les générations seront mises en attente sur la toile.",
"canvas": "Toile",
"savedToGalleryOk": "Enregistré dans la galerie",
"addPositivePrompt": "Ajouter $t(controlLayers.prompt)",
"showProgressOnCanvas": "Afficher la progression sur la Toile",
"newGallerySession": "Nouvelle session de galerie",
"newCanvasSession": "Nouvelle session de toile",
"showingType": "Afficher {{type}}",
"viewProgressInViewer": "Voir les progrès et les résultats dans le <Btn>Visionneur d'images</Btn>.",
"deletePrompt": "Supprimer le prompt",
"addControlLayer": "Ajouter $t(controlLayers.controlLayer)",
"global": "Global",
"newGlobalReferenceImageOk": "Image de référence globale créée",
@@ -2024,6 +2120,16 @@
"newRasterLayerError": "Problème de création de couche de rastérisation",
"negativePrompt": "Prompt négatif",
"weight": "Poids",
"globalReferenceImages_withCount_hidden": "Images de référence globales ({{count}} cachées)",
"inpaintMasks_withCount_hidden": "Masques de remplissage ({{count}} cachés)",
"regionalGuidance_withCount_visible": "Guidage Régional ({{count}})",
"globalReferenceImage_withCount_one": "$t(controlLayers.globalReferenceImage)",
"globalReferenceImage_withCount_many": "Images de référence globales",
"globalReferenceImage_withCount_other": "Images de référence globales",
"layer_withCount_one": "Couche {{count}}",
"layer_withCount_many": "Couches {{count}}",
"layer_withCount_other": "Couches {{count}}",
"globalReferenceImages_withCount_visible": "Images de référence globales ({{count}})",
"controlMode": {
"controlMode": "Mode de contrôle",
"balanced": "Équilibré",
@@ -2047,14 +2153,18 @@
},
"fitBboxToLayers": "Ajuster la bounding box aux calques",
"regionIsEmpty": "La zone sélectionnée est vide",
"controlLayers_withCount_visible": "Couches de contrôle ({{count}})",
"cropLayerToBbox": "Rogner la couche selon la bounding box",
"sendingToCanvas": "Mise en attente des Générations sur la Toile",
"copyToClipboard": "Copier dans le presse-papiers",
"regionalGuidance_withCount_one": "$t(controlLayers.regionalGuidance)",
"regionalGuidance_withCount_many": "Guidage Régional",
"regionalGuidance_withCount_other": "Guidage Régional",
"newGallerySessionDesc": "Cela effacera la toile et tous les paramètres, sauf votre sélection de modèle. Les générations seront envoyées à la galerie.",
"inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)",
"inpaintMask_withCount_many": "Remplir les masques",
"inpaintMask_withCount_other": "Remplir les masques",
"newImg2ImgCanvasFromImage": "Nouvelle Img2Img à partir de l'image",
"bboxOverlay": "Afficher la superposition des Bounding Box",
"moveToFront": "Déplacer vers le permier plan",
"moveToBack": "Déplacer vers l'arrière plan",
@@ -2069,6 +2179,7 @@
"inpaintMask": "Masque de remplissage",
"deleteReferenceImage": "Supprimer l'image de référence",
"addReferenceImage": "Ajouter $t(controlLayers.referenceImage)",
"addGlobalReferenceImage": "Ajouter $t(controlLayers.globalReferenceImage)",
"removeBookmark": "Supprimer le marque-page",
"regionalGuidance": "Guide régional",
"regionalReferenceImage": "Image de référence régionale",
@@ -2097,12 +2208,16 @@
"pointType": "Type de point",
"exclude": "Exclure",
"process": "Traiter",
"reset": "Réinitialiser"
"reset": "Réinitialiser",
"help1": "Sélectionnez un seul objet cible. Ajoutez des points <Bold>Inclure</Bold> et <Bold>Exclure</Bold> pour indiquer quelles parties de la couche font partie de l'objet cible.",
"help2": "Commencez par un point <Bold>Inclure</Bold> au sein de l'objet cible. Ajoutez d'autres points pour affiner la sélection. Moins de points produisent généralement de meilleurs résultats.",
"help3": "Inversez la sélection pour sélectionner tout sauf l'objet cible."
},
"convertRegionalGuidanceTo": "Convertir $t(controlLayers.regionalGuidance) vers",
"copyRasterLayerTo": "Copier $t(controlLayers.rasterLayer) vers",
"newControlLayer": "Nouveau $t(controlLayers.controlLayer)",
"newRegionalGuidance": "Nouveau $t(controlLayers.regionalGuidance)",
"replaceCurrent": "Remplacer Actuel",
"convertControlLayerTo": "Convertir $t(controlLayers.controlLayer) vers",
"convertInpaintMaskTo": "Convertir $t(controlLayers.inpaintMask) vers",
"copyControlLayerTo": "Copier $t(controlLayers.controlLayer) vers",
@@ -2148,7 +2263,9 @@
"pasteToBboxDesc": "Nouvelle couche (dans Bbox)",
"pasteToCanvasDesc": "Nouvelle couche (dans la Toile)",
"useImage": "Utiliser l'image",
"referenceImageEmptyState": "<UploadButton>Séléctionner une image</UploadButton> ou faites glisser une image depuis la <GalleryButton>galerie</GalleryButton> sur cette couche pour commencer."
"pastedTo": "Collé à {{destination}}",
"referenceImageEmptyState": "<UploadButton>Séléctionner une image</UploadButton> ou faites glisser une image depuis la <GalleryButton>galerie</GalleryButton> sur cette couche pour commencer.",
"referenceImageGlobal": "Image de référence (Globale)"
},
"upscaling": {
"exceedsMaxSizeDetails": "La limite maximale d'agrandissement est de {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Veuillez essayer une image plus petite ou réduire votre sélection d'échelle.",

View File

@@ -50,7 +50,8 @@
"gallery": {
"galleryImageSize": "גודל תמונה",
"gallerySettings": "הגדרות גלריה",
"autoSwitchNewImages": "החלף אוטומטית לתמונות חדשות"
"autoSwitchNewImages": "החלף אוטומטית לתמונות חדשות",
"noImagesInGallery": "אין תמונות בגלריה"
},
"parameters": {
"images": "תמונות",
@@ -69,10 +70,12 @@
"tileSize": "גודל אריח",
"symmetry": "סימטריה",
"copyImage": "העתקת תמונה",
"downloadImage": "הורדת תמונה",
"usePrompt": "שימוש בבקשה",
"useSeed": "שימוש בזרע",
"useAll": "שימוש בהכל",
"info": "פרטים",
"showOptionsPanel": "הצג חלונית אפשרויות",
"shuffle": "ערבוב",
"noiseThreshold": "סף רעש",
"perlinNoise": "רעש פרלין",

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@
"openInNewTab": "新しいタブで開く",
"controlNet": "コントロールネット",
"linear": "リニア",
"imageFailedToLoad": "画像が読み込めません",
"modelManager": "モデルマネージャー",
"learnMore": "もっと学ぶ",
"random": "ランダム",
@@ -55,6 +56,7 @@
"details": "詳細",
"inpaint": "inpaint",
"delete": "削除",
"nextPage": "次のページ",
"copy": "コピー",
"error": "エラー",
"file": "ファイル",
@@ -62,10 +64,13 @@
"input": "インプット",
"format": "形式",
"installed": "インストール済み",
"localSystem": "ローカルシステム",
"outputs": "アウトプット",
"prevPage": "前のページ",
"unknownError": "未知のエラー",
"orderBy": "並び順:",
"enabled": "有効",
"notInstalled": "未 $t(common.installed)",
"positivePrompt": "ポジティブプロンプト",
"negativePrompt": "ネガティブプロンプト",
"selected": "選択済み",
@@ -91,6 +96,7 @@
"close": "閉じる",
"warnings": "警告",
"dontShowMeThese": "次回から表示しない",
"goTo": "移動",
"generating": "生成中",
"loadingModel": "モデルをロード中",
"layout": "レイアウト",
@@ -101,6 +107,7 @@
"min": "最小",
"max": "最大",
"values": "値",
"resetToDefaults": "デフォルトに戻す",
"row": "行",
"column": "列",
"board": "ボード",
@@ -124,6 +131,7 @@
"gallery": {
"galleryImageSize": "画像のサイズ",
"gallerySettings": "ギャラリーの設定",
"noImagesInGallery": "表示する画像がありません",
"autoSwitchNewImages": "新しい画像に自動切替",
"copy": "コピー",
"image": "画像",
@@ -137,6 +145,7 @@
"deleteImage_other": "画像 {{count}} 枚を削除",
"deleteImagePermanent": "削除された画像は復元できません。",
"download": "ダウンロード",
"unableToLoad": "ギャラリーをロードできません",
"bulkDownloadRequested": "ダウンロード準備中",
"bulkDownloadRequestedDesc": "ダウンロードの準備中です。しばらくお待ちください。",
"bulkDownloadRequestFailed": "ダウンロード準備中に問題が発生",
@@ -151,6 +160,7 @@
"compareImage": "比較画像",
"openInViewer": "ビューアで開く",
"selectForCompare": "比較対象として選択",
"selectAnImageToCompare": "比較する画像を選択",
"slider": "スライダー",
"sideBySide": "横並び",
"hover": "ホバー",
@@ -162,6 +172,8 @@
"compareHelp4": "<Kbd>[Z</Kbd>]または<Kbd>[Esc</Kbd>]を押して終了します。",
"compareHelp2": "<Kbd>M</Kbd> キーを押して比較モードを切り替えます。",
"move": "移動",
"openViewer": "ビューアを開く",
"closeViewer": "ビューアを閉じる",
"exitSearch": "画像検索を終了",
"oldestFirst": "最古から",
"showStarredImagesFirst": "スター付き画像を最初に",
@@ -170,6 +182,7 @@
"searchImages": "メタデータで検索",
"gallery": "ギャラリー",
"newestFirst": "最新から",
"jump": "ジャンプ",
"go": "進む",
"sortDirection": "並び替え順",
"displayBoardSearch": "ボード検索",
@@ -312,6 +325,10 @@
"desc": "リスト内の前のレイヤーを選択します。",
"title": "前のレイヤー"
},
"setFillToWhite": {
"title": "ツール色を白に設定",
"desc": "現在のツールの色を白色に設定します。"
},
"selectViewTool": {
"title": "表示ツール",
"desc": "表示ツールを選択します。"
@@ -592,6 +609,7 @@
"scanResults": "結果をスキャン",
"scanPlaceholder": "ローカルフォルダへのパス",
"typePhraseHere": "ここにフレーズを入力",
"ipAdapters": "IPアダプター",
"modelImageUpdated": "モデル画像アップデート",
"installAll": "全てインストール",
"installRepo": "リポジトリをインストール",
@@ -633,6 +651,7 @@
"spandrelImageToImage": "Image to Image(スパンドレル)",
"starterBundles": "スターターバンドル",
"starterModels": "スターターモデル",
"starterModelsInModelManager": "スターターモデルがモデルマネージャーで見つかりました",
"modelImageDeleteFailed": "モデル画像の削除失敗",
"urlForbidden": "このモデルにアクセスできません",
"urlForbiddenErrorMessage": "このモデルを配布しているサイトからリクエスト権限が必要かもしれません.",
@@ -641,10 +660,12 @@
"inplaceInstall": "定位置にインストール",
"fileSize": "ファイルサイズ",
"modelPickerFallbackNoModelsInstalled2": "<LinkComponent>モデルマネージャー</LinkComponent> にアクセスしてモデルをインストールしてください.",
"filterModels": "フィルターモデル",
"modelPickerFallbackNoModelsInstalled": "モデルがインストールされていません.",
"manageModels": "モデル管理",
"hfTokenReset": "ハギングフェイストークンリセット",
"relatedModels": "関連のあるモデル",
"showOnlyRelatedModels": "関連している",
"installedModelsCount": "{{total}} モデルのうち {{installed}} 個がインストールされています。",
"allNModelsInstalled": "{{count}} 個のモデルがすべてインストールされています",
"nToInstall": "{{count}}個をインストールする",
@@ -661,8 +682,12 @@
"scanFolderDescription": "ローカルフォルダをスキャンしてモデルを自動的に検出し、インストールします。",
"recommendedModels": "推奨モデル",
"exploreStarter": "または、利用可能なすべてのスターターモデルを参照してください",
"quickStart": "クイックスタートバンドル",
"bundleDescription": "各バンドルには各モデルファミリーの必須モデルと、開始するための厳選されたベースモデルが含まれています。",
"sdxl": "SDXL"
"browseAll": "または、利用可能なすべてのモデルを参照してください。",
"stableDiffusion15": "Stable Diffusion1.5",
"sdxl": "SDXL",
"fluxDev": "FLUX.1 dev"
}
},
"parameters": {
@@ -678,10 +703,12 @@
"scaleBeforeProcessing": "処理前のスケール",
"scaledWidth": "幅のスケール",
"scaledHeight": "高さのスケール",
"downloadImage": "画像をダウンロード",
"usePrompt": "プロンプトを使用",
"useSeed": "シード値を使用",
"useAll": "すべてを使用",
"info": "情報",
"showOptionsPanel": "サイドパネルを表示 (O or T)",
"iterations": "生成回数",
"general": "基本設定",
"setToOptimalSize": "サイズをモデルに最適化",
@@ -695,6 +722,7 @@
"collectionNumberLTExclusiveMin": "{{value}} <= {{exclusiveMinimum}} (最小値を除く)",
"missingInputForField": "入力の欠落",
"noModelSelected": "モデルが選択されていません",
"emptyBatches": "空のバッチ",
"collectionStringTooLong": "長すぎます,最大{{maxLength}}",
"batchNodeCollectionSizeMismatchNoGroupId": "バッチグループのコレクションサイズが合いません",
"invoke": "呼び出す",
@@ -706,6 +734,7 @@
"missingNodeTemplate": "ノードテンプレートの欠落",
"batchNodeNotConnected": "バッチノードが: {{label}}につながっていない",
"collectionNumberLTMin": "{{value}} < {{minimum}} (最小増加)",
"fluxModelIncompatibleScaledBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), スケーリングされたbboxの高さは{{height}}です",
"fluxModelMultipleControlLoRAs": "コントロールLoRAは1度に1つしか使用できません",
"noPrompts": "プロンプトが生成されません",
"noNodesInGraph": "グラフにノードがありません",
@@ -713,6 +742,7 @@
"canvasIsFiltering": "キャンバスがビジー状態(フィルタリング)",
"canvasIsCompositing": "キャンバスがビジー状態(合成)",
"systemDisconnected": "システムが切断されました",
"fluxModelIncompatibleScaledBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), 拡大縮小されたbboxの幅は{{width}}です",
"canvasIsTransforming": "キャンバスがビジー状態(変換)",
"canvasIsRasterizing": "キャンバスがビジー状態(ラスタライズ)",
"modelIncompatibleBboxHeight": "Bboxの高さは{{height}}ですが,{{model}}は{{multiple}}の倍数が必要です",
@@ -720,9 +750,12 @@
"modelIncompatibleBboxWidth": "Bboxの幅は{{width}}ですが, {{model}}は{{multiple}}の倍数が必要です",
"modelIncompatibleScaledBboxWidth": "bboxの幅は{{width}}ですが,{{model}}は{{multiple}}の倍数が必要です",
"canvasIsSelectingObject": "キャンバスがビジー状態(オブジェクトの選択)",
"fluxModelIncompatibleBboxWidth": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bboxの幅は{{width}}です",
"fluxModelIncompatibleBboxHeight": "$t(parameters.invoke.fluxRequiresDimensionsToBeMultipleOf16), bboxの高さは{{height}}です",
"noFLUXVAEModelSelected": "FLUX生成にVAEモデルが選択されていません",
"noT5EncoderModelSelected": "FLUX生成にT5エンコーダモデルが選択されていません",
"modelDisabledForTrial": "{{modelName}} を使用した生成はトライアルアカウントではご利用いただけません.アカウント設定にアクセスしてアップグレードしてください。",
"fluxKontextMultipleReferenceImages": "Flux Kontext では一度に 1 つの参照画像しか使用できません",
"promptExpansionPending": "プロンプト拡張が進行中",
"promptExpansionResultPending": "プロンプト拡張結果を受け入れるか破棄してください"
},
@@ -797,6 +830,8 @@
"enableHighlightFocusedRegions": "重点領域を強調表示",
"clearIntermediatesDesc1": "中間物をクリアすると、キャンバスとコントロールネットの状態がリセットされます.",
"showProgressInViewer": "ビューアで進行状況画像を表示する",
"modelDescriptionsDisabled": "ドロップダウンのモデル説明が無効になっています",
"modelDescriptionsDisabledDesc": "ドロップダウンのモデル説明が無効になっています.設定で有効にしてください.",
"clearIntermediatesDisabled": "中間物をクリアするにはキューが空でなければなりません",
"clearIntermediatesDesc2": "中間画像は生成時に生成される副産物であり、ギャラリーに表示される結果画像とは異なります.中間画像を削除するとディスク容量が解放されます.",
"intermediatesClearedFailed": "中間物をクリアする問題",
@@ -827,9 +862,11 @@
"imagesWillBeAddedTo": "アップロードされた画像はボード {{boardName}} のアセットに追加されます.",
"layerCopiedToClipboard": "レイヤーがクリップボードにコピーされました",
"pasteFailed": "貼り付け失敗",
"imageSavingFailed": "画像保存に失敗しました",
"importSuccessful": "インポートが成功しました",
"problemDownloadingImage": "画像をダウンロードできません",
"modelAddedSimple": "モデルがキューに追加されました",
"uploadFailedInvalidUploadDesc_withCount_other": "PNG、JPEG、または WEBP 画像は最大 1 つにする必要があります.",
"outOfMemoryErrorDesc": "現在の生成設定はシステム容量を超えています.設定を調整してもう一度お試しください.",
"parametersSet": "パラメーターが呼び出されました",
"modelImportCanceled": "モデルのインポートがキャンセルされました",
@@ -844,11 +881,14 @@
"linkCopied": "リンクがコピーされました",
"unableToLoadImage": "画像をロードできません",
"unableToLoadImageMetadata": "画像のメタデータをロードできません",
"imageSaved": "画像が保存されました",
"importFailed": "インポートに失敗しました",
"invalidUpload": "無効なアップロードです",
"outOfMemoryError": "メモリ不足エラー",
"parameterSetDesc": "{{parameter}}を呼び出し",
"errorCopied": "エラーがコピーされました",
"sentToCanvas": "キャンバスに送信",
"setControlImage": "コントロール画像としてセット",
"workflowLoaded": "ワークフローがロードされました",
"unableToCopy": "コピーできません",
"unableToCopyDesc": "あなたのブラウザはクリップボードアクセスをサポートしていません.Firefoxユーザーの場合は、以下の手順で修正できる可能性があります. ",
@@ -862,23 +902,32 @@
"parameterNotSetDescWithMessage": "{{parameter}}: {{message}}を呼び出せません",
"problemCopyingLayer": "レイヤーをコピーできません",
"problemSavingLayer": "レイヤー保存ができません",
"setNodeField": "ノードフィールドとしてセット",
"layerSavedToAssets": "レイヤーがアセットに保存されました",
"outOfMemoryErrorDescLocal": "OOM を削減するには、<LinkComponent>低 VRAM ガイド</LinkComponent> に従ってください.",
"parameterNotSet": "パラメーターが呼び出されていません",
"addedToBoard": "{{name}} 個の資産をボードに追加しました",
"addedToUncategorized": "$t(boards.uncategorized)個のアセットがボードに追加されました",
"problemDeletingWorkflow": "ワークフローが削除された問題",
"imageNotLoadedDesc": "画像を見つけられません",
"parameterNotSetDesc": "{{parameter}}を呼び出せません",
"chatGPT4oIncompatibleGenerationMode": "ChatGPT 4oは,テキストから画像への生成と画像から画像への生成のみをサポートしています.インペインティングおよび,アウトペインティングタスクには他のモデルを使用してください.",
"imagenIncompatibleGenerationMode": "Google {{model}} はテキストから画像への変換のみをサポートしています. 画像から画像への変換, インペインティング,アウトペインティングのタスクには他のモデルを使用してください.",
"noRasterLayers": "ラスターレイヤーが見つかりません",
"noRasterLayersDesc": "PSDにエクスポートするには、少なくとも1つのラスターレイヤーを作成します",
"noActiveRasterLayers": "アクティブなラスターレイヤーがありません",
"noActiveRasterLayersDesc": "PSD にエクスポートするには、少なくとも 1 つのラスター レイヤーを有効にします",
"noVisibleRasterLayers": "表示されるラスター レイヤーがありません",
"noVisibleRasterLayersDesc": "PSD にエクスポートするには、少なくとも 1 つのラスター レイヤーを有効にします",
"invalidCanvasDimensions": "キャンバスのサイズが無効です",
"canvasTooLarge": "キャンバスが大きすぎます",
"canvasTooLargeDesc": "キャンバスのサイズがPSDエクスポートの最大許容サイズを超えています。キャンバス全体の幅と高さを小さくしてから、もう一度お試しください。",
"failedToProcessLayers": "レイヤーの処理に失敗しました",
"psdExportSuccess": "PSDエクスポート完了",
"psdExportSuccessDesc": "{{count}} 個のレイヤーを PSD ファイルに正常にエクスポートしました",
"problemExportingPSD": "PSD のエクスポート中に問題が発生しました",
"canvasManagerNotAvailable": "キャンバスマネージャーは利用できません",
"noValidLayerAdapters": "有効なレイヤーアダプタが見つかりません",
"fluxKontextIncompatibleGenerationMode": "Flux Kontext はテキストから画像への変換のみをサポートしています。画像から画像への変換、インペインティング、アウトペインティングのタスクには他のモデルを使用してください。",
"promptGenerationStarted": "プロンプト生成が開始されました",
"uploadAndPromptGenerationFailed": "画像のアップロードとプロンプトの生成に失敗しました",
@@ -910,6 +959,7 @@
"positivePrompt": "ポジティブプロンプト",
"strength": "Image to Image 強度",
"recallParameters": "パラメータを再使用",
"recallParameter": "{{label}} を再使用",
"imageDimensions": "画像サイズ",
"imageDetails": "画像の詳細",
"model": "モデル",
@@ -924,6 +974,7 @@
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)",
"canvasV2Metadata": "キャンバス",
"guidance": "手引き",
"parsingFailed": "解析に失敗しました",
"seamlessXAxis": "シームレスX軸",
"seamlessYAxis": "シームレスY軸",
"parameterSet": "パラメーター {{parameter}} が設定されました",
@@ -981,6 +1032,7 @@
"clearQueueAlertDialog2": "キューをクリアしてもよろしいですか?",
"item": "項目",
"graphFailedToQueue": "グラフをキューに追加できませんでした",
"batchFieldValues": "バッチの詳細",
"openQueue": "キューを開く",
"time": "時間",
"completedIn": "完了まで",
@@ -1010,12 +1062,14 @@
"models": {
"noMatchingModels": "一致するモデルがありません",
"loading": "読み込み中",
"noMatchingLoRAs": "一致するLoRAがありません",
"noModelsAvailable": "使用可能なモデルがありません",
"selectModel": "モデルを選択してください",
"concepts": "コンセプト",
"addLora": "LoRAを追加",
"lora": "LoRA",
"defaultVAE": "デフォルトVAE",
"noLoRAsInstalled": "インストールされているLoRAはありません",
"noRefinerModelsInstalled": "インストールされているSDXLリファイナーモデルはありません",
"noCompatibleLoRAs": "互換性のあるLoRAはありません"
},
@@ -1025,6 +1079,7 @@
"addNodeToolTip": "ノードを追加 (Shift+A, Space)",
"missingTemplate": "Invalid node: タイプ {{type}} のノード {{node}} にテンプレートがありません(未インストール?)",
"loadWorkflow": "ワークフローを読み込み",
"hideLegendNodes": "フィールドタイプの凡例を非表示",
"float": "浮動小数点",
"integer": "整数",
"nodeTemplate": "ノードテンプレート",
@@ -1068,11 +1123,13 @@
"enum": "Enum",
"arithmeticSequence": "等差数列",
"linearDistribution": "線形分布",
"addLinearView": "ライナービューに追加",
"animatedEdges": "アニメーションエッジ",
"uniformRandomDistribution": "一様ランダム分布",
"noBatchGroup": "グループなし",
"parseString": "文字列の解析",
"generatorImagesFromBoard": "ボードからの画像",
"generatorLoading": "読み込み中",
"missingNode": "呼び出しノードがありません",
"missingSourceOrTargetNode": "ソースまたはターゲットノードがありません",
"missingSourceOrTargetHandle": "ソースまたはターゲットハンドルがありません",
@@ -1093,6 +1150,7 @@
"missingInvocationTemplate": "呼び出しテンプレートがありません",
"nodePack": "ノードパック",
"targetNodeFieldDoesNotExist": "無効なエッジ:ターゲット/インプットフィールド{{node}}.{{field}} が存在しません",
"mismatchedVersion": "無効なノード:ノード {{node}} のタイプ {{type}} はバージョンとミスマッチしています (アップデートを試されますか?)",
"dynamicPromptsCombinatorial": "ダイナミックプロンプト(組み合わせ)",
"cannotMixAndMatchCollectionItemTypes": "コレクション・アイテムの種類を組み合わせることはできません",
"missingFieldTemplate": "フィールドテンプレートがありません",
@@ -1102,6 +1160,7 @@
"collectionOrScalarFieldType": "{{name}} (単数またはコレクション)",
"unableToUpdateNode": "ノードアップロード失敗:ノード {{node}} のタイプ {{type}} (削除か再生成が必要かもしれません)",
"deletedInvalidEdge": "無効なエッジを削除しました{{source}} -> {{target}}",
"noFieldsLinearview": "線形ビューに追加されたフィールドがありません",
"collectionFieldType": "{{name}} (コレクション)",
"colorCodeEdgesHelp": "接続されたフィールドによるカラーコードエッジ",
"showEdgeLabelsHelp": "エッジのラベルを表示,接続されているノードを示す",
@@ -1116,6 +1175,7 @@
"loadWorkflowDesc2": "現在のワークフローは保存されていない変更があります.",
"clearWorkflowDesc": "このワークフローをクリアして新しいワークフローにしますか?",
"updateNode": "ノードをアップデート",
"versionUnknown": " バージョン不明",
"graph": "グラフ",
"workflowContact": "お問い合わせ",
"outputFieldTypeParseError": "出力フィールド {{node}}.{{field}} の型を解析できません({{message}})",
@@ -1134,28 +1194,36 @@
"unableToExtractSchemaNameFromRef": "参照からスキーマ名を抽出できません",
"unableToUpdateNodes_other": "{{count}} 個のノードをアップデートできません",
"workflowSettings": "ワークフローエディター設定",
"generateValues": "値を生成",
"floatRangeGenerator": "浮動小数点レンジ生成器",
"integerRangeGenerator": "整数レンジ生成器",
"specialDesc": "この呼び出しは,アプリ内で特別な処理を行います.例えば,バッチードは1つのワークフローから複数のグラフをキューに入れるために使用されます.",
"modelAccessError": "モデル {{key}}が見つからないので,デフォルトにリセットします",
"betaDesc": "この呼び出しはベータ版です.安定するまでは,アプリのアップデートの際に変更される可能性があります.この呼び出しは長期的にサポートする予定です.",
"internalDesc": "この呼び出しはInvokeによって内部的に使用されます.アプリの更新時に変更される可能性があり,いつでも削除される可能性があります.",
"noFieldsViewMode": "このワークフローには表示する選択フィールドがありません.値を設定するためにはワークフロー全体を表示します.",
"clearWorkflow": "ワークフローをクリア",
"removeLinearView": "線形ビューから削除",
"snapToGrid": "グリッドにスナップ",
"showMinimapnodes": "ミニマップを表示",
"reorderLinearView": "線形ビューの並び替え",
"description": "説明",
"notesDescription": "ワークフローに関するメモを追加する",
"newWorkflowDesc2": "現在のワークフローに保存されていない変更があります.",
"unknownField": "不明なフィールド",
"unexpectedField_withName": "予期しないフィールド\"{{name}}\"",
"loadingTemplates": "読み込み中 {{name}}",
"validateConnectionsHelp": "無効な接続が行われたり,無効なグラフが呼び出されたりしないようにします",
"validateConnections": "接続とグラフを確認する",
"saveToGallery": "ギャラリーに保存",
"newWorkflowDesc": "新しいワークフローを作りますか?",
"unknownFieldType": "$t(nodes.unknownField)型: {{type}}",
"unsupportedArrayItemType": "サポートされていない配列項目型です \"{{type}}\"",
"unableToLoadWorkflow": "ワークフローが読み込めません",
"unableToValidateWorkflow": "ワークフローを確認できません",
"unknownErrorValidatingWorkflow": "ワークフローの確認で不明なエラーが発生",
"clearWorkflowDesc2": "現在のワークフローは保存されていない変更があります.",
"showLegendNodes": "フィールドタイプの凡例を表示",
"unsupportedMismatchedUnion": "CollectionOrScalar型とベース型{{firstType}}および{{secondType}}が不一致です",
"updateApp": "アプリケーションをアップデート",
"noGraph": "グラフなし",
@@ -1173,8 +1241,10 @@
"workflowDescription": "短い説明",
"workflowValidation": "ワークフロー検証エラー",
"noOutputRecorded": "記録されたアウトプットがありません",
"unknownTemplate": "不明なテンプレート",
"nodeOpacity": "ノードの不透明度",
"unableToParseFieldType": "フィールドタイプを解析できません"
"unableToParseFieldType": "フィールドタイプを解析できません",
"unknownInput": "不明な入力: {{name}}"
},
"boards": {
"autoAddBoard": "自動追加するボード",
@@ -1198,6 +1268,7 @@
"deleteBoardOnly": "ボードのみ削除",
"deletedBoardsCannotbeRestored": "削除したボードと画像は復元できません。「ボードのみ削除」を選択すると、画像は未分類の状態になります。",
"movingImagesToBoard_other": "{{count}} の画像をボードに移動:",
"hideBoards": "ボードを隠す",
"assetsWithCount_other": "{{count}} のアセット",
"addPrivateBoard": "プライベートボードを追加",
"addSharedBoard": "共有ボードを追加",
@@ -1212,8 +1283,10 @@
"selectedForAutoAdd": "自動追加に選択済み",
"deletedPrivateBoardsCannotbeRestored": "削除されたボードと画像は復元できません。「ボードのみ削除」を選択すると、画像は作成者に対して非公開の未分類状態になります。",
"noBoards": "{{boardType}} ボードがありません",
"viewBoards": "ボードを表示",
"uncategorizedImages": "分類されていない画像",
"deleteAllUncategorizedImages": "分類されていないすべての画像を削除"
"deleteAllUncategorizedImages": "分類されていないすべての画像を削除",
"deletedImagesCannotBeRestored": "削除した画像は復元できません."
},
"invocationCache": {
"invocationCache": "呼び出しキャッシュ",
@@ -1685,7 +1758,9 @@
"strength": "高解像修復の強度",
"enabled": "高解像修復が有効"
},
"hrf": "高解像修復"
"enableHrf": "高解像修復を有効",
"hrf": "高解像修復",
"upscaleMethod": "アップスケール手法"
},
"prompt": {
"addPromptTrigger": "プロンプトトリガーを追加",
@@ -1695,7 +1770,10 @@
"expandCurrentPrompt": "現在のプロンプトを展開",
"uploadImageForPromptGeneration": "プロンプト生成用の画像をアップロードする",
"expandingPrompt": "プロンプトを展開しています...",
"resultTitle": "プロンプト拡張完了",
"resultSubtitle": "拡張プロンプトの処理方法を選択します:",
"replace": "交換する",
"insert": "挿入する",
"discard": "破棄する"
},
"ui": {
@@ -1761,9 +1839,11 @@
}
},
"controlLayers": {
"globalReferenceImage_withCount_other": "全域参照画像",
"regionalReferenceImage": "領域参照画像",
"saveLayerToAssets": "レイヤーをアセットに保存",
"global": "全域",
"inpaintMasks_withCount_hidden": "インペイントマスク ({{count}} hidden)",
"opacity": "透明度",
"canvasContextMenu": {
"newRegionalGuidance": "新規領域ガイダンス",
@@ -1815,6 +1895,7 @@
"duplicate": "複製",
"addLayer": "レイヤーを追加",
"rasterLayer": "ラスターレイヤー",
"inpaintMasks_withCount_visible": "({{count}}) インペイントマスク",
"regional": "領域",
"rectangle": "矩形",
"moveBackward": "背面へ移動",
@@ -2016,6 +2097,7 @@
"autoNegative": "オートネガティブ",
"enableAutoNegative": "オートネガティブを有効にする",
"disableAutoNegative": "オートネガティブを無効にする",
"deletePrompt": "プロンプトを削除",
"deleteReferenceImage": "参照画像を削除",
"showHUD": "HUDを表示",
"maskFill": "マスク塗りつぶし",
@@ -2027,22 +2109,41 @@
"addControlLayer": "$t(controlLayers.controlLayer)を追加します",
"addInpaintMask": "$t(controlLayers.inpaintMask)を追加します",
"addRegionalGuidance": "$t(controlLayers.regionalGuidance)を追加します",
"addGlobalReferenceImage": "$t(controlLayers.globalReferenceImage)を追加します",
"addDenoiseLimit": "$t(controlLayers.denoiseLimit)を追加します",
"controlLayer": "コントロールレイヤー",
"inpaintMask": "インペイントマスク",
"referenceImageRegional": "参考画像(地域別)",
"referenceImageGlobal": "参考画像(グローバル)",
"asRasterLayer": "$t(controlLayers.rasterLayer) として",
"asRasterLayerResize": "$t(controlLayers.rasterLayer) として (リサイズ)",
"asControlLayer": "$t(controlLayers.controlLayer) として",
"asControlLayerResize": "$t(controlLayers.controlLayer) として (リサイズ)",
"referenceImage": "参照画像",
"sendingToCanvas": "キャンバスに生成をのせる",
"sendingToGallery": "生成をギャラリーに送る",
"sendToGallery": "ギャラリーに送る",
"sendToGalleryDesc": "Invokeを押すとユニークな画像が生成され、ギャラリーに保存されます。",
"sendToCanvas": "キャンバスに送る",
"newLayerFromImage": "画像から新規レイヤー",
"newCanvasFromImage": "画像から新規キャンバス",
"newImg2ImgCanvasFromImage": "画像からの新規 Img2Img",
"copyToClipboard": "クリップボードにコピー",
"sendToCanvasDesc": "Invokeを押すと、進行中の作品がキャンバス上にステージされます。",
"viewProgressInViewer": "<Btn>画像ビューア</Btn>で進行状況と出力を表示します。",
"viewProgressOnCanvas": "<Btn>キャンバス</Btn> で進行状況とステージ出力を表示します。",
"rasterLayer_withCount_other": "ラスターレイヤー",
"controlLayer_withCount_other": "コントロールレイヤー",
"regionalGuidance_withCount_hidden": "地域ガイダンス({{count}} 件非表示)",
"controlLayers_withCount_hidden": "コントロールレイヤー({{count}} 個非表示)",
"rasterLayers_withCount_hidden": "ラスター レイヤー ({{count}} 個非表示)",
"globalReferenceImages_withCount_hidden": "グローバル参照画像({{count}} 枚非表示)",
"regionalGuidance_withCount_visible": "地域ガイダンス ({{count}})",
"controlLayers_withCount_visible": "コントロールレイヤー ({{count}})",
"rasterLayers_withCount_visible": "ラスターレイヤー({{count}}",
"globalReferenceImages_withCount_visible": "グローバル参照画像 ({{count}})",
"layer_other": "レイヤー",
"layer_withCount_other": "レイヤー ({{count}})",
"convertRasterLayerTo": "$t(controlLayers.rasterLayer) を変換する",
"convertControlLayerTo": "$t(controlLayers.controlLayer) を変換する",
"convertRegionalGuidanceTo": "$t(controlLayers.regionalGuidance) を変換する",
@@ -2060,6 +2161,7 @@
"pasteToBboxDesc": "新しいレイヤーBbox内",
"pasteToCanvas": "キャンバス",
"pasteToCanvasDesc": "新しいレイヤー(キャンバス内)",
"pastedTo": "{{destination}} に貼り付けました",
"transparency": "透明性",
"enableTransparencyEffect": "透明効果を有効にする",
"disableTransparencyEffect": "透明効果を無効にする",
@@ -2072,6 +2174,7 @@
"locked": "ロックされています",
"unlocked": "ロック解除",
"deleteSelected": "選択項目を削除",
"stagingOnCanvas": "ステージング画像",
"replaceLayer": "レイヤーの置き換え",
"pullBboxIntoLayer": "Bboxをレイヤーに引き込む",
"pullBboxIntoReferenceImage": "Bboxを参照画像に取り込む",
@@ -2079,11 +2182,17 @@
"useImage": "画像を使う",
"negativePrompt": "ネガティブプロンプト",
"beginEndStepPercentShort": "開始/終了 %",
"newGallerySession": "新しいギャラリーセッション",
"newGallerySessionDesc": "これにより、キャンバスとモデル選択以外のすべての設定がクリアされます。生成した画像はギャラリーに送信されます。",
"newCanvasSession": "新規キャンバスセッション",
"newCanvasSessionDesc": "これにより、キャンバスとモデル選択以外のすべての設定がクリアされます。生成はキャンバス上でステージングされます。",
"resetCanvasLayers": "キャンバスレイヤーをリセット",
"resetGenerationSettings": "生成設定をリセット",
"replaceCurrent": "現在のものを置き換える",
"controlLayerEmptyState": "<UploadButton>画像をアップロード</UploadButton>、<GalleryButton>ギャラリー</GalleryButton>からこのレイヤーに画像をドラッグ、<PullBboxButton>境界ボックスをこのレイヤーにプル</PullBboxButton>、またはキャンバスに描画して開始します。",
"referenceImageEmptyStateWithCanvasOptions": "開始するには、<UploadButton>画像をアップロード</UploadButton>するか、<GalleryButton>ギャラリー</GalleryButton>からこの参照画像に画像をドラッグするか、<PullBboxButton>境界ボックスをこの参照画像にプル</PullBboxButton>します。",
"referenceImageEmptyState": "開始するには、<UploadButton>画像をアップロード</UploadButton>するか、<GalleryButton>ギャラリー</GalleryButton>からこの参照画像に画像をドラッグします。",
"uploadOrDragAnImage": "ギャラリーから画像をドラッグするか、<UploadButton>画像をアップロード</UploadButton>します。",
"imageNoise": "画像ノイズ",
"denoiseLimit": "ノイズ除去制限",
"warnings": {
@@ -2149,6 +2258,9 @@
"saveAs": "名前を付けて保存",
"cancel": "キャンセル",
"process": "プロセス",
"help1": "ターゲットオブジェクトを1つ選択します。<Bold>含める</Bold>ポイントと<Bold>除外</Bold>ポイントを追加して、レイヤーのどの部分がターゲットオブジェクトの一部であるかを示します。",
"help2": "対象オブジェクト内に<Bold>含める</Bold>ポイントを1つ選択するところから始めます。ポイントを追加して選択範囲を絞り込みます。ポイントが少ないほど、通常はより良い結果が得られます。",
"help3": "選択を反転して、ターゲットオブジェクト以外のすべてを選択します。",
"clickToAdd": "レイヤーをクリックしてポイントを追加します",
"dragToMove": "ポイントをドラッグして移動します",
"clickToRemove": "ポイントをクリックして削除します"
@@ -2249,8 +2361,12 @@
"loading": "ロード中...",
"steps": "ステップ",
"refiner": "Refiner",
"negStylePrompt": "ネガティブスタイルプロンプト",
"noModelsAvailable": "利用できるモデルがありません",
"posStylePrompt": "ポジティブスタイルプロンプト",
"cfgScale": "CFGスケール",
"concatPromptStyle": "リンキングプロンプトとスタイル",
"freePromptStyle": "手動スタイルプロンプト",
"posAestheticScore": "ポジティブ美的スコア",
"refinerSteps": "リファイナーステップ",
"refinerStart": "リファイナースタート",
@@ -2268,6 +2384,8 @@
"name": "名前",
"descending": "降順",
"searchPlaceholder": "名前、説明、タグで検索",
"projectWorkflows": "プロジェクトワークフロー",
"searchWorkflows": "ワークフローを検索",
"updated": "アップデート",
"published": "公表",
"builder": {
@@ -2293,8 +2411,10 @@
"addToForm": "フォームに追加",
"headingPlaceholder": "空の見出し",
"nodeFieldTooltip": "ノード フィールドを追加するには、ワークフロー エディターのフィールドにある小さなプラス記号ボタンをクリックするか、フィールド名をフォームにドラッグします。",
"workflowBuilderAlphaWarning": "ワークフロービルダーは現在アルファ版です。安定版リリースまでに互換性に影響する変更が発生する可能性があります。",
"component": "コンポーネント",
"textPlaceholder": "空のテキスト",
"emptyRootPlaceholderViewMode": "このワークフローのフォームの作成を開始するには、[編集] をクリックします。",
"addOption": "オプションを追加",
"singleLine": "単線",
"numberInput": "数値入力",
@@ -2345,15 +2465,20 @@
"convertGraph": "グラフを変換",
"downloadWorkflow": "ファイルに保存",
"saveWorkflow": "ワークフローを保存",
"userWorkflows": "ユーザーワークフロー",
"yourWorkflows": "あなたのワークフロー",
"edit": "編集",
"workflowLibrary": "ワークフローライブラリ",
"workflowSaved": "ワークフローが保存されました",
"clearWorkflowSearchFilter": "ワークフロー検索フィルタをクリア",
"workflowCleared": "ワークフローが作成されました",
"autoLayout": "オートレイアウト",
"view": "ビュー",
"saveChanges": "変更を保存",
"noDescription": "説明なし",
"recommended": "あなたへのおすすめ",
"noRecentWorkflows": "最近のワークフローがありません",
"problemLoading": "ワークフローのローディングに関する問題",
"newWorkflowCreated": "新しいワークフローが作成されました",
"noWorkflows": "ワークフローがありません",
"copyShareLink": "共有リンクをコピー",
@@ -2361,16 +2486,21 @@
"workflowThumbnail": "ワークフローサムネイル",
"loadWorkflow": "$t(common.load) ワークフロー",
"shared": "共有",
"openWorkflow": "ワークフローを開く",
"emptyStringPlaceholder": "<空の文字列>",
"browseWorkflows": "ワークフローを閲覧する",
"saveWorkflowAs": "ワークフローとして保存",
"private": "プライベート",
"deselectAll": "すべて選択解除",
"delete": "削除",
"openLibrary": "ライブラリを開く",
"loadMore": "もっと読み込む",
"saveWorkflowToProject": "ワークフローをプロジェクトに保存",
"created": "作成されました",
"workflowEditorMenu": "ワークフローエディターメニュー",
"defaultWorkflows": "デフォルトワークフロー",
"allLoaded": "すべてのワークフローが読み込まれました",
"filterByTags": "タグでフィルター",
"recentlyOpened": "最近開いた",
"opened": "オープン",
"deleteWorkflow": "ワークフローを削除",
@@ -2416,6 +2546,7 @@
"perIterationDesc": "それぞれのいてレーションに別のシードを使う"
},
"showDynamicPrompts": "ダイナミックプロンプトを表示する",
"promptsToGenerate": "生成するプロンプト",
"dynamicPrompts": "ダイナミックプロンプト",
"loading": "ダイナミックプロンプトを生成...",
"maxPrompts": "最大プロンプト"
@@ -2441,7 +2572,8 @@
"キャンバス: SDXL のアスペクト比がスマートになり、スクロールによるズームが改善されました。"
],
"readReleaseNotes": "リリースノートを読む",
"watchRecentReleaseVideos": "最近のリリースビデオを見る"
"watchRecentReleaseVideos": "最近のリリースビデオを見る",
"watchUiUpdatesOverview": "Watch UI アップデートの概要"
},
"supportVideos": {
"supportVideos": "サポートビデオ",

View File

@@ -27,6 +27,7 @@
"save": "저장",
"created": "생성됨",
"error": "에러",
"prevPage": "이전 페이지",
"ipAdapter": "IP 어댑터",
"installed": "설치됨",
"accept": "수락",
@@ -41,6 +42,7 @@
"outputs": "결과물",
"unknownError": "알려지지 않은 에러",
"linear": "선형",
"imageFailedToLoad": "이미지를 로드할 수 없음",
"direction": "방향",
"data": "데이터",
"somethingWentWrong": "뭔가 잘못됐어요",
@@ -50,6 +52,7 @@
"orderBy": "정렬 기준",
"copyError": "$t(gallery.copy) 에러",
"learnMore": "더 알아보기",
"nextPage": "다음 페이지",
"saveAs": "다른 이름으로 저장",
"loading": "불러오는 중",
"random": "랜덤",
@@ -57,15 +60,18 @@
"postprocessing": "후처리",
"advanced": "고급",
"input": "입력",
"details": "세부사항"
"details": "세부사항",
"notInstalled": "설치되지 않음"
},
"gallery": {
"galleryImageSize": "이미지 크기",
"gallerySettings": "갤러리 설정",
"deleteSelection": "선택 항목 삭제",
"featuresWillReset": "이 이미지를 삭제하면 해당 기능이 즉시 재설정됩니다.",
"noImagesInGallery": "보여줄 이미지가 없음",
"autoSwitchNewImages": "새로운 이미지로 자동 전환",
"loading": "불러오는 중",
"unableToLoad": "갤러리를 로드할 수 없음",
"image": "이미지",
"drop": "드랍",
"downloadSelection": "선택 항목 다운로드",
@@ -145,6 +151,8 @@
"loadWorkflow": "Workflow 불러오기",
"noOutputRecorded": "기록된 출력 없음",
"colorCodeEdgesHelp": "연결된 필드에 따른 색상 코드 선",
"hideLegendNodes": "필드 유형 범례 숨기기",
"addLinearView": "Linear View에 추가",
"float": "실수",
"targetNodeFieldDoesNotExist": "잘못된 모서리: 대상/입력 필드 {{node}}. {{field}}이(가) 없습니다",
"animatedEdges": "애니메이션 모서리",
@@ -152,6 +160,7 @@
"nodeTemplate": "노드 템플릿",
"nodeOpacity": "노드 불투명도",
"sourceNodeDoesNotExist": "잘못된 모서리: 소스/출력 노드 {{node}}이(가) 없습니다",
"noFieldsLinearview": "Linear View에 추가된 필드 없음",
"nodeSearch": "노드 검색",
"inputMayOnlyHaveOneConnection": "입력에 하나의 연결만 있을 수 있습니다",
"notes": "메모",
@@ -186,6 +195,7 @@
"notesDescription": "Workflow에 대한 메모 추가",
"colorCodeEdges": "색상-코드 선",
"targetNodeDoesNotExist": "잘못된 모서리: 대상/입력 노드 {{node}}이(가) 없습니다",
"mismatchedVersion": "잘못된 노드: {{type}} 유형의 {{node}} 노드에 일치하지 않는 버전이 있습니다(업데이트 해보시겠습니까?)",
"addNodeToolTip": "노드 추가(Shift+A, Space)",
"collectionOrScalarFieldType": "{{name}} 컬렉션|Scalar",
"nodeVersion": "노드 버전",
@@ -232,6 +242,7 @@
"next": "다음",
"cancelBatch": "Batch 취소",
"back": "back",
"batchFieldValues": "Batch 필드 값들",
"cancel": "취소",
"session": "세션",
"time": "시간",
@@ -285,6 +296,8 @@
"cacheSize": "캐시 크기"
},
"hrf": {
"enableHrf": "이용 가능한 고해상도 고정",
"upscaleMethod": "업스케일 방법",
"metadata": {
"strength": "고해상도 고정 강도",
"enabled": "고해상도 고정 사용",
@@ -295,10 +308,12 @@
"models": {
"noMatchingModels": "일치하는 모델 없음",
"loading": "로딩중",
"noMatchingLoRAs": "일치하는 LoRA 없음",
"noModelsAvailable": "사용 가능한 모델이 없음",
"addLora": "LoRA 추가",
"selectModel": "모델 선택",
"noRefinerModelsInstalled": "SDXL Refiner 모델이 설치되지 않음"
"noRefinerModelsInstalled": "SDXL Refiner 모델이 설치되지 않음",
"noLoRAsInstalled": "설치된 LoRA 없음"
},
"boards": {
"autoAddBoard": "자동 추가 Board",

View File

@@ -30,10 +30,12 @@
"ipAdapter": "IP-adapter",
"auto": "Autom.",
"controlNet": "ControlNet",
"imageFailedToLoad": "Kan afbeelding niet laden",
"learnMore": "Meer informatie",
"advanced": "Uitgebreid",
"file": "Bestand",
"installed": "Geïnstalleerd",
"notInstalled": "Niet $t(common.installed)",
"simple": "Eenvoudig",
"somethingWentWrong": "Er ging iets mis",
"add": "Voeg toe",
@@ -41,12 +43,14 @@
"details": "Details",
"outputs": "Uitvoeren",
"save": "Bewaar",
"nextPage": "Volgende pagina",
"blue": "Blauw",
"alpha": "Alfa",
"red": "Rood",
"editor": "Editor",
"folder": "Map",
"format": "structuur",
"goTo": "Ga naar",
"template": "Sjabloon",
"input": "Invoer",
"safetensors": "Safetensors",
@@ -58,6 +62,7 @@
"negativePrompt": "Negatieve prompt",
"selected": "Geselecteerd",
"orderBy": "Sorteer op",
"prevPage": "Vorige pagina",
"beta": "Bèta",
"copyError": "$t(gallery.copy) Fout",
"toResolve": "Op te lossen",
@@ -74,18 +79,21 @@
"delete": "Verwijder",
"direction": "Richting",
"error": "Fout",
"localSystem": "Lokaal systeem",
"unknownError": "Onbekende fout"
},
"gallery": {
"galleryImageSize": "Afbeeldingsgrootte",
"gallerySettings": "Instellingen galerij",
"autoSwitchNewImages": "Wissel autom. naar nieuwe afbeeldingen",
"noImagesInGallery": "Geen afbeeldingen om te tonen",
"deleteImage_one": "Verwijder afbeelding",
"deleteImage_other": "",
"deleteImagePermanent": "Verwijderde afbeeldingen kunnen niet worden hersteld.",
"autoAssignBoardOnClick": "Ken automatisch bord toe bij klikken",
"featuresWillReset": "Als je deze afbeelding verwijdert, dan worden deze functies onmiddellijk teruggezet.",
"loading": "Bezig met laden",
"unableToLoad": "Kan galerij niet laden",
"downloadSelection": "Download selectie",
"currentlyInUse": "Deze afbeelding is momenteel in gebruik door de volgende functies:",
"copy": "Kopieer",
@@ -191,10 +199,12 @@
"scaledHeight": "Geschaalde H",
"infillMethod": "Infill-methode",
"tileSize": "Grootte tegel",
"downloadImage": "Download afbeelding",
"usePrompt": "Hergebruik invoertekst",
"useSeed": "Hergebruik seed",
"useAll": "Hergebruik alles",
"info": "Info",
"showOptionsPanel": "Toon deelscherm Opties (O of T)",
"symmetry": "Symmetrie",
"cancel": {
"cancel": "Annuleer"
@@ -283,12 +293,15 @@
"baseModelChangedCleared_one": "Basismodel is gewijzigd: {{count}} niet-compatibel submodel weggehaald of uitgeschakeld",
"baseModelChangedCleared_other": "Basismodel is gewijzigd: {{count}} niet-compatibele submodellen weggehaald of uitgeschakeld",
"loadedWithWarnings": "Werkstroom geladen met waarschuwingen",
"setControlImage": "Ingesteld als controle-afbeelding",
"setNodeField": "Ingesteld als knooppuntveld",
"imageUploaded": "Afbeelding geüpload",
"addedToBoard": "Toegevoegd aan bord",
"workflowLoaded": "Werkstroom geladen",
"modelAddedSimple": "Model toegevoegd aan wachtrij",
"imageUploadFailed": "Fout bij uploaden afbeelding",
"workflowDeleted": "Werkstroom verwijderd",
"invalidUpload": "Ongeldige upload",
"problemRetrievingWorkflow": "Fout bij ophalen van werkstroom",
"parameters": "Parameters",
"modelImportCanceled": "Importeren model geannuleerd",
@@ -312,14 +325,17 @@
"zoomOutNodes": "Uitzoomen",
"fitViewportNodes": "Aanpassen aan beeld",
"hideMinimapnodes": "Minimap verbergen",
"showLegendNodes": "Typelegende veld tonen",
"zoomInNodes": "Inzoomen",
"showMinimapnodes": "Minimap tonen",
"hideLegendNodes": "Typelegende veld verbergen",
"reloadNodeTemplates": "Herlaad knooppuntsjablonen",
"loadWorkflow": "Laad werkstroom",
"downloadWorkflow": "Download JSON van werkstroom",
"scheduler": "Planner",
"missingTemplate": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een ontbrekend sjabloon (niet geïnstalleerd?)",
"workflowDescription": "Korte beschrijving",
"versionUnknown": " Versie onbekend",
"noNodeSelected": "Geen knooppunt gekozen",
"addNode": "Voeg knooppunt toe",
"unableToValidateWorkflow": "Kan werkstroom niet valideren",
@@ -333,7 +349,9 @@
"integer": "Geheel getal",
"nodeTemplate": "Sjabloon knooppunt",
"nodeOpacity": "Dekking knooppunt",
"unableToLoadWorkflow": "Fout bij laden werkstroom",
"snapToGrid": "Lijn uit op raster",
"noFieldsLinearview": "Geen velden toegevoegd aan lineaire weergave",
"nodeSearch": "Zoek naar knooppunten",
"updateNode": "Werk knooppunt bij",
"version": "Versie",
@@ -352,7 +370,9 @@
"edge": "Rand",
"animatedEdgesHelp": "Animeer gekozen randen en randen verbonden met de gekozen knooppunten",
"cannotDuplicateConnection": "Kan geen dubbele verbindingen maken",
"unknownTemplate": "Onbekend sjabloon",
"noWorkflow": "Geen werkstroom",
"removeLinearView": "Verwijder uit lineaire weergave",
"workflowTags": "Labels",
"fullyContainNodesHelp": "Knooppunten moeten zich volledig binnen het keuzevak bevinden om te worden gekozen",
"workflowValidation": "Validatiefout werkstroom",
@@ -377,11 +397,14 @@
"unknownField": "Onbekend veld",
"colorCodeEdges": "Kleurgecodeerde randen",
"unknownNode": "Onbekend knooppunt",
"mismatchedVersion": "Ongeldig knooppunt: knooppunt {{node}} van het soort {{type}} heeft een niet-overeenkomende versie (probeer het bij te werken?)",
"addNodeToolTip": "Voeg knooppunt toe (Shift+A, spatie)",
"loadingNodes": "Bezig met laden van knooppunten...",
"snapToGridHelp": "Lijn knooppunten uit op raster bij verplaatsing",
"workflowSettings": "Instellingen werkstroomeditor",
"addLinearView": "Voeg toe aan lineaire weergave",
"nodePack": "Knooppuntpakket",
"unknownInput": "Onbekende invoer: {{name}}",
"sourceNodeFieldDoesNotExist": "Ongeldige rand: bron-/uitvoerveld {{node}}.{{field}} bestaat niet",
"collectionFieldType": "Verzameling {{name}}",
"deletedInvalidEdge": "Ongeldige hoek {{source}} -> {{target}} verwijderd",
@@ -396,6 +419,7 @@
"sourceNodeDoesNotExist": "Ongeldige rand: bron-/uitvoerknooppunt {{node}} bestaat niet",
"unsupportedArrayItemType": "niet-ondersteunde soort van het array-onderdeel \"{{type}}\"",
"targetNodeFieldDoesNotExist": "Ongeldige rand: doel-/invoerveld {{node}}.{{field}} bestaat niet",
"reorderLinearView": "Herorden lineaire weergave",
"newWorkflowDesc": "Een nieuwe werkstroom aanmaken?",
"collectionOrScalarFieldType": "Verzameling|scalair {{name}}",
"newWorkflow": "Nieuwe werkstroom",
@@ -710,21 +734,27 @@
"refinerStart": "Startwaarde verfijning",
"scheduler": "Planner",
"cfgScale": "CFG-schaal",
"negStylePrompt": "Negatieve-stijlprompt",
"noModelsAvailable": "Geen modellen beschikbaar",
"refiner": "Verfijning",
"negAestheticScore": "Negatieve esthetische score",
"denoisingStrength": "Sterkte ontruising",
"refinermodel": "Verfijningsmodel",
"posAestheticScore": "Positieve esthetische score",
"concatPromptStyle": "Koppelen van prompt en stijl",
"loading": "Bezig met laden...",
"steps": "Stappen",
"posStylePrompt": "Positieve-stijlprompt",
"freePromptStyle": "Handmatige stijlprompt",
"refinerSteps": "Aantal stappen verfijner"
},
"models": {
"noMatchingModels": "Geen overeenkomend modellen",
"loading": "bezig met laden",
"noMatchingLoRAs": "Geen overeenkomende LoRA's",
"noModelsAvailable": "Geen modellen beschikbaar",
"selectModel": "Kies een model",
"noLoRAsInstalled": "Geen LoRA's geïnstalleerd",
"noRefinerModelsInstalled": "Geen SDXL-verfijningsmodellen geïnstalleerd",
"defaultVAE": "Standaard-VAE",
"lora": "LoRA",
@@ -792,12 +822,14 @@
}
},
"hrf": {
"upscaleMethod": "Opschaalmethode",
"metadata": {
"strength": "Sterkte oplossing voor hoge resolutie",
"method": "Methode oplossing voor hoge resolutie",
"enabled": "Oplossing voor hoge resolutie ingeschakeld"
},
"hrf": "Oplossing voor hoge resolutie"
"hrf": "Oplossing voor hoge resolutie",
"enableHrf": "Schakel oplossing in voor hoge resolutie"
},
"prompt": {
"addPromptTrigger": "Voeg prompttrigger toe",

View File

@@ -41,9 +41,11 @@
"somethingWentWrong": "Coś poszło nie tak",
"green": "Zielony",
"red": "Czerwony",
"imageFailedToLoad": "Nie można załadować obrazu",
"saveAs": "Zapisz jako",
"outputs": "Wyjścia",
"data": "Dane",
"localSystem": "System Lokalny",
"t2iAdapter": "Adapter T2I",
"selected": "Zaznaczone",
"warnings": "Ostrzeżenia",
@@ -62,10 +64,12 @@
"openInViewer": "Otwórz podgląd",
"safetensors": "Bezpieczniki",
"ok": "Ok",
"goTo": "Idź do",
"loadingImage": "wczytywanie zdjęcia",
"input": "Wejście",
"view": "Podgląd",
"learnMore": "Dowiedz się więcej",
"notInstalled": "Nie $t(common.installed)",
"loadingModel": "Wczytywanie modelu",
"postprocessing": "Przetwarzanie końcowe",
"random": "Losowo",
@@ -79,8 +83,10 @@
"delete": "Usuń",
"template": "Szablon",
"txt2img": "Tekst na obraz",
"prevPage": "Poprzednia strona",
"file": "Plik",
"toResolve": "Do rozwiązania",
"nextPage": "Następna strona",
"unknownError": "Nieznany błąd",
"placeholderSelectAModel": "Wybierz model",
"new": "Nowy",
@@ -93,6 +99,7 @@
"galleryImageSize": "Rozmiar obrazów",
"gallerySettings": "Ustawienia galerii",
"autoSwitchNewImages": "Przełączaj na nowe obrazy",
"noImagesInGallery": "Brak obrazów w galerii",
"gallery": "Galeria",
"alwaysShowImageSizeBadge": "Zawsze pokazuj odznakę wielkości obrazu",
"assetsTab": "Pliki, które wrzuciłeś do użytku w twoich projektach.",
@@ -121,10 +128,12 @@
"scaledHeight": "Sk. do wys.",
"infillMethod": "Metoda wypełniania",
"tileSize": "Rozmiar kafelka",
"downloadImage": "Pobierz obraz",
"usePrompt": "Skopiuj sugestie",
"useSeed": "Skopiuj inicjator",
"useAll": "Skopiuj wszystko",
"info": "Informacje"
"info": "Informacje",
"showOptionsPanel": "Pokaż panel ustawień"
},
"settings": {
"models": "Modele",
@@ -177,6 +186,8 @@
"selectedForAutoAdd": "Wybrany do automatycznego dodania",
"deleteBoard": "Usuń tablicę",
"clearSearch": "Usuń historię",
"hideBoards": "Ukryj tablice",
"viewBoards": "Zobacz tablice",
"addSharedBoard": "Dodaj udostępnioną tablicę",
"boards": "Tablice",
"addPrivateBoard": "Dodaj prywatną tablicę",
@@ -222,7 +233,8 @@
"strength": "Moc poprawki wysokiej rozdzielczości",
"method": "Metoda High Resolution Fix"
},
"hrf": "Poprawka \"Wysoka rozdzielczość\""
"hrf": "Poprawka \"Wysoka rozdzielczość\"",
"enableHrf": "Włącz poprawkę wysokiej rozdzielczości"
},
"queue": {
"cancelTooltip": "Anuluj aktualną pozycję",
@@ -284,6 +296,7 @@
"completed": "Zakończono",
"item": "Pozycja",
"failed": "Niepowodzenie",
"batchFieldValues": "Masowe Wartości pól",
"graphFailedToQueue": "NIe udało się dodać tabeli do kolejki",
"workflows": "Przepływy pracy",
"next": "Następny",

View File

@@ -17,7 +17,8 @@
"gallery": {
"galleryImageSize": "Tamanho da Imagem",
"gallerySettings": "Configurações de Galeria",
"autoSwitchNewImages": "Trocar para Novas Imagens Automaticamente"
"autoSwitchNewImages": "Trocar para Novas Imagens Automaticamente",
"noImagesInGallery": "Sem Imagens na Galeria"
},
"modelManager": {
"modelManager": "Gerente de Modelo",
@@ -73,10 +74,12 @@
"scaledHeight": "A Escalada",
"infillMethod": "Método de Preenchimento",
"tileSize": "Tamanho do Ladrilho",
"downloadImage": "Baixar Imagem",
"usePrompt": "Usar Prompt",
"useSeed": "Usar Seed",
"useAll": "Usar Todos",
"info": "Informações",
"showOptionsPanel": "Mostrar Painel de Opções",
"symmetry": "Simetria",
"copyImage": "Copiar imagem",
"denoisingStrength": "A força de remoção de ruído",

Some files were not shown because too many files have changed in this diff Show More