mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'development' into fnformat
This commit is contained in:
commit
0e551a3844
@ -49,21 +49,16 @@ class InvokeAIWebServer:
|
||||
engineio_logger = True if args.web_verbose else False
|
||||
max_http_buffer_size = 10000000
|
||||
|
||||
# CORS Allowed Setup
|
||||
cors_allowed_origins = [
|
||||
'http://127.0.0.1:5173',
|
||||
'http://localhost:5173',
|
||||
'http://localhost:9090'
|
||||
]
|
||||
additional_allowed_origins = (
|
||||
opt.cors if opt.cors else []
|
||||
) # additional CORS allowed origins
|
||||
cors_allowed_origins.append(f'http://{self.host}:{self.port}')
|
||||
if self.host == '127.0.0.1' or self.host == '0.0.0.0':
|
||||
cors_allowed_origins.append(f'http://localhost:{self.port}')
|
||||
cors_allowed_origins = (
|
||||
cors_allowed_origins + additional_allowed_origins
|
||||
)
|
||||
socketio_args = {
|
||||
'logger': logger,
|
||||
'engineio_logger': engineio_logger,
|
||||
'max_http_buffer_size': max_http_buffer_size,
|
||||
'ping_interval': (50, 50),
|
||||
'ping_timeout': 60,
|
||||
}
|
||||
|
||||
if opt.cors:
|
||||
socketio_args['cors_allowed_origins'] = opt.cors
|
||||
|
||||
self.app = Flask(
|
||||
__name__, static_url_path='', static_folder='../frontend/dist/'
|
||||
@ -71,12 +66,7 @@ class InvokeAIWebServer:
|
||||
|
||||
self.socketio = SocketIO(
|
||||
self.app,
|
||||
logger=logger,
|
||||
engineio_logger=engineio_logger,
|
||||
max_http_buffer_size=max_http_buffer_size,
|
||||
cors_allowed_origins=cors_allowed_origins,
|
||||
ping_interval=(50, 50),
|
||||
ping_timeout=60,
|
||||
**socketio_args
|
||||
)
|
||||
|
||||
# Keep Server Alive Route
|
||||
|
@ -32,7 +32,7 @@ model:
|
||||
placeholder_strings: ["*"]
|
||||
initializer_words: ['face', 'man', 'photo', 'africanmale']
|
||||
per_image_tokens: false
|
||||
num_vectors_per_token: 6
|
||||
num_vectors_per_token: 1
|
||||
progressive_words: False
|
||||
|
||||
unet_config:
|
||||
|
BIN
frontend/dist/assets/image2img.dde6a9f1.png
vendored
BIN
frontend/dist/assets/image2img.dde6a9f1.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 336 KiB |
1
frontend/dist/assets/index.22ee377a.css
vendored
Normal file
1
frontend/dist/assets/index.22ee377a.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.60ca0ee5.css
vendored
1
frontend/dist/assets/index.60ca0ee5.css
vendored
File diff suppressed because one or more lines are too long
483
frontend/dist/assets/index.9e9b1310.js
vendored
Normal file
483
frontend/dist/assets/index.9e9b1310.js
vendored
Normal file
File diff suppressed because one or more lines are too long
483
frontend/dist/assets/index.a0198006.js
vendored
483
frontend/dist/assets/index.a0198006.js
vendored
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="shortcut icon" type="icon" href="/assets/favicon.0d253ced.ico" />
|
||||
<script type="module" crossorigin src="/assets/index.a0198006.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.60ca0ee5.css">
|
||||
<script type="module" crossorigin src="/assets/index.9e9b1310.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.22ee377a.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -15,3 +15,7 @@
|
||||
width: $app-width;
|
||||
height: $app-height;
|
||||
}
|
||||
|
||||
.app-console {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
@ -26,7 +26,9 @@ const App = () => {
|
||||
<SiteHeader />
|
||||
<InvokeTabs />
|
||||
</div>
|
||||
<Console />
|
||||
<div className="app-console">
|
||||
<Console />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
addLogEntry,
|
||||
setIsProcessing,
|
||||
} from '../../features/system/systemSlice';
|
||||
import { tabMap, tab_dict } from '../../features/tabs/InvokeTabs';
|
||||
import * as InvokeAI from '../invokeai';
|
||||
|
||||
/**
|
||||
@ -23,8 +24,14 @@ const makeSocketIOEmitters = (
|
||||
emitGenerateImage: () => {
|
||||
dispatch(setIsProcessing(true));
|
||||
|
||||
const options = { ...getState().options };
|
||||
|
||||
if (tabMap[options.activeTab] === 'txt2img') {
|
||||
options.shouldUseInitImage = false;
|
||||
}
|
||||
|
||||
const { generationParameters, esrganParameters, gfpganParameters } =
|
||||
frontendToBackendParameters(getState().options, getState().system);
|
||||
frontendToBackendParameters(options, getState().system);
|
||||
|
||||
socketio.emit(
|
||||
'generateImage',
|
||||
|
65
frontend/src/common/components/InvokeImageUploader.tsx
Normal file
65
frontend/src/common/components/InvokeImageUploader.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { Button, useToast } from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import ImageUploader from '../../features/options/ImageUploader';
|
||||
|
||||
interface InvokeImageUploaderProps {
|
||||
label?: string;
|
||||
icon?: any;
|
||||
onMouseOver?: any;
|
||||
OnMouseout?: any;
|
||||
dispatcher: any;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
|
||||
const { label, icon, dispatcher, styleClass, onMouseOver, OnMouseout } =
|
||||
props;
|
||||
|
||||
const toast = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Callbacks to for handling file upload attempts
|
||||
const fileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(dispatcher(file)),
|
||||
[dispatch, dispatcher]
|
||||
);
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={fileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
styleClass={styleClass}
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={OnMouseout}
|
||||
leftIcon={icon}
|
||||
width={'100%'}
|
||||
>
|
||||
{label ? label : null}
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
);
|
||||
}
|
@ -18,6 +18,7 @@ export const optionsSelector = createSelector(
|
||||
maskPath: options.maskPath,
|
||||
initialImagePath: options.initialImagePath,
|
||||
seed: options.seed,
|
||||
activeTab: options.activeTab,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -55,6 +56,7 @@ const useCheckParameters = (): boolean => {
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
seed,
|
||||
activeTab,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
@ -65,6 +67,10 @@ const useCheckParameters = (): boolean => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prompt && !initialImagePath && activeTab === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImagePath) {
|
||||
return false;
|
||||
@ -100,6 +106,7 @@ const useCheckParameters = (): boolean => {
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
seed,
|
||||
activeTab,
|
||||
]);
|
||||
};
|
||||
|
||||
|
@ -6,9 +6,11 @@ import * as InvokeAI from '../../app/invokeai';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
setActiveTab,
|
||||
setAllParameters,
|
||||
setInitialImagePath,
|
||||
setSeed,
|
||||
setShouldShowImageDetails,
|
||||
} from '../options/optionsSlice';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
@ -41,21 +43,19 @@ const systemSelector = createSelector(
|
||||
|
||||
type CurrentImageButtonsProps = {
|
||||
image: InvokeAI.Image;
|
||||
shouldShowImageDetails: boolean;
|
||||
setShouldShowImageDetails: (b: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Row of buttons for common actions:
|
||||
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
|
||||
*/
|
||||
const CurrentImageButtons = ({
|
||||
image,
|
||||
shouldShowImageDetails,
|
||||
setShouldShowImageDetails,
|
||||
}: CurrentImageButtonsProps) => {
|
||||
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const intermediateImage = useAppSelector(
|
||||
@ -73,8 +73,11 @@ const CurrentImageButtons = ({
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const handleClickUseAsInitialImage = () =>
|
||||
const handleClickUseAsInitialImage = () => {
|
||||
dispatch(setInitialImagePath(image.url));
|
||||
dispatch(setActiveTab(1));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'shift+i',
|
||||
() => {
|
||||
@ -215,7 +218,8 @@ const CurrentImageButtons = ({
|
||||
);
|
||||
|
||||
const handleClickShowImageDetails = () =>
|
||||
setShouldShowImageDetails(!shouldShowImageDetails);
|
||||
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
||||
|
||||
useHotkeys(
|
||||
'i',
|
||||
() => {
|
||||
@ -237,8 +241,8 @@ const CurrentImageButtons = ({
|
||||
<div className="current-image-options">
|
||||
<IAIIconButton
|
||||
icon={<MdImage />}
|
||||
tooltip="Use As Initial Image"
|
||||
aria-label="Use As Initial Image"
|
||||
tooltip="Send To Image To Image"
|
||||
aria-label="Send To Image To Image"
|
||||
onClick={handleClickUseAsInitialImage}
|
||||
/>
|
||||
|
||||
|
@ -11,23 +11,7 @@
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.current-image-display-placeholder {
|
||||
background-color: var(--background-color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.current-image-tools {
|
||||
grid-area: current-image-tools;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
@ -58,34 +42,37 @@
|
||||
align-items: center;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-areas: 'current-image-content';
|
||||
|
||||
img {
|
||||
grid-area: current-image-content;
|
||||
background-color: var(--img2img-img-bg-color);
|
||||
border-radius: 0.5rem;
|
||||
object-fit: contain;
|
||||
width: auto;
|
||||
height: $app-gallery-height;
|
||||
max-height: $app-gallery-height;
|
||||
}
|
||||
}
|
||||
|
||||
.current-image-metadata {
|
||||
grid-area: current-image-preview;
|
||||
}
|
||||
|
||||
.current-image-next-prev-buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
grid-area: current-image-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: calc(100% - 2rem);
|
||||
padding: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
z-index: 1;
|
||||
height: calc($app-metadata-height - 1rem);
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.next-prev-button-trigger-area {
|
||||
width: 7rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
|
||||
@ -99,31 +86,25 @@
|
||||
}
|
||||
|
||||
.next-prev-button {
|
||||
font-size: 5rem;
|
||||
fill: var(--text-color-secondary);
|
||||
font-size: 4rem;
|
||||
fill: var(--white);
|
||||
filter: drop-shadow(0 0 1rem var(--text-color-secondary));
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
.current-image-metadata-viewer {
|
||||
.current-image-display-placeholder {
|
||||
background-color: var(--background-color-secondary);
|
||||
display: grid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0.5rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: calc(100% - 2rem);
|
||||
padding: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
background-color: var(--metadata-bg-color);
|
||||
z-index: 1;
|
||||
overflow: scroll;
|
||||
height: calc($app-metadata-height - 1rem);
|
||||
}
|
||||
|
||||
.current-image-json-viewer {
|
||||
border-radius: 0.5rem;
|
||||
margin: 0 0.5rem 1rem 0.5rem;
|
||||
padding: 1rem;
|
||||
overflow-x: scroll;
|
||||
word-break: break-all;
|
||||
background-color: var(--metadata-json-bg-color);
|
||||
svg {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
}
|
||||
|
@ -1,100 +1,35 @@
|
||||
import { IconButton, Image } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useState } from 'react';
|
||||
import ImageMetadataViewer from './ImageMetadataViewer';
|
||||
import { RootState, useAppSelector } from '../../app/store';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { MdPhoto } from 'react-icons/md';
|
||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
|
||||
|
||||
/**
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const [shouldShowImageDetails, setShouldShowImageDetails] =
|
||||
useState<boolean>(false);
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
const handleCurrentImagePreviewMouseOver = () => {
|
||||
setShouldShowNextPrevButtons(true);
|
||||
};
|
||||
|
||||
const handleCurrentImagePreviewMouseOut = () => {
|
||||
setShouldShowNextPrevButtons(false);
|
||||
};
|
||||
|
||||
const handleClickPrevButton = () => {
|
||||
dispatch(selectPrevImage());
|
||||
};
|
||||
|
||||
const handleClickNextButton = () => {
|
||||
dispatch(selectNextImage());
|
||||
};
|
||||
|
||||
return imageToDisplay ? (
|
||||
<div className="current-image-display">
|
||||
<div className="current-image-tools">
|
||||
<CurrentImageButtons
|
||||
<CurrentImageButtons image={imageToDisplay} />
|
||||
</div>
|
||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||
{shouldShowImageDetails && (
|
||||
<ImageMetadataViewer
|
||||
image={imageToDisplay}
|
||||
shouldShowImageDetails={shouldShowImageDetails}
|
||||
setShouldShowImageDetails={setShouldShowImageDetails}
|
||||
styleClass="current-image-metadata"
|
||||
/>
|
||||
</div>
|
||||
<div className="current-image-preview">
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit="contain"
|
||||
maxWidth={'100%'}
|
||||
maxHeight={'100%'}
|
||||
/>
|
||||
{shouldShowImageDetails && (
|
||||
<div className="current-image-metadata-viewer">
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</div>
|
||||
)}
|
||||
{!shouldShowImageDetails && (
|
||||
<div className="current-image-next-prev-buttons">
|
||||
<div
|
||||
className="next-prev-button-trigger-area prev-button-trigger-area"
|
||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
||||
>
|
||||
{shouldShowNextPrevButtons && (
|
||||
<IconButton
|
||||
aria-label="Previous image"
|
||||
icon={<FaAngleLeft className="next-prev-button" />}
|
||||
variant="unstyled"
|
||||
onClick={handleClickPrevButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="next-prev-button-trigger-area next-button-trigger-area"
|
||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
||||
>
|
||||
{shouldShowNextPrevButtons && (
|
||||
<IconButton
|
||||
aria-label="Next image"
|
||||
icon={<FaAngleRight className="next-prev-button" />}
|
||||
variant="unstyled"
|
||||
onClick={handleClickNextButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="current-image-display-placeholder">
|
||||
|
105
frontend/src/features/gallery/CurrentImagePreview.tsx
Normal file
105
frontend/src/features/gallery/CurrentImagePreview.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { IconButton, Image } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
|
||||
const imagesSelector = createSelector(
|
||||
(state: RootState) => state.gallery,
|
||||
(gallery: GalleryState) => {
|
||||
const currentImageIndex = gallery.images.findIndex(
|
||||
(i) => i.uuid === gallery?.currentImage?.uuid
|
||||
);
|
||||
const imagesLength = gallery.images.length;
|
||||
return {
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
isOnLastImage:
|
||||
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface CurrentImagePreviewProps {
|
||||
imageToDisplay: InvokeAI.Image;
|
||||
}
|
||||
|
||||
export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
const { imageToDisplay } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { isOnFirstImage, isOnLastImage } = useAppSelector(imagesSelector);
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
);
|
||||
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const handleCurrentImagePreviewMouseOver = () => {
|
||||
setShouldShowNextPrevButtons(true);
|
||||
};
|
||||
|
||||
const handleCurrentImagePreviewMouseOut = () => {
|
||||
setShouldShowNextPrevButtons(false);
|
||||
};
|
||||
|
||||
const handleClickPrevButton = () => {
|
||||
dispatch(selectPrevImage());
|
||||
};
|
||||
|
||||
const handleClickNextButton = () => {
|
||||
dispatch(selectNextImage());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="current-image-preview">
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit="contain"
|
||||
maxWidth={'100%'}
|
||||
maxHeight={'100%'}
|
||||
/>
|
||||
{!shouldShowImageDetails && (
|
||||
<div className="current-image-next-prev-buttons">
|
||||
<div
|
||||
className="next-prev-button-trigger-area prev-button-trigger-area"
|
||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
||||
>
|
||||
{shouldShowNextPrevButtons && !isOnFirstImage && (
|
||||
<IconButton
|
||||
aria-label="Previous image"
|
||||
icon={<FaAngleLeft className="next-prev-button" />}
|
||||
variant="unstyled"
|
||||
onClick={handleClickPrevButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="next-prev-button-trigger-area next-button-trigger-area"
|
||||
onMouseOver={handleCurrentImagePreviewMouseOver}
|
||||
onMouseOut={handleCurrentImagePreviewMouseOut}
|
||||
>
|
||||
{shouldShowNextPrevButtons && !isOnLastImage && (
|
||||
<IconButton
|
||||
aria-label="Next image"
|
||||
icon={<FaAngleRight className="next-prev-button" />}
|
||||
variant="unstyled"
|
||||
onClick={handleClickNextButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -7,12 +7,17 @@ import {
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { setCurrentImage } from './gallerySlice';
|
||||
import { FaCheck, FaSeedling, FaTrashAlt } from 'react-icons/fa';
|
||||
import { FaCheck, FaImage, FaSeedling, FaTrashAlt } from 'react-icons/fa';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { memo, SyntheticEvent, useState } from 'react';
|
||||
import { setAllParameters, setSeed } from '../options/optionsSlice';
|
||||
import {
|
||||
setActiveTab,
|
||||
setAllParameters,
|
||||
setInitialImagePath,
|
||||
setSeed,
|
||||
} from '../options/optionsSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
|
||||
@ -33,6 +38,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const activeTab = useAppSelector(
|
||||
(state: RootState) => state.options.activeTab
|
||||
);
|
||||
|
||||
const checkColor = useColorModeValue('green.600', 'green.300');
|
||||
const bgColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const bgGradient = useColorModeValue(
|
||||
@ -56,6 +65,14 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
dispatch(setSeed(image.metadata.image.seed));
|
||||
};
|
||||
|
||||
const handleSetInitImage = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setInitialImagePath(image.url));
|
||||
if (activeTab !== 1) {
|
||||
dispatch(setActiveTab(1));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickImage = () => dispatch(setCurrentImage(image));
|
||||
|
||||
return (
|
||||
@ -131,6 +148,16 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="Send To Image To Image">
|
||||
<IconButton
|
||||
aria-label="Send To Image To Image"
|
||||
icon={<FaImage />}
|
||||
size="xs"
|
||||
fontSize={16}
|
||||
variant={'imageHoverIconButton'}
|
||||
onClickCapture={handleSetInitImage}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
@ -1,43 +1,67 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.image-gallery-area {
|
||||
.image-gallery-popup-btn {
|
||||
@include Button(
|
||||
$btn-width: 3rem,
|
||||
$btn-height: 3rem,
|
||||
$icon-size: 22px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-popup {
|
||||
background-color: var(--tab-color);
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
animation: slideOut 0.3s ease-out;
|
||||
display: grid;
|
||||
grid-auto-rows: max-content;
|
||||
row-gap: 1rem;
|
||||
border-left-width: 0.2rem;
|
||||
border-color: var(--gallery-resizeable-color);
|
||||
}
|
||||
|
||||
.image-gallery-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-close-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-container {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
grid-auto-rows: max-content;
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
.image-gallery-container-placeholder {
|
||||
display: grid;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
place-items: center;
|
||||
padding: 2rem 0;
|
||||
|
||||
p {
|
||||
color: var(--subtext-color-bright);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
gap: 1rem;
|
||||
max-height: $app-gallery-popover-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.image-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, auto));
|
||||
gap: 0.6rem;
|
||||
justify-items: center;
|
||||
max-height: $app-gallery-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.image-gallery-load-more-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
font-size: 0.85rem !important;
|
||||
font-family: Inter;
|
||||
|
||||
&:disabled {
|
||||
&:hover {
|
||||
@ -49,3 +73,22 @@
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery-container-placeholder {
|
||||
display: grid;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
place-items: center;
|
||||
padding: 2rem 0;
|
||||
|
||||
p {
|
||||
color: var(--subtext-color-bright);
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,47 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import { Resizable } from 're-resizable';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
|
||||
import { requestImages } from '../../app/socketio/actions';
|
||||
import { RootState, useAppDispatch } from '../../app/store';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import {
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
setShouldShowGallery,
|
||||
} from './gallerySlice';
|
||||
import HoverableImage from './HoverableImage';
|
||||
|
||||
/**
|
||||
* Simple image gallery.
|
||||
*/
|
||||
const ImageGallery = () => {
|
||||
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
export default function ImageGallery() {
|
||||
const {
|
||||
images,
|
||||
currentImageUuid,
|
||||
areMoreImagesAvailable,
|
||||
shouldShowGallery,
|
||||
} = useAppSelector((state: RootState) => state.gallery);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
/**
|
||||
* I don't like that this needs to rerender whenever the current image is changed.
|
||||
* What if we have a large number of images? I suppose pagination (planned) will
|
||||
* mitigate this issue.
|
||||
*
|
||||
* TODO: Refactor if performance complaints, or after migrating to new API which supports pagination.
|
||||
*/
|
||||
|
||||
const handleShowGalleryToggle = () => {
|
||||
dispatch(setShouldShowGallery(!shouldShowGallery));
|
||||
};
|
||||
|
||||
const handleGalleryClose = () => {
|
||||
dispatch(setShouldShowGallery(false));
|
||||
};
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
dispatch(requestImages());
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'g',
|
||||
() => {
|
||||
handleShowGalleryToggle();
|
||||
},
|
||||
[shouldShowGallery]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
@ -44,41 +59,64 @@ const ImageGallery = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="image-gallery-container">
|
||||
{images.length ? (
|
||||
<>
|
||||
<p>
|
||||
<strong>Your Invocations</strong>
|
||||
</p>
|
||||
<div className="image-gallery">
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="image-gallery-container-placeholder">
|
||||
<div className="image-gallery-area">
|
||||
{!shouldShowGallery && (
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
onClick={handleShowGalleryToggle}
|
||||
className="image-gallery-popup-btn"
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
<p>No Images In Gallery</p>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{shouldShowGallery && (
|
||||
<Resizable
|
||||
defaultSize={{ width: '300', height: '100%' }}
|
||||
minWidth={'300'}
|
||||
className="image-gallery-popup"
|
||||
>
|
||||
<div className="image-gallery-header">
|
||||
<h1>Your Invocations</h1>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
aria-label={'Close Gallery'}
|
||||
onClick={handleGalleryClose}
|
||||
className="image-gallery-close-btn"
|
||||
icon={<MdClear />}
|
||||
/>
|
||||
</div>
|
||||
<div className="image-gallery-container">
|
||||
{images.length ? (
|
||||
<div className="image-gallery">
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="image-gallery-container-placeholder">
|
||||
<MdPhotoLibrary />
|
||||
<p>No Images In Gallery</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
className="image-gallery-load-more-btn"
|
||||
>
|
||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||
</Button>
|
||||
</div>
|
||||
</Resizable>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
className="image-gallery-load-more-btn"
|
||||
>
|
||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGallery;
|
||||
}
|
||||
|
129
frontend/src/features/gallery/ImageGalleryOld.tsx
Normal file
129
frontend/src/features/gallery/ImageGalleryOld.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { requestImages } from '../../app/socketio/actions';
|
||||
import { RootState, useAppDispatch } from '../../app/store';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
import HoverableImage from './HoverableImage';
|
||||
|
||||
/**
|
||||
* Simple image gallery.
|
||||
*/
|
||||
const ImageGalleryOld = () => {
|
||||
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
/**
|
||||
* I don't like that this needs to rerender whenever the current image is changed.
|
||||
* What if we have a large number of images? I suppose pagination (planned) will
|
||||
* mitigate this issue.
|
||||
*
|
||||
* TODO: Refactor if performance complaints, or after migrating to new API which supports pagination.
|
||||
*/
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
dispatch(requestImages());
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'g',
|
||||
() => {
|
||||
if (isOpen) {
|
||||
onClose();
|
||||
} else {
|
||||
onOpen();
|
||||
}
|
||||
},
|
||||
[isOpen]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
dispatch(selectPrevImage());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
dispatch(selectNextImage());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="image-gallery-area">
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
onClick={onOpen}
|
||||
className="image-gallery-popup-btn"
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</Button>
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
autoFocus={false}
|
||||
trapFocus={false}
|
||||
closeOnOverlayClick={false}
|
||||
>
|
||||
<DrawerContent className="image-gallery-popup">
|
||||
<div className="image-gallery-header">
|
||||
<DrawerHeader>Your Invocations</DrawerHeader>
|
||||
<DrawerCloseButton />
|
||||
</div>
|
||||
<DrawerBody className="image-gallery-body">
|
||||
<div className="image-gallery-container">
|
||||
{images.length ? (
|
||||
<div className="image-gallery">
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="image-gallery-container-placeholder">
|
||||
<MdPhotoLibrary />
|
||||
<p>No Images In Gallery</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
className="image-gallery-load-more-btn"
|
||||
>
|
||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||
</Button>
|
||||
</div>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGalleryOld;
|
@ -0,0 +1,20 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.image-metadata-viewer {
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--metadata-bg-color);
|
||||
overflow: scroll;
|
||||
max-height: $app-metadata-height;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.image-json-viewer {
|
||||
border-radius: 0.5rem;
|
||||
margin: 0 0.5rem 1rem 0.5rem;
|
||||
padding: 1rem;
|
||||
overflow-x: scroll;
|
||||
word-break: break-all;
|
||||
background-color: var(--metadata-json-bg-color);
|
||||
}
|
@ -0,0 +1,360 @@
|
||||
import {
|
||||
Center,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { memo } from 'react';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { useAppDispatch } from '../../../app/store';
|
||||
import * as InvokeAI from '../../../app/invokeai';
|
||||
import {
|
||||
setCfgScale,
|
||||
setGfpganStrength,
|
||||
setHeight,
|
||||
setImg2imgStrength,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
setPrompt,
|
||||
setSampler,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
setShouldFitToWidthHeight,
|
||||
setSteps,
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
setWidth,
|
||||
} from '../../options/optionsSlice';
|
||||
import promptToString from '../../../common/util/promptToString';
|
||||
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
|
||||
import { FaCopy } from 'react-icons/fa';
|
||||
|
||||
type MetadataItemProps = {
|
||||
isLink?: boolean;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
value: number | string | boolean;
|
||||
labelPosition?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display an individual metadata item or parameter.
|
||||
*/
|
||||
const MetadataItem = ({
|
||||
label,
|
||||
value,
|
||||
onClick,
|
||||
isLink,
|
||||
labelPosition,
|
||||
}: MetadataItemProps) => {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
{onClick && (
|
||||
<Tooltip label={`Recall ${label}`}>
|
||||
<IconButton
|
||||
aria-label="Use this parameter"
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
size={'xs'}
|
||||
variant={'ghost'}
|
||||
fontSize={20}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Flex direction={labelPosition ? 'column' : 'row'}>
|
||||
<Text fontWeight={'semibold'} whiteSpace={'pre-wrap'} pr={2}>
|
||||
{label}:
|
||||
</Text>
|
||||
{isLink ? (
|
||||
<Link href={value.toString()} isExternal wordBreak={'break-all'}>
|
||||
{value.toString()} <ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
) : (
|
||||
<Text overflowY={'scroll'} wordBreak={'break-all'}>
|
||||
{value.toString()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
image: InvokeAI.Image;
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
// TODO: I don't know if this is needed.
|
||||
const memoEqualityCheck = (
|
||||
prev: ImageMetadataViewerProps,
|
||||
next: ImageMetadataViewerProps
|
||||
) => prev.image.uuid === next.image.uuid;
|
||||
|
||||
// TODO: Show more interesting information in this component.
|
||||
|
||||
/**
|
||||
* Image metadata viewer overlays currently selected image and provides
|
||||
* access to any of its metadata for use in processing.
|
||||
*/
|
||||
const ImageMetadataViewer = memo(
|
||||
({ image, styleClass }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
|
||||
const metadata = image?.metadata?.image || {};
|
||||
const {
|
||||
type,
|
||||
postprocessing,
|
||||
sampler,
|
||||
prompt,
|
||||
seed,
|
||||
variations,
|
||||
steps,
|
||||
cfg_scale,
|
||||
seamless,
|
||||
width,
|
||||
height,
|
||||
strength,
|
||||
fit,
|
||||
init_image_path,
|
||||
mask_image_path,
|
||||
orig_path,
|
||||
scale,
|
||||
} = metadata;
|
||||
|
||||
const metadataJSON = JSON.stringify(metadata, null, 2);
|
||||
|
||||
return (
|
||||
<div className={`image-metadata-viewer ${styleClass}`}>
|
||||
<Flex gap={1} direction={'column'} width={'100%'}>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>File:</Text>
|
||||
<Link href={image.url} isExternal>
|
||||
{image.url}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
</Flex>
|
||||
{Object.keys(metadata).length > 0 ? (
|
||||
<>
|
||||
{type && <MetadataItem label="Generation type" value={type} />}
|
||||
{['esrgan', 'gfpgan'].includes(type) && (
|
||||
<MetadataItem label="Original image" value={orig_path} />
|
||||
)}
|
||||
{type === 'gfpgan' && strength !== undefined && (
|
||||
<MetadataItem
|
||||
label="Fix faces strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setGfpganStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{type === 'esrgan' && scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="Upscaling scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
)}
|
||||
{type === 'esrgan' && strength !== undefined && (
|
||||
<MetadataItem
|
||||
label="Upscaling strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setUpscalingStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{prompt && (
|
||||
<MetadataItem
|
||||
label="Prompt"
|
||||
labelPosition="top"
|
||||
value={promptToString(prompt)}
|
||||
onClick={() => dispatch(setPrompt(prompt))}
|
||||
/>
|
||||
)}
|
||||
{seed !== undefined && (
|
||||
<MetadataItem
|
||||
label="Seed"
|
||||
value={seed}
|
||||
onClick={() => dispatch(setSeed(seed))}
|
||||
/>
|
||||
)}
|
||||
{sampler && (
|
||||
<MetadataItem
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onClick={() => dispatch(setSampler(sampler))}
|
||||
/>
|
||||
)}
|
||||
{steps && (
|
||||
<MetadataItem
|
||||
label="Steps"
|
||||
value={steps}
|
||||
onClick={() => dispatch(setSteps(steps))}
|
||||
/>
|
||||
)}
|
||||
{cfg_scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="CFG scale"
|
||||
value={cfg_scale}
|
||||
onClick={() => dispatch(setCfgScale(cfg_scale))}
|
||||
/>
|
||||
)}
|
||||
{variations && variations.length > 0 && (
|
||||
<MetadataItem
|
||||
label="Seed-weight pairs"
|
||||
value={seedWeightsToString(variations)}
|
||||
onClick={() =>
|
||||
dispatch(setSeedWeights(seedWeightsToString(variations)))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{seamless && (
|
||||
<MetadataItem
|
||||
label="Seamless"
|
||||
value={seamless}
|
||||
onClick={() => dispatch(setWidth(seamless))}
|
||||
/>
|
||||
)}
|
||||
{width && (
|
||||
<MetadataItem
|
||||
label="Width"
|
||||
value={width}
|
||||
onClick={() => dispatch(setWidth(width))}
|
||||
/>
|
||||
)}
|
||||
{height && (
|
||||
<MetadataItem
|
||||
label="Height"
|
||||
value={height}
|
||||
onClick={() => dispatch(setHeight(height))}
|
||||
/>
|
||||
)}
|
||||
{init_image_path && (
|
||||
<MetadataItem
|
||||
label="Initial image"
|
||||
value={init_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setInitialImagePath(init_image_path))}
|
||||
/>
|
||||
)}
|
||||
{mask_image_path && (
|
||||
<MetadataItem
|
||||
label="Mask image"
|
||||
value={mask_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setMaskPath(mask_image_path))}
|
||||
/>
|
||||
)}
|
||||
{type === 'img2img' && strength && (
|
||||
<MetadataItem
|
||||
label="Image to image strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setImg2imgStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{fit && (
|
||||
<MetadataItem
|
||||
label="Image to image fit"
|
||||
value={fit}
|
||||
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
|
||||
/>
|
||||
)}
|
||||
{postprocessing && postprocessing.length > 0 && (
|
||||
<>
|
||||
<Heading size={'sm'}>Postprocessing</Heading>
|
||||
{postprocessing.map(
|
||||
(
|
||||
postprocess: InvokeAI.PostProcessedImageMetadata,
|
||||
i: number
|
||||
) => {
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength } = postprocess;
|
||||
return (
|
||||
<Flex
|
||||
key={i}
|
||||
pl={'2rem'}
|
||||
gap={1}
|
||||
direction={'column'}
|
||||
>
|
||||
<Text size={'md'}>{`${
|
||||
i + 1
|
||||
}: Upscale (ESRGAN)`}</Text>
|
||||
<MetadataItem
|
||||
label="Scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() =>
|
||||
dispatch(setUpscalingStrength(strength))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
} else if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
return (
|
||||
<Flex
|
||||
key={i}
|
||||
pl={'2rem'}
|
||||
gap={1}
|
||||
direction={'column'}
|
||||
>
|
||||
<Text size={'md'}>{`${
|
||||
i + 1
|
||||
}: Face restoration (GFPGAN)`}</Text>
|
||||
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() =>
|
||||
dispatch(setGfpganStrength(strength))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<Flex gap={2}>
|
||||
<Tooltip label={`Copy metadata JSON`}>
|
||||
<IconButton
|
||||
aria-label="Copy metadata JSON"
|
||||
icon={<FaCopy />}
|
||||
size={'xs'}
|
||||
variant={'ghost'}
|
||||
fontSize={14}
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(metadataJSON)
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text fontWeight={'semibold'}>Metadata JSON:</Text>
|
||||
</Flex>
|
||||
<div className={'image-json-viewer'}>
|
||||
<pre>{metadataJSON}</pre>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width={'100%'} pt={10}>
|
||||
<Text fontSize={'lg'} fontWeight="semibold">
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
memoEqualityCheck
|
||||
);
|
||||
|
||||
export default ImageMetadataViewer;
|
@ -1,338 +0,0 @@
|
||||
import {
|
||||
Center,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { memo } from 'react';
|
||||
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import {
|
||||
setCfgScale,
|
||||
setGfpganStrength,
|
||||
setHeight,
|
||||
setImg2imgStrength,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
setPrompt,
|
||||
setSampler,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
setShouldFitToWidthHeight,
|
||||
setSteps,
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
setWidth,
|
||||
} from '../options/optionsSlice';
|
||||
import promptToString from '../../common/util/promptToString';
|
||||
import { seedWeightsToString } from '../../common/util/seedWeightPairs';
|
||||
import { FaCopy } from 'react-icons/fa';
|
||||
|
||||
type MetadataItemProps = {
|
||||
isLink?: boolean;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
value: number | string | boolean;
|
||||
labelPosition?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display an individual metadata item or parameter.
|
||||
*/
|
||||
const MetadataItem = ({
|
||||
label,
|
||||
value,
|
||||
onClick,
|
||||
isLink,
|
||||
labelPosition,
|
||||
}: MetadataItemProps) => {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
{onClick && (
|
||||
<Tooltip label={`Recall ${label}`}>
|
||||
<IconButton
|
||||
aria-label="Use this parameter"
|
||||
icon={<IoArrowUndoCircleOutline />}
|
||||
size={'xs'}
|
||||
variant={'ghost'}
|
||||
fontSize={20}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Flex direction={labelPosition ? 'column' : 'row'}>
|
||||
<Text fontWeight={'semibold'} whiteSpace={'nowrap'} pr={2}>
|
||||
{label}:
|
||||
</Text>
|
||||
{isLink ? (
|
||||
<Link href={value.toString()} isExternal wordBreak={'break-all'}>
|
||||
{value.toString()} <ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
) : (
|
||||
<Text overflowY={'scroll'} wordBreak={'break-all'}>
|
||||
{value.toString()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
image: InvokeAI.Image;
|
||||
};
|
||||
|
||||
// TODO: I don't know if this is needed.
|
||||
const memoEqualityCheck = (
|
||||
prev: ImageMetadataViewerProps,
|
||||
next: ImageMetadataViewerProps
|
||||
) => prev.image.uuid === next.image.uuid;
|
||||
|
||||
// TODO: Show more interesting information in this component.
|
||||
|
||||
/**
|
||||
* Image metadata viewer overlays currently selected image and provides
|
||||
* access to any of its metadata for use in processing.
|
||||
*/
|
||||
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
|
||||
const metadata = image?.metadata?.image || {};
|
||||
const {
|
||||
type,
|
||||
postprocessing,
|
||||
sampler,
|
||||
prompt,
|
||||
seed,
|
||||
variations,
|
||||
steps,
|
||||
cfg_scale,
|
||||
seamless,
|
||||
width,
|
||||
height,
|
||||
strength,
|
||||
fit,
|
||||
init_image_path,
|
||||
mask_image_path,
|
||||
orig_path,
|
||||
scale,
|
||||
} = metadata;
|
||||
|
||||
const metadataJSON = JSON.stringify(metadata, null, 2);
|
||||
|
||||
return (
|
||||
<Flex gap={1} direction={'column'} width={'100%'}>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>File:</Text>
|
||||
<Link href={image.url} isExternal>
|
||||
{image.url}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
</Flex>
|
||||
{Object.keys(metadata).length > 0 ? (
|
||||
<>
|
||||
{type && <MetadataItem label="Generation type" value={type} />}
|
||||
{['esrgan', 'gfpgan'].includes(type) && (
|
||||
<MetadataItem label="Original image" value={orig_path} />
|
||||
)}
|
||||
{type === 'gfpgan' && strength !== undefined && (
|
||||
<MetadataItem
|
||||
label="Fix faces strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setGfpganStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{type === 'esrgan' && scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="Upscaling scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
)}
|
||||
{type === 'esrgan' && strength !== undefined && (
|
||||
<MetadataItem
|
||||
label="Upscaling strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setUpscalingStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{prompt && (
|
||||
<MetadataItem
|
||||
label="Prompt"
|
||||
labelPosition="top"
|
||||
value={promptToString(prompt)}
|
||||
onClick={() => dispatch(setPrompt(prompt))}
|
||||
/>
|
||||
)}
|
||||
{seed !== undefined && (
|
||||
<MetadataItem
|
||||
label="Seed"
|
||||
value={seed}
|
||||
onClick={() => dispatch(setSeed(seed))}
|
||||
/>
|
||||
)}
|
||||
{sampler && (
|
||||
<MetadataItem
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onClick={() => dispatch(setSampler(sampler))}
|
||||
/>
|
||||
)}
|
||||
{steps && (
|
||||
<MetadataItem
|
||||
label="Steps"
|
||||
value={steps}
|
||||
onClick={() => dispatch(setSteps(steps))}
|
||||
/>
|
||||
)}
|
||||
{cfg_scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="CFG scale"
|
||||
value={cfg_scale}
|
||||
onClick={() => dispatch(setCfgScale(cfg_scale))}
|
||||
/>
|
||||
)}
|
||||
{variations && variations.length > 0 && (
|
||||
<MetadataItem
|
||||
label="Seed-weight pairs"
|
||||
value={seedWeightsToString(variations)}
|
||||
onClick={() =>
|
||||
dispatch(setSeedWeights(seedWeightsToString(variations)))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{seamless && (
|
||||
<MetadataItem
|
||||
label="Seamless"
|
||||
value={seamless}
|
||||
onClick={() => dispatch(setWidth(seamless))}
|
||||
/>
|
||||
)}
|
||||
{width && (
|
||||
<MetadataItem
|
||||
label="Width"
|
||||
value={width}
|
||||
onClick={() => dispatch(setWidth(width))}
|
||||
/>
|
||||
)}
|
||||
{height && (
|
||||
<MetadataItem
|
||||
label="Height"
|
||||
value={height}
|
||||
onClick={() => dispatch(setHeight(height))}
|
||||
/>
|
||||
)}
|
||||
{init_image_path && (
|
||||
<MetadataItem
|
||||
label="Initial image"
|
||||
value={init_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setInitialImagePath(init_image_path))}
|
||||
/>
|
||||
)}
|
||||
{mask_image_path && (
|
||||
<MetadataItem
|
||||
label="Mask image"
|
||||
value={mask_image_path}
|
||||
isLink
|
||||
onClick={() => dispatch(setMaskPath(mask_image_path))}
|
||||
/>
|
||||
)}
|
||||
{type === 'img2img' && strength && (
|
||||
<MetadataItem
|
||||
label="Image to image strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setImg2imgStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{fit && (
|
||||
<MetadataItem
|
||||
label="Image to image fit"
|
||||
value={fit}
|
||||
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
|
||||
/>
|
||||
)}
|
||||
{postprocessing && postprocessing.length > 0 && (
|
||||
<>
|
||||
<Heading size={'sm'}>Postprocessing</Heading>
|
||||
{postprocessing.map(
|
||||
(
|
||||
postprocess: InvokeAI.PostProcessedImageMetadata,
|
||||
i: number
|
||||
) => {
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={'2rem'} gap={1} direction={'column'}>
|
||||
<Text size={'md'}>{`${i + 1}: Upscale (ESRGAN)`}</Text>
|
||||
<MetadataItem
|
||||
label="Scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() =>
|
||||
dispatch(setUpscalingStrength(strength))
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
} else if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
return (
|
||||
<Flex key={i} pl={'2rem'} gap={1} direction={'column'}>
|
||||
<Text size={'md'}>{`${
|
||||
i + 1
|
||||
}: Face restoration (GFPGAN)`}</Text>
|
||||
|
||||
<MetadataItem
|
||||
label="Strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setGfpganStrength(strength))}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<Flex gap={2}>
|
||||
<Tooltip label={`Copy metadata JSON`}>
|
||||
<IconButton
|
||||
aria-label="Copy metadata JSON"
|
||||
icon={<FaCopy />}
|
||||
size={'xs'}
|
||||
variant={'ghost'}
|
||||
fontSize={14}
|
||||
onClick={() => navigator.clipboard.writeText(metadataJSON)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text fontWeight={'semibold'}>Metadata JSON:</Text>
|
||||
</Flex>
|
||||
<div className={'current-image-json-viewer'}>
|
||||
<pre>{metadataJSON}</pre>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width={'100%'} pt={10}>
|
||||
<Text fontSize={'lg'} fontWeight="semibold">
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
|
||||
export default ImageMetadataViewer;
|
@ -11,12 +11,14 @@ export interface GalleryState {
|
||||
areMoreImagesAvailable: boolean;
|
||||
latest_mtime?: number;
|
||||
earliest_mtime?: number;
|
||||
shouldShowGallery: boolean;
|
||||
}
|
||||
|
||||
const initialState: GalleryState = {
|
||||
currentImageUuid: '',
|
||||
images: [],
|
||||
areMoreImagesAvailable: true,
|
||||
shouldShowGallery: false,
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@ -138,6 +140,9 @@ export const gallerySlice = createSlice({
|
||||
state.areMoreImagesAvailable = areMoreImagesAvailable;
|
||||
}
|
||||
},
|
||||
setShouldShowGallery: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowGallery = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -150,6 +155,7 @@ export const {
|
||||
setIntermediateImage,
|
||||
selectNextImage,
|
||||
selectPrevImage,
|
||||
setShouldShowGallery,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
@ -8,7 +8,7 @@ import React, { ReactElement } from 'react';
|
||||
import { Feature } from '../../../app/features';
|
||||
import GuideIcon from '../../../common/components/GuideIcon';
|
||||
|
||||
interface InvokeAccordionItemProps {
|
||||
export interface InvokeAccordionItemProps {
|
||||
header: ReactElement;
|
||||
feature: Feature;
|
||||
options: ReactElement;
|
||||
|
@ -19,7 +19,7 @@ export default function ImageFit() {
|
||||
|
||||
return (
|
||||
<IAISwitch
|
||||
label="Fit initial image to output size"
|
||||
label="Fit Initial Image To Output Size"
|
||||
isChecked={shouldFitToWidthHeight}
|
||||
onChange={handleChangeFit}
|
||||
/>
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldUseInitImage } from '../../optionsSlice';
|
||||
|
||||
export default function ImageToImage() {
|
||||
export default function ImageToImageAccordion() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const initialImagePath = useAppSelector(
|
@ -7,7 +7,13 @@ import {
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import { setImg2imgStrength } from '../../optionsSlice';
|
||||
|
||||
export default function ImageToImageStrength() {
|
||||
interface ImageToImageStrengthProps {
|
||||
label?: string;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
|
||||
const { label = 'Strength', styleClass } = props;
|
||||
const img2imgStrength = useAppSelector(
|
||||
(state: RootState) => state.options.img2imgStrength
|
||||
);
|
||||
@ -18,7 +24,7 @@ export default function ImageToImageStrength() {
|
||||
|
||||
return (
|
||||
<IAINumberInput
|
||||
label="Strength"
|
||||
label={label}
|
||||
step={0.01}
|
||||
min={0.01}
|
||||
max={0.99}
|
||||
@ -26,6 +32,7 @@ export default function ImageToImageStrength() {
|
||||
value={img2imgStrength}
|
||||
width="90px"
|
||||
isInteger={false}
|
||||
styleClass={styleClass}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ type ImageUploaderProps = {
|
||||
* Callback to handle a file being rejected.
|
||||
*/
|
||||
fileRejectionCallback: (rejection: FileRejection) => void;
|
||||
// Styling
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -25,6 +27,7 @@ const ImageUploader = ({
|
||||
children,
|
||||
fileAcceptedCallback,
|
||||
fileRejectionCallback,
|
||||
styleClass,
|
||||
}: ImageUploaderProps) => {
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
@ -52,7 +55,7 @@ const ImageUploader = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box {...getRootProps()} flexGrow={3}>
|
||||
<Box {...getRootProps()} flexGrow={3} className={`${styleClass}`}>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickUploadIcon,
|
||||
|
@ -1,4 +1,3 @@
|
||||
import MainAdvancedOptions from './MainAdvancedOptions';
|
||||
import MainCFGScale from './MainCFGScale';
|
||||
import MainHeight from './MainHeight';
|
||||
import MainIterations from './MainIterations';
|
||||
@ -23,7 +22,6 @@ export default function MainOptions() {
|
||||
<MainHeight />
|
||||
<MainSampler />
|
||||
</div>
|
||||
<MainAdvancedOptions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,34 +1,25 @@
|
||||
import {
|
||||
Box,
|
||||
Accordion,
|
||||
ExpandedIndex,
|
||||
// ExpandedIndex,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
// import { RootState } from '../../app/store';
|
||||
// import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
// import { setOpenAccordions } from '../system/systemSlice';
|
||||
|
||||
import OutputOptions from './OutputOptions';
|
||||
import ImageToImageOptions from './AdvancedOptions/ImageToImage/ImageToImageOptions';
|
||||
import { Feature } from '../../app/features';
|
||||
import SeedOptions from './AdvancedOptions/Seed/SeedOptions';
|
||||
import Upscale from './AdvancedOptions/Upscale/Upscale';
|
||||
import UpscaleOptions from './AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import FaceRestore from './AdvancedOptions/FaceRestore/FaceRestore';
|
||||
import FaceRestoreOptions from './AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
import ImageToImage from './AdvancedOptions/ImageToImage/ImageToImage';
|
||||
import { Accordion, ExpandedIndex } from '@chakra-ui/react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { setOpenAccordions } from '../system/systemSlice';
|
||||
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem';
|
||||
import Variations from './AdvancedOptions/Variations/Variations';
|
||||
import VariationsOptions from './AdvancedOptions/Variations/VariationsOptions';
|
||||
import InvokeAccordionItem, {
|
||||
InvokeAccordionItemProps,
|
||||
} from './AccordionItems/InvokeAccordionItem';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
type OptionsAccordionType = {
|
||||
[optionAccordionKey: string]: InvokeAccordionItemProps;
|
||||
};
|
||||
|
||||
type OptionAccordionsType = {
|
||||
accordionInfo: OptionsAccordionType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main container for generation and processing parameters.
|
||||
*/
|
||||
const OptionsAccordion = () => {
|
||||
const OptionsAccordion = (props: OptionAccordionsType) => {
|
||||
const { accordionInfo } = props;
|
||||
|
||||
const openAccordions = useAppSelector(
|
||||
(state: RootState) => state.system.openAccordions
|
||||
);
|
||||
@ -41,6 +32,23 @@ const OptionsAccordion = () => {
|
||||
const handleChangeAccordionState = (openAccordions: ExpandedIndex) =>
|
||||
dispatch(setOpenAccordions(openAccordions));
|
||||
|
||||
const renderAccordions = () => {
|
||||
const accordionsToRender: ReactElement[] = [];
|
||||
if (accordionInfo) {
|
||||
Object.keys(accordionInfo).forEach((key) => {
|
||||
accordionsToRender.push(
|
||||
<InvokeAccordionItem
|
||||
key={key}
|
||||
header={accordionInfo[key as keyof typeof accordionInfo].header}
|
||||
feature={accordionInfo[key as keyof typeof accordionInfo].feature}
|
||||
options={accordionInfo[key as keyof typeof accordionInfo].options}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
return accordionsToRender;
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultIndex={openAccordions}
|
||||
@ -49,49 +57,7 @@ const OptionsAccordion = () => {
|
||||
onChange={handleChangeAccordionState}
|
||||
className="advanced-settings"
|
||||
>
|
||||
<InvokeAccordionItem
|
||||
header={
|
||||
<Box flex="1" textAlign="left">
|
||||
Seed
|
||||
</Box>
|
||||
}
|
||||
feature={Feature.SEED}
|
||||
options={<SeedOptions />}
|
||||
/>
|
||||
|
||||
<InvokeAccordionItem
|
||||
header={<Variations />}
|
||||
feature={Feature.VARIATIONS}
|
||||
options={<VariationsOptions />}
|
||||
/>
|
||||
|
||||
<InvokeAccordionItem
|
||||
header={<FaceRestore />}
|
||||
feature={Feature.FACE_CORRECTION}
|
||||
options={<FaceRestoreOptions />}
|
||||
/>
|
||||
|
||||
<InvokeAccordionItem
|
||||
header={<Upscale />}
|
||||
feature={Feature.UPSCALE}
|
||||
options={<UpscaleOptions />}
|
||||
/>
|
||||
|
||||
<InvokeAccordionItem
|
||||
header={<ImageToImage />}
|
||||
feature={Feature.IMAGE_TO_IMAGE}
|
||||
options={<ImageToImageOptions />}
|
||||
/>
|
||||
|
||||
<InvokeAccordionItem
|
||||
header={
|
||||
<Box flex="1" textAlign="left">
|
||||
Other
|
||||
</Box>
|
||||
}
|
||||
feature={Feature.OTHER}
|
||||
options={<OutputOptions />}
|
||||
/>
|
||||
{renderAccordions()}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ export interface OptionsState {
|
||||
upscalingLevel: UpscalingLevel;
|
||||
upscalingStrength: number;
|
||||
shouldUseInitImage: boolean;
|
||||
initialImagePath: string;
|
||||
initialImagePath: string | null;
|
||||
maskPath: string;
|
||||
seamless: boolean;
|
||||
shouldFitToWidthHeight: boolean;
|
||||
@ -33,6 +33,8 @@ export interface OptionsState {
|
||||
shouldRunGFPGAN: boolean;
|
||||
shouldRandomizeSeed: boolean;
|
||||
showAdvancedOptions: boolean;
|
||||
activeTab: number;
|
||||
shouldShowImageDetails: boolean;
|
||||
}
|
||||
|
||||
const initialOptionsState: OptionsState = {
|
||||
@ -49,7 +51,7 @@ const initialOptionsState: OptionsState = {
|
||||
seamless: false,
|
||||
shouldUseInitImage: false,
|
||||
img2imgStrength: 0.75,
|
||||
initialImagePath: '',
|
||||
initialImagePath: null,
|
||||
maskPath: '',
|
||||
shouldFitToWidthHeight: true,
|
||||
shouldGenerateVariations: false,
|
||||
@ -62,6 +64,8 @@ const initialOptionsState: OptionsState = {
|
||||
gfpganStrength: 0.8,
|
||||
shouldRandomizeSeed: true,
|
||||
showAdvancedOptions: true,
|
||||
activeTab: 0,
|
||||
shouldShowImageDetails: false,
|
||||
};
|
||||
|
||||
const initialState: OptionsState = initialOptionsState;
|
||||
@ -121,7 +125,7 @@ export const optionsSlice = createSlice({
|
||||
setShouldUseInitImage: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldUseInitImage = action.payload;
|
||||
},
|
||||
setInitialImagePath: (state, action: PayloadAction<string>) => {
|
||||
setInitialImagePath: (state, action: PayloadAction<string | null>) => {
|
||||
const newInitialImagePath = action.payload;
|
||||
state.shouldUseInitImage = newInitialImagePath ? true : false;
|
||||
state.initialImagePath = newInitialImagePath;
|
||||
@ -269,6 +273,12 @@ export const optionsSlice = createSlice({
|
||||
setShowAdvancedOptions: (state, action: PayloadAction<boolean>) => {
|
||||
state.showAdvancedOptions = action.payload;
|
||||
},
|
||||
setActiveTab: (state, action: PayloadAction<number>) => {
|
||||
state.activeTab = action.payload;
|
||||
},
|
||||
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowImageDetails = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -303,6 +313,8 @@ export const {
|
||||
setShouldRunESRGAN,
|
||||
setShouldRandomizeSeed,
|
||||
setShowAdvancedOptions,
|
||||
setActiveTab,
|
||||
setShouldShowImageDetails,
|
||||
} = optionsSlice.actions;
|
||||
|
||||
export default optionsSlice.reducer;
|
||||
|
@ -7,6 +7,7 @@ import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const logSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@ -66,6 +67,14 @@ const Console = () => {
|
||||
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'`',
|
||||
() => {
|
||||
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
|
||||
},
|
||||
[shouldShowLogViewer]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowLogViewer && (
|
||||
|
@ -23,6 +23,11 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
const hotkeys = [
|
||||
{ title: 'Invoke', desc: 'Generate an image', hotkey: 'Ctrl+Enter' },
|
||||
{ title: 'Cancel', desc: 'Cancel image generation', hotkey: 'Shift+X' },
|
||||
{
|
||||
title: 'Toggle Gallery',
|
||||
desc: 'Open and close the gallery drawer',
|
||||
hotkey: 'G',
|
||||
},
|
||||
{
|
||||
title: 'Set Seed',
|
||||
desc: 'Use the seed of the current image',
|
||||
@ -61,6 +66,21 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
desc: 'Display the next image in the gallery',
|
||||
hotkey: 'Arrow right',
|
||||
},
|
||||
{
|
||||
title: 'Change Tabs',
|
||||
desc: 'Switch to another workspace',
|
||||
hotkey: '1-6',
|
||||
},
|
||||
{
|
||||
title: 'Theme Toggle',
|
||||
desc: 'Switch between dark and light modes',
|
||||
hotkey: 'Shift+D',
|
||||
},
|
||||
{
|
||||
title: 'Console Toggle',
|
||||
desc: 'Open and close console',
|
||||
hotkey: '`',
|
||||
},
|
||||
];
|
||||
|
||||
const renderHotkeyModalItems = () => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { IconButton, Link, useColorMode } from '@chakra-ui/react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
|
||||
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
|
||||
@ -15,6 +16,14 @@ import StatusIndicator from './StatusIndicator';
|
||||
const SiteHeader = () => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
|
||||
useHotkeys(
|
||||
'shift+d',
|
||||
() => {
|
||||
toggleColorMode();
|
||||
},
|
||||
[colorMode, toggleColorMode]
|
||||
);
|
||||
|
||||
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
|
||||
|
||||
// Make FaMoon and FaSun icon apparent size consistent
|
||||
|
148
frontend/src/features/tabs/ImageToImage/ImageToImage.scss
Normal file
148
frontend/src/features/tabs/ImageToImage/ImageToImage.scss
Normal file
@ -0,0 +1,148 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.image-to-image-workarea {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.image-to-image-panel {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
grid-auto-rows: max-content;
|
||||
width: $options-bar-max-width;
|
||||
height: $app-content-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.image-to-image-display-area {
|
||||
display: grid;
|
||||
grid-template-areas: 'image-to-image-display-area';
|
||||
|
||||
.image-to-image-display {
|
||||
grid-area: image-to-image-display-area;
|
||||
}
|
||||
|
||||
.image-gallery-area {
|
||||
grid-area: image-to-image-display-area;
|
||||
z-index: 2;
|
||||
place-self: end;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.image-to-image-strength-main-option {
|
||||
display: grid;
|
||||
grid-template-columns: none !important;
|
||||
|
||||
.number-input-entry {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.image-to-image-display {
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--background-color-secondary);
|
||||
display: grid;
|
||||
|
||||
.current-image-options {
|
||||
grid-auto-columns: max-content;
|
||||
justify-self: center;
|
||||
align-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
.image-to-image-single-preview {
|
||||
display: grid;
|
||||
column-gap: 0.5rem;
|
||||
padding: 0 1rem;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.image-to-image-dual-preview-container {
|
||||
display: grid;
|
||||
grid-template-areas: 'img2img-preview';
|
||||
}
|
||||
|
||||
.image-to-image-dual-preview {
|
||||
grid-area: img2img-preview;
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content;
|
||||
column-gap: 0.5rem;
|
||||
padding: 0 1rem;
|
||||
place-content: center;
|
||||
|
||||
.current-image-preview {
|
||||
img {
|
||||
height: calc($app-gallery-height - 2rem);
|
||||
max-height: calc($app-gallery-height - 2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img2img-metadata {
|
||||
grid-area: img2img-preview;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.init-image-preview {
|
||||
display: grid;
|
||||
grid-template-areas: 'init-image-content';
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.init-image-preview-header {
|
||||
grid-area: init-image-content;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
height: max-content;
|
||||
align-items: center;
|
||||
align-self: start;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
h1 {
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.4rem;
|
||||
background-color: var(--tab-hover-color);
|
||||
width: max-content;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.init-image-image {
|
||||
grid-area: init-image-content;
|
||||
|
||||
img {
|
||||
border-radius: 0.5rem;
|
||||
object-fit: contain;
|
||||
background-color: var(--img2img-img-bg-color);
|
||||
width: auto;
|
||||
height: calc($app-gallery-height - 2rem);
|
||||
max-height: calc($app-gallery-height - 2rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-to-image-upload-btn {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: $app-content-height;
|
||||
|
||||
button {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: var(--background-color-secondary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--img2img-img-bg-color);
|
||||
}
|
||||
}
|
||||
}
|
16
frontend/src/features/tabs/ImageToImage/ImageToImage.tsx
Normal file
16
frontend/src/features/tabs/ImageToImage/ImageToImage.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import ImageToImagePanel from './ImageToImagePanel';
|
||||
import ImageToImageDisplay from './ImageToImageDisplay';
|
||||
import ImageGallery from '../../gallery/ImageGallery';
|
||||
|
||||
export default function ImageToImage() {
|
||||
return (
|
||||
<div className="image-to-image-workarea">
|
||||
<ImageToImagePanel />
|
||||
<div className="image-to-image-display-area">
|
||||
<ImageToImageDisplay />
|
||||
<ImageGallery />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
import { uploadInitialImage } from '../../../app/socketio/actions';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
|
||||
import CurrentImageButtons from '../../gallery/CurrentImageButtons';
|
||||
import CurrentImagePreview from '../../gallery/CurrentImagePreview';
|
||||
import ImageMetadataViewer from '../../gallery/ImageMetaDataViewer/ImageMetadataViewer';
|
||||
|
||||
import InitImagePreview from './InitImagePreview';
|
||||
|
||||
export default function ImageToImageDisplay() {
|
||||
const initialImagePath = useAppSelector(
|
||||
(state: RootState) => state.options.initialImagePath
|
||||
);
|
||||
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="image-to-image-display"
|
||||
style={
|
||||
imageToDisplay
|
||||
? { gridAutoRows: 'max-content auto' }
|
||||
: { gridAutoRows: 'auto' }
|
||||
}
|
||||
>
|
||||
{initialImagePath ? (
|
||||
<>
|
||||
{imageToDisplay ? (
|
||||
<>
|
||||
<CurrentImageButtons image={imageToDisplay} />
|
||||
<div className="image-to-image-dual-preview-container">
|
||||
<div className="image-to-image-dual-preview">
|
||||
<InitImagePreview />
|
||||
<div className="image-to-image-current-image-display">
|
||||
<CurrentImagePreview imageToDisplay={imageToDisplay} />
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowImageDetails && (
|
||||
<ImageMetadataViewer
|
||||
image={imageToDisplay}
|
||||
styleClass="img2img-metadata"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="image-to-image-single-preview">
|
||||
<InitImagePreview />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="upload-image">
|
||||
<InvokeImageUploader
|
||||
label="Upload or Drop Image Here"
|
||||
icon={<FaUpload />}
|
||||
styleClass="image-to-image-upload-btn"
|
||||
dispatcher={uploadInitialImage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Feature } from '../../../app/features';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
import FaceRestore from '../../options/AdvancedOptions/FaceRestore/FaceRestore';
|
||||
import FaceRestoreOptions from '../../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
import ImageFit from '../../options/AdvancedOptions/ImageToImage/ImageFit';
|
||||
import ImageToImageStrength from '../../options/AdvancedOptions/ImageToImage/ImageToImageStrength';
|
||||
import SeedOptions from '../../options/AdvancedOptions/Seed/SeedOptions';
|
||||
import Upscale from '../../options/AdvancedOptions/Upscale/Upscale';
|
||||
import UpscaleOptions from '../../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import Variations from '../../options/AdvancedOptions/Variations/Variations';
|
||||
import VariationsOptions from '../../options/AdvancedOptions/Variations/VariationsOptions';
|
||||
import MainAdvancedOptions from '../../options/MainOptions/MainAdvancedOptions';
|
||||
import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import OutputOptions from '../../options/OutputOptions';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
|
||||
export default function ImageToImagePanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
(state: RootState) => state.options.showAdvancedOptions
|
||||
);
|
||||
|
||||
const imageToImageAccordions = {
|
||||
seed: {
|
||||
header: (
|
||||
<Box flex="1" textAlign="left">
|
||||
Seed
|
||||
</Box>
|
||||
),
|
||||
feature: Feature.SEED,
|
||||
options: <SeedOptions />,
|
||||
},
|
||||
variations: {
|
||||
header: <Variations />,
|
||||
feature: Feature.VARIATIONS,
|
||||
options: <VariationsOptions />,
|
||||
},
|
||||
face_restore: {
|
||||
header: <FaceRestore />,
|
||||
feature: Feature.FACE_CORRECTION,
|
||||
options: <FaceRestoreOptions />,
|
||||
},
|
||||
upscale: {
|
||||
header: <Upscale />,
|
||||
feature: Feature.UPSCALE,
|
||||
options: <UpscaleOptions />,
|
||||
},
|
||||
other: {
|
||||
header: (
|
||||
<Box flex="1" textAlign="left">
|
||||
Other
|
||||
</Box>
|
||||
),
|
||||
feature: Feature.OTHER,
|
||||
options: <OutputOptions />,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-to-image-panel">
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
<ImageToImageStrength
|
||||
label="Image To Image Strength"
|
||||
styleClass="main-option-block image-to-image-strength-main-option"
|
||||
/>
|
||||
<ImageFit />
|
||||
<MainAdvancedOptions />
|
||||
{showAdvancedOptions ? (
|
||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
37
frontend/src/features/tabs/ImageToImage/InitImagePreview.tsx
Normal file
37
frontend/src/features/tabs/ImageToImage/InitImagePreview.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { IconButton, Image } from '@chakra-ui/react';
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import { MdClear } from 'react-icons/md';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import { setInitialImagePath } from '../../options/optionsSlice';
|
||||
|
||||
export default function InitImagePreview() {
|
||||
const initialImagePath = useAppSelector(
|
||||
(state: RootState) => state.options.initialImagePath
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClickResetInitialImage = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setInitialImagePath(null));
|
||||
};
|
||||
return (
|
||||
<div className="init-image-preview">
|
||||
<div className="init-image-preview-header">
|
||||
<h1>Initial Image</h1>
|
||||
<IconButton
|
||||
isDisabled={!initialImagePath}
|
||||
size={'sm'}
|
||||
aria-label={'Reset Initial Image'}
|
||||
onClick={handleClickResetInitialImage}
|
||||
icon={<MdClear />}
|
||||
/>
|
||||
</div>
|
||||
{initialImagePath && (
|
||||
<div className="init-image-image">
|
||||
<Image fit={'contain'} src={initialImagePath} rounded={'md'} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
|
||||
export default function InitialImageOverlay() {
|
||||
const initialImagePath = useAppSelector(
|
||||
(state: RootState) => state.options.initialImagePath
|
||||
);
|
||||
|
||||
return initialImagePath ? (
|
||||
<Image
|
||||
fit={'contain'}
|
||||
src={initialImagePath}
|
||||
rounded={'md'}
|
||||
className={'checkerboard'}
|
||||
/>
|
||||
) : null;
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { Tab, TabPanel, TabPanels, Tabs, Tooltip } from '@chakra-ui/react';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { ImageToImageWIP } from '../../common/components/WorkInProgress/ImageToImageWIP';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import InpaintingWIP from '../../common/components/WorkInProgress/InpaintingWIP';
|
||||
import NodesWIP from '../../common/components/WorkInProgress/NodesWIP';
|
||||
import OutpaintingWIP from '../../common/components/WorkInProgress/OutpaintingWIP';
|
||||
@ -11,41 +13,74 @@ import NodesIcon from '../../common/icons/NodesIcon';
|
||||
import OutpaintIcon from '../../common/icons/OutpaintIcon';
|
||||
import PostprocessingIcon from '../../common/icons/PostprocessingIcon';
|
||||
import TextToImageIcon from '../../common/icons/TextToImageIcon';
|
||||
import { setActiveTab } from '../options/optionsSlice';
|
||||
import ImageToImage from './ImageToImage/ImageToImage';
|
||||
import TextToImage from './TextToImage/TextToImage';
|
||||
|
||||
export const tab_dict = {
|
||||
txt2img: {
|
||||
title: <TextToImageIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <TextToImage />,
|
||||
tooltip: 'Text To Image',
|
||||
},
|
||||
img2img: {
|
||||
title: <ImageToImageIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <ImageToImage />,
|
||||
tooltip: 'Image To Image',
|
||||
},
|
||||
inpainting: {
|
||||
title: <InpaintIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <InpaintingWIP />,
|
||||
tooltip: 'Inpainting',
|
||||
},
|
||||
outpainting: {
|
||||
title: <OutpaintIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <OutpaintingWIP />,
|
||||
tooltip: 'Outpainting',
|
||||
},
|
||||
nodes: {
|
||||
title: <NodesIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <NodesWIP />,
|
||||
tooltip: 'Nodes',
|
||||
},
|
||||
postprocess: {
|
||||
title: <PostprocessingIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <PostProcessingWIP />,
|
||||
tooltip: 'Post Processing',
|
||||
},
|
||||
};
|
||||
|
||||
export const tabMap = _.map(tab_dict, (tab, key) => key);
|
||||
|
||||
export default function InvokeTabs() {
|
||||
const tab_dict = {
|
||||
txt2img: {
|
||||
title: <TextToImageIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <TextToImage />,
|
||||
tooltip: 'Text To Image',
|
||||
},
|
||||
img2img: {
|
||||
title: <ImageToImageIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <ImageToImageWIP />,
|
||||
tooltip: 'Image To Image',
|
||||
},
|
||||
inpainting: {
|
||||
title: <InpaintIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <InpaintingWIP />,
|
||||
tooltip: 'Inpainting',
|
||||
},
|
||||
outpainting: {
|
||||
title: <OutpaintIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <OutpaintingWIP />,
|
||||
tooltip: 'Outpainting',
|
||||
},
|
||||
nodes: {
|
||||
title: <NodesIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <NodesWIP />,
|
||||
tooltip: 'Nodes',
|
||||
},
|
||||
postprocess: {
|
||||
title: <PostprocessingIcon fill={'black'} boxSize={'2.5rem'} />,
|
||||
panel: <PostProcessingWIP />,
|
||||
tooltip: 'Post Processing',
|
||||
},
|
||||
};
|
||||
const activeTab = useAppSelector(
|
||||
(state: RootState) => state.options.activeTab
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useHotkeys('1', () => {
|
||||
dispatch(setActiveTab(0));
|
||||
});
|
||||
|
||||
useHotkeys('2', () => {
|
||||
dispatch(setActiveTab(1));
|
||||
});
|
||||
|
||||
useHotkeys('3', () => {
|
||||
dispatch(setActiveTab(2));
|
||||
});
|
||||
|
||||
useHotkeys('4', () => {
|
||||
dispatch(setActiveTab(3));
|
||||
});
|
||||
|
||||
useHotkeys('5', () => {
|
||||
dispatch(setActiveTab(4));
|
||||
});
|
||||
|
||||
useHotkeys('6', () => {
|
||||
dispatch(setActiveTab(5));
|
||||
});
|
||||
|
||||
const renderTabs = () => {
|
||||
const tabsToRender: ReactElement[] = [];
|
||||
@ -76,7 +111,16 @@ export default function InvokeTabs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs className="app-tabs" variant={'unstyled'}>
|
||||
<Tabs
|
||||
isLazy
|
||||
className="app-tabs"
|
||||
variant={'unstyled'}
|
||||
defaultIndex={activeTab}
|
||||
index={activeTab}
|
||||
onChange={(index: number) => {
|
||||
dispatch(setActiveTab(index));
|
||||
}}
|
||||
>
|
||||
<div className="app-tabs-list">{renderTabs()}</div>
|
||||
<TabPanels className="app-tabs-panels">{renderTabPanels()}</TabPanels>
|
||||
</Tabs>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
.text-to-image-workarea {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto max-content;
|
||||
grid-template-columns: max-content auto;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
@ -14,3 +14,20 @@
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.text-to-image-display {
|
||||
display: grid;
|
||||
grid-template-areas: 'text-to-image-display';
|
||||
|
||||
.current-image-display,
|
||||
.current-image-display-placeholder {
|
||||
grid-area: text-to-image-display;
|
||||
}
|
||||
|
||||
.image-gallery-area {
|
||||
grid-area: text-to-image-display;
|
||||
z-index: 2;
|
||||
place-self: end;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import TextToImagePanel from './TextToImagePanel';
|
||||
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||
import ImageGallery from '../../gallery/ImageGallery';
|
||||
import TextToImagePanel from './TextToImagePanel';
|
||||
|
||||
export default function TextToImage() {
|
||||
return (
|
||||
<div className="text-to-image-workarea">
|
||||
<TextToImagePanel />
|
||||
<CurrentImageDisplay />
|
||||
<ImageGallery />
|
||||
<div className="text-to-image-display">
|
||||
<CurrentImageDisplay />
|
||||
<ImageGallery />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,20 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Feature } from '../../../app/features';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
import FaceRestore from '../../options/AdvancedOptions/FaceRestore/FaceRestore';
|
||||
import FaceRestoreOptions from '../../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
import ImageToImageAccordion from '../../options/AdvancedOptions/ImageToImage/ImageToImageAccordion';
|
||||
import ImageToImageOptions from '../../options/AdvancedOptions/ImageToImage/ImageToImageOptions';
|
||||
import SeedOptions from '../../options/AdvancedOptions/Seed/SeedOptions';
|
||||
import Upscale from '../../options/AdvancedOptions/Upscale/Upscale';
|
||||
import UpscaleOptions from '../../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import Variations from '../../options/AdvancedOptions/Variations/Variations';
|
||||
import VariationsOptions from '../../options/AdvancedOptions/Variations/VariationsOptions';
|
||||
import MainAdvancedOptions from '../../options/MainOptions/MainAdvancedOptions';
|
||||
import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import OutputOptions from '../../options/OutputOptions';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
|
||||
@ -9,12 +22,57 @@ export default function TextToImagePanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
(state: RootState) => state.options.showAdvancedOptions
|
||||
);
|
||||
|
||||
const textToImageAccordions = {
|
||||
seed: {
|
||||
header: (
|
||||
<Box flex="1" textAlign="left">
|
||||
Seed
|
||||
</Box>
|
||||
),
|
||||
feature: Feature.SEED,
|
||||
options: <SeedOptions />,
|
||||
},
|
||||
variations: {
|
||||
header: <Variations />,
|
||||
feature: Feature.VARIATIONS,
|
||||
options: <VariationsOptions />,
|
||||
},
|
||||
face_restore: {
|
||||
header: <FaceRestore />,
|
||||
feature: Feature.FACE_CORRECTION,
|
||||
options: <FaceRestoreOptions />,
|
||||
},
|
||||
upscale: {
|
||||
header: <Upscale />,
|
||||
feature: Feature.UPSCALE,
|
||||
options: <UpscaleOptions />,
|
||||
},
|
||||
// img2img: {
|
||||
// header: <ImageToImageAccordion />,
|
||||
// feature: Feature.IMAGE_TO_IMAGE,
|
||||
// options: <ImageToImageOptions />,
|
||||
// },
|
||||
other: {
|
||||
header: (
|
||||
<Box flex="1" textAlign="left">
|
||||
Other
|
||||
</Box>
|
||||
),
|
||||
feature: Feature.OTHER,
|
||||
options: <OutputOptions />,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-to-image-panel">
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
{showAdvancedOptions ? <OptionsAccordion /> : null}
|
||||
<MainAdvancedOptions />
|
||||
{showAdvancedOptions ? (
|
||||
<OptionsAccordion accordionInfo={textToImageAccordions} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,9 @@ $app-width: calc(100vw - $app-cutoff);
|
||||
$app-height: calc(100vh - $app-cutoff);
|
||||
$app-content-height: calc(100vh - $app-content-height-cutoff);
|
||||
$app-gallery-height: calc(100vh - ($app-content-height-cutoff + 6rem));
|
||||
$app-gallery-popover-height: calc(
|
||||
100vh - ($app-content-height-cutoff - 2.5rem)
|
||||
);
|
||||
$app-metadata-height: calc(100vh - ($app-content-height-cutoff + 4.4rem));
|
||||
|
||||
// option bar
|
||||
|
8
frontend/src/styles/_Animations.scss
Normal file
8
frontend/src/styles/_Animations.scss
Normal file
@ -0,0 +1,8 @@
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(10rem);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@
|
||||
--btn-red-hover: rgb(255, 75, 75);
|
||||
|
||||
--btn-load-more: rgb(30, 32, 42);
|
||||
--btn-load-more-hover: rgb(36, 38, 48);
|
||||
--btn-load-more-hover: rgb(54, 56, 66);
|
||||
|
||||
// Switch
|
||||
--switch-bg-color: rgb(100, 102, 110);
|
||||
@ -89,4 +89,10 @@
|
||||
|
||||
--console-icon-button-bg-color: rgb(50, 53, 64);
|
||||
--console-icon-button-bg-color-hover: rgb(70, 73, 84);
|
||||
|
||||
// Img2Img
|
||||
--img2img-img-bg-color: rgb(30, 32, 42);
|
||||
|
||||
// Gallery
|
||||
--gallery-resizeable-color: rgb(36, 38, 48);
|
||||
}
|
||||
|
@ -46,7 +46,7 @@
|
||||
--btn-red-hover: rgb(255, 55, 55);
|
||||
|
||||
--btn-load-more: rgb(202, 204, 206);
|
||||
--btn-load-more-hover: rgb(206, 208, 210);
|
||||
--btn-load-more-hover: rgb(178, 180, 182);
|
||||
|
||||
// Switch
|
||||
--switch-bg-color: rgb(178, 180, 182);
|
||||
@ -88,4 +88,10 @@
|
||||
--console-border-color: rgb(160, 162, 164);
|
||||
--console-icon-button-bg-color: var(--switch-bg-color);
|
||||
--console-icon-button-bg-color-hover: var(--console-border-color);
|
||||
|
||||
// Img2Img
|
||||
--img2img-img-bg-color: rgb(180, 182, 184);
|
||||
|
||||
// Gallery
|
||||
--gallery-resizeable-color: rgb(192, 194, 196);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
@use 'Colors_Dark';
|
||||
@use 'Colors_Light';
|
||||
@use 'Fonts';
|
||||
@use 'Animations';
|
||||
|
||||
// Component Styles
|
||||
//app
|
||||
@ -26,10 +27,12 @@
|
||||
@use '../features/gallery/CurrentImageDisplay.scss';
|
||||
@use '../features/gallery/ImageGallery.scss';
|
||||
@use '../features/gallery/InvokePopover.scss';
|
||||
@use '../features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss';
|
||||
|
||||
// Tabs
|
||||
@use '../features/tabs/InvokeTabs.scss';
|
||||
@use '../features/tabs/TextToImage/TextToImage.scss';
|
||||
@use '../features/tabs/ImageToImage/ImageToImage.scss';
|
||||
|
||||
// Component Shared
|
||||
@use '../common/components/IAINumberInput.scss';
|
||||
|
@ -82,6 +82,7 @@ with metadata_from_png():
|
||||
|
||||
import argparse
|
||||
from argparse import Namespace, RawTextHelpFormatter
|
||||
import pydoc
|
||||
import shlex
|
||||
import json
|
||||
import hashlib
|
||||
@ -115,6 +116,36 @@ PRECISION_CHOICES = [
|
||||
APP_ID = 'lstein/stable-diffusion'
|
||||
APP_VERSION = 'v1.15'
|
||||
|
||||
class ArgFormatter(argparse.RawTextHelpFormatter):
|
||||
# use defined argument order to display usage
|
||||
def _format_usage(self, usage, actions, groups, prefix):
|
||||
if prefix is None:
|
||||
prefix = 'usage: '
|
||||
|
||||
# if usage is specified, use that
|
||||
if usage is not None:
|
||||
usage = usage % dict(prog=self._prog)
|
||||
|
||||
# if no optionals or positionals are available, usage is just prog
|
||||
elif usage is None and not actions:
|
||||
usage = 'invoke>'
|
||||
elif usage is None:
|
||||
prog='invoke>'
|
||||
# build full usage string
|
||||
action_usage = self._format_actions_usage(actions, groups) # NEW
|
||||
usage = ' '.join([s for s in [prog, action_usage] if s])
|
||||
# omit the long line wrapping code
|
||||
# prefix with 'usage:'
|
||||
return '%s%s\n\n' % (prefix, usage)
|
||||
|
||||
class PagingArgumentParser(argparse.ArgumentParser):
|
||||
'''
|
||||
A custom ArgumentParser that uses pydoc to page its output.
|
||||
'''
|
||||
def print_help(self, file=None):
|
||||
text = self.format_help()
|
||||
pydoc.pager(text)
|
||||
|
||||
class Args(object):
|
||||
def __init__(self,arg_parser=None,cmd_parser=None):
|
||||
'''
|
||||
@ -481,8 +512,8 @@ class Args(object):
|
||||
|
||||
# This creates the parser that processes commands on the dream> command line
|
||||
def _create_dream_cmd_parser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
parser = PagingArgumentParser(
|
||||
formatter_class=ArgFormatter,
|
||||
description=
|
||||
"""
|
||||
*Image generation:*
|
||||
|
@ -122,8 +122,8 @@ class Generator():
|
||||
raise NotImplementedError("get_noise() must be implemented in a descendent class")
|
||||
|
||||
def get_perlin_noise(self,width,height):
|
||||
return torch.stack([rand_perlin_2d((height, width), (8, 8)).to(self.model.device) for _ in range(self.latent_channels)], dim=0)
|
||||
|
||||
fixdevice = 'cpu' if (self.model.device.type == 'mps') else self.model.device
|
||||
return torch.stack([rand_perlin_2d((height, width), (8, 8), device = self.model.device).to(fixdevice) for _ in range(self.latent_channels)], dim=0).to(self.model.device)
|
||||
|
||||
def new_seed(self):
|
||||
self.seed = random.randrange(0, np.iinfo(np.uint32).max)
|
||||
|
@ -49,6 +49,7 @@ class Img2Img(Generator):
|
||||
img_callback = step_callback,
|
||||
unconditional_guidance_scale=cfg_scale,
|
||||
unconditional_conditioning=uc,
|
||||
init_latent = self.init_latent, # changes how noising is performed in ksampler
|
||||
)
|
||||
|
||||
return self.sample_to_image(samples)
|
||||
|
@ -849,6 +849,7 @@ class Generate:
|
||||
print(
|
||||
f'>> loaded input image of size {image.width}x{image.height}'
|
||||
)
|
||||
image = ImageOps.exif_transpose(image)
|
||||
return image
|
||||
|
||||
def _create_init_image(self, image, width, height, fit=True):
|
||||
@ -857,7 +858,6 @@ class Generate:
|
||||
image = self._fit_image(image, (width, height))
|
||||
else:
|
||||
image = self._squeeze_image(image)
|
||||
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = image[None].transpose(0, 3, 1, 2)
|
||||
image = torch.from_numpy(image)
|
||||
@ -874,7 +874,6 @@ class Generate:
|
||||
image = self._fit_image(image, (width, height))
|
||||
else:
|
||||
image = self._squeeze_image(image)
|
||||
|
||||
image = image.resize((image.width//downsampling, image.height //
|
||||
downsampling), resample=Image.Resampling.NEAREST)
|
||||
image = np.array(image)
|
||||
|
@ -97,6 +97,7 @@ class KSampler(Sampler):
|
||||
rho=7.,
|
||||
device=self.device,
|
||||
)
|
||||
self.sigmas = self.karras_sigmas
|
||||
|
||||
# ALERT: We are completely overriding the sample() method in the base class, which
|
||||
# means that inpainting will not work. To get this to work we need to be able to
|
||||
@ -170,11 +171,16 @@ class KSampler(Sampler):
|
||||
img_callback(k_callback_values['x'],k_callback_values['i'])
|
||||
|
||||
# sigmas are set up in make_schedule - we take the last steps items
|
||||
total_steps = len(self.karras_sigmas)
|
||||
sigmas = self.karras_sigmas[-S-1:]
|
||||
|
||||
total_steps = len(self.sigmas)
|
||||
sigmas = self.sigmas[-S-1:]
|
||||
|
||||
# x_T is variation noise. When an init image is provided (in x0) we need to add
|
||||
# more randomness to the starting image.
|
||||
if x_T is not None:
|
||||
x = x_T + torch.randn([batch_size, *shape], device=self.device) * sigmas[0]
|
||||
if x0 is not None:
|
||||
x = x_T + torch.randn_like(x0, device=self.device) * sigmas[0]
|
||||
else:
|
||||
x = x_T * sigmas[0]
|
||||
else:
|
||||
x = torch.randn([batch_size, *shape], device=self.device) * sigmas[0]
|
||||
|
||||
|
10
ldm/util.py
10
ldm/util.py
@ -214,15 +214,19 @@ def parallel_data_prefetch(
|
||||
else:
|
||||
return gather_res
|
||||
|
||||
def rand_perlin_2d(shape, res, fade = lambda t: 6*t**5 - 15*t**4 + 10*t**3):
|
||||
def rand_perlin_2d(shape, res, device, fade = lambda t: 6*t**5 - 15*t**4 + 10*t**3):
|
||||
delta = (res[0] / shape[0], res[1] / shape[1])
|
||||
d = (shape[0] // res[0], shape[1] // res[1])
|
||||
|
||||
grid = torch.stack(torch.meshgrid(torch.arange(0, res[0], delta[0]), torch.arange(0, res[1], delta[1]), indexing='ij'), dim = -1) % 1
|
||||
angles = 2*math.pi*torch.rand(res[0]+1, res[1]+1)
|
||||
grid = torch.stack(torch.meshgrid(torch.arange(0, res[0], delta[0]), torch.arange(0, res[1], delta[1]), indexing='ij'), dim = -1).to(device) % 1
|
||||
|
||||
rand_val = torch.rand(res[0]+1, res[1]+1)
|
||||
|
||||
angles = 2*math.pi*rand_val
|
||||
gradients = torch.stack((torch.cos(angles), torch.sin(angles)), dim = -1)
|
||||
|
||||
tile_grads = lambda slice1, slice2: gradients[slice1[0]:slice1[1], slice2[0]:slice2[1]].repeat_interleave(d[0], 0).repeat_interleave(d[1], 1)
|
||||
|
||||
dot = lambda grad, shift: (torch.stack((grid[:shape[0],:shape[1],0] + shift[0], grid[:shape[0],:shape[1], 1] + shift[1] ), dim = -1) * grad[:shape[0], :shape[1]]).sum(dim = -1)
|
||||
|
||||
n00 = dot(tile_grads([0, -1], [0, -1]), [0, 0])
|
||||
|
Loading…
Reference in New Issue
Block a user