diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py index f02e4d476a..7926f76ce2 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 @@ -11,6 +10,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, @@ -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).") @@ -402,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""" @@ -496,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""" @@ -614,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.""" @@ -641,9 +643,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/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,), 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/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}`} 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 = ( 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..d164cb1208 100644 --- a/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx +++ b/invokeai/frontend/web/src/features/controlNet/components/ipAdapter/ParamIPAdapterModelSelect.tsx @@ -88,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} /> ); }; 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 []; diff --git a/pyproject.toml b/pyproject.toml index fb3317a82e..a5868c1fc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,16 +163,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" = ["**"]