From d9ce9c62acd0cea26656133369b9606d87ca5130 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 16:35:04 +1000 Subject: [PATCH 1/5] feat(ui): disable invoke button when t2i adapter used w/ image dims that are not multiples of 64 --- invokeai/frontend/web/public/locales/en.json | 3 ++- invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index b36fe0e26c..5006c257ca 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -934,7 +934,8 @@ "noModelSelected": "No model selected", "noPrompts": "No prompts generated", "noNodesInGraph": "No nodes in graph", - "systemDisconnected": "System disconnected" + "systemDisconnected": "System disconnected", + "t2iAdapterMismatchedDimensions": "T2I Adapters require image dimensions to be multiples of 64" }, "maskBlur": "Mask Blur", "negativePromptPlaceholder": "Negative Prompt", diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 2aac5b8e72..09ffd445de 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -29,6 +29,7 @@ const selector = createMemoizedSelector( ], (controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => { const { model } = generation; + const { size } = controlLayers.present; const { positivePrompt } = controlLayers.present; const { isConnected } = system; @@ -143,6 +144,9 @@ const selector = createMemoizedSelector( }) ); } + if (ca.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) { + reasons.push(i18n.t('parameters.invoke.t2iAdapterMismatchedDimensions')); + } }); } else { // Handling for all other tabs From 19f5a9c3a91e2c93889742f62915615da91dcd5e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 18:08:11 +1000 Subject: [PATCH 2/5] feat(ui): better invoke button checks - Improved/more thorough checking before invoking for control layers - Improved styling for the tooltip --- invokeai/frontend/web/public/locales/en.json | 14 +- .../src/common/hooks/useIsReadyToEnqueue.ts | 169 ++++++++++-------- .../components/InvokeQueueBackButton.tsx | 39 ++-- .../queue/components/QueueButtonTooltip.tsx | 26 ++- .../queue/components/QueueFrontButton.tsx | 19 +- .../FloatingParametersPanelButtons.tsx | 21 +-- 6 files changed, 173 insertions(+), 115 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 5006c257ca..ed94dd09f4 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -935,7 +935,19 @@ "noPrompts": "No prompts generated", "noNodesInGraph": "No nodes in graph", "systemDisconnected": "System disconnected", - "t2iAdapterMismatchedDimensions": "T2I Adapters require image dimensions to be multiples of 64" + "layer": { + "initialImageNoImageSelected": "no initial image selected", + "controlAdapterNoModelSelected": "no Control Adapter model selected", + "controlAdapterIncompatibleBaseModel": "incompatible Control Adapter base model", + "controlAdapterNoImageSelected": "no Control Adapter image selected", + "controlAdapterImageNotProcessed": "Control Adapter image not processed", + "t2iAdapterIncompatibleDimensions": "T2I Adapter requires image dimension to be multiples of 64", + "ipAdapterNoModelSelected": "no IP adapter selected", + "ipAdapterIncompatibleBaseModel": "incompatible IP Adapter base model", + "ipAdapterNoImageSelected": "no IP Adapter image selected", + "rgNoPromptsOrIPAdapters": "no text prompts or IP Adapters", + "rgNoRegion": "no region selected" + } }, "maskBlur": "Mask Blur", "negativePromptPlaceholder": "Negative Prompt", diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index 09ffd445de..3c863d0c93 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -6,6 +6,7 @@ import { } from 'features/controlAdapters/store/controlAdaptersSlice'; import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types'; import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; +import type { Layer } from 'features/controlLayers/store/types'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; @@ -14,9 +15,16 @@ import { selectGenerationSlice } from 'features/parameters/store/generationSlice import { selectSystemSlice } from 'features/system/store/systemSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import i18n from 'i18next'; -import { forEach } from 'lodash-es'; +import { forEach, upperFirst } from 'lodash-es'; import { getConnectedEdges } from 'reactflow'; +const LAYER_TYPE_TO_TKEY: Record = { + initial_image_layer: 'controlLayers.globalInitialImage', + control_adapter_layer: 'controlLayers.globalControlAdapter', + ip_adapter_layer: 'controlLayers.globalIPAdapter', + regional_guidance_layer: 'controlLayers.regionalGuidance', +}; + const selector = createMemoizedSelector( [ selectControlAdaptersSlice, @@ -34,17 +42,17 @@ const selector = createMemoizedSelector( const { isConnected } = system; - const reasons: string[] = []; + const reasons: { prefix?: string; content: string }[] = []; // Cannot generate if not connected if (!isConnected) { - reasons.push(i18n.t('parameters.invoke.systemDisconnected')); + reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') }); } if (activeTabName === 'workflows') { if (nodes.shouldValidateGraph) { if (!nodes.nodes.length) { - reasons.push(i18n.t('parameters.invoke.noNodesInGraph')); + reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') }); } nodes.nodes.forEach((node) => { @@ -56,7 +64,7 @@ const selector = createMemoizedSelector( if (!nodeTemplate) { // Node type not found - reasons.push(i18n.t('parameters.invoke.missingNodeTemplate')); + reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') }); return; } @@ -69,17 +77,17 @@ const selector = createMemoizedSelector( ); if (!fieldTemplate) { - reasons.push(i18n.t('parameters.invoke.missingFieldTemplate')); + reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') }); return; } if (fieldTemplate.required && field.value === undefined && !hasConnection) { - reasons.push( - i18n.t('parameters.invoke.missingInputForField', { + reasons.push({ + content: i18n.t('parameters.invoke.missingInputForField', { nodeLabel: node.data.label || nodeTemplate.title, fieldLabel: field.label || fieldTemplate.title, - }) - ); + }), + }); return; } }); @@ -87,65 +95,94 @@ const selector = createMemoizedSelector( } } else { if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) { - reasons.push(i18n.t('parameters.invoke.noPrompts')); + reasons.push({ content: i18n.t('parameters.invoke.noPrompts') }); } if (!model) { - reasons.push(i18n.t('parameters.invoke.noModelSelected')); + reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') }); } if (activeTabName === 'generation') { // Handling for generation tab controlLayers.present.layers .filter((l) => l.isEnabled) - .flatMap((l) => { + .forEach((l, i) => { + const layerLiteral = i18n.t('controlLayers.layers_one'); + const layerNumber = i + 1; + const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]); + const prefix = `${layerLiteral} #${layerNumber} (${layerType})`; + const problems: string[] = []; if (l.type === 'control_adapter_layer') { - return l.controlAdapter; - } else if (l.type === 'ip_adapter_layer') { - return l.ipAdapter; - } else if (l.type === 'regional_guidance_layer') { - return l.ipAdapters; + // Must have model + if (!l.controlAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected')); + } + // Model base must match + if (l.controlAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel')); + } + // Must have a control image OR, if it has a processor, it must have a processed image + if (!l.controlAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected')); + } else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) { + problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed')); + } + // T2I Adapters require images have dimensions that are multiples of 64 + if (l.controlAdapter.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) { + problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions')); + } } - return []; - }) - .forEach((ca, i) => { - const hasNoModel = !ca.model; - const mismatchedModelBase = ca.model?.base !== model?.base; - const hasNoImage = !ca.image; - const imageNotProcessed = - (ca.type === 'controlnet' || ca.type === 't2i_adapter') && !ca.processedImage && ca.processorConfig; - if (hasNoModel) { - reasons.push( - i18n.t('parameters.invoke.noModelForControlAdapter', { - number: i + 1, - }) - ); + if (l.type === 'ip_adapter_layer') { + // Must have model + if (!l.ipAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); + } + // Model base must match + if (l.ipAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); + } + // Must have an image + if (!l.ipAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); + } } - if (mismatchedModelBase) { - // This should never happen, just a sanity check - reasons.push( - i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { - number: i + 1, - }) - ); + + if (l.type === 'initial_image_layer') { + // Must have an image + if (!l.image) { + problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected')); + } } - if (hasNoImage) { - reasons.push( - i18n.t('parameters.invoke.noControlImageForControlAdapter', { - number: i + 1, - }) - ); + + if (l.type === 'regional_guidance_layer') { + // Must have a region + if (l.maskObjects.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoRegion')); + } + // Must have at least 1 prompt or IP Adapter + if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) { + problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters')); + } + l.ipAdapters.forEach((ipAdapter) => { + // Must have model + if (!ipAdapter.model) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected')); + } + // Model base must match + if (ipAdapter.model?.base !== model?.base) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel')); + } + // Must have an image + if (!ipAdapter.image) { + problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected')); + } + }); } - if (imageNotProcessed) { - reasons.push( - i18n.t('parameters.invoke.imageNotProcessedForControlAdapter', { - number: i + 1, - }) - ); - } - if (ca.type === 't2i_adapter' && (size.width % 64 !== 0 || size.height % 64 !== 0)) { - reasons.push(i18n.t('parameters.invoke.t2iAdapterMismatchedDimensions')); + + if (problems.length) { + const content = upperFirst(problems.join(', ')); + reasons.push({ prefix, content }); } }); } else { @@ -158,29 +195,19 @@ const selector = createMemoizedSelector( } if (!ca.model) { - reasons.push( - i18n.t('parameters.invoke.noModelForControlAdapter', { - number: i + 1, - }) - ); + reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) }); } else if (ca.model.base !== model?.base) { // This should never happen, just a sanity check - reasons.push( - i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { - number: i + 1, - }) - ); + reasons.push({ + content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }), + }); } if ( !ca.controlImage || (isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none') ) { - reasons.push( - i18n.t('parameters.invoke.noControlImageForControlAdapter', { - number: i + 1, - }) - ); + reasons.push({ content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }) }); } }); } @@ -191,6 +218,6 @@ const selector = createMemoizedSelector( ); export const useIsReadyToEnqueue = () => { - const { isReady, reasons } = useAppSelector(selector); - return { isReady, reasons }; + const value = useAppSelector(selector); + return value; }; diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index 314616da59..52dc5e24af 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -16,25 +16,26 @@ export const InvokeQueueBackButton = memo(() => { return ( - + + + ); }); diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index f63e96c45f..c865f3a60f 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -1,10 +1,11 @@ -import { Divider, Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library'; +import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; +import type { PropsWithChildren } from 'react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useEnqueueBatchMutation } from 'services/api/endpoints/queue'; @@ -21,7 +22,15 @@ type Props = { prepend?: boolean; }; -export const QueueButtonTooltip = memo(({ prepend = false }: Props) => { +export const QueueButtonTooltip = (props: PropsWithChildren) => { + return ( + } maxW={512}> + {props.children} + + ); +}; + +const TooltipContent = memo(({ prepend = false }: Props) => { const { t } = useTranslation(); const { isReady, reasons } = useIsReadyToEnqueue(); const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); @@ -64,8 +73,15 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => { {reasons.map((reason, i) => ( - - {reason} + + + {reason.prefix && ( + + {reason.prefix}:{' '} + + )} + {reason.content} + ))} @@ -82,4 +98,4 @@ export const QueueButtonTooltip = memo(({ prepend = false }: Props) => { ); }); -QueueButtonTooltip.displayName = 'QueueButtonTooltip'; +TooltipContent.displayName = 'QueueButtonTooltipContent'; diff --git a/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx b/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx index 07ad0f5b3c..eb0e72950f 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueFrontButton.tsx @@ -10,15 +10,16 @@ const QueueFrontButton = () => { const { t } = useTranslation(); const { queueFront, isLoading, isDisabled } = useQueueFront(); return ( - } - icon={} - size="lg" - /> + + } + size="lg" + /> + ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index 32611b2354..5a8273b7fc 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -63,16 +63,17 @@ const FloatingSidePanelButtons = (props: Props) => { sx={floatingButtonStyles} icon={} /> - } - sx={floatingButtonStyles} - /> + + + From 6ff1c7d54182995fd7c005ba0ceea6804640a292 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 18:23:37 +1000 Subject: [PATCH 3/5] feat(ui): add group by base & type to useGroupedModelCombobox hook This allows comboboxes for models to have more granular groupings. For example, Control Adapter models can be grouped by base model & model type. Before: - `SD-1` - `SDXL` After: - `SD-1 / ControlNet` - `SD-1 / T2I Adapter` - `SDXL / ControlNet` - `SDXL / T2I Adapter` --- .../web/src/common/hooks/useGroupedModelCombobox.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index 55887eb3be..5b57fcd2bb 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -13,6 +13,7 @@ type UseGroupedModelComboboxArg = { onChange: (value: T | null) => void; getIsDisabled?: (model: T) => boolean; isLoading?: boolean; + groupByType?: boolean; }; type UseGroupedModelComboboxReturn = { @@ -23,17 +24,21 @@ type UseGroupedModelComboboxReturn = { noOptionsMessage: () => string; }; +const groupByBaseFunc = (model: T) => model.base.toUpperCase(); +const groupByBaseAndTypeFunc = (model: T) => + `${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`; + export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): UseGroupedModelComboboxReturn => { const { t } = useTranslation(); const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl'); - const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading } = arg; + const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg; const options = useMemo[]>(() => { if (!modelConfigs) { return []; } - const groupedModels = groupBy(modelConfigs, 'base'); + const groupedModels = groupBy(modelConfigs, groupByType ? groupByBaseAndTypeFunc : groupByBaseFunc); const _options = reduce( groupedModels, (acc, val, label) => { @@ -49,9 +54,9 @@ export const useGroupedModelCombobox = ( }, [] as GroupBase[] ); - _options.sort((a) => (a.label === base_model ? -1 : 1)); + _options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1)); return _options; - }, [getIsDisabled, modelConfigs, base_model]); + }, [modelConfigs, groupByType, getIsDisabled, base_model]); const value = useMemo( () => From 8dd0bfb06840c8406be24e2e05a970fa10331740 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 10 May 2024 18:23:56 +1000 Subject: [PATCH 4/5] feat(ui): use new model type grouping for control adapters in control layers --- .../ControlAndIPAdapter/ControlAdapterModelCombobox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx index a4b1d6b744..535f3067a4 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlAndIPAdapter/ControlAdapterModelCombobox.tsx @@ -42,6 +42,7 @@ export const ControlAdapterModelCombobox = memo(({ modelKey, onChange: onChangeM selectedModel, getIsDisabled, isLoading, + groupByType: true, }); return ( From 4ea8416c68d67b562d96418c331e51d577afb6ef Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 13 May 2024 07:44:33 +1000 Subject: [PATCH 5/5] fix(ui): use pluralization for invoke button tooltip --- invokeai/frontend/web/public/locales/en.json | 11 +++++++---- .../queue/components/QueueButtonTooltip.tsx | 17 +++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ed94dd09f4..7aa4b03b8c 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -261,7 +261,6 @@ "queue": "Queue", "queueFront": "Add to Front of Queue", "queueBack": "Add to Queue", - "queueCountPrediction": "{{promptsCount}} prompts \u00d7 {{iterations}} iterations -> {{count}} generations", "queueEmpty": "Queue Empty", "enqueueing": "Queueing Batch", "resume": "Resume", @@ -314,7 +313,13 @@ "batchFailedToQueue": "Failed to Queue Batch", "graphQueued": "Graph queued", "graphFailedToQueue": "Failed to queue graph", - "openQueue": "Open Queue" + "openQueue": "Open Queue", + "prompts_one": "Prompt", + "prompts_other": "Prompts", + "iterations_one": "Iteration", + "iterations_other": "Iterations", + "generations_one": "Generation", + "generations_other": "Generations" }, "invocationCache": { "invocationCache": "Invocation Cache", @@ -958,8 +963,6 @@ "positivePromptPlaceholder": "Positive Prompt", "globalPositivePromptPlaceholder": "Global Positive Prompt", "iterations": "Iterations", - "iterationsWithCount_one": "{{count}} Iteration", - "iterationsWithCount_other": "{{count}} Iterations", "scale": "Scale", "scaleBeforeProcessing": "Scale Before Processing", "scaledHeight": "Scaled H", diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index c865f3a60f..498414d377 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -35,12 +35,19 @@ const TooltipContent = memo(({ prepend = false }: Props) => { const { isReady, reasons } = useIsReadyToEnqueue(); const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); const promptsCount = useAppSelector(selectPromptsCount); - const iterations = useAppSelector((s) => s.generation.iterations); + const iterationsCount = useAppSelector((s) => s.generation.iterations); const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); const autoAddBoardName = useBoardName(autoAddBoardId); const [_, { isLoading }] = useEnqueueBatchMutation({ fixedCacheKey: 'enqueueBatch', }); + const queueCountPredictionLabel = useMemo(() => { + const generationCount = Math.min(promptsCount * iterationsCount, 10000); + const prompts = t('queue.prompts', { count: promptsCount }); + const iterations = t('queue.iterations', { count: iterationsCount }); + const generations = t('queue.generations', { count: generationCount }); + return `${promptsCount} ${prompts} \u00d7 ${iterationsCount} ${iterations} -> ${generationCount} ${generations}`.toLowerCase(); + }, [iterationsCount, promptsCount, t]); const label = useMemo(() => { if (isLoading) { @@ -61,13 +68,7 @@ const TooltipContent = memo(({ prepend = false }: Props) => { return ( {label} - - {t('queue.queueCountPrediction', { - promptsCount, - iterations, - count: Math.min(promptsCount * iterations, 10000), - })} - + {queueCountPredictionLabel} {reasons.length > 0 && ( <>