mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): control image auto-process
This commit is contained in:
parent
fa290aff8d
commit
72b4371804
@ -71,7 +71,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa
|
|||||||
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
|
import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingAreaImageListener';
|
||||||
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
|
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
|
||||||
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
|
||||||
import { addControlNetProcessorParamsChangedListener } from './listeners/controlNetProcessorParamsChanged';
|
import { addControlNetAutoProcessListener } from './listeners/controlNetProcessorParamsChanged';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -178,4 +178,4 @@ addImageCategoriesChangedListener();
|
|||||||
|
|
||||||
// ControlNet
|
// ControlNet
|
||||||
addControlNetImageProcessedListener();
|
addControlNetImageProcessedListener();
|
||||||
addControlNetProcessorParamsChangedListener();
|
addControlNetAutoProcessListener();
|
||||||
|
@ -15,7 +15,10 @@ const moduleLog = log.child({ namespace: 'controlNet' });
|
|||||||
export const addControlNetImageProcessedListener = () => {
|
export const addControlNetImageProcessedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: controlNetImageProcessed,
|
actionCreator: controlNetImageProcessed,
|
||||||
effect: async (action, { dispatch, getState, take }) => {
|
effect: async (
|
||||||
|
action,
|
||||||
|
{ dispatch, getState, take, unsubscribe, subscribe }
|
||||||
|
) => {
|
||||||
const { controlNetId } = action.payload;
|
const { controlNetId } = action.payload;
|
||||||
const controlNet = getState().controlNet.controlNets[controlNetId];
|
const controlNet = getState().controlNet.controlNets[controlNetId];
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { startAppListening } from '..';
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
|
import { controlNetImageProcessed } from 'features/controlNet/store/actions';
|
||||||
import {
|
import {
|
||||||
|
controlNetImageChanged,
|
||||||
controlNetProcessorParamsChanged,
|
controlNetProcessorParamsChanged,
|
||||||
controlNetProcessorTypeChanged,
|
controlNetProcessorTypeChanged,
|
||||||
} from 'features/controlNet/store/controlNetSlice';
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
@ -13,10 +14,11 @@ const moduleLog = log.child({ namespace: 'controlNet' });
|
|||||||
*
|
*
|
||||||
* The network request is debounced by 1 second.
|
* The network request is debounced by 1 second.
|
||||||
*/
|
*/
|
||||||
export const addControlNetProcessorParamsChangedListener = () => {
|
export const addControlNetAutoProcessListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
predicate: (action) =>
|
predicate: (action) =>
|
||||||
controlNetProcessorParamsChanged.match(action) ||
|
controlNetProcessorParamsChanged.match(action) ||
|
||||||
|
controlNetImageChanged.match(action) ||
|
||||||
controlNetProcessorTypeChanged.match(action),
|
controlNetProcessorTypeChanged.match(action),
|
||||||
effect: async (
|
effect: async (
|
||||||
action,
|
action,
|
||||||
@ -35,11 +37,19 @@ export const addControlNetProcessorParamsChangedListener = () => {
|
|||||||
|
|
||||||
const { controlNetId } = action.payload;
|
const { controlNetId } = action.payload;
|
||||||
|
|
||||||
|
if (!state.controlNet.controlNets[controlNetId].controlImage) {
|
||||||
|
moduleLog.trace(
|
||||||
|
{ data: { controlNetId } },
|
||||||
|
'No ControlNet image to auto-process'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel any in-progress instances of this listener
|
// Cancel any in-progress instances of this listener
|
||||||
cancelActiveListeners();
|
cancelActiveListeners();
|
||||||
|
|
||||||
// Delay before starting actual work
|
// Delay before starting actual work
|
||||||
await delay(1000);
|
await delay(300);
|
||||||
|
|
||||||
dispatch(controlNetImageProcessed({ controlNetId }));
|
dispatch(controlNetImageProcessed({ controlNetId }));
|
||||||
},
|
},
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useState } from 'react';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import {
|
import {
|
||||||
ControlNet,
|
ControlNet,
|
||||||
controlNetImageChanged,
|
controlNetImageChanged,
|
||||||
controlNetProcessedImageChanged,
|
controlNetProcessedImageChanged,
|
||||||
controlNetRemoved,
|
controlNetRemoved,
|
||||||
|
controlNetSelector,
|
||||||
} from '../store/controlNetSlice';
|
} from '../store/controlNetSlice';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import ParamControlNetModel from './parameters/ParamControlNetModel';
|
import ParamControlNetModel from './parameters/ParamControlNetModel';
|
||||||
import ParamControlNetWeight from './parameters/ParamControlNetWeight';
|
import ParamControlNetWeight from './parameters/ParamControlNetWeight';
|
||||||
import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct';
|
import ParamControlNetBeginStepPct from './parameters/ParamControlNetBeginStepPct';
|
||||||
import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct';
|
import ParamControlNetEndStepPct from './parameters/ParamControlNetEndStepPct';
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
|
Spinner,
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
@ -22,10 +25,15 @@ import {
|
|||||||
import IAISelectableImage from './parameters/IAISelectableImage';
|
import IAISelectableImage from './parameters/IAISelectableImage';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
import { FaUndo } from 'react-icons/fa';
|
import { FaUndo } from 'react-icons/fa';
|
||||||
|
import { TbSquareToggle } from 'react-icons/tb';
|
||||||
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
|
import ParamControlNetProcessorSelect from './parameters/ParamControlNetProcessorSelect';
|
||||||
import ControlNetProcessorComponent from './ControlNetProcessorComponent';
|
import ControlNetProcessorComponent from './ControlNetProcessorComponent';
|
||||||
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
|
|
||||||
import ControlNetPreprocessButton from './ControlNetPreprocessButton';
|
import ControlNetPreprocessButton from './ControlNetPreprocessButton';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import ParamControlNetBeginEnd from './parameters/ParamControlNetBeginEnd';
|
||||||
|
import ControlNetImagePreview from './ControlNetImagePreview';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
|
||||||
type ControlNetProps = {
|
type ControlNetProps = {
|
||||||
controlNet: ControlNet;
|
controlNet: ControlNet;
|
||||||
@ -45,15 +53,6 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
processorNode,
|
processorNode,
|
||||||
} = props.controlNet;
|
} = props.controlNet;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isReady = useIsApplicationReady();
|
|
||||||
|
|
||||||
const handleControlImageChanged = useCallback(
|
|
||||||
(controlImage: ImageDTO) => {
|
|
||||||
dispatch(controlNetImageChanged({ controlNetId, controlImage }));
|
|
||||||
},
|
|
||||||
[controlNetId, dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
controlNetProcessedImageChanged({
|
controlNetProcessedImageChanged({
|
||||||
@ -63,21 +62,16 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
);
|
);
|
||||||
}, [controlNetId, dispatch]);
|
}, [controlNetId, dispatch]);
|
||||||
|
|
||||||
const handleControlImageReset = useCallback(() => {
|
|
||||||
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));
|
|
||||||
}, [controlNetId, dispatch]);
|
|
||||||
|
|
||||||
const handleControlNetRemoved = useCallback(() => {
|
const handleControlNetRemoved = useCallback(() => {
|
||||||
dispatch(controlNetRemoved(controlNetId));
|
dispatch(controlNetRemoved(controlNetId));
|
||||||
}, [controlNetId, dispatch]);
|
}, [controlNetId, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex sx={{ flexDir: 'column', gap: 3 }}>
|
<Flex sx={{ flexDir: 'column', gap: 3 }}>
|
||||||
<IAISelectableImage
|
<ControlNetImagePreview
|
||||||
image={processedControlImage || controlImage}
|
controlNetId={controlNetId}
|
||||||
onChange={handleControlImageChanged}
|
controlImage={controlImage}
|
||||||
onReset={handleControlImageReset}
|
processedControlImage={processedControlImage}
|
||||||
resetIconSize="sm"
|
|
||||||
/>
|
/>
|
||||||
<ParamControlNetModel controlNetId={controlNetId} model={model} />
|
<ParamControlNetModel controlNetId={controlNetId} model={model} />
|
||||||
<Tabs
|
<Tabs
|
||||||
@ -105,12 +99,9 @@ const ControlNet = (props: ControlNetProps) => {
|
|||||||
controlNetId={controlNetId}
|
controlNetId={controlNetId}
|
||||||
weight={weight}
|
weight={weight}
|
||||||
/>
|
/>
|
||||||
<ParamControlNetBeginStepPct
|
<ParamControlNetBeginEnd
|
||||||
controlNetId={controlNetId}
|
controlNetId={controlNetId}
|
||||||
beginStepPct={beginStepPct}
|
beginStepPct={beginStepPct}
|
||||||
/>
|
|
||||||
<ParamControlNetEndStepPct
|
|
||||||
controlNetId={controlNetId}
|
|
||||||
endStepPct={endStepPct}
|
endStepPct={endStepPct}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -0,0 +1,168 @@
|
|||||||
|
import { memo, useCallback, useState } from 'react';
|
||||||
|
import { ImageDTO } from 'services/api';
|
||||||
|
import {
|
||||||
|
controlNetImageChanged,
|
||||||
|
controlNetSelector,
|
||||||
|
} from '../store/controlNetSlice';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { Box, Flex, Spinner } from '@chakra-ui/react';
|
||||||
|
import IAISelectableImage from './parameters/IAISelectableImage';
|
||||||
|
import { TbSquareToggle } from 'react-icons/tb';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
controlNetSelector,
|
||||||
|
(controlNet) => {
|
||||||
|
const { isProcessingControlImage } = controlNet;
|
||||||
|
return { isProcessingControlImage };
|
||||||
|
},
|
||||||
|
defaultSelectorOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
controlNetId: string;
|
||||||
|
controlImage: ImageDTO | null;
|
||||||
|
processedControlImage: ImageDTO | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ControlNetImagePreview = (props: Props) => {
|
||||||
|
const { controlNetId, controlImage, processedControlImage } = props;
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { isProcessingControlImage } = useAppSelector(selector);
|
||||||
|
|
||||||
|
const [shouldShowProcessedImage, setShouldShowProcessedImage] =
|
||||||
|
useState(true);
|
||||||
|
|
||||||
|
const handleControlImageChanged = useCallback(
|
||||||
|
(controlImage: ImageDTO) => {
|
||||||
|
dispatch(controlNetImageChanged({ controlNetId, controlImage }));
|
||||||
|
},
|
||||||
|
[controlNetId, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleControlImageReset = useCallback(() => {
|
||||||
|
dispatch(controlNetImageChanged({ controlNetId, controlImage: null }));
|
||||||
|
}, [controlNetId, dispatch]);
|
||||||
|
|
||||||
|
const shouldShowProcessedImageBackdrop =
|
||||||
|
Number(controlImage?.width) > Number(processedControlImage?.width) ||
|
||||||
|
Number(controlImage?.height) > Number(processedControlImage?.height);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ position: 'relative', aspectRatio: '1/1' }}>
|
||||||
|
<IAISelectableImage
|
||||||
|
image={controlImage}
|
||||||
|
onChange={handleControlImageChanged}
|
||||||
|
onReset={handleControlImageReset}
|
||||||
|
isDropDisabled={Boolean(processedControlImage)}
|
||||||
|
fallback={<ProcessedImageFallback />}
|
||||||
|
withResetIcon
|
||||||
|
resetIconSize="sm"
|
||||||
|
/>
|
||||||
|
<AnimatePresence>
|
||||||
|
{controlImage &&
|
||||||
|
processedControlImage &&
|
||||||
|
shouldShowProcessedImage &&
|
||||||
|
!isProcessingControlImage && (
|
||||||
|
<motion.div
|
||||||
|
initial={{
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shouldShowProcessedImageBackdrop && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
bg: 'base.900',
|
||||||
|
opacity: 0.7,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IAISelectableImage
|
||||||
|
image={processedControlImage}
|
||||||
|
onChange={handleControlImageChanged}
|
||||||
|
onReset={handleControlImageReset}
|
||||||
|
withResetIcon
|
||||||
|
resetIconSize="sm"
|
||||||
|
fallback={<ProcessedImageFallback />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
{isProcessingControlImage && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
insetInlineStart: 0,
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProcessedImageFallback />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{processedControlImage && !isProcessingControlImage && (
|
||||||
|
<Box sx={{ position: 'absolute', bottom: 0, insetInlineEnd: 0, p: 2 }}>
|
||||||
|
<IAIIconButton
|
||||||
|
aria-label="Hide Preview"
|
||||||
|
icon={<TbSquareToggle />}
|
||||||
|
size="sm"
|
||||||
|
onMouseOver={() => setShouldShowProcessedImage(false)}
|
||||||
|
onMouseOut={() => setShouldShowProcessedImage(true)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ControlNetImagePreview);
|
||||||
|
|
||||||
|
const ProcessedImageFallback = () => (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
bg: 'base.900',
|
||||||
|
opacity: 0.7,
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Flex>
|
||||||
|
);
|
@ -12,7 +12,7 @@ import IAIIconButton from 'common/components/IAIIconButton';
|
|||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
import { useGetUrl } from 'common/util/getUrl';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { SyntheticEvent } from 'react';
|
import { ReactElement, SyntheticEvent } from 'react';
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
import { FaImage, FaTimes } from 'react-icons/fa';
|
import { FaImage, FaTimes } from 'react-icons/fa';
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
@ -26,14 +26,29 @@ type IAISelectableImageProps = {
|
|||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
|
||||||
resetIconSize?: IconButtonProps['size'];
|
resetIconSize?: IconButtonProps['size'];
|
||||||
|
withResetIcon?: boolean;
|
||||||
|
withMetadataOverlay?: boolean;
|
||||||
|
isDropDisabled?: boolean;
|
||||||
|
fallback?: ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IAISelectableImage = (props: IAISelectableImageProps) => {
|
const IAISelectableImage = (props: IAISelectableImageProps) => {
|
||||||
const { image, onChange, onReset, onError, resetIconSize = 'md' } = props;
|
const {
|
||||||
|
image,
|
||||||
|
onChange,
|
||||||
|
onReset,
|
||||||
|
onError,
|
||||||
|
resetIconSize = 'md',
|
||||||
|
withResetIcon = false,
|
||||||
|
withMetadataOverlay = false,
|
||||||
|
isDropDisabled = false,
|
||||||
|
fallback = <ImageFallback />,
|
||||||
|
} = props;
|
||||||
const droppableId = useRef(uuidv4());
|
const droppableId = useRef(uuidv4());
|
||||||
const { getUrl } = useGetUrl();
|
const { getUrl } = useGetUrl();
|
||||||
const { isOver, setNodeRef, active } = useDroppable({
|
const { isOver, setNodeRef, active } = useDroppable({
|
||||||
id: droppableId.current,
|
id: droppableId.current,
|
||||||
|
disabled: isDropDisabled,
|
||||||
data: {
|
data: {
|
||||||
handleDrop: onChange,
|
handleDrop: onChange,
|
||||||
},
|
},
|
||||||
@ -54,6 +69,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
|
|||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
w: 'full',
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -62,15 +78,15 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
|
|||||||
<Image
|
<Image
|
||||||
src={getUrl(image.image_url)}
|
src={getUrl(image.image_url)}
|
||||||
fallbackStrategy="beforeLoadOrError"
|
fallbackStrategy="beforeLoadOrError"
|
||||||
fallback={<ImageFallback />}
|
fallback={fallback}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ImageMetadataOverlay image={image} />
|
{withMetadataOverlay && <ImageMetadataOverlay image={image} />}
|
||||||
{onReset && (
|
{onReset && withResetIcon && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
HStack,
|
||||||
|
RangeSlider,
|
||||||
|
RangeSliderFilledTrack,
|
||||||
|
RangeSliderMark,
|
||||||
|
RangeSliderThumb,
|
||||||
|
RangeSliderTrack,
|
||||||
|
Tooltip,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
import {
|
||||||
|
controlNetBeginStepPctChanged,
|
||||||
|
controlNetEndStepPctChanged,
|
||||||
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { BiReset } from 'react-icons/bi';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
controlNetId: string;
|
||||||
|
beginStepPct: number;
|
||||||
|
endStepPct: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPct = (v: number) => `${Math.round(v * 100)}%`;
|
||||||
|
|
||||||
|
const ParamControlNetBeginEnd = (props: Props) => {
|
||||||
|
const { controlNetId, beginStepPct, endStepPct } = props;
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleStepPctChanged = useCallback(
|
||||||
|
(v: number[]) => {
|
||||||
|
dispatch(
|
||||||
|
controlNetBeginStepPctChanged({ controlNetId, beginStepPct: v[0] })
|
||||||
|
);
|
||||||
|
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: v[1] }));
|
||||||
|
},
|
||||||
|
[controlNetId, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStepPctReset = useCallback(() => {
|
||||||
|
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
|
||||||
|
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 1 }));
|
||||||
|
}, [controlNetId, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Begin & End Step %</FormLabel>
|
||||||
|
<HStack w="100%" gap={2} alignItems="center">
|
||||||
|
<RangeSlider
|
||||||
|
aria-label={['Begin Step %', 'End Step %']}
|
||||||
|
value={[beginStepPct, endStepPct]}
|
||||||
|
onChange={handleStepPctChanged}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
minStepsBetweenThumbs={5}
|
||||||
|
>
|
||||||
|
<RangeSliderTrack>
|
||||||
|
<RangeSliderFilledTrack />
|
||||||
|
</RangeSliderTrack>
|
||||||
|
<Tooltip label={formatPct(beginStepPct)} placement="top" hasArrow>
|
||||||
|
<RangeSliderThumb index={0} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={formatPct(endStepPct)} placement="top" hasArrow>
|
||||||
|
<RangeSliderThumb index={1} />
|
||||||
|
</Tooltip>
|
||||||
|
<RangeSliderMark
|
||||||
|
value={0}
|
||||||
|
sx={{
|
||||||
|
fontSize: 'xs',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: 'base.200',
|
||||||
|
insetInlineStart: '0 !important',
|
||||||
|
insetInlineEnd: 'unset !important',
|
||||||
|
mt: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
0%
|
||||||
|
</RangeSliderMark>
|
||||||
|
<RangeSliderMark
|
||||||
|
value={0.5}
|
||||||
|
sx={{
|
||||||
|
fontSize: 'xs',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: 'base.200',
|
||||||
|
mt: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
50%
|
||||||
|
</RangeSliderMark>
|
||||||
|
<RangeSliderMark
|
||||||
|
value={1}
|
||||||
|
sx={{
|
||||||
|
fontSize: 'xs',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: 'base.200',
|
||||||
|
insetInlineStart: 'unset !important',
|
||||||
|
insetInlineEnd: '0 !important',
|
||||||
|
mt: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
100%
|
||||||
|
</RangeSliderMark>
|
||||||
|
</RangeSlider>
|
||||||
|
|
||||||
|
<IAIIconButton
|
||||||
|
size="sm"
|
||||||
|
aria-label={t('accessibility.reset')}
|
||||||
|
tooltip={t('accessibility.reset')}
|
||||||
|
icon={<BiReset />}
|
||||||
|
onClick={handleStepPctReset}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ParamControlNetBeginEnd);
|
@ -1,6 +1,9 @@
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
import { controlNetBeginStepPctChanged } from 'features/controlNet/store/controlNetSlice';
|
import {
|
||||||
|
controlNetBeginStepPctChanged,
|
||||||
|
controlNetEndStepPctChanged,
|
||||||
|
} from 'features/controlNet/store/controlNetSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
|
|
||||||
type ParamControlNetBeginStepPctProps = {
|
type ParamControlNetBeginStepPctProps = {
|
||||||
@ -21,9 +24,20 @@ const ParamControlNetBeginStepPct = (
|
|||||||
[controlNetId, dispatch]
|
[controlNetId, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBeginStepPctReset = () => {
|
const handleBeginStepPctReset = useCallback(() => {
|
||||||
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
|
dispatch(controlNetBeginStepPctChanged({ controlNetId, beginStepPct: 0 }));
|
||||||
};
|
}, [controlNetId, dispatch]);
|
||||||
|
|
||||||
|
const handleEndStepPctChanged = useCallback(
|
||||||
|
(endStepPct: number) => {
|
||||||
|
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct }));
|
||||||
|
},
|
||||||
|
[controlNetId, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEndStepPctReset = useCallback(() => {
|
||||||
|
dispatch(controlNetEndStepPctChanged({ controlNetId, endStepPct: 0 }));
|
||||||
|
}, [controlNetId, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAISlider
|
<IAISlider
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
RequiredControlNetProcessorNode,
|
RequiredControlNetProcessorNode,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { CONTROLNET_PROCESSORS } from './constants';
|
import { CONTROLNET_PROCESSORS } from './constants';
|
||||||
|
import { controlNetImageProcessed } from './actions';
|
||||||
|
|
||||||
export const CONTROLNET_MODELS = [
|
export const CONTROLNET_MODELS = [
|
||||||
'lllyasviel/sd-controlnet-canny',
|
'lllyasviel/sd-controlnet-canny',
|
||||||
@ -52,12 +53,14 @@ export type ControlNetState = {
|
|||||||
controlNets: Record<string, ControlNet>;
|
controlNets: Record<string, ControlNet>;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
shouldAutoProcess: boolean;
|
shouldAutoProcess: boolean;
|
||||||
|
isProcessingControlImage: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialControlNetState: ControlNetState = {
|
export const initialControlNetState: ControlNetState = {
|
||||||
controlNets: {},
|
controlNets: {},
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
shouldAutoProcess: true,
|
shouldAutoProcess: true,
|
||||||
|
isProcessingControlImage: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const controlNetSlice = createSlice({
|
export const controlNetSlice = createSlice({
|
||||||
@ -107,6 +110,9 @@ export const controlNetSlice = createSlice({
|
|||||||
const { controlNetId, controlImage } = action.payload;
|
const { controlNetId, controlImage } = action.payload;
|
||||||
state.controlNets[controlNetId].controlImage = controlImage;
|
state.controlNets[controlNetId].controlImage = controlImage;
|
||||||
state.controlNets[controlNetId].processedControlImage = null;
|
state.controlNets[controlNetId].processedControlImage = null;
|
||||||
|
if (state.shouldAutoProcess && controlImage !== null) {
|
||||||
|
state.isProcessingControlImage = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isControlNetImageProcessedToggled: (
|
isControlNetImageProcessedToggled: (
|
||||||
state,
|
state,
|
||||||
@ -128,6 +134,7 @@ export const controlNetSlice = createSlice({
|
|||||||
const { controlNetId, processedControlImage } = action.payload;
|
const { controlNetId, processedControlImage } = action.payload;
|
||||||
state.controlNets[controlNetId].processedControlImage =
|
state.controlNets[controlNetId].processedControlImage =
|
||||||
processedControlImage;
|
processedControlImage;
|
||||||
|
state.isProcessingControlImage = false;
|
||||||
},
|
},
|
||||||
controlNetModelChanged: (
|
controlNetModelChanged: (
|
||||||
state,
|
state,
|
||||||
@ -190,6 +197,15 @@ export const controlNetSlice = createSlice({
|
|||||||
state.shouldAutoProcess = !state.shouldAutoProcess;
|
state.shouldAutoProcess = !state.shouldAutoProcess;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder.addCase(controlNetImageProcessed, (state, action) => {
|
||||||
|
if (
|
||||||
|
state.controlNets[action.payload.controlNetId].controlImage !== null
|
||||||
|
) {
|
||||||
|
state.isProcessingControlImage = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
@ -8,7 +8,7 @@ import { isEqual } from 'lodash-es';
|
|||||||
import { gallerySelector } from '../store/gallerySelectors';
|
import { gallerySelector } from '../store/gallerySelectors';
|
||||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||||
import NextPrevImageButtons from './NextPrevImageButtons';
|
import NextPrevImageButtons from './NextPrevImageButtons';
|
||||||
import { DragEvent, memo, useCallback } from 'react';
|
import { DragEvent, memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||||
import ImageFallbackSpinner from './ImageFallbackSpinner';
|
import ImageFallbackSpinner from './ImageFallbackSpinner';
|
||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
@ -55,6 +55,7 @@ const CurrentImagePreview = () => {
|
|||||||
const { getUrl } = useGetUrl();
|
const { getUrl } = useGetUrl();
|
||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef } = useDraggable({
|
const { attributes, listeners, setNodeRef } = useDraggable({
|
||||||
id: `currentImage_${image?.image_name}`,
|
id: `currentImage_${image?.image_name}`,
|
||||||
@ -74,11 +75,15 @@ const CurrentImagePreview = () => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, toaster, shouldFetchImages]);
|
}, [dispatch, toaster, shouldFetchImages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoaded(false);
|
||||||
|
}, [image]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: 'full',
|
||||||
height: '100%',
|
height: 'full',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -91,8 +96,8 @@ const CurrentImagePreview = () => {
|
|||||||
height={progressImage.height}
|
height={progressImage.height}
|
||||||
sx={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
maxWidth: '100%',
|
maxWidth: 'full',
|
||||||
maxHeight: '100%',
|
maxHeight: 'full',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
borderRadius: 'base',
|
borderRadius: 'base',
|
||||||
@ -124,8 +129,11 @@ const CurrentImagePreview = () => {
|
|||||||
touchAction: 'none',
|
touchAction: 'none',
|
||||||
}}
|
}}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
|
onLoad={() => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ImageMetadataOverlay image={image} />
|
{isLoaded && <ImageMetadataOverlay image={image} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -14,7 +14,8 @@ const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
color: 'base.400',
|
color: 'base.400',
|
||||||
minH: 40,
|
minH: 36,
|
||||||
|
minW: 36,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Spinner size={size} {...rest} />
|
<Spinner size={size} {...rest} />
|
||||||
|
Loading…
Reference in New Issue
Block a user