Skip to content

Commit 038010a

Browse files
maryhippMary HippMary Hipppsychedelicious
authored
feat(ui): prompt expansion (#8140)
* initializing prompt expansion and putting response in prompt box working for all methods * properly disable UI and show loading state on prompt box when there is a pending prompt expansion item * misc wrapup: disable apploying prompt templates, dont block textarea resize handle * update progress to differentiate between prompt expansion and non * cleanup * lint * more cleanup * add image to background of loading state * add allowPromptExpansion for front-end gating * updated readiness text for needing to accept or discard * fix tsc * lint * lint * refactor(ui): prompt expansion logic * tidy(ui): remove unnecessary changes * revert(ui): unused arg on useImageUploadButton * feat(ui): simplify prompt expansion state * set pending for dragndrop and context menu * add readiness logic for generate tab * missing translation * update error handling for prompt expansion --------- Co-authored-by: Mary Hipp <maryhipp@Marys-Air.lan> Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local> Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
1 parent 2dd1bc5 commit 038010a

File tree

20 files changed

+739
-71
lines changed

20 files changed

+739
-71
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,16 @@
225225
"prompt": {
226226
"addPromptTrigger": "Add Prompt Trigger",
227227
"compatibleEmbeddings": "Compatible Embeddings",
228-
"noMatchingTriggers": "No matching triggers"
228+
"noMatchingTriggers": "No matching triggers",
229+
"generateFromImage": "Generate prompt from image",
230+
"expandCurrentPrompt": "Expand Current Prompt",
231+
"uploadImageForPromptGeneration": "Upload Image for Prompt Generation",
232+
"expandingPrompt": "Expanding prompt...",
233+
"resultTitle": "Prompt Expansion Complete",
234+
"resultSubtitle": "Choose how to handle the expanded prompt:",
235+
"replace": "Replace",
236+
"insert": "Insert",
237+
"discard": "Discard"
229238
},
230239
"queue": {
231240
"queue": "Queue",
@@ -342,7 +351,7 @@
342351
"copy": "Copy",
343352
"currentlyInUse": "This image is currently in use in the following features:",
344353
"drop": "Drop",
345-
"dropOrUpload": "$t(gallery.drop) or Upload",
354+
"dropOrUpload": "Drop or Upload",
346355
"dropToUpload": "$t(gallery.drop) to Upload",
347356
"deleteImage_one": "Delete Image",
348357
"deleteImage_other": "Delete {{count}} Images",
@@ -396,7 +405,8 @@
396405
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit.",
397406
"openViewer": "Open Viewer",
398407
"closeViewer": "Close Viewer",
399-
"move": "Move"
408+
"move": "Move",
409+
"useForPromptGeneration": "Use for Prompt Generation"
400410
},
401411
"hotkeys": {
402412
"hotkeys": "Hotkeys",
@@ -938,7 +948,8 @@
938948
"selectModel": "Select a Model",
939949
"noLoRAsInstalled": "No LoRAs installed",
940950
"noRefinerModelsInstalled": "No SDXL Refiner models installed",
941-
"defaultVAE": "Default VAE"
951+
"defaultVAE": "Default VAE",
952+
"noCompatibleLoRAs": "No Compatible LoRAs"
942953
},
943954
"nodes": {
944955
"arithmeticSequence": "Arithmetic Sequence",
@@ -1188,7 +1199,9 @@
11881199
"canvasIsSelectingObject": "Canvas is busy (selecting object)",
11891200
"noPrompts": "No prompts generated",
11901201
"noNodesInGraph": "No nodes in graph",
1191-
"systemDisconnected": "System disconnected"
1202+
"systemDisconnected": "System disconnected",
1203+
"promptExpansionPending": "Prompt expansion in progress",
1204+
"promptExpansionResultPending": "Please accept or discard your prompt expansion result"
11921205
},
11931206
"maskBlur": "Mask Blur",
11941207
"negativePromptPlaceholder": "Negative Prompt",
@@ -1389,7 +1402,12 @@
13891402
"fluxKontextIncompatibleGenerationMode": "Flux Kontext supports Text to Image only. Use other models for Image to Image, Inpainting and Outpainting tasks.",
13901403
"problemUnpublishingWorkflow": "Problem Unpublishing Workflow",
13911404
"problemUnpublishingWorkflowDescription": "There was a problem unpublishing the workflow. Please try again.",
1392-
"workflowUnpublished": "Workflow Unpublished"
1405+
"workflowUnpublished": "Workflow Unpublished",
1406+
"sentToCanvas": "Sent to Canvas",
1407+
"sentToUpscale": "Sent to Upscale",
1408+
"promptGenerationStarted": "Prompt generation started",
1409+
"uploadAndPromptGenerationFailed": "Failed to upload image and generate prompt",
1410+
"promptExpansionFailed": "Prompt expansion failed"
13931411
},
13941412
"popovers": {
13951413
"clipSkip": {

invokeai/frontend/web/src/app/types/invokeai.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export type AppConfig = {
7878
allowPrivateStylePresets: boolean;
7979
allowClientSideUpload: boolean;
8080
allowPublishWorkflows: boolean;
81+
allowPromptExpansion: boolean;
8182
disabledTabs: TabName[];
8283
disabledFeatures: AppFeature[];
8384
disabledSDFeatures: SDFeature[];

invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ type UseImageUploadButtonArgs =
2121
isDisabled?: boolean;
2222
allowMultiple: false;
2323
onUpload?: (imageDTO: ImageDTO) => void;
24+
onUploadStarted?: (files: File) => void;
25+
onError?: (error: unknown) => void;
2426
}
2527
| {
2628
isDisabled?: boolean;
2729
allowMultiple: true;
2830
onUpload?: (imageDTOs: ImageDTO[]) => void;
31+
onUploadStarted?: (files: File[]) => void;
32+
onError?: (error: unknown) => void;
2933
};
3034

3135
const log = logger('gallery');
@@ -49,7 +53,13 @@ const log = logger('gallery');
4953
* <Button {...getUploadButtonProps()} /> // will open the file dialog on click
5054
* <input {...getUploadInputProps()} /> // hidden, handles native upload functionality
5155
*/
52-
export const useImageUploadButton = ({ onUpload, isDisabled, allowMultiple }: UseImageUploadButtonArgs) => {
56+
export const useImageUploadButton = ({
57+
onUpload,
58+
isDisabled,
59+
allowMultiple,
60+
onUploadStarted,
61+
onError,
62+
}: UseImageUploadButtonArgs) => {
5363
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
5464
const isClientSideUploadEnabled = useAppSelector(selectIsClientSideUploadEnabled);
5565
const [uploadImage, request] = useUploadImageMutation();
@@ -71,6 +81,7 @@ export const useImageUploadButton = ({ onUpload, isDisabled, allowMultiple }: Us
7181
}
7282
const file = files[0];
7383
assert(file !== undefined); // should never happen
84+
onUploadStarted?.(file);
7485
const imageDTO = await uploadImage({
7586
file,
7687
image_category: 'user',
@@ -82,6 +93,8 @@ export const useImageUploadButton = ({ onUpload, isDisabled, allowMultiple }: Us
8293
onUpload(imageDTO);
8394
}
8495
} else {
96+
onUploadStarted?.(files);
97+
8598
let imageDTOs: ImageDTO[] = [];
8699
if (isClientSideUploadEnabled && files.length > 1) {
87100
imageDTOs = await Promise.all(files.map((file, i) => clientSideUpload(file, i)));
@@ -102,14 +115,25 @@ export const useImageUploadButton = ({ onUpload, isDisabled, allowMultiple }: Us
102115
}
103116
}
104117
} catch (error) {
118+
onError?.(error);
105119
toast({
106120
id: 'UPLOAD_FAILED',
107121
title: t('toast.imageUploadFailed'),
108122
status: 'error',
109123
});
110124
}
111125
},
112-
[allowMultiple, autoAddBoardId, onUpload, uploadImage, isClientSideUploadEnabled, clientSideUpload, t]
126+
[
127+
allowMultiple,
128+
onUploadStarted,
129+
uploadImage,
130+
autoAddBoardId,
131+
onUpload,
132+
isClientSideUploadEnabled,
133+
clientSideUpload,
134+
onError,
135+
t,
136+
]
113137
);
114138

115139
const onDropRejected = useCallback(

invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ export class CanvasStateApiModule extends CanvasModuleBase {
270270
outputNodeId: string;
271271
options?: RunGraphOptions;
272272
}): Promise<ImageDTO> => {
273-
const dependencies = buildRunGraphDependencies(this.store, this.manager.socket);
273+
const dependencies = buildRunGraphDependencies(this.store.dispatch, this.manager.socket);
274274

275275
const { output } = await runGraph({
276276
dependencies,

invokeai/frontend/web/src/features/controlLayers/store/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
RgbColor,
2020
T2IAdapterConfig,
2121
} from 'features/controlLayers/store/types';
22+
import type { ImageField } from 'features/nodes/types/common';
2223
import type { ImageDTO } from 'services/api/types';
2324
import { assert } from 'tsafe';
2425
import type { PartialDeep } from 'type-fest';
@@ -59,6 +60,8 @@ export const imageDTOToImageWithDims = ({ image_name, width, height }: ImageDTO)
5960
height,
6061
});
6162

63+
export const imageDTOToImageField = ({ image_name }: ImageDTO): ImageField => ({ image_name });
64+
6265
const DEFAULT_RG_MASK_FILL_COLORS: RgbColor[] = [
6366
{ r: 121, g: 157, b: 219 }, // rgb(121, 157, 219)
6467
{ r: 131, g: 214, b: 131 }, // rgb(131, 214, 131)

invokeai/frontend/web/src/features/dnd/dnd.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
2323
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
2424
import { type FieldIdentifier, isImageFieldCollectionInputInstance } from 'features/nodes/types/field';
25+
import { expandPrompt } from 'features/prompt/PromptExpansion/expand';
26+
import { promptExpansionApi } from 'features/prompt/PromptExpansion/state';
2527
import type { ImageDTO } from 'services/api/types';
2628
import type { JsonObject } from 'type-fest';
2729

@@ -515,23 +517,48 @@ export const removeImageFromBoardDndTarget: DndTarget<
515517

516518
//#endregion
517519

520+
//#region Prompt Generation From Image
521+
const _promptGenerationFromImage = buildTypeAndKey('prompt-generation-from-image');
522+
export type PromptGenerationFromImageDndTargetData = DndData<
523+
typeof _promptGenerationFromImage.type,
524+
typeof _promptGenerationFromImage.key,
525+
void
526+
>;
527+
export const promptGenerationFromImageDndTarget: DndTarget<
528+
PromptGenerationFromImageDndTargetData,
529+
SingleImageDndSourceData
530+
> = {
531+
..._promptGenerationFromImage,
532+
typeGuard: buildTypeGuard(_promptGenerationFromImage.key),
533+
getData: buildGetData(_promptGenerationFromImage.key, _promptGenerationFromImage.type),
534+
isValid: ({ sourceData }) => {
535+
if (singleImageDndSource.typeGuard(sourceData)) {
536+
return true;
537+
}
538+
return false;
539+
},
540+
handler: ({ sourceData, dispatch, getState }) => {
541+
const { imageDTO } = sourceData.payload;
542+
promptExpansionApi.setPending(imageDTO);
543+
expandPrompt({ dispatch, getState, imageDTO });
544+
},
545+
};
546+
//#endregion
547+
518548
export const dndTargets = [
519-
// Single Image
520549
setGlobalReferenceImageDndTarget,
550+
addGlobalReferenceImageDndTarget,
521551
setRegionalGuidanceReferenceImageDndTarget,
522552
setUpscaleInitialImageDndTarget,
523553
setNodeImageFieldImageDndTarget,
554+
addImagesToNodeImageFieldCollectionDndTarget,
524555
setComparisonImageDndTarget,
525556
newCanvasEntityFromImageDndTarget,
526-
replaceCanvasEntityObjectsWithImageDndTarget,
527-
addImageToBoardDndTarget,
528-
removeImageFromBoardDndTarget,
529557
newCanvasFromImageDndTarget,
530-
addGlobalReferenceImageDndTarget,
531-
// Single or Multiple Image
558+
replaceCanvasEntityObjectsWithImageDndTarget,
532559
addImageToBoardDndTarget,
533560
removeImageFromBoardDndTarget,
534-
addImagesToNodeImageFieldCollectionDndTarget,
561+
promptGenerationFromImageDndTarget,
535562
] as const;
536563

537564
export type AnyDndTarget = (typeof dndTargets)[number];
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { MenuItem } from '@invoke-ai/ui-library';
2+
import { useStore } from '@nanostores/react';
3+
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
4+
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
5+
import { expandPrompt } from 'features/prompt/PromptExpansion/expand';
6+
import { promptExpansionApi } from 'features/prompt/PromptExpansion/state';
7+
import { selectAllowPromptExpansion } from 'features/system/store/configSlice';
8+
import { toast } from 'features/toast/toast';
9+
import { memo, useCallback } from 'react';
10+
import { useTranslation } from 'react-i18next';
11+
import { PiTextTBold } from 'react-icons/pi';
12+
13+
export const ImageMenuItemUseForPromptGeneration = memo(() => {
14+
const { t } = useTranslation();
15+
const { dispatch, getState } = useAppStore();
16+
const imageDTO = useImageDTOContext();
17+
const { isPending } = useStore(promptExpansionApi.$state);
18+
const isPromptExpansionEnabled = useAppSelector(selectAllowPromptExpansion);
19+
20+
const handleUseForPromptGeneration = useCallback(() => {
21+
promptExpansionApi.setPending(imageDTO);
22+
expandPrompt({ dispatch, getState, imageDTO });
23+
toast({
24+
id: 'PROMPT_GENERATION_STARTED',
25+
title: t('toast.promptGenerationStarted'),
26+
status: 'info',
27+
});
28+
}, [dispatch, getState, imageDTO, t]);
29+
30+
if (!isPromptExpansionEnabled) {
31+
return null;
32+
}
33+
34+
return (
35+
<MenuItem
36+
icon={<PiTextTBold />}
37+
onClickCapture={handleUseForPromptGeneration}
38+
id="use-for-prompt-generation"
39+
isDisabled={isPending}
40+
>
41+
{t('gallery.useForPromptGeneration')}
42+
</MenuItem>
43+
);
44+
});
45+
46+
ImageMenuItemUseForPromptGeneration.displayName = 'ImageMenuItemUseForPromptGeneration';

invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ImageMenuItemSelectForCompare } from 'features/gallery/components/Image
1414
import { ImageMenuItemSendToUpscale } from 'features/gallery/components/ImageContextMenu/ImageMenuItemSendToUpscale';
1515
import { ImageMenuItemStarUnstar } from 'features/gallery/components/ImageContextMenu/ImageMenuItemStarUnstar';
1616
import { ImageMenuItemUseAsRefImage } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseAsRefImage';
17+
import { ImageMenuItemUseForPromptGeneration } from 'features/gallery/components/ImageContextMenu/ImageMenuItemUseForPromptGeneration';
1718
import { ImageDTOContextProvider } from 'features/gallery/contexts/ImageDTOContext';
1819
import { memo } from 'react';
1920
import type { ImageDTO } from 'services/api/types';
@@ -38,6 +39,7 @@ const SingleSelectionMenuItems = ({ imageDTO }: SingleSelectionMenuItemsProps) =
3839
<ImageMenuItemMetadataRecallActions />
3940
<MenuDivider />
4041
<ImageMenuItemSendToUpscale />
42+
<ImageMenuItemUseForPromptGeneration />
4143
<ImageMenuItemUseAsRefImage />
4244
<ImageMenuItemNewCanvasFromImageSubMenu />
4345
<ImageMenuItemNewLayerFromImageSubMenu />

0 commit comments

Comments
 (0)