mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Download events and invocation status events (including progress images) are very frequent. There's no real need for these to pass through redux. Handling them outside redux is a significant performance win - far fewer store subscription calls, far fewer trips through middleware. All event handling is moved outside middleware. Cleanup of unused actions and listeners to follow.
247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
import { useStore } from '@nanostores/react';
|
|
import { $isConnected } from 'app/hooks/useSocketIO';
|
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|
import { useAppSelector } from 'app/store/storeHooks';
|
|
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
|
|
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
|
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
|
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
|
import type { Templates } from 'features/nodes/store/types';
|
|
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
|
import { selectUpscalelice } from 'features/parameters/store/upscaleSlice';
|
|
import { selectConfigSlice } from 'features/system/store/configSlice';
|
|
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
import i18n from 'i18next';
|
|
import { forEach, upperFirst } from 'lodash-es';
|
|
import { useMemo } from 'react';
|
|
import { getConnectedEdges } from 'reactflow';
|
|
|
|
const LAYER_TYPE_TO_TKEY = {
|
|
ip_adapter: 'controlLayers.ipAdapter',
|
|
inpaint_mask: 'controlLayers.inpaintMask',
|
|
regional_guidance: 'controlLayers.regionalGuidance',
|
|
raster_layer: 'controlLayers.raster',
|
|
control_layer: 'controlLayers.globalControlAdapter',
|
|
} as const;
|
|
|
|
const createSelector = (templates: Templates, isConnected: boolean) =>
|
|
createMemoizedSelector(
|
|
[
|
|
selectSystemSlice,
|
|
selectNodesSlice,
|
|
selectWorkflowSettingsSlice,
|
|
selectDynamicPromptsSlice,
|
|
selectCanvasV2Slice,
|
|
selectUpscalelice,
|
|
selectConfigSlice,
|
|
activeTabNameSelector,
|
|
],
|
|
(system, nodes, workflowSettings, dynamicPrompts, canvasV2, upscale, config, activeTabName) => {
|
|
const { bbox } = canvasV2;
|
|
const { model, positivePrompt } = canvasV2.params;
|
|
|
|
const reasons: { prefix?: string; content: string }[] = [];
|
|
|
|
// Cannot generate if not connected
|
|
if (!isConnected) {
|
|
reasons.push({ content: i18n.t('parameters.invoke.systemDisconnected') });
|
|
}
|
|
|
|
if (activeTabName === 'workflows') {
|
|
if (workflowSettings.shouldValidateGraph) {
|
|
if (!nodes.nodes.length) {
|
|
reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') });
|
|
}
|
|
|
|
nodes.nodes.forEach((node) => {
|
|
if (!isInvocationNode(node)) {
|
|
return;
|
|
}
|
|
|
|
const nodeTemplate = templates[node.data.type];
|
|
|
|
if (!nodeTemplate) {
|
|
// Node type not found
|
|
reasons.push({ content: i18n.t('parameters.invoke.missingNodeTemplate') });
|
|
return;
|
|
}
|
|
|
|
const connectedEdges = getConnectedEdges([node], nodes.edges);
|
|
|
|
forEach(node.data.inputs, (field) => {
|
|
const fieldTemplate = nodeTemplate.inputs[field.name];
|
|
const hasConnection = connectedEdges.some(
|
|
(edge) => edge.target === node.id && edge.targetHandle === field.name
|
|
);
|
|
|
|
if (!fieldTemplate) {
|
|
reasons.push({ content: i18n.t('parameters.invoke.missingFieldTemplate') });
|
|
return;
|
|
}
|
|
|
|
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
|
|
reasons.push({
|
|
content: i18n.t('parameters.invoke.missingInputForField', {
|
|
nodeLabel: node.data.label || nodeTemplate.title,
|
|
fieldLabel: field.label || fieldTemplate.title,
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} else if (activeTabName === 'upscaling') {
|
|
if (!upscale.upscaleInitialImage) {
|
|
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
|
|
} else if (config.maxUpscaleDimension) {
|
|
const { width, height } = upscale.upscaleInitialImage;
|
|
const { scale } = upscale;
|
|
|
|
const maxPixels = config.maxUpscaleDimension ** 2;
|
|
const upscaledPixels = width * scale * height * scale;
|
|
|
|
if (upscaledPixels > maxPixels) {
|
|
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
|
|
}
|
|
}
|
|
if (!upscale.upscaleModel) {
|
|
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
|
|
}
|
|
if (!upscale.tileControlnetModel) {
|
|
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
|
|
}
|
|
} else {
|
|
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
|
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
|
|
}
|
|
|
|
if (!model) {
|
|
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
|
|
}
|
|
|
|
canvasV2.controlLayers.entities
|
|
.filter((controlLayer) => controlLayer.isEnabled)
|
|
.forEach((controlLayer, i) => {
|
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
|
const layerNumber = i + 1;
|
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']);
|
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
|
const problems: string[] = [];
|
|
// Must have model
|
|
if (!controlLayer.controlAdapter.model) {
|
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
|
|
}
|
|
// Model base must match
|
|
if (controlLayer.controlAdapter.model?.base !== model?.base) {
|
|
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
|
|
}
|
|
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
|
|
if (controlLayer.controlAdapter.type === 't2i_adapter') {
|
|
const multiple = model?.base === 'sdxl' ? 32 : 64;
|
|
if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) {
|
|
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
|
|
}
|
|
}
|
|
|
|
if (problems.length) {
|
|
const content = upperFirst(problems.join(', '));
|
|
reasons.push({ prefix, content });
|
|
}
|
|
});
|
|
|
|
canvasV2.ipAdapters.entities
|
|
.filter((entity) => entity.isEnabled)
|
|
.forEach((entity, i) => {
|
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
|
const layerNumber = i + 1;
|
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
|
const problems: string[] = [];
|
|
|
|
// Must have model
|
|
if (!entity.ipAdapter.model) {
|
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
|
}
|
|
// Model base must match
|
|
if (entity.ipAdapter.model?.base !== model?.base) {
|
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
|
}
|
|
// Must have an image
|
|
if (!entity.ipAdapter.image) {
|
|
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
|
}
|
|
|
|
if (problems.length) {
|
|
const content = upperFirst(problems.join(', '));
|
|
reasons.push({ prefix, content });
|
|
}
|
|
});
|
|
|
|
canvasV2.regions.entities
|
|
.filter((entity) => entity.isEnabled)
|
|
.forEach((entity, i) => {
|
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
|
const layerNumber = i + 1;
|
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
|
const problems: string[] = [];
|
|
// Must have a region
|
|
if (entity.objects.length === 0) {
|
|
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
|
}
|
|
// Must have at least 1 prompt or IP Adapter
|
|
if (entity.positivePrompt === null && entity.negativePrompt === null && entity.ipAdapters.length === 0) {
|
|
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
|
|
}
|
|
entity.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 (problems.length) {
|
|
const content = upperFirst(problems.join(', '));
|
|
reasons.push({ prefix, content });
|
|
}
|
|
});
|
|
|
|
canvasV2.rasterLayers.entities
|
|
.filter((entity) => entity.isEnabled)
|
|
.forEach((entity, i) => {
|
|
const layerLiteral = i18n.t('controlLayers.layers_one');
|
|
const layerNumber = i + 1;
|
|
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
|
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
|
const problems: string[] = [];
|
|
|
|
if (problems.length) {
|
|
const content = upperFirst(problems.join(', '));
|
|
reasons.push({ prefix, content });
|
|
}
|
|
});
|
|
}
|
|
|
|
return { isReady: !reasons.length, reasons };
|
|
}
|
|
);
|
|
|
|
export const useIsReadyToEnqueue = () => {
|
|
const templates = useStore($templates);
|
|
const isConnected = useStore($isConnected)
|
|
const selector = useMemo(() => createSelector(templates, isConnected), [templates, isConnected]);
|
|
const value = useAppSelector(selector);
|
|
return value;
|
|
};
|