From 6c8270dae2e8663c9bde7f7b3b1f7a47c9291ef0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:04:19 +1100 Subject: [PATCH 01/10] fix(ui): canvas staging area works after undo --- .../frontend/web/src/features/canvas/store/canvasSelectors.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts index 8f1e246aaa..509237944c 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSelectors.ts @@ -6,7 +6,9 @@ export const canvasSelector = (state: RootState): CanvasState => state.canvas; export const isStagingSelector = createSelector( [stateSelector], - ({ canvas }) => canvas.batchIds.length > 0 + ({ canvas }) => + canvas.batchIds.length > 0 || + canvas.layerState.stagingArea.images.length > 0 ); export const initialCanvasImageSelector = ( From dcbb25dfea38a6b6360b98156aca172acb751939 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:04:29 +1100 Subject: [PATCH 02/10] feat(ui): staging styling tweak --- .../canvas/components/IAICanvasStagingAreaToolbar.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx index 8bb45840d0..12b005387a 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx @@ -151,15 +151,10 @@ const IAICanvasStagingAreaToolbar = () => { isDisabled={!shouldShowStagingImage} /> {`${currentIndex + 1}/${total}`} Date: Tue, 3 Oct 2023 08:48:50 -0400 Subject: [PATCH 03/10] add font Inter-Regular.ttf to installed assets --- invokeai/app/invocations/facetools.py | 7 ++++--- pyproject.toml | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index f02e4d476a..53b4602a5b 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -11,6 +11,7 @@ from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageOps from PIL.Image import Image as ImageType from pydantic import validator +import invokeai.assets.fonts as font_assets from invokeai.app.invocations.baseinvocation import ( BaseInvocation, InputField, @@ -641,9 +642,9 @@ class FaceIdentifierInvocation(BaseInvocation): draw_mesh=False, ) - path = Path(__file__).resolve().parent.parent.parent - font_path = os.path.abspath(path / "assets/fonts/inter/Inter-Regular.ttf") - font = ImageFont.truetype(font_path, FONT_SIZE) + # Note - font may be found either in the repo if running an editable install, or in the venv if running a package install + font_path = [x for x in [Path(y, "inter/Inter-Regular.ttf") for y in font_assets.__path__] if x.exists()] + font = ImageFont.truetype(font_path[0].as_posix(), FONT_SIZE) # Paste face IDs on the output image draw = ImageDraw.Draw(image) diff --git a/pyproject.toml b/pyproject.toml index 21cda5eebd..da9dccd71d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,16 +161,16 @@ version = { attr = "invokeai.version.__version__" } [tool.setuptools.packages.find] "where" = ["."] "include" = [ - "invokeai.assets.web*","invokeai.version*", + "invokeai.assets.fonts*","invokeai.version*", "invokeai.generator*","invokeai.backend*", "invokeai.frontend*", "invokeai.frontend.web.dist*", "invokeai.frontend.web.static*", "invokeai.configs*", - "invokeai.app*","ldm*", + "invokeai.app*", ] [tool.setuptools.package-data] -"invokeai.assets.web" = ["**.png","**.js","**.woff2","**.css"] +"invokeai.assets.fonts" = ["**/*.ttf"] "invokeai.backend" = ["**.png"] "invokeai.configs" = ["*.example", "**/*.yaml", "*.txt"] "invokeai.frontend.web.dist" = ["**"] From 920c5dd68691447251997544829bc2f842b9d025 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Tue, 3 Oct 2023 08:53:47 -0400 Subject: [PATCH 04/10] remove unneeded os import --- invokeai/app/invocations/facetools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index 53b4602a5b..b13f0e81cc 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -1,5 +1,4 @@ import math -import os import re from pathlib import Path from typing import Optional, TypedDict From 24d73d484a4f5d960a983bf70b410b9ebf0f6a78 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 3 Oct 2023 13:28:15 -0400 Subject: [PATCH 05/10] IP adapter UI --- .../components/ipAdapter/IPAdapterPanel.tsx | 40 ++++++++++++++++--- .../ipAdapter/ParamIPAdapterImage.tsx | 3 +- .../ipAdapter/ParamIPAdapterModelSelect.tsx | 23 ++++++++++- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/IPAdapterPanel.tsx b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/IPAdapterPanel.tsx index b0a1fcc731..fa269fefdc 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/IPAdapterPanel.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/IPAdapterPanel.tsx @@ -5,8 +5,23 @@ import ParamIPAdapterFeatureToggle from './ParamIPAdapterFeatureToggle'; import ParamIPAdapterImage from './ParamIPAdapterImage'; import ParamIPAdapterModelSelect from './ParamIPAdapterModelSelect'; import ParamIPAdapterWeight from './ParamIPAdapterWeight'; +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from '../../../../app/store/store'; +import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions'; +import { useAppSelector } from '../../../../app/store/storeHooks'; + +const selector = createSelector( + stateSelector, + (state) => { + const { isIPAdapterEnabled } = state.controlNet; + + return { isIPAdapterEnabled }; + }, + defaultSelectorOptions +); const IPAdapterPanel = () => { + const { isIPAdapterEnabled } = useAppSelector(selector); return ( { gap: 3, paddingInline: 3, paddingBlock: 2, - paddingBottom: 5, borderRadius: 'base', position: 'relative', bg: 'base.250', @@ -24,10 +38,26 @@ const IPAdapterPanel = () => { }} > - - - - + {isIPAdapterEnabled && ( + <> + + + + + + + + + + )} ); }; diff --git a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterImage.tsx b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterImage.tsx index 9c5087d297..b8e3bdbae0 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterImage.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterImage.tsx @@ -66,7 +66,8 @@ const ParamIPAdapterImage = () => { layerStyle="second" sx={{ position: 'relative', - w: 'full', + h: 28, + w: 28, alignItems: 'center', justifyContent: 'center', aspectRatio: '1/1', diff --git a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx index 8fb738d025..4cf2352f6a 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx @@ -6,7 +6,7 @@ import { ipAdapterModelChanged } from 'features/controlNet/store/controlNetSlice import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToIPAdapterModelParam } from 'features/parameters/util/modelIdToIPAdapterModelParams'; import { forEach } from 'lodash-es'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models'; @@ -24,6 +24,27 @@ const ParamIPAdapterModelSelect = () => { const { data: ipAdapterModels } = useGetIPAdapterModelsQuery(); + const firstModel = useMemo(() => { + if (!ipAdapterModels || !Object.keys(ipAdapterModels.entities).length) { + return undefined; + } + const firstModelId = Object.keys(ipAdapterModels.entities)[0]; + + if (!firstModelId) { + return undefined; + } + + const firstModel = ipAdapterModels.entities[firstModelId]; + + return firstModel ? firstModel : undefined; + }, [ipAdapterModels]); + + useEffect(() => { + if (firstModel) { + dispatch(ipAdapterModelChanged(firstModel)); + } + }, [firstModel, dispatch]); + // grab the full model entity from the RTK Query cache const selectedModel = useMemo( () => From 069d8b581234daebcc84bc5e9b4112ee0c1617ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Oct 2023 08:37:51 +1100 Subject: [PATCH 06/10] feat(ui): move initial IP adapter model selection to listener --- invokeai/frontend/web/public/locales/en.json | 2 +- .../listeners/modelsLoaded.ts | 51 ++++++++++++++++++- .../ipAdapter/ParamIPAdapterModelSelect.tsx | 33 +++--------- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 34e4dd79d6..b635db997a 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -697,7 +697,7 @@ "noLoRAsAvailable": "No LoRAs available", "noMatchingLoRAs": "No matching LoRAs", "noMatchingModels": "No matching Models", - "noModelsAvailable": "No Modelss available", + "noModelsAvailable": "No models available", "selectLoRA": "Select a LoRA", "selectModel": "Select a Model" }, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts index 4d30ee3b8b..e7dc4e9291 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/modelsLoaded.ts @@ -1,5 +1,8 @@ import { logger } from 'app/logging/logger'; -import { controlNetRemoved } from 'features/controlNet/store/controlNetSlice'; +import { + controlNetRemoved, + ipAdapterModelChanged, +} from 'features/controlNet/store/controlNetSlice'; import { loraRemoved } from 'features/lora/store/loraSlice'; import { modelChanged, @@ -16,12 +19,14 @@ import { } from 'features/sdxl/store/sdxlSlice'; import { forEach, some } from 'lodash-es'; import { + ipAdapterModelsAdapter, mainModelsAdapter, modelsApi, vaeModelsAdapter, } from 'services/api/endpoints/models'; import { TypeGuardFor } from 'services/api/types'; import { startAppListening } from '..'; +import { zIPAdapterModel } from 'features/nodes/types/types'; export const addModelsLoadedListener = () => { startAppListening({ @@ -234,6 +239,50 @@ export const addModelsLoadedListener = () => { }); }, }); + startAppListening({ + matcher: modelsApi.endpoints.getIPAdapterModels.matchFulfilled, + effect: async (action, { getState, dispatch }) => { + // ControlNet models loaded - need to remove missing ControlNets from state + const log = logger('models'); + log.info( + { models: action.payload.entities }, + `IP Adapter models loaded (${action.payload.ids.length})` + ); + + const { model } = getState().controlNet.ipAdapterInfo; + + const isModelAvailable = some( + action.payload.entities, + (m) => + m?.model_name === model?.model_name && + m?.base_model === model?.base_model + ); + + if (isModelAvailable) { + return; + } + + const firstModel = ipAdapterModelsAdapter + .getSelectors() + .selectAll(action.payload)[0]; + + if (!firstModel) { + dispatch(ipAdapterModelChanged(null)); + } + + const result = zIPAdapterModel.safeParse(firstModel); + + if (!result.success) { + log.error( + { error: result.error.format() }, + 'Failed to parse IP Adapter model' + ); + return; + } + + dispatch(ipAdapterModelChanged(result.data)); + }, + }); startAppListening({ matcher: modelsApi.endpoints.getTextualInversionModels.matchFulfilled, effect: async (action) => { diff --git a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx index 4cf2352f6a..d164cb1208 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx @@ -6,7 +6,7 @@ import { ipAdapterModelChanged } from 'features/controlNet/store/controlNetSlice import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { modelIdToIPAdapterModelParam } from 'features/parameters/util/modelIdToIPAdapterModelParams'; import { forEach } from 'lodash-es'; -import { memo, useCallback, useEffect, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetIPAdapterModelsQuery } from 'services/api/endpoints/models'; @@ -24,27 +24,6 @@ const ParamIPAdapterModelSelect = () => { const { data: ipAdapterModels } = useGetIPAdapterModelsQuery(); - const firstModel = useMemo(() => { - if (!ipAdapterModels || !Object.keys(ipAdapterModels.entities).length) { - return undefined; - } - const firstModelId = Object.keys(ipAdapterModels.entities)[0]; - - if (!firstModelId) { - return undefined; - } - - const firstModel = ipAdapterModels.entities[firstModelId]; - - return firstModel ? firstModel : undefined; - }, [ipAdapterModels]); - - useEffect(() => { - if (firstModel) { - dispatch(ipAdapterModelChanged(firstModel)); - } - }, [firstModel, dispatch]); - // grab the full model entity from the RTK Query cache const selectedModel = useMemo( () => @@ -109,12 +88,16 @@ const ParamIPAdapterModelSelect = () => { className="nowheel nodrag" tooltip={selectedModel?.description} value={selectedModel?.id ?? null} - placeholder="Pick one" - error={!selectedModel} + placeholder={ + data.length > 0 + ? t('models.selectModel') + : t('models.noModelsAvailable') + } + error={!selectedModel && data.length > 0} data={data} onChange={handleValueChanged} sx={{ width: '100%' }} - disabled={!isEnabled} + disabled={!isEnabled || data.length === 0} /> ); }; From f4ba7be9189b0c35ed772c291a6b7105a93b747d Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Tue, 3 Oct 2023 12:05:07 -0400 Subject: [PATCH 07/10] refetch baord list when image is starred or unstarred --- .../frontend/web/src/services/api/endpoints/images.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts index e653a6ec3e..239dfaa982 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/images.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts @@ -473,6 +473,7 @@ export const imagesApi = api.injectEndpoints({ if (images[0]) { const categories = getCategories(images[0]); const boardId = images[0].board_id; + return [ { type: 'ImageList', @@ -481,6 +482,10 @@ export const imagesApi = api.injectEndpoints({ categories, }), }, + { + type: 'Board', + id: boardId, + }, ]; } return []; @@ -595,6 +600,10 @@ export const imagesApi = api.injectEndpoints({ categories, }), }, + { + type: 'Board', + id: boardId, + }, ]; } return []; From 5a1019d8589ef7ae72bf2039ef6ec3de29bfb573 Mon Sep 17 00:00:00 2001 From: maryhipp Date: Tue, 3 Oct 2023 12:08:00 -0400 Subject: [PATCH 08/10] sort by starred and then created_at to get board cover image --- invokeai/app/services/image_record_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/app/services/image_record_storage.py b/invokeai/app/services/image_record_storage.py index f743fd1fe2..21afcaf0bf 100644 --- a/invokeai/app/services/image_record_storage.py +++ b/invokeai/app/services/image_record_storage.py @@ -584,7 +584,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase): FROM images JOIN board_images ON images.image_name = board_images.image_name WHERE board_images.board_id = ? - ORDER BY images.created_at DESC + ORDER BY images.starred DESC, images.created_at DESC LIMIT 1; """, (board_id,), From 67366921c0688597fc88dc3f20f41d6f1ba989d4 Mon Sep 17 00:00:00 2001 From: ymgenesis Date: Tue, 3 Oct 2023 18:05:09 +0200 Subject: [PATCH 09/10] add checkbounds bool - don't check bounds on first detection before chunking, allows larger faces to be detected --- invokeai/app/invocations/facetools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index b13f0e81cc..7965b69619 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -138,6 +138,7 @@ def generate_face_box_mask( chunk_x_offset: int = 0, chunk_y_offset: int = 0, draw_mesh: bool = True, + check_bounds: bool = True, ) -> list[FaceResultData]: result = [] mask_pil = None @@ -217,7 +218,7 @@ def generate_face_box_mask( im_width, im_height = pil_image.size over_w = im_width * 0.1 over_h = im_height * 0.1 - if ( + if not check_bounds or ( (left_side >= -over_w) and (right_side < im_width + over_w) and (top_side >= -over_h) @@ -345,6 +346,7 @@ def get_faces_list( chunk_x_offset=0, chunk_y_offset=0, draw_mesh=draw_mesh, + check_bounds=False, ) if should_chunk or len(result) == 0: context.services.logger.info("FaceTools --> Chunking image (chunk toggled on, or no face found in full image).") From dedead672f2e9f995b6b96d7ada37456dab15b55 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:27:13 +1100 Subject: [PATCH 10/10] chore(facetools): bump node patch versions The helper function `generate_face_box_mask()` had a bug that prevented larger faces from being detected in some situations. This is resolved, and its dependent nodes (all the FaceTools nodes) have a patch version bump. --- invokeai/app/invocations/facetools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index 7965b69619..7926f76ce2 100644 --- a/invokeai/app/invocations/facetools.py +++ b/invokeai/app/invocations/facetools.py @@ -404,7 +404,7 @@ def get_faces_list( return all_faces -@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.0.0") +@invocation("face_off", title="FaceOff", tags=["image", "faceoff", "face", "mask"], category="image", version="1.0.1") class FaceOffInvocation(BaseInvocation): """Bound, extract, and mask a face from an image using MediaPipe detection""" @@ -498,7 +498,7 @@ class FaceOffInvocation(BaseInvocation): return output -@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.0.0") +@invocation("face_mask_detection", title="FaceMask", tags=["image", "face", "mask"], category="image", version="1.0.1") class FaceMaskInvocation(BaseInvocation): """Face mask creation using mediapipe face detection""" @@ -616,7 +616,7 @@ class FaceMaskInvocation(BaseInvocation): @invocation( - "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.0.0" + "face_identifier", title="FaceIdentifier", tags=["image", "face", "identifier"], category="image", version="1.0.1" ) class FaceIdentifierInvocation(BaseInvocation): """Outputs an image with detected face IDs printed on each face. For use with other FaceTools."""