feat(ui): add flux redux image influence to canvas

This commit is contained in:
psychedelicious
2025-04-08 09:28:45 +10:00
parent 49622c37ed
commit 5956f96e57
10 changed files with 204 additions and 5 deletions

View File

@@ -0,0 +1,60 @@
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import type { FLUXReduxImageInfluence as FLUXReduxImageInfluenceType } from 'features/controlLayers/store/types';
import { isFLUXReduxImageInfluence } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe';
type Props = {
imageInfluence: FLUXReduxImageInfluenceType;
onChange: (imageInfluence: FLUXReduxImageInfluenceType) => void;
};
export const FLUXReduxImageInfluence = memo(({ imageInfluence, onChange }: Props) => {
const { t } = useTranslation();
const options = useMemo(
() =>
[
{
label: t('controlLayers.fluxReduxImageInfluence.lowest'),
value: 'lowest',
},
{
label: t('controlLayers.fluxReduxImageInfluence.low'),
value: 'low',
},
{
label: t('controlLayers.fluxReduxImageInfluence.medium'),
value: 'medium',
},
{
label: t('controlLayers.fluxReduxImageInfluence.high'),
value: 'high',
},
{
label: t('controlLayers.fluxReduxImageInfluence.highest'),
value: 'highest',
},
] satisfies { label: string; value: FLUXReduxImageInfluenceType }[],
[t]
);
const _onChange = useCallback<ComboboxOnChange>(
(v) => {
assert(isFLUXReduxImageInfluence(v?.value));
onChange(v.value);
},
[onChange]
);
const value = useMemo(() => options.find((o) => o.value === imageInfluence), [options, imageInfluence]);
return (
<FormControl>
<FormLabel m={0}>{t('controlLayers.fluxReduxImageInfluence.imageInfluence')}</FormLabel>
<Combobox value={value} options={options} onChange={_onChange} />
</FormControl>
);
});
FLUXReduxImageInfluence.displayName = 'FLUXReduxImageInfluence';

View File

@@ -61,7 +61,7 @@ export const IPAdapterImagePreview = memo(
)}
{imageDTO && (
<>
<DndImage imageDTO={imageDTO} />
<DndImage imageDTO={imageDTO} borderWidth={1} borderStyle='solid' />
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<DndImageIcon
onClick={handleResetControlImage}

View File

@@ -50,7 +50,7 @@ export const IPAdapterMethod = memo(({ method, onChange }: Props) => {
return (
<FormControl>
<InformationalPopover feature="ipAdapterMethod">
<FormLabel>{t('controlLayers.ipAdapterMethod.ipAdapterMethod')}</FormLabel>
<FormLabel m={0}>{t('controlLayers.ipAdapterMethod.ipAdapterMethod')}</FormLabel>
</InformationalPopover>
<Combobox value={value} options={options} onChange={_onChange} />
</FormControl>

View File

@@ -5,6 +5,7 @@ import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginE
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
import { IPAdapterSettingsEmptyState } from 'features/controlLayers/components/IPAdapter/IPAdapterSettingsEmptyState';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
@@ -13,6 +14,7 @@ import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import {
referenceImageIPAdapterBeginEndStepPctChanged,
referenceImageIPAdapterCLIPVisionModelChanged,
referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
referenceImageIPAdapterImageChanged,
referenceImageIPAdapterMethodChanged,
referenceImageIPAdapterModelChanged,
@@ -20,7 +22,12 @@ import {
} from 'features/controlLayers/store/canvasSlice';
import { selectIsFLUX } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice, selectEntity, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type {
CanvasEntityIdentifier,
CLIPVisionModelV2,
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
IPMethodV2,
} from 'features/controlLayers/store/types';
import type { SetGlobalReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setGlobalReferenceImageDndTarget } from 'features/dnd/dnd';
import { memo, useCallback, useMemo } from 'react';
@@ -65,6 +72,13 @@ const IPAdapterSettingsContent = memo(() => {
[dispatch, entityIdentifier]
);
const onChangeFLUXReduxImageInfluence = useCallback(
(imageInfluence: FLUXReduxImageInfluenceType) => {
dispatch(referenceImageIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, imageInfluence }));
},
[dispatch, entityIdentifier]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig) => {
dispatch(referenceImageIPAdapterModelChanged({ entityIdentifier, modelConfig }));
@@ -124,6 +138,14 @@ const IPAdapterSettingsContent = memo(() => {
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex>
)}
{ipAdapter.type === 'flux_redux' && (
<Flex flexDir="column" gap={2} w="full" alignItems="flex-start">
<FLUXReduxImageInfluence
imageInfluence={ipAdapter.imageInfluence ?? 'lowest'}
onChange={onChangeFLUXReduxImageInfluence}
/>
</Flex>
)}
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
<IPAdapterImagePreview
image={ipAdapter.image}

View File

@@ -4,6 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { CLIPVisionModel } from 'features/controlLayers/components/IPAdapter/CLIPVisionModel';
import { FLUXReduxImageInfluence } from 'features/controlLayers/components/IPAdapter/FLUXReduxImageInfluence';
import { IPAdapterImagePreview } from 'features/controlLayers/components/IPAdapter/IPAdapterImagePreview';
import { IPAdapterMethod } from 'features/controlLayers/components/IPAdapter/IPAdapterMethod';
import { IPAdapterModel } from 'features/controlLayers/components/IPAdapter/IPAdapterModel';
@@ -15,13 +16,19 @@ import {
rgIPAdapterBeginEndStepPctChanged,
rgIPAdapterCLIPVisionModelChanged,
rgIPAdapterDeleted,
rgIPAdapterFLUXReduxImageInfluenceChanged,
rgIPAdapterImageChanged,
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterWeightChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectRegionalGuidanceReferenceImage } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, CLIPVisionModelV2, IPMethodV2 } from 'features/controlLayers/store/types';
import type {
CanvasEntityIdentifier,
CLIPVisionModelV2,
FLUXReduxImageInfluence as FLUXReduxImageInfluenceType,
IPMethodV2,
} from 'features/controlLayers/store/types';
import type { SetRegionalGuidanceReferenceImageDndTargetData } from 'features/dnd/dnd';
import { setRegionalGuidanceReferenceImageDndTarget } from 'features/dnd/dnd';
import { memo, useCallback, useMemo } from 'react';
@@ -73,6 +80,13 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeFLUXReduxImageInfluence = useCallback(
(imageInfluence: FLUXReduxImageInfluenceType) => {
dispatch(rgIPAdapterFLUXReduxImageInfluenceChanged({ entityIdentifier, referenceImageId, imageInfluence }));
},
[dispatch, entityIdentifier, referenceImageId]
);
const onChangeModel = useCallback(
(modelConfig: IPAdapterModelConfig | FLUXReduxModelConfig) => {
dispatch(rgIPAdapterModelChanged({ entityIdentifier, referenceImageId, modelConfig }));
@@ -151,6 +165,14 @@ const RegionalGuidanceIPAdapterSettingsContent = memo(({ referenceImageId }: Pro
<BeginEndStepPct beginEndStepPct={ipAdapter.beginEndStepPct} onChange={onChangeBeginEndStepPct} />
</Flex>
)}
{ipAdapter.type === 'flux_redux' && (
<Flex flexDir="column" gap={2} w="full">
<FLUXReduxImageInfluence
imageInfluence={ipAdapter.imageInfluence ?? 'lowest'}
onChange={onChangeFLUXReduxImageInfluence}
/>
</Flex>
)}
<Flex alignItems="center" justifyContent="center" h={32} w={32} aspectRatio="1/1" flexGrow={1}>
<IPAdapterImagePreview
image={ipAdapter.image}

View File

@@ -21,6 +21,7 @@ import type {
ControlLoRAConfig,
EntityMovedByPayload,
FillStyle,
FLUXReduxImageInfluence,
RegionalGuidanceReferenceImageState,
RgbColor,
} from 'features/controlLayers/store/types';
@@ -626,6 +627,20 @@ export const canvasSlice = createSlice({
}
entity.ipAdapter.method = method;
},
referenceImageIPAdapterFLUXReduxImageInfluenceChanged: (
state,
action: PayloadAction<EntityIdentifierPayload<{ imageInfluence: FLUXReduxImageInfluence }, 'reference_image'>>
) => {
const { entityIdentifier, imageInfluence } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
if (entity.ipAdapter.type !== 'flux_redux') {
return;
}
entity.ipAdapter.imageInfluence = imageInfluence;
},
referenceImageIPAdapterModelChanged: (
state,
action: PayloadAction<
@@ -926,6 +941,26 @@ export const canvasSlice = createSlice({
referenceImage.ipAdapter.method = method;
},
rgIPAdapterFLUXReduxImageInfluenceChanged: (
state,
action: PayloadAction<
EntityIdentifierPayload<
{ referenceImageId: string; imageInfluence: FLUXReduxImageInfluence },
'regional_guidance'
>
>
) => {
const { entityIdentifier, referenceImageId, imageInfluence } = action.payload;
const referenceImage = selectRegionalGuidanceReferenceImage(state, entityIdentifier, referenceImageId);
if (!referenceImage) {
return;
}
if (referenceImage.ipAdapter.type !== 'flux_redux') {
return;
}
referenceImage.ipAdapter.imageInfluence = imageInfluence;
},
rgIPAdapterModelChanged: (
state,
action: PayloadAction<
@@ -1731,6 +1766,7 @@ export const {
referenceImageIPAdapterCLIPVisionModelChanged,
referenceImageIPAdapterWeightChanged,
referenceImageIPAdapterBeginEndStepPctChanged,
referenceImageIPAdapterFLUXReduxImageInfluenceChanged,
// Regions
rgAdded,
// rgRecalled,
@@ -1746,6 +1782,7 @@ export const {
rgIPAdapterMethodChanged,
rgIPAdapterModelChanged,
rgIPAdapterCLIPVisionModelChanged,
rgIPAdapterFLUXReduxImageInfluenceChanged,
// Inpaint mask
inpaintMaskAdded,
inpaintMaskConvertedToRegionalGuidance,

View File

@@ -233,10 +233,15 @@ const zIPAdapterConfig = z.object({
});
export type IPAdapterConfig = z.infer<typeof zIPAdapterConfig>;
const zFLUXReduxImageInfluence = z.enum(['lowest', 'low', 'medium', 'high', 'highest']);
export const isFLUXReduxImageInfluence = (v: unknown): v is FLUXReduxImageInfluence =>
zFLUXReduxImageInfluence.safeParse(v).success;
export type FLUXReduxImageInfluence = z.infer<typeof zFLUXReduxImageInfluence>;
const zFLUXReduxConfig = z.object({
type: z.literal('flux_redux'),
image: zImageWithDims.nullable(),
model: zServerValidatedModelIdentifierField.nullable(),
imageInfluence: zFLUXReduxImageInfluence.default('highest'),
});
export type FLUXReduxConfig = z.infer<typeof zFLUXReduxConfig>;

View File

@@ -75,6 +75,7 @@ export const initialFLUXRedux: FLUXReduxConfig = {
type: 'flux_redux',
image: null,
model: null,
imageInfluence: 'highest',
};
export const initialT2IAdapter: T2IAdapterConfig = {
type: 't2i_adapter',