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."""