From df4d1162b54ff9baad5707679cf6ad7979719666 Mon Sep 17 00:00:00 2001 From: "Claus F. Strasburger" Date: Thu, 15 Sep 2022 13:21:17 +0200 Subject: [PATCH 01/19] docs: VARIATIONS.md used wrong syntax in examples (#589) --- docs/features/VARIATIONS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/VARIATIONS.md b/docs/features/VARIATIONS.md index a6c5c936c1..99b891576d 100644 --- a/docs/features/VARIATIONS.md +++ b/docs/features/VARIATIONS.md @@ -74,7 +74,7 @@ We combine the two variations using `-V` (--with_variations). Again, we must pro this to work. ``` -dream> "prompt" -S3357757885 -V3647897225,0.1;1614299449,0.1 +dream> "prompt" -S3357757885 -V3647897225:0.1,1614299449:0.1 Outputs: ./outputs/Xena/000003.1614299449.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1 -S3357757885 ``` @@ -86,7 +86,7 @@ Here we are providing equal weights (0.1 and 0.1) for both the subseeds. The res We could either try combining the images with different weights, or we can generate more variations around the almost-but-not-quite image. We do the latter, using both the `-V` (combining) and `-v` (variation strength) options. Note that we use `-n6` to generate 6 variations: ``` -dream> "prompt" -S3357757885 -V3647897225,0.1;1614299449,0.1 -v0.05 -n6 +dream> "prompt" -S3357757885 -V3647897225:0.1,1614299449:0.1 -v0.05 -n6 Outputs: ./outputs/Xena/000004.3279757577.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,3279757577:0.05 -S3357757885 ./outputs/Xena/000004.2853129515.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,2853129515:0.05 -S3357757885 From 30e69f8b32fbe90d35bcc73d3cabdac8693b033b Mon Sep 17 00:00:00 2001 From: William Becher Date: Thu, 15 Sep 2022 08:40:27 -0300 Subject: [PATCH 02/19] Fix image location on webpage - windows (#568) --- ldm/dream/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ldm/dream/server.py b/ldm/dream/server.py index 54118c7dd4..96ffd7f2ef 100644 --- a/ldm/dream/server.py +++ b/ldm/dream/server.py @@ -107,7 +107,7 @@ class DreamServer(BaseHTTPRequestHandler): out_dir = os.path.realpath(self.outdir.rstrip('/')) if self.path.startswith('/static/dream_web/'): path = '.' + self.path - elif out_dir.endswith(path_dir): + elif out_dir.replace('\\', '/').endswith(path_dir): file = os.path.basename(self.path) path = os.path.join(self.outdir,file) else: From ccb2b7c2fbfda243ec7fd3c523e9f6eb5dd46c65 Mon Sep 17 00:00:00 2001 From: Mihai <299015+mh-dm@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:41:24 +0300 Subject: [PATCH 03/19] Use cuda only when available in main.py. (#567) Allows testing textual inversion / training flow on cpu only (very slow though). Context: #508 --- main.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/main.py b/main.py index c45194db44..72aaa49c3b 100644 --- a/main.py +++ b/main.py @@ -40,7 +40,8 @@ def load_model_from_config(config, ckpt, verbose=False): print('unexpected keys:') print(u) - model.cuda() + if torch.cuda.is_available(): + model.cuda() return model @@ -549,23 +550,26 @@ class CUDACallback(Callback): # see https://github.com/SeanNaren/minGPT/blob/master/mingpt/callback.py def on_train_epoch_start(self, trainer, pl_module): # Reset the memory use counter - torch.cuda.reset_peak_memory_stats(trainer.root_gpu) - torch.cuda.synchronize(trainer.root_gpu) + if torch.cuda.is_available(): + torch.cuda.reset_peak_memory_stats(trainer.root_gpu) + torch.cuda.synchronize(trainer.root_gpu) self.start_time = time.time() def on_train_epoch_end(self, trainer, pl_module, outputs): - torch.cuda.synchronize(trainer.root_gpu) - max_memory = ( - torch.cuda.max_memory_allocated(trainer.root_gpu) / 2**20 - ) + if torch.cuda.is_available(): + torch.cuda.synchronize(trainer.root_gpu) epoch_time = time.time() - self.start_time try: - max_memory = trainer.training_type_plugin.reduce(max_memory) epoch_time = trainer.training_type_plugin.reduce(epoch_time) - rank_zero_info(f'Average Epoch time: {epoch_time:.2f} seconds') - rank_zero_info(f'Average Peak memory {max_memory:.2f}MiB') + + if torch.cuda.is_available(): + max_memory = ( + torch.cuda.max_memory_allocated(trainer.root_gpu) / 2**20 + ) + max_memory = trainer.training_type_plugin.reduce(max_memory) + rank_zero_info(f'Average Peak memory {max_memory:.2f}MiB') except AttributeError: pass @@ -872,7 +876,6 @@ if __name__ == '__main__': config.data.params.validation.params.data_root = opt.data_root data = instantiate_from_config(config.data) - data = instantiate_from_config(config.data) # NOTE according to https://pytorch-lightning.readthedocs.io/en/latest/datamodules.html # calling these ourselves should not be necessary but it is. # lightning still takes care of proper multiprocessing though From 9df743e2bf54829eabe87fbbb1f4aaa1c75ba0c1 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Thu, 15 Sep 2022 07:43:43 -0400 Subject: [PATCH 04/19] Web cleanup (#539) * Refactor generate.py and dream.py * config file path (models.yaml) is parsed inside Generate() to simplify API * Better handling of keyboard interrupts in file loading mode vs interactive * Removed oodles of unused variables. * move nonfunctional inpainting out of the scripts directory * fix ugly ddim tqdm formatting * fix embiggen breakage, formatting fixes --- ldm/dream/server.py | 7 +++++++ scripts/dream.py | 2 ++ static/dream_web/index.css | 14 +++++++++++--- static/dream_web/index.html | 27 +++++++++++++++------------ 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/ldm/dream/server.py b/ldm/dream/server.py index 96ffd7f2ef..b56e21f6c7 100644 --- a/ldm/dream/server.py +++ b/ldm/dream/server.py @@ -22,6 +22,12 @@ def build_opt(post_data, seed, gfpgan_model_exists): setattr(opt, 'invert_mask', 'invert_mask' in post_data) setattr(opt, 'cfg_scale', float(post_data['cfg_scale'])) setattr(opt, 'sampler_name', post_data['sampler_name']) + + # embiggen not practical at this point because we have no way of feeding images back into img2img + # however, this code is here against that eventuality + setattr(opt, 'embiggen', None) + setattr(opt, 'embiggen_tiles', None) + setattr(opt, 'gfpgan_strength', float(post_data['gfpgan_strength']) if gfpgan_model_exists else 0) setattr(opt, 'upscale', [int(post_data['upscale_level']), float(post_data['upscale_strength'])] if post_data['upscale_level'] != '' else None) setattr(opt, 'progress_images', 'progress_images' in post_data) @@ -155,6 +161,7 @@ class DreamServer(BaseHTTPRequestHandler): def image_done(image, seed, upscaled=False): name = f'{prefix}.{seed}.png' iter_opt = argparse.Namespace(**vars(opt)) # copy + print(f'iter_opt = {iter_opt}') if opt.variation_amount > 0: this_variation = [[seed, opt.variation_amount]] if opt.with_variations is None: diff --git a/scripts/dream.py b/scripts/dream.py index aec27506c3..4044af7cb8 100755 --- a/scripts/dream.py +++ b/scripts/dream.py @@ -620,6 +620,7 @@ def create_cmd_parser(): ) parser.add_argument( '-embiggen', + '--embiggen', nargs='+', default=None, type=float, @@ -627,6 +628,7 @@ def create_cmd_parser(): ) parser.add_argument( '-embiggen_tiles', + '--embiggen_tiles', nargs='+', default=None, type=int, diff --git a/static/dream_web/index.css b/static/dream_web/index.css index cbe753d423..51f0f267c3 100644 --- a/static/dream_web/index.css +++ b/static/dream_web/index.css @@ -91,6 +91,7 @@ header h1 { } #fieldset-config { line-height:2em; + background-color: #F0F0F0; } input[type="number"] { width: 60px; @@ -122,6 +123,9 @@ label { cursor: pointer; color: red; } +#basic-parameters { + background-color: #EEEEEE; +} #txt2img { background-color: #DCDCDC; } @@ -129,15 +133,19 @@ label { background-color: #EEEEEE; } #img2img { - background-color: #F5F5F5; + background-color: #DCDCDC; } #gfpgan { - background-color: #DCDCDC; + background-color: #EEEEEE; } #progress-section { background-color: #F5F5F5; } - +.section-header { + text-align: left; + font-weight: bold; + padding: 0 0 0 0; +} #no-results-message:not(:only-child) { display: none; } diff --git a/static/dream_web/index.html b/static/dream_web/index.html index 628cfb4b6c..1e194c0205 100644 --- a/static/dream_web/index.html +++ b/static/dream_web/index.html @@ -25,6 +25,7 @@
+
Basic options
@@ -39,11 +40,11 @@ - + - +
-
+ -
-
- - - + +
+
+
Image-to-image options
+ + +
- - +
+
Post-processing options
- + + {validValues.map((opt) => { + return typeof opt === 'string' || + typeof opt === 'number' ? ( + + ) : ( + + ); + })} + + + + ); +}; + +export default SDSelect; diff --git a/frontend/src/components/SDSwitch.tsx b/frontend/src/components/SDSwitch.tsx new file mode 100644 index 0000000000..df2b811a2b --- /dev/null +++ b/frontend/src/components/SDSwitch.tsx @@ -0,0 +1,42 @@ +import { + Flex, + FormControl, + FormLabel, + Switch, + SwitchProps, +} from '@chakra-ui/react'; + +interface Props extends SwitchProps { + label?: string; + width?: string | number; +} + +const SDSwitch = (props: Props) => { + const { + label, + isDisabled = false, + fontSize = 'md', + size = 'md', + width, + ...rest + } = props; + return ( + + + {label && ( + + {label} + + )} + + + + ); +}; + +export default SDSwitch; diff --git a/frontend/src/features/gallery/CurrentImage.tsx b/frontend/src/features/gallery/CurrentImage.tsx new file mode 100644 index 0000000000..e2698dd09d --- /dev/null +++ b/frontend/src/features/gallery/CurrentImage.tsx @@ -0,0 +1,161 @@ +import { Center, Flex, Image, useColorModeValue } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { RootState } from '../../app/store'; +import { setAllParameters, setInitialImagePath, setSeed } from '../sd/sdSlice'; +import { useState } from 'react'; +import ImageMetadataViewer from './ImageMetadataViewer'; +import DeleteImageModalButton from './DeleteImageModalButton'; +import SDButton from '../../components/SDButton'; +import { runESRGAN, runGFPGAN } from '../../app/socketio'; +import { createSelector } from '@reduxjs/toolkit'; +import { SystemState } from '../system/systemSlice'; +import { isEqual } from 'lodash'; + +const height = 'calc(100vh - 238px)'; + +const systemSelector = createSelector( + (state: RootState) => state.system, + (system: SystemState) => { + return { + isProcessing: system.isProcessing, + isConnected: system.isConnected, + isGFPGANAvailable: system.isGFPGANAvailable, + isESRGANAvailable: system.isESRGANAvailable, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const CurrentImage = () => { + const { currentImage, intermediateImage } = useAppSelector( + (state: RootState) => state.gallery + ); + const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } = + useAppSelector(systemSelector); + + const dispatch = useAppDispatch(); + + const bgColor = useColorModeValue( + 'rgba(255, 255, 255, 0.85)', + 'rgba(0, 0, 0, 0.8)' + ); + + const [shouldShowImageDetails, setShouldShowImageDetails] = + useState(false); + + const imageToDisplay = intermediateImage || currentImage; + + return ( + + {imageToDisplay && ( + + + dispatch(setInitialImagePath(imageToDisplay.url)) + } + /> + + + dispatch(setAllParameters(imageToDisplay.metadata)) + } + /> + + + dispatch(setSeed(imageToDisplay.metadata.seed!)) + } + /> + + dispatch(runESRGAN(imageToDisplay))} + /> + dispatch(runGFPGAN(imageToDisplay))} + /> + + setShouldShowImageDetails(!shouldShowImageDetails) + } + /> + + + + + )} +
+ {imageToDisplay && ( + + )} + {imageToDisplay && shouldShowImageDetails && ( + + + + )} +
+
+ ); +}; + +export default CurrentImage; diff --git a/frontend/src/features/gallery/DeleteImageModalButton.tsx b/frontend/src/features/gallery/DeleteImageModalButton.tsx new file mode 100644 index 0000000000..eca53c8e9f --- /dev/null +++ b/frontend/src/features/gallery/DeleteImageModalButton.tsx @@ -0,0 +1,94 @@ +import { + IconButtonProps, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, + useDisclosure, +} from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { + cloneElement, + ReactElement, + SyntheticEvent, +} from 'react'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { deleteImage } from '../../app/socketio'; +import { RootState } from '../../app/store'; +import SDButton from '../../components/SDButton'; +import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice'; +import { SDImage } from './gallerySlice'; + +interface Props extends IconButtonProps { + image: SDImage; + 'aria-label': string; + children: ReactElement; +} + +const systemSelector = createSelector( + (state: RootState) => state.system, + (system: SystemState) => system.shouldConfirmOnDelete +); + +/* +TODO: The modal and button to open it should be two different components, +but their state is closely related and I'm not sure how best to accomplish it. +*/ +const DeleteImageModalButton = (props: Omit) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const dispatch = useAppDispatch(); + const shouldConfirmOnDelete = useAppSelector(systemSelector); + + const handleClickDelete = (e: SyntheticEvent) => { + e.stopPropagation(); + shouldConfirmOnDelete ? onOpen() : handleDelete(); + }; + + const { image, children } = props; + + const handleDelete = () => { + dispatch(deleteImage(image)); + onClose(); + }; + + const handleDeleteAndDontAsk = () => { + dispatch(deleteImage(image)); + dispatch(setShouldConfirmOnDelete(false)); + onClose(); + }; + + return ( + <> + {cloneElement(children, { + onClick: handleClickDelete, + })} + + + + + Are you sure you want to delete this image? + + + It will be deleted forever! + + + + + + + + + + + ); +}; + +export default DeleteImageModalButton; diff --git a/frontend/src/features/gallery/ImageMetadataViewer.tsx b/frontend/src/features/gallery/ImageMetadataViewer.tsx new file mode 100644 index 0000000000..3f051f59b6 --- /dev/null +++ b/frontend/src/features/gallery/ImageMetadataViewer.tsx @@ -0,0 +1,124 @@ +import { + Center, + Flex, + IconButton, + Link, + List, + ListItem, + Text, +} from '@chakra-ui/react'; +import { FaPlus } from 'react-icons/fa'; +import { PARAMETERS } from '../../app/constants'; +import { useAppDispatch } from '../../app/hooks'; +import SDButton from '../../components/SDButton'; +import { setAllParameters, setParameter } from '../sd/sdSlice'; +import { SDImage, SDMetadata } from './gallerySlice'; + +type Props = { + image: SDImage; +}; + +const ImageMetadataViewer = ({ image }: Props) => { + const dispatch = useAppDispatch(); + + const keys = Object.keys(PARAMETERS); + + const metadata: Array<{ + label: string; + key: string; + value: string | number | boolean; + }> = []; + + keys.forEach((key) => { + const value = image.metadata[key as keyof SDMetadata]; + if (value !== undefined) { + metadata.push({ label: PARAMETERS[key], key, value }); + } + }); + + return ( + + dispatch(setAllParameters(image.metadata))} + /> + + File: + + {image.url} + + + {metadata.length ? ( + <> + + {metadata.map((parameter, i) => { + const { label, key, value } = parameter; + return ( + + + } + size={'xs'} + onClick={() => + dispatch( + setParameter({ + key, + value, + }) + ) + } + /> + + {label}: + + + {value === undefined || + value === null || + value === '' || + value === 0 ? ( + + None + + ) : ( + + {value.toString()} + + )} + + + ); + })} + + + Raw: + + {JSON.stringify(image.metadata)} + + + + ) : ( +
+ + No metadata available + +
+ )} +
+ ); +}; + +export default ImageMetadataViewer; diff --git a/frontend/src/features/gallery/ImageRoll.tsx b/frontend/src/features/gallery/ImageRoll.tsx new file mode 100644 index 0000000000..b624db3aaa --- /dev/null +++ b/frontend/src/features/gallery/ImageRoll.tsx @@ -0,0 +1,150 @@ +import { + Box, + Flex, + Icon, + IconButton, + Image, + useColorModeValue, +} from '@chakra-ui/react'; +import { RootState } from '../../app/store'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { SDImage, setCurrentImage } from './gallerySlice'; +import { FaCheck, FaCopy, FaSeedling, FaTrash } from 'react-icons/fa'; +import DeleteImageModalButton from './DeleteImageModalButton'; +import { memo, SyntheticEvent, useState } from 'react'; +import { setAllParameters, setSeed } from '../sd/sdSlice'; + +interface HoverableImageProps { + image: SDImage; + isSelected: boolean; +} + +const HoverableImage = memo( + (props: HoverableImageProps) => { + const [isHovered, setIsHovered] = useState(false); + const dispatch = useAppDispatch(); + + const checkColor = useColorModeValue('green.600', 'green.300'); + const bgColor = useColorModeValue('gray.200', 'gray.700'); + const bgGradient = useColorModeValue( + 'radial-gradient(circle, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.7) 20%, rgba(0,0,0,0) 100%)', + 'radial-gradient(circle, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.7) 20%, rgba(0,0,0,0) 100%)' + ); + + const { image, isSelected } = props; + const { url, uuid, metadata } = image; + + const handleMouseOver = () => setIsHovered(true); + const handleMouseOut = () => setIsHovered(false); + const handleClickSetAllParameters = (e: SyntheticEvent) => { + e.stopPropagation(); + dispatch(setAllParameters(metadata)); + }; + const handleClickSetSeed = (e: SyntheticEvent) => { + e.stopPropagation(); + dispatch(setSeed(image.metadata.seed!)); // component not rendered unless this exists + }; + + return ( + + + dispatch(setCurrentImage(image))} + onMouseOver={handleMouseOver} + onMouseOut={handleMouseOut} + > + {isSelected && ( + + )} + {isHovered && ( + + + } + size='xs' + fontSize={15} + /> + + } + size='xs' + fontSize={15} + onClickCapture={handleClickSetAllParameters} + /> + {image.metadata.seed && ( + } + size='xs' + fontSize={16} + onClickCapture={handleClickSetSeed} + /> + )} + + )} + + + ); + }, + (prev, next) => + prev.image.uuid === next.image.uuid && + prev.isSelected === next.isSelected +); + +const ImageRoll = () => { + const { images, currentImageUuid } = useAppSelector( + (state: RootState) => state.gallery + ); + + return ( + + {[...images].reverse().map((image) => { + const { uuid } = image; + const isSelected = currentImageUuid === uuid; + return ( + + ); + })} + + ); +}; + +export default ImageRoll; diff --git a/frontend/src/features/gallery/gallerySlice.ts b/frontend/src/features/gallery/gallerySlice.ts new file mode 100644 index 0000000000..3c7611724f --- /dev/null +++ b/frontend/src/features/gallery/gallerySlice.ts @@ -0,0 +1,144 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import { v4 as uuidv4 } from 'uuid'; +import { UpscalingLevel } from '../sd/sdSlice'; +import { backendToFrontendParameters } from '../../app/parameterTranslation'; + +// TODO: Revise pending metadata RFC: https://github.com/lstein/stable-diffusion/issues/266 +export interface SDMetadata { + prompt?: string; + steps?: number; + cfgScale?: number; + height?: number; + width?: number; + sampler?: string; + seed?: number; + img2imgStrength?: number; + gfpganStrength?: number; + upscalingLevel?: UpscalingLevel; + upscalingStrength?: number; + initialImagePath?: string; + maskPath?: string; + seamless?: boolean; + shouldFitToWidthHeight?: boolean; +} + +export interface SDImage { + // TODO: I have installed @types/uuid but cannot figure out how to use them here. + uuid: string; + url: string; + metadata: SDMetadata; +} + +export interface GalleryState { + currentImageUuid: string; + images: Array; + intermediateImage?: SDImage; + currentImage?: SDImage; +} + +const initialState: GalleryState = { + currentImageUuid: '', + images: [], +}; + +export const gallerySlice = createSlice({ + name: 'gallery', + initialState, + reducers: { + setCurrentImage: (state, action: PayloadAction) => { + state.currentImage = action.payload; + state.currentImageUuid = action.payload.uuid; + }, + removeImage: (state, action: PayloadAction) => { + const { uuid } = action.payload; + + const newImages = state.images.filter((image) => image.uuid !== uuid); + + const imageToDeleteIndex = state.images.findIndex( + (image) => image.uuid === uuid + ); + + const newCurrentImageIndex = Math.min( + Math.max(imageToDeleteIndex, 0), + newImages.length - 1 + ); + + state.images = newImages; + + state.currentImage = newImages.length + ? newImages[newCurrentImageIndex] + : undefined; + + state.currentImageUuid = newImages.length + ? newImages[newCurrentImageIndex].uuid + : ''; + }, + addImage: (state, action: PayloadAction) => { + state.images.push(action.payload); + state.currentImageUuid = action.payload.uuid; + state.intermediateImage = undefined; + state.currentImage = action.payload; + }, + setIntermediateImage: (state, action: PayloadAction) => { + state.intermediateImage = action.payload; + }, + clearIntermediateImage: (state) => { + state.intermediateImage = undefined; + }, + setGalleryImages: ( + state, + action: PayloadAction< + Array<{ + path: string; + metadata: { [key: string]: string | number | boolean }; + }> + > + ) => { + // TODO: Revise pending metadata RFC: https://github.com/lstein/stable-diffusion/issues/266 + const images = action.payload; + + if (images.length === 0) { + // there are no images on disk, clear the gallery + state.images = []; + state.currentImageUuid = ''; + state.currentImage = undefined; + } else { + // Filter image urls that are already in the rehydrated state + const filteredImages = action.payload.filter( + (image) => !state.images.find((i) => i.url === image.path) + ); + + const preparedImages = filteredImages.map((image): SDImage => { + return { + uuid: uuidv4(), + url: image.path, + metadata: backendToFrontendParameters(image.metadata), + }; + }); + + const newImages = [...state.images].concat(preparedImages); + + // if previous currentimage no longer exists, set a new one + if (!newImages.find((image) => image.uuid === state.currentImageUuid)) { + const newCurrentImage = newImages[newImages.length - 1]; + state.currentImage = newCurrentImage; + state.currentImageUuid = newCurrentImage.uuid; + } + + state.images = newImages; + } + }, + }, +}); + +export const { + setCurrentImage, + removeImage, + addImage, + setGalleryImages, + setIntermediateImage, + clearIntermediateImage, +} = gallerySlice.actions; + +export default gallerySlice.reducer; diff --git a/frontend/src/features/header/ProgressBar.tsx b/frontend/src/features/header/ProgressBar.tsx new file mode 100644 index 0000000000..28d7b44c63 --- /dev/null +++ b/frontend/src/features/header/ProgressBar.tsx @@ -0,0 +1,35 @@ +import { Progress } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { useAppSelector } from '../../app/hooks'; +import { RootState } from '../../app/store'; +import { SDState } from '../sd/sdSlice'; + +const sdSelector = createSelector( + (state: RootState) => state.sd, + (sd: SDState) => { + return { + realSteps: sd.realSteps, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const ProgressBar = () => { + const { realSteps } = useAppSelector(sdSelector); + const { currentStep } = useAppSelector((state: RootState) => state.system); + const progress = Math.round((currentStep * 100) / realSteps); + return ( + + ); +}; + +export default ProgressBar; diff --git a/frontend/src/features/header/SiteHeader.tsx b/frontend/src/features/header/SiteHeader.tsx new file mode 100644 index 0000000000..f950eea2c0 --- /dev/null +++ b/frontend/src/features/header/SiteHeader.tsx @@ -0,0 +1,93 @@ +import { + Flex, + Heading, + IconButton, + Link, + Spacer, + Text, + useColorMode, +} from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; + +import { FaSun, FaMoon, FaGithub } from 'react-icons/fa'; +import { MdHelp, MdSettings } from 'react-icons/md'; +import { useAppSelector } from '../../app/hooks'; +import { RootState } from '../../app/store'; +import SettingsModal from '../system/SettingsModal'; +import { SystemState } from '../system/systemSlice'; + +const systemSelector = createSelector( + (state: RootState) => state.system, + (system: SystemState) => { + return { isConnected: system.isConnected }; + }, + { + memoizeOptions: { resultEqualityCheck: isEqual }, + } +); + +const SiteHeader = () => { + const { colorMode, toggleColorMode } = useColorMode(); + const { isConnected } = useAppSelector(systemSelector); + + return ( + + Stable Diffusion Dream Server + + + + + {isConnected ? `Connected to server` : 'No connection to server'} + + + + } + /> + + + + + + } + /> + + + + + } + /> + + : } + /> + + ); +}; + +export default SiteHeader; diff --git a/frontend/src/features/sd/ESRGANOptions.tsx b/frontend/src/features/sd/ESRGANOptions.tsx new file mode 100644 index 0000000000..928215523b --- /dev/null +++ b/frontend/src/features/sd/ESRGANOptions.tsx @@ -0,0 +1,84 @@ +import { Flex } from '@chakra-ui/react'; + +import { RootState } from '../../app/store'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; + +import { + setUpscalingLevel, + setUpscalingStrength, + UpscalingLevel, + SDState, +} from '../sd/sdSlice'; + +import SDNumberInput from '../../components/SDNumberInput'; +import SDSelect from '../../components/SDSelect'; + +import { UPSCALING_LEVELS } from '../../app/constants'; +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { SystemState } from '../system/systemSlice'; + +const sdSelector = createSelector( + (state: RootState) => state.sd, + (sd: SDState) => { + return { + upscalingLevel: sd.upscalingLevel, + upscalingStrength: sd.upscalingStrength, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const systemSelector = createSelector( + (state: RootState) => state.system, + (system: SystemState) => { + return { + isESRGANAvailable: system.isESRGANAvailable, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); +const ESRGANOptions = () => { + const { upscalingLevel, upscalingStrength } = useAppSelector(sdSelector); + + const { isESRGANAvailable } = useAppSelector(systemSelector); + + const dispatch = useAppDispatch(); + + return ( + + + dispatch( + setUpscalingLevel( + Number(e.target.value) as UpscalingLevel + ) + ) + } + validValues={UPSCALING_LEVELS} + /> + dispatch(setUpscalingStrength(Number(v)))} + value={upscalingStrength} + /> + + ); +}; + +export default ESRGANOptions; diff --git a/frontend/src/features/sd/GFPGANOptions.tsx b/frontend/src/features/sd/GFPGANOptions.tsx new file mode 100644 index 0000000000..ae6a00de40 --- /dev/null +++ b/frontend/src/features/sd/GFPGANOptions.tsx @@ -0,0 +1,63 @@ +import { Flex } from '@chakra-ui/react'; + +import { RootState } from '../../app/store'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; + +import { SDState, setGfpganStrength } from '../sd/sdSlice'; + +import SDNumberInput from '../../components/SDNumberInput'; + +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { SystemState } from '../system/systemSlice'; + +const sdSelector = createSelector( + (state: RootState) => state.sd, + (sd: SDState) => { + return { + gfpganStrength: sd.gfpganStrength, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const systemSelector = createSelector( + (state: RootState) => state.system, + (system: SystemState) => { + return { + isGFPGANAvailable: system.isGFPGANAvailable, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); +const GFPGANOptions = () => { + const { gfpganStrength } = useAppSelector(sdSelector); + + const { isGFPGANAvailable } = useAppSelector(systemSelector); + + const dispatch = useAppDispatch(); + + return ( + + dispatch(setGfpganStrength(Number(v)))} + value={gfpganStrength} + /> + + ); +}; + +export default GFPGANOptions; diff --git a/frontend/src/features/sd/ImageToImageOptions.tsx b/frontend/src/features/sd/ImageToImageOptions.tsx new file mode 100644 index 0000000000..379a8d231c --- /dev/null +++ b/frontend/src/features/sd/ImageToImageOptions.tsx @@ -0,0 +1,54 @@ +import { Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { RootState } from '../../app/store'; +import SDNumberInput from '../../components/SDNumberInput'; +import SDSwitch from '../../components/SDSwitch'; +import InitImage from './InitImage'; +import { + SDState, + setImg2imgStrength, + setShouldFitToWidthHeight, +} from './sdSlice'; + +const sdSelector = createSelector( + (state: RootState) => state.sd, + (sd: SDState) => { + return { + initialImagePath: sd.initialImagePath, + img2imgStrength: sd.img2imgStrength, + shouldFitToWidthHeight: sd.shouldFitToWidthHeight, + }; + } +); + +const ImageToImageOptions = () => { + const { initialImagePath, img2imgStrength, shouldFitToWidthHeight } = + useAppSelector(sdSelector); + + const dispatch = useAppDispatch(); + return ( + + dispatch(setImg2imgStrength(Number(v)))} + value={img2imgStrength} + /> + + dispatch(setShouldFitToWidthHeight(e.target.checked)) + } + /> + + + ); +}; + +export default ImageToImageOptions; diff --git a/frontend/src/features/sd/InitImage.css b/frontend/src/features/sd/InitImage.css new file mode 100644 index 0000000000..31fe87fa17 --- /dev/null +++ b/frontend/src/features/sd/InitImage.css @@ -0,0 +1,20 @@ +.checkerboard { + background-position: 0px 0px, 10px 10px; + background-size: 20px 20px; + background-image: linear-gradient( + 45deg, + #eee 25%, + transparent 25%, + transparent 75%, + #eee 75%, + #eee 100% + ), + linear-gradient( + 45deg, + #eee 25%, + white 25%, + white 75%, + #eee 75%, + #eee 100% + ); +} diff --git a/frontend/src/features/sd/InitImage.tsx b/frontend/src/features/sd/InitImage.tsx new file mode 100644 index 0000000000..5d7c0aa2d4 --- /dev/null +++ b/frontend/src/features/sd/InitImage.tsx @@ -0,0 +1,155 @@ +import { + Button, + Flex, + IconButton, + Image, + useToast, +} from '@chakra-ui/react'; +import { SyntheticEvent, useCallback, useState } from 'react'; +import { FileRejection, useDropzone } from 'react-dropzone'; +import { FaTrash } from 'react-icons/fa'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { RootState } from '../../app/store'; +import { + SDState, + setInitialImagePath, + setMaskPath, +} from '../../features/sd/sdSlice'; +import MaskUploader from './MaskUploader'; +import './InitImage.css'; +import { uploadInitialImage } from '../../app/socketio'; +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; + +const sdSelector = createSelector( + (state: RootState) => state.sd, + (sd: SDState) => { + return { + initialImagePath: sd.initialImagePath, + maskPath: sd.maskPath, + }; + }, + { memoizeOptions: { resultEqualityCheck: isEqual } } +); + +const InitImage = () => { + const toast = useToast(); + const dispatch = useAppDispatch(); + const { initialImagePath, maskPath } = useAppSelector(sdSelector); + + const onDrop = useCallback( + (acceptedFiles: Array, fileRejections: Array) => { + fileRejections.forEach((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, + }); + }); + + acceptedFiles.forEach((file: File) => { + dispatch(uploadInitialImage(file)); + }); + }, + [dispatch, toast] + ); + + const { getRootProps, getInputProps, open } = useDropzone({ + onDrop, + accept: { + 'image/jpeg': ['.jpg', '.jpeg', '.png'], + }, + }); + + const [shouldShowMask, setShouldShowMask] = useState(false); + const handleClickUploadIcon = (e: SyntheticEvent) => { + e.stopPropagation(); + open(); + }; + const handleClickResetInitialImageAndMask = (e: SyntheticEvent) => { + e.stopPropagation(); + dispatch(setInitialImagePath('')); + dispatch(setMaskPath('')); + }; + + const handleMouseOverInitialImageUploadButton = () => + setShouldShowMask(false); + const handleMouseOutInitialImageUploadButton = () => setShouldShowMask(true); + + const handleMouseOverMaskUploadButton = () => setShouldShowMask(true); + const handleMouseOutMaskUploadButton = () => setShouldShowMask(true); + + return ( + e.stopPropagation() : undefined, + })} + direction={'column'} + alignItems={'center'} + gap={2} + > + + + + + + + + } + /> + + {initialImagePath && ( + + + {shouldShowMask && maskPath && ( + + )} + + )} + + ); +}; + +export default InitImage; diff --git a/frontend/src/features/sd/MaskUploader.tsx b/frontend/src/features/sd/MaskUploader.tsx new file mode 100644 index 0000000000..173cf1880c --- /dev/null +++ b/frontend/src/features/sd/MaskUploader.tsx @@ -0,0 +1,61 @@ +import { useToast } from '@chakra-ui/react'; +import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react'; +import { FileRejection, useDropzone } from 'react-dropzone'; +import { useAppDispatch } from '../../app/hooks'; +import { uploadMaskImage } from '../../app/socketio'; + +type Props = { + children: ReactElement; +}; + +const MaskUploader = ({ children }: Props) => { + const dispatch = useAppDispatch(); + const toast = useToast(); + + const onDrop = useCallback( + (acceptedFiles: Array, fileRejections: Array) => { + fileRejections.forEach((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, + }); + }); + + acceptedFiles.forEach((file: File) => { + dispatch(uploadMaskImage(file)); + }); + }, + [dispatch, toast] + ); + + const { getRootProps, getInputProps, open } = useDropzone({ + onDrop, + accept: { + 'image/jpeg': ['.jpg', '.jpeg', '.png'], + }, + }); + + const handleClickUploadIcon = (e: SyntheticEvent) => { + e.stopPropagation(); + open(); + }; + + return ( +
+ + {cloneElement(children, { + onClick: handleClickUploadIcon, + })} +
+ ); +}; + +export default MaskUploader; diff --git a/frontend/src/features/sd/OptionsAccordion.tsx b/frontend/src/features/sd/OptionsAccordion.tsx new file mode 100644 index 0000000000..d2a02450f2 --- /dev/null +++ b/frontend/src/features/sd/OptionsAccordion.tsx @@ -0,0 +1,211 @@ +import { + Flex, + Box, + Text, + Accordion, + AccordionItem, + AccordionButton, + AccordionIcon, + AccordionPanel, + Switch, +} from '@chakra-ui/react'; + +import { RootState } from '../../app/store'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; + +import { + setShouldRunGFPGAN, + setShouldRunESRGAN, + SDState, + setShouldUseInitImage, +} from '../sd/sdSlice'; +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { setOpenAccordions, SystemState } from '../system/systemSlice'; +import SeedVariationOptions from './SeedVariationOptions'; +import SamplerOptions from './SamplerOptions'; +import ESRGANOptions from './ESRGANOptions'; +import GFPGANOptions from './GFPGANOptions'; +import OutputOptions from './OutputOptions'; +import ImageToImageOptions from './ImageToImageOptions'; + +const sdSelector = createSelector( + (state: RootState) => state.sd, + (sd: SDState) => { + return { + initialImagePath: sd.initialImagePath, + shouldUseInitImage: sd.shouldUseInitImage, + shouldRunESRGAN: sd.shouldRunESRGAN, + shouldRunGFPGAN: sd.shouldRunGFPGAN, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const systemSelector = createSelector( + (state: RootState) => state.system, + (system: SystemState) => { + return { + isGFPGANAvailable: system.isGFPGANAvailable, + isESRGANAvailable: system.isESRGANAvailable, + openAccordions: system.openAccordions, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const OptionsAccordion = () => { + const { + shouldRunESRGAN, + shouldRunGFPGAN, + shouldUseInitImage, + initialImagePath, + } = useAppSelector(sdSelector); + + const { isGFPGANAvailable, isESRGANAvailable, openAccordions } = + useAppSelector(systemSelector); + + const dispatch = useAppDispatch(); + + return ( + + dispatch(setOpenAccordions(openAccordions)) + } + > + +

+ + + Seed & Variation + + + +

+ + + +
+ +

+ + + Sampler + + + +

+ + + +
+ +

+ + + Upscale (ESRGAN) + + dispatch( + setShouldRunESRGAN(e.target.checked) + ) + } + /> + + + +

+ + + +
+ +

+ + + Fix Faces (GFPGAN) + + dispatch( + setShouldRunGFPGAN(e.target.checked) + ) + } + /> + + + +

+ + + +
+ +

+ + + Image to Image + + dispatch( + setShouldUseInitImage(e.target.checked) + ) + } + /> + + + +

+ + + +
+ +

+ + + Output + + + +

+ + + +
+
+ ); +}; + +export default OptionsAccordion; diff --git a/frontend/src/features/sd/OutputOptions.tsx b/frontend/src/features/sd/OutputOptions.tsx new file mode 100644 index 0000000000..abf8acd114 --- /dev/null +++ b/frontend/src/features/sd/OutputOptions.tsx @@ -0,0 +1,66 @@ +import { Flex } from '@chakra-ui/react'; + +import { RootState } from '../../app/store'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; + +import { setHeight, setWidth, setSeamless, SDState } from '../sd/sdSlice'; + +import SDSelect from '../../components/SDSelect'; + +import { HEIGHTS, WIDTHS } from '../../app/constants'; +import SDSwitch from '../../components/SDSwitch'; +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; + +const sdSelector = createSelector( + (state: RootState) => state.sd, + (sd: SDState) => { + return { + height: sd.height, + width: sd.width, + seamless: sd.seamless, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const OutputOptions = () => { + const { height, width, seamless } = useAppSelector(sdSelector); + + const dispatch = useAppDispatch(); + + return ( + + + dispatch(setWidth(Number(e.target.value)))} + validValues={WIDTHS} + /> + + dispatch(setHeight(Number(e.target.value))) + } + validValues={HEIGHTS} + /> + + dispatch(setSeamless(e.target.checked))} + /> + + ); +}; + +export default OutputOptions; diff --git a/frontend/src/features/sd/ProcessButtons.tsx b/frontend/src/features/sd/ProcessButtons.tsx new file mode 100644 index 0000000000..8a85d87fcb --- /dev/null +++ b/frontend/src/features/sd/ProcessButtons.tsx @@ -0,0 +1,58 @@ +import { Flex } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { cancelProcessing, generateImage } from '../../app/socketio'; +import { RootState } from '../../app/store'; +import SDButton from '../../components/SDButton'; +import { SystemState } from '../system/systemSlice'; +import useCheckParameters from '../system/useCheckParameters'; + +const systemSelector = createSelector( + (state: RootState) => state.system, + (system: SystemState) => { + return { + isProcessing: system.isProcessing, + isConnected: system.isConnected, + }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +const ProcessButtons = () => { + const { isProcessing, isConnected } = useAppSelector(systemSelector); + + const dispatch = useAppDispatch(); + + const isReady = useCheckParameters(); + + return ( + + dispatch(generateImage())} + /> + dispatch(cancelProcessing())} + /> + + ); +}; + +export default ProcessButtons; diff --git a/frontend/src/features/sd/PromptInput.tsx b/frontend/src/features/sd/PromptInput.tsx new file mode 100644 index 0000000000..1eeb27b293 --- /dev/null +++ b/frontend/src/features/sd/PromptInput.tsx @@ -0,0 +1,25 @@ +import { Textarea } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import { RootState } from '../../app/store'; +import { setPrompt } from '../sd/sdSlice'; + +const PromptInput = () => { + const { prompt } = useAppSelector((state: RootState) => state.sd); + const dispatch = useAppDispatch(); + + return ( + @@ -30,10 +32,10 @@ - - - - + + - - + + @@ -114,7 +116,7 @@
- Postprocessing...1/3 + Postprocessing...1/3
diff --git a/static/dream_web/index.js b/static/dream_web/index.js index ac68034920..3af0308fb5 100644 --- a/static/dream_web/index.js +++ b/static/dream_web/index.js @@ -1,3 +1,41 @@ +const socket = io(); + +function resetForm() { + var form = document.getElementById('generate-form'); + form.querySelector('fieldset').removeAttribute('disabled'); +} + +function initProgress(totalSteps, showProgressImages) { + // TODO: Progress could theoretically come from multiple jobs at the same time (in the future) + let progressSectionEle = document.querySelector('#progress-section'); + progressSectionEle.style.display = 'initial'; + let progressEle = document.querySelector('#progress-bar'); + progressEle.setAttribute('max', totalSteps); + + let progressImageEle = document.querySelector('#progress-image'); + progressImageEle.src = BLANK_IMAGE_URL; + progressImageEle.style.display = showProgressImages ? 'initial': 'none'; +} + +function setProgress(step, totalSteps, src) { + let progressEle = document.querySelector('#progress-bar'); + progressEle.setAttribute('value', step); + + if (src) { + let progressImageEle = document.querySelector('#progress-image'); + progressImageEle.src = src; + } +} + +function resetProgress(hide = true) { + if (hide) { + let progressSectionEle = document.querySelector('#progress-section'); + progressSectionEle.style.display = 'none'; + } + let progressEle = document.querySelector('#progress-bar'); + progressEle.setAttribute('value', 0); +} + function toBase64(file) { return new Promise((resolve, reject) => { const r = new FileReader(); @@ -9,29 +47,17 @@ function toBase64(file) { function appendOutput(src, seed, config) { let outputNode = document.createElement("figure"); - - let variations = config.with_variations; - if (config.variation_amount > 0) { - variations = (variations ? variations + ',' : '') + seed + ':' + config.variation_amount; - } - let baseseed = (config.with_variations || config.variation_amount > 0) ? config.seed : seed; - let altText = baseseed + ' | ' + (variations ? variations + ' | ' : '') + config.prompt; + let altText = seed.toString() + " | " + config.prompt; - // img needs width and height for lazy loading to work const figureContents = ` - ${altText} + ${altText}
${seed}
`; outputNode.innerHTML = figureContents; - let figcaption = outputNode.querySelector('figcaption'); + let figcaption = outputNode.querySelector('figcaption') // Reload image config figcaption.addEventListener('click', () => { @@ -40,17 +66,28 @@ function appendOutput(src, seed, config) { if (k == 'initimg') { continue; } form.querySelector(`*[name=${k}]`).value = config[k]; } + if (config.variation_amount > 0 || config.with_variations != '') { + document.querySelector("#seed").value = config.seed; + } else { + document.querySelector("#seed").value = seed; + } - document.querySelector("#seed").value = baseseed; - document.querySelector("#with_variations").value = variations || ''; - if (document.querySelector("#variation_amount").value <= 0) { - document.querySelector("#variation_amount").value = 0.2; + if (config.variation_amount > 0) { + let oldVarAmt = document.querySelector("#variation_amount").value + let oldVariations = document.querySelector("#with_variations").value + let varSep = '' + document.querySelector("#variation_amount").value = 0; + if (document.querySelector("#with_variations").value != '') { + varSep = "," + } + document.querySelector("#with_variations").value = oldVariations + varSep + seed + ':' + config.variation_amount } saveFields(document.querySelector("#generate-form")); }); document.querySelector("#results").prepend(outputNode); + document.querySelector("#no-results-message")?.remove(); } function saveFields(form) { @@ -79,9 +116,8 @@ function clearFields(form) { const BLANK_IMAGE_URL = 'data:image/svg+xml,'; async function generateSubmit(form) { - const prompt = document.querySelector("#prompt").value; - // Convert file data to base64 + // TODO: Should probably uplaod files with formdata or something, and store them in the backend? let formData = Object.fromEntries(new FormData(form)); formData.initimg_name = formData.initimg.name formData.initimg = formData.initimg.name !== '' ? await toBase64(formData.initimg) : null; @@ -89,90 +125,75 @@ async function generateSubmit(form) { let strength = formData.strength; let totalSteps = formData.initimg ? Math.floor(strength * formData.steps) : formData.steps; - let progressSectionEle = document.querySelector('#progress-section'); - progressSectionEle.style.display = 'initial'; - let progressEle = document.querySelector('#progress-bar'); - progressEle.setAttribute('max', totalSteps); - let progressImageEle = document.querySelector('#progress-image'); - progressImageEle.src = BLANK_IMAGE_URL; + // Initialize the progress bar + initProgress(totalSteps); - progressImageEle.style.display = {}.hasOwnProperty.call(formData, 'progress_images') ? 'initial': 'none'; - - // Post as JSON, using Fetch streaming to get results + // POST, use response to listen for events fetch(form.action, { method: form.method, + headers: new Headers({'content-type': 'application/json'}), body: JSON.stringify(formData), - }).then(async (response) => { - const reader = response.body.getReader(); - - let noOutputs = true; - while (true) { - let {value, done} = await reader.read(); - value = new TextDecoder().decode(value); - if (done) { - progressSectionEle.style.display = 'none'; - break; - } - - for (let event of value.split('\n').filter(e => e !== '')) { - const data = JSON.parse(event); - - if (data.event === 'result') { - noOutputs = false; - appendOutput(data.url, data.seed, data.config); - progressEle.setAttribute('value', 0); - progressEle.setAttribute('max', totalSteps); - } else if (data.event === 'upscaling-started') { - document.getElementById("processing_cnt").textContent=data.processed_file_cnt; - document.getElementById("scaling-inprocess-message").style.display = "block"; - } else if (data.event === 'upscaling-done') { - document.getElementById("scaling-inprocess-message").style.display = "none"; - } else if (data.event === 'step') { - progressEle.setAttribute('value', data.step); - if (data.url) { - progressImageEle.src = data.url; - } - } else if (data.event === 'canceled') { - // avoid alerting as if this were an error case - noOutputs = false; - } - } - } - - // Re-enable form, remove no-results-message - form.querySelector('fieldset').removeAttribute('disabled'); - document.querySelector("#prompt").value = prompt; - document.querySelector('progress').setAttribute('value', '0'); - - if (noOutputs) { - alert("Error occurred while generating."); - } + }) + .then(response => response.json()) + .then(data => { + var dreamId = data.dreamId; + socket.emit('join_room', { 'room': dreamId }); }); - // Disable form while generating form.querySelector('fieldset').setAttribute('disabled',''); - document.querySelector("#prompt").value = `Generating: "${prompt}"`; } -async function fetchRunLog() { - try { - let response = await fetch('/run_log.json') - const data = await response.json(); - for(let item of data.run_log) { - appendOutput(item.url, item.seed, item); - } - } catch (e) { - console.error(e); - } -} +// Socket listeners +socket.on('job_started', (data) => {}) -window.onload = async () => { - document.querySelector("#prompt").addEventListener("keydown", (e) => { - if (e.key === "Enter" && !e.shiftKey) { - const form = e.target.form; - generateSubmit(form); - } - }); +socket.on('dream_result', (data) => { + var jobId = data.jobId; + var dreamId = data.dreamId; + var dreamRequest = data.dreamRequest; + var src = 'api/images/' + dreamId; + + appendOutput(src, dreamRequest.seed, dreamRequest); + + resetProgress(false); +}) + +socket.on('dream_progress', (data) => { + // TODO: it'd be nice if we could get a seed reported here, but the generator would need to be updated + var step = data.step; + var totalSteps = data.totalSteps; + var jobId = data.jobId; + var dreamId = data.dreamId; + + var progressType = data.progressType + if (progressType === 'GENERATION') { + var src = data.hasProgressImage ? + 'api/intermediates/' + dreamId + '/' + step + : null; + setProgress(step, totalSteps, src); + } else if (progressType === 'UPSCALING_STARTED') { + // step and totalSteps are used for upscale count on this message + document.getElementById("processing_cnt").textContent = step; + document.getElementById("processing_total").textContent = totalSteps; + document.getElementById("scaling-inprocess-message").style.display = "block"; + } else if (progressType == 'UPSCALING_DONE') { + document.getElementById("scaling-inprocess-message").style.display = "none"; + } +}) + +socket.on('job_canceled', (data) => { + resetForm(); + resetProgress(); +}) + +socket.on('job_done', (data) => { + jobId = data.jobId + socket.emit('leave_room', { 'room': jobId }); + + resetForm(); + resetProgress(); +}) + +window.onload = () => { document.querySelector("#generate-form").addEventListener('submit', (e) => { e.preventDefault(); const form = e.target; @@ -183,7 +204,7 @@ window.onload = async () => { saveFields(e.target.form); }); document.querySelector("#reset-seed").addEventListener('click', (e) => { - document.querySelector("#seed").value = -1; + document.querySelector("#seed").value = 0; saveFields(e.target.form); }); document.querySelector("#reset-all").addEventListener('click', (e) => { @@ -199,15 +220,8 @@ window.onload = async () => { console.error(e); }); }); - document.documentElement.addEventListener('keydown', (e) => { - if (e.key === "Escape") - fetch('/cancel').catch(err => { - console.error(err); - }); - }); if (!config.gfpgan_model_exists) { document.querySelector("#gfpgan").style.display = 'none'; } - await fetchRunLog() }; From 00d2d0e90eb45bdf87a9594eeab7be730fc3eb32 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 16 Sep 2022 14:52:25 -0400 Subject: [PATCH 13/19] Flask/React web server now merged, but needs fixes. * due to changes in the metadata written to PNG files, web server cannot display images * issue is identified and will be fixed in next 24h * Python 3.9 required for flask/react web server; environment must be updated. --- backend/server.py | 8 ++++---- environment.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/server.py b/backend/server.py index db53ae45e5..8dbbc65cb1 100644 --- a/backend/server.py +++ b/backend/server.py @@ -19,8 +19,7 @@ from uuid import uuid4 from ldm.gfpgan.gfpgan_tools import real_esrgan_upscale from ldm.gfpgan.gfpgan_tools import run_gfpgan from ldm.generate import Generate -from ldm.dream.pngwriter import PngWriter, PromptFormatter - +from ldm.dream.pngwriter import PngWriter from modules.parameters import parameters_to_command, create_cmd_parser @@ -29,10 +28,11 @@ USER CONFIG """ output_dir = "outputs/" # Base output directory for images -host = 'localhost' # Web & socket.io host +#host = 'localhost' # Web & socket.io host +host = '0.0.0.0' # Web & socket.io host port = 9090 # Web & socket.io port verbose = False # enables copious socket.io logging -additional_allowed_origins = ['http://localhost:5173'] # additional CORS allowed origins +additional_allowed_origins = ['http://localhost:9090'] # additional CORS allowed origins """ diff --git a/environment.yaml b/environment.yaml index b0e2eb8090..6ca403e204 100644 --- a/environment.yaml +++ b/environment.yaml @@ -3,7 +3,7 @@ channels: - pytorch - defaults dependencies: - - python=3.8.5 + - python>=3.9 - pip=20.3 - cudatoolkit=11.3 - pytorch=1.11.0 @@ -20,7 +20,7 @@ dependencies: - realesrgan==0.2.5.0 - test-tube>=0.7.5 - streamlit==1.12.0 - - pillow==9.2.0 + - pillow==6.2.0 - einops==0.3.0 - torch-fidelity==0.3.0 - transformers==4.19.2 From cbac95b02a578df632502f66496b985bcf7ac4ae Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 16 Sep 2022 16:35:34 -0400 Subject: [PATCH 14/19] Merge with PR #602 - New and improved web api - Author: @Kyle0654 --- .gitignore | 1 + docs/other/CONTRIBUTORS.md | 1 + ldm/dream/pngwriter.py | 5 +- ldm/dream/server.py | 2 +- ldm/generate.py | 2 +- requirements.txt | 5 + server/application.py | 69 ++++----- server/containers.py | 26 ++-- server/models.py | 229 ++++++++++++++++++++++------- server/services.py | 278 ++++++++++++++++++++++++++---------- server/views.py | 61 ++++++-- static/dream_web/index.css | 61 +++++--- static/dream_web/index.html | 274 ++++++++++++++++++++--------------- static/dream_web/index.js | 243 ++++++++++++++++++++++++++----- 14 files changed, 899 insertions(+), 358 deletions(-) diff --git a/.gitignore b/.gitignore index e8d7c1f189..7e4dd4bea9 100644 --- a/.gitignore +++ b/.gitignore @@ -191,3 +191,4 @@ checkpoints .scratch/ .vscode/ gfpgan/ +models/ldm/stable-diffusion-v1/model.sha256 diff --git a/docs/other/CONTRIBUTORS.md b/docs/other/CONTRIBUTORS.md index 78f33d93d1..948795d3f2 100644 --- a/docs/other/CONTRIBUTORS.md +++ b/docs/other/CONTRIBUTORS.md @@ -51,6 +51,7 @@ We thank them for all of their time and hard work. - [Any Winter](https://github.com/any-winter-4079) - [Doggettx](https://github.com/doggettx) - [Matthias Wild](https://github.com/mauwii) +- [Kyle Schouviller](https://github.com/kyle0654) ## __Original CompVis Authors:__ diff --git a/ldm/dream/pngwriter.py b/ldm/dream/pngwriter.py index a8d2425b91..6291059585 100644 --- a/ldm/dream/pngwriter.py +++ b/ldm/dream/pngwriter.py @@ -33,11 +33,12 @@ class PngWriter: # saves image named _image_ to outdir/name, writing metadata from prompt # returns full path of output - def save_image_and_prompt_to_png(self, image, dream_prompt, metadata, name): + def save_image_and_prompt_to_png(self, image, dream_prompt, name, metadata=None): path = os.path.join(self.outdir, name) info = PngImagePlugin.PngInfo() info.add_text('Dream', dream_prompt) - info.add_text('sd-metadata', json.dumps(metadata)) + if metadata: # TODO: merge command line app's method of writing metadata and always just write metadata + info.add_text('sd-metadata', json.dumps(metadata)) image.save(path, 'PNG', pnginfo=info) return path diff --git a/ldm/dream/server.py b/ldm/dream/server.py index 003ec70533..cde3957a1f 100644 --- a/ldm/dream/server.py +++ b/ldm/dream/server.py @@ -230,7 +230,7 @@ class DreamServer(BaseHTTPRequestHandler): image = self.model.sample_to_image(sample) name = f'{prefix}.{opt.seed}.{step_index}.png' metadata = f'{opt.prompt} -S{opt.seed} [intermediate]' - path = step_writer.save_image_and_prompt_to_png(image, metadata, name) + path = step_writer.save_image_and_prompt_to_png(image, dream_prompt=metadata, name=name) step_index += 1 self.wfile.write(bytes(json.dumps( {'event': 'step', 'step': step + 1, 'url': path} diff --git a/ldm/generate.py b/ldm/generate.py index 6b5cb3d794..1b3c8544e0 100644 --- a/ldm/generate.py +++ b/ldm/generate.py @@ -181,7 +181,7 @@ class Generate: for image, seed in results: name = f'{prefix}.{seed}.png' path = pngwriter.save_image_and_prompt_to_png( - image, f'{prompt} -S{seed}', name) + image, dream_prompt=f'{prompt} -S{seed}', name=name) outputs.append([path, seed]) return outputs diff --git a/requirements.txt b/requirements.txt index 2007ca4caf..efc55b7971 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,11 @@ test-tube torch-fidelity torchmetrics transformers +flask==2.1.3 +flask_socketio==5.3.0 +flask_cors==3.0.10 +dependency_injector==4.40.0 +eventlet git+https://github.com/openai/CLIP.git@main#egg=clip git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k-diffusion git+https://github.com/lstein/GFPGAN@fix-dark-cast-images#egg=gfpgan diff --git a/server/application.py b/server/application.py index 2e8d77ce0f..2501f4b63d 100644 --- a/server/application.py +++ b/server/application.py @@ -7,9 +7,10 @@ import os import sys from flask import Flask from flask_cors import CORS -from flask_socketio import SocketIO, join_room, leave_room +from flask_socketio import SocketIO from omegaconf import OmegaConf from dependency_injector.wiring import inject, Provide +from ldm.dream.args import Args from server import views from server.containers import Container from server.services import GeneratorService, SignalService @@ -58,6 +59,8 @@ def run_app(config, host, port) -> Flask: # TODO: Get storage root from config app.add_url_rule('/api/images/', view_func=views.ApiImages.as_view('api_images', '../')) + app.add_url_rule('/api/images//metadata', view_func=views.ApiImagesMetadata.as_view('api_images_metadata', '../')) + app.add_url_rule('/api/images', view_func=views.ApiImagesList.as_view('api_images_list')) app.add_url_rule('/api/intermediates//', view_func=views.ApiIntermediates.as_view('api_intermediates', '../')) app.static_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../static/dream_web/')) @@ -79,30 +82,28 @@ def run_app(config, host, port) -> Flask: def main(): """Initialize command-line parsers and the diffusion model""" - from scripts.dream import create_argv_parser - arg_parser = create_argv_parser() + arg_parser = Args() opt = arg_parser.parse_args() if opt.laion400m: - print('--laion400m flag has been deprecated. Please use --model laion400m instead.') - sys.exit(-1) - if opt.weights != 'model': - print('--weights argument has been deprecated. Please configure ./configs/models.yaml, and call it using --model instead.') - sys.exit(-1) + print('--laion400m flag has been deprecated. Please use --model laion400m instead.') + sys.exit(-1) + if opt.weights: + print('--weights argument has been deprecated. Please edit ./configs/models.yaml, and select the weights using --model instead.') + sys.exit(-1) - try: - models = OmegaConf.load(opt.config) - width = models[opt.model].width - height = models[opt.model].height - config = models[opt.model].config - weights = models[opt.model].weights - except (FileNotFoundError, IOError, KeyError) as e: - print(f'{e}. Aborting.') - sys.exit(-1) + # try: + # models = OmegaConf.load(opt.config) + # width = models[opt.model].width + # height = models[opt.model].height + # config = models[opt.model].config + # weights = models[opt.model].weights + # except (FileNotFoundError, IOError, KeyError) as e: + # print(f'{e}. Aborting.') + # sys.exit(-1) - print('* Initializing, be patient...\n') + #print('* Initializing, be patient...\n') sys.path.append('.') - from pytorch_lightning import logging # these two lines prevent a horrible warning message from appearing # when the frozen CLIP tokenizer is imported @@ -110,26 +111,28 @@ def main(): transformers.logging.set_verbosity_error() - appConfig = { - "model": { - "width": width, - "height": height, - "sampler_name": opt.sampler_name, - "weights": weights, - "full_precision": opt.full_precision, - "config": config, - "grid": opt.grid, - "latent_diffusion_weights": opt.laion400m, - "embedding_path": opt.embedding_path, - "device_type": opt.device - } - } + appConfig = opt.__dict__ + + # appConfig = { + # "model": { + # "width": width, + # "height": height, + # "sampler_name": opt.sampler_name, + # "weights": weights, + # "full_precision": opt.full_precision, + # "config": config, + # "grid": opt.grid, + # "latent_diffusion_weights": opt.laion400m, + # "embedding_path": opt.embedding_path + # } + # } # make sure the output directory exists if not os.path.exists(opt.outdir): os.makedirs(opt.outdir) # gets rid of annoying messages about random seed + from pytorch_lightning import logging logging.getLogger('pytorch_lightning').setLevel(logging.ERROR) print('\n* starting api server...') diff --git a/server/containers.py b/server/containers.py index 08ef01c4b6..a3318c5ff0 100644 --- a/server/containers.py +++ b/server/containers.py @@ -17,18 +17,24 @@ class Container(containers.DeclarativeContainer): app = None ) + # TODO: Add a model provider service that provides model(s) dynamically model_singleton = providers.ThreadSafeSingleton( Generate, - width = config.model.width, - height = config.model.height, - sampler_name = config.model.sampler_name, - weights = config.model.weights, - full_precision = config.model.full_precision, - config = config.model.config, - grid = config.model.grid, - seamless = config.model.seamless, - embedding_path = config.model.embedding_path, - device_type = config.model.device_type + model = config.model, + sampler_name = config.sampler_name, + embedding_path = config.embedding_path, + full_precision = config.full_precision + # config = config.model.config, + + # width = config.model.width, + # height = config.model.height, + # sampler_name = config.model.sampler_name, + # weights = config.model.weights, + # full_precision = config.model.full_precision, + # grid = config.model.grid, + # seamless = config.model.seamless, + # embedding_path = config.model.embedding_path, + # device_type = config.model.device_type ) # TODO: get location from config diff --git a/server/models.py b/server/models.py index 17c6d0dfe4..fc4a5f41c4 100644 --- a/server/models.py +++ b/server/models.py @@ -1,77 +1,182 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +from base64 import urlsafe_b64encode import json import string from copy import deepcopy from datetime import datetime, timezone from enum import Enum +from typing import Any, Dict, List, Union +from uuid import uuid4 -class DreamRequest(): - prompt: string - initimg: string - strength: float - iterations: int - steps: int - width: int - height: int - fit = None - cfgscale: float - sampler_name: string - gfpgan_strength: float - upscale_level: int - upscale_strength: float + +class DreamBase(): + # Id + id: str + + # Initial Image + enable_init_image: bool + initimg: string = None + + # Img2Img + enable_img2img: bool # TODO: support this better + strength: float = 0 # TODO: name this something related to img2img to make it clearer? + fit = None # Fit initial image dimensions + + # Generation + enable_generate: bool + prompt: string = "" + seed: int = 0 # 0 is random + steps: int = 10 + width: int = 512 + height: int = 512 + cfg_scale: float = 7.5 + sampler_name: string = 'klms' + seamless: bool = False + model: str = None # The model to use (currently unused) + embeddings = None # The embeddings to use (currently unused) + progress_images: bool = False + + # GFPGAN + enable_gfpgan: bool + gfpgan_strength: float = 0 + + # Upscale + enable_upscale: bool upscale: None - progress_images = None - seed: int + upscale_level: int = None + upscale_strength: float = 0.75 + + # Embiggen + enable_embiggen: bool + embiggen: Union[None, List[float]] = None + embiggen_tiles: Union[None, List[int]] = None + + # Metadata time: int + def __init__(self): + self.id = urlsafe_b64encode(uuid4().bytes).decode('ascii') + + def parse_json(self, j, new_instance=False): + # Id + if 'id' in j and not new_instance: + self.id = j.get('id') + + # Initial Image + self.enable_init_image = 'enable_init_image' in j and bool(j.get('enable_init_image')) + if self.enable_init_image: + self.initimg = j.get('initimg') + + # Img2Img + self.enable_img2img = 'enable_img2img' in j and bool(j.get('enable_img2img')) + if self.enable_img2img: + self.strength = float(j.get('strength')) + self.fit = 'fit' in j + + # Generation + self.enable_generate = 'enable_generate' in j and bool(j.get('enable_generate')) + if self.enable_generate: + self.prompt = j.get('prompt') + self.seed = int(j.get('seed')) + self.steps = int(j.get('steps')) + self.width = int(j.get('width')) + self.height = int(j.get('height')) + self.cfg_scale = float(j.get('cfgscale') or j.get('cfg_scale')) + self.sampler_name = j.get('sampler') or j.get('sampler_name') + # model: str = None # The model to use (currently unused) + # embeddings = None # The embeddings to use (currently unused) + self.seamless = 'seamless' in j + self.progress_images = 'progress_images' in j + + # GFPGAN + self.enable_gfpgan = 'enable_gfpgan' in j and bool(j.get('enable_gfpgan')) + if self.enable_gfpgan: + self.gfpgan_strength = float(j.get('gfpgan_strength')) + + # Upscale + self.enable_upscale = 'enable_upscale' in j and bool(j.get('enable_upscale')) + if self.enable_upscale: + self.upscale_level = j.get('upscale_level') + self.upscale_strength = j.get('upscale_strength') + self.upscale = None if self.upscale_level in {None,''} else [int(self.upscale_level),float(self.upscale_strength)] + + # Embiggen + self.enable_embiggen = 'enable_embiggen' in j and bool(j.get('enable_embiggen')) + if self.enable_embiggen: + self.embiggen = j.get('embiggen') + self.embiggen_tiles = j.get('embiggen_tiles') + + # Metadata + self.time = int(j.get('time')) if ('time' in j and not new_instance) else int(datetime.now(timezone.utc).timestamp()) + + +class DreamResult(DreamBase): + # Result + has_upscaled: False + has_gfpgan: False + # TODO: use something else for state tracking images_generated: int = 0 images_upscaled: int = 0 - def id(self, seed = None, upscaled = False) -> str: - return f"{self.time}.{seed or self.seed}{'.u' if upscaled else ''}" + def __init__(self): + super().__init__() - # TODO: handle this more cleanly (probably by splitting this into a Job and Result class) - # TODO: Set iterations to 1 or remove it from the dream result? And just keep it on the job? - def clone_without_image(self, seed = None): - data = deepcopy(self) - data.initimg = None - if seed: - data.seed = seed + def clone_without_img(self): + copy = deepcopy(self) + copy.initimg = None + return copy - return data - - def to_json(self, seed: int = None): - copy = self.clone_without_image(seed) - return json.dumps(copy.__dict__) + def to_json(self): + copy = deepcopy(self) + copy.initimg = None + j = json.dumps(copy.__dict__) + return j @staticmethod def from_json(j, newTime: bool = False): - d = DreamRequest() - d.prompt = j.get('prompt') - d.initimg = j.get('initimg') - d.strength = float(j.get('strength')) - d.iterations = int(j.get('iterations')) - d.steps = int(j.get('steps')) - d.width = int(j.get('width')) - d.height = int(j.get('height')) - d.fit = 'fit' in j - d.seamless = 'seamless' in j - d.cfgscale = float(j.get('cfgscale')) - d.sampler_name = j.get('sampler') - d.variation_amount = float(j.get('variation_amount')) - d.with_variations = j.get('with_variations') - d.gfpgan_strength = float(j.get('gfpgan_strength')) - d.upscale_level = j.get('upscale_level') - d.upscale_strength = j.get('upscale_strength') - d.upscale = [int(d.upscale_level),float(d.upscale_strength)] if d.upscale_level != '' else None - d.progress_images = 'progress_images' in j - d.seed = int(j.get('seed')) - d.time = int(datetime.now(timezone.utc).timestamp()) if newTime else int(j.get('time')) + d = DreamResult() + d.parse_json(j) return d +# TODO: switch this to a pipelined request, with pluggable steps +# Will likely require generator code changes to accomplish +class JobRequest(DreamBase): + # Iteration + iterations: int = 1 + variation_amount = None + with_variations = None + + # Results + results: List[DreamResult] = [] + + def __init__(self): + super().__init__() + + def newDreamResult(self) -> DreamResult: + result = DreamResult() + result.parse_json(self.__dict__, new_instance=True) + return result + + @staticmethod + def from_json(j): + job = JobRequest() + job.parse_json(j) + + # Metadata + job.time = int(j.get('time')) if ('time' in j) else int(datetime.now(timezone.utc).timestamp()) + + # Iteration + if job.enable_generate: + job.iterations = int(j.get('iterations')) + job.variation_amount = float(j.get('variation_amount')) + job.with_variations = j.get('with_variations') + + return job + + class ProgressType(Enum): GENERATION = 1 UPSCALING_STARTED = 2 @@ -102,11 +207,11 @@ class Signal(): # TODO: use a result id or something? Like a sub-job @staticmethod - def image_result(jobId: str, dreamId: str, dreamRequest: DreamRequest): + def image_result(jobId: str, dreamId: str, dreamResult: DreamResult): return Signal('dream_result', { 'jobId': jobId, 'dreamId': dreamId, - 'dreamRequest': dreamRequest.__dict__ + 'dreamRequest': dreamResult.clone_without_img().__dict__ }, room=jobId, broadcast=True) @staticmethod @@ -126,3 +231,21 @@ class Signal(): return Signal('job_canceled', { 'jobId': jobId }, room=jobId, broadcast=True) + + +class PaginatedItems(): + items: List[Any] + page: int # Current Page + pages: int # Total number of pages + per_page: int # Number of items per page + total: int # Total number of items in result + + def __init__(self, items: List[Any], page: int, pages: int, per_page: int, total: int): + self.items = items + self.page = page + self.pages = pages + self.per_page = per_page + self.total = total + + def to_json(self): + return json.dumps(self.__dict__) diff --git a/server/services.py b/server/services.py index 0b53cc9141..444f47cccf 100644 --- a/server/services.py +++ b/server/services.py @@ -1,25 +1,33 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) +from argparse import ArgumentParser import base64 +from datetime import datetime, timezone +import glob +import json import os +from pathlib import Path from queue import Empty, Queue +import shlex from threading import Thread import time -from flask import app, url_for from flask_socketio import SocketIO, join_room, leave_room +from ldm.dream.args import Args +from ldm.dream.generator import embiggen +from PIL import Image from ldm.dream.pngwriter import PngWriter from ldm.dream.server import CanceledException from ldm.generate import Generate -from server.models import DreamRequest, ProgressType, Signal +from server.models import DreamResult, JobRequest, PaginatedItems, ProgressType, Signal class JobQueueService: __queue: Queue = Queue() - def push(self, dreamRequest: DreamRequest): + def push(self, dreamRequest: DreamResult): self.__queue.put(dreamRequest) - def get(self, timeout: float = None) -> DreamRequest: + def get(self, timeout: float = None) -> DreamResult: return self.__queue.get(timeout= timeout) class SignalQueueService: @@ -85,31 +93,116 @@ class LogService: self.__location = location self.__logFile = file - def log(self, dreamRequest: DreamRequest, seed = None, upscaled = False): + def log(self, dreamResult: DreamResult, seed = None, upscaled = False): with open(os.path.join(self.__location, self.__logFile), "a") as log: - log.write(f"{dreamRequest.id(seed, upscaled)}: {dreamRequest.to_json(seed)}\n") + log.write(f"{dreamResult.id}: {dreamResult.to_json()}\n") class ImageStorageService: __location: str __pngWriter: PngWriter + __legacyParser: ArgumentParser def __init__(self, location): self.__location = location self.__pngWriter = PngWriter(self.__location) + self.__legacyParser = Args() # TODO: inject this? def __getName(self, dreamId: str, postfix: str = '') -> str: return f'{dreamId}{postfix}.png' - def save(self, image, dreamRequest, seed = None, upscaled = False, postfix: str = '', metadataPostfix: str = '') -> str: - name = self.__getName(dreamRequest.id(seed, upscaled), postfix) - path = self.__pngWriter.save_image_and_prompt_to_png(image, f'{dreamRequest.prompt} -S{seed or dreamRequest.seed}{metadataPostfix}', name) + def save(self, image, dreamResult: DreamResult, postfix: str = '') -> str: + name = self.__getName(dreamResult.id, postfix) + meta = dreamResult.to_json() # TODO: make all methods consistent with writing metadata. Standardize metadata. + path = self.__pngWriter.save_image_and_prompt_to_png(image, dream_prompt=meta, metadata=None, name=name) return path def path(self, dreamId: str, postfix: str = '') -> str: name = self.__getName(dreamId, postfix) path = os.path.join(self.__location, name) return path + + # Returns true if found, false if not found or error + def delete(self, dreamId: str, postfix: str = '') -> bool: + path = self.path(dreamId, postfix) + if (os.path.exists(path)): + os.remove(path) + return True + else: + return False + + def getMetadata(self, dreamId: str, postfix: str = '') -> DreamResult: + path = self.path(dreamId, postfix) + image = Image.open(path) + text = image.text + if text.__contains__('Dream'): + dreamMeta = text.get('Dream') + try: + j = json.loads(dreamMeta) + return DreamResult.from_json(j) + except ValueError: + # Try to parse command-line format (legacy metadata format) + try: + opt = self.__parseLegacyMetadata(dreamMeta) + optd = opt.__dict__ + if (not 'width' in optd) or (optd.get('width') is None): + optd['width'] = image.width + if (not 'height' in optd) or (optd.get('height') is None): + optd['height'] = image.height + if (not 'steps' in optd) or (optd.get('steps') is None): + optd['steps'] = 10 # No way around this unfortunately - seems like it wasn't storing this previously + + optd['time'] = os.path.getmtime(path) # Set timestamp manually (won't be exactly correct though) + + return DreamResult.from_json(optd) + + except: + return None + else: + return None + + def __parseLegacyMetadata(self, command: str) -> DreamResult: + # before splitting, escape single quotes so as not to mess + # up the parser + command = command.replace("'", "\\'") + + try: + elements = shlex.split(command) + except ValueError as e: + return None + + # rearrange the arguments to mimic how it works in the Dream bot. + switches = [''] + switches_started = False + + for el in elements: + if el[0] == '-' and not switches_started: + switches_started = True + if switches_started: + switches.append(el) + else: + switches[0] += el + switches[0] += ' ' + switches[0] = switches[0][: len(switches[0]) - 1] + + try: + opt = self.__legacyParser.parse_cmd(switches) + return opt + except SystemExit: + return None + + def list_files(self, page: int, perPage: int) -> PaginatedItems: + files = sorted(glob.glob(os.path.join(self.__location,'*.png')), key=os.path.getmtime, reverse=True) + count = len(files) + + startId = page * perPage + pageCount = int(count / perPage) + 1 + endId = min(startId + perPage, count) + items = [] if startId >= count else files[startId:endId] + + items = list(map(lambda f: Path(f).stem, items)) + + return PaginatedItems(items, page, pageCount, perPage, count) class GeneratorService: @@ -144,13 +237,11 @@ class GeneratorService: # TODO: Consider moving this to its own service if there's benefit in separating the generator def __process(self): # preload the model + # TODO: support multiple models print('Preloading model') - tic = time.time() self.__model.load_model() - print( - f'>> model loaded in', '%4.2fs' % (time.time() - tic) - ) + print(f'>> model loaded in', '%4.2fs' % (time.time() - tic)) print('Started generation queue processor') try: @@ -162,103 +253,136 @@ class GeneratorService: print('Generation queue processor stopped') - def __start(self, dreamRequest: DreamRequest): - if dreamRequest.start_callback: - dreamRequest.start_callback() - self.__signal_service.emit(Signal.job_started(dreamRequest.id())) + def __on_start(self, jobRequest: JobRequest): + self.__signal_service.emit(Signal.job_started(jobRequest.id)) - def __done(self, dreamRequest: DreamRequest, image, seed, upscaled=False): - self.__imageStorage.save(image, dreamRequest, seed, upscaled) + def __on_image_result(self, jobRequest: JobRequest, image, seed, upscaled=False): + dreamResult = jobRequest.newDreamResult() + dreamResult.seed = seed + dreamResult.has_upscaled = upscaled + dreamResult.iterations = 1 + jobRequest.results.append(dreamResult) + # TODO: Separate status of GFPGAN? + + self.__imageStorage.save(image, dreamResult) - # TODO: handle upscaling logic better (this is appending data to log, but only on first generation) if not upscaled: - self.__log.log(dreamRequest, seed, upscaled) + self.__log.log(dreamResult) - self.__signal_service.emit(Signal.image_result(dreamRequest.id(), dreamRequest.id(seed, upscaled), dreamRequest.clone_without_image(seed))) + # Send result signal + self.__signal_service.emit(Signal.image_result(jobRequest.id, dreamResult.id, dreamResult)) - upscaling_requested = dreamRequest.upscale or dreamRequest.gfpgan_strength>0 + upscaling_requested = dreamResult.enable_upscale or dreamResult.enable_gfpgan - if upscaled: - dreamRequest.images_upscaled += 1 - else: - dreamRequest.images_generated +=1 - if upscaling_requested: - # action = None - if dreamRequest.images_generated >= dreamRequest.iterations: - progressType = ProgressType.UPSCALING_DONE - if dreamRequest.images_upscaled < dreamRequest.iterations: - progressType = ProgressType.UPSCALING_STARTED - self.__signal_service.emit(Signal.image_progress(dreamRequest.id(), dreamRequest.id(seed), dreamRequest.images_upscaled+1, dreamRequest.iterations, progressType)) + # Report upscaling status + # TODO: this is very coupled to logic inside the generator. Fix that. + if upscaling_requested and any(result.has_upscaled for result in jobRequest.results): + progressType = ProgressType.UPSCALING_STARTED if len(jobRequest.results) < 2 * jobRequest.iterations else ProgressType.UPSCALING_DONE + upscale_count = sum(1 for i in jobRequest.results if i.has_upscaled) + self.__signal_service.emit(Signal.image_progress(jobRequest.id, dreamResult.id, upscale_count, jobRequest.iterations, progressType)) - def __progress(self, dreamRequest, sample, step): + def __on_progress(self, jobRequest: JobRequest, sample, step): if self.__cancellationRequested: self.__cancellationRequested = False raise CanceledException + # TODO: Progress per request will be easier once the seeds (and ids) can all be pre-generated hasProgressImage = False - if dreamRequest.progress_images and step % 5 == 0 and step < dreamRequest.steps - 1: + s = str(len(jobRequest.results)) + if jobRequest.progress_images and step % 5 == 0 and step < jobRequest.steps - 1: image = self.__model._sample_to_image(sample) - self.__intermediateStorage.save(image, dreamRequest, self.__model.seed, postfix=f'.{step}', metadataPostfix=f' [intermediate]') + + # TODO: clean this up, use a pre-defined dream result + result = DreamResult() + result.parse_json(jobRequest.__dict__, new_instance=False) + self.__intermediateStorage.save(image, result, postfix=f'.{s}.{step}') hasProgressImage = True - self.__signal_service.emit(Signal.image_progress(dreamRequest.id(), dreamRequest.id(self.__model.seed), step, dreamRequest.steps, ProgressType.GENERATION, hasProgressImage)) + self.__signal_service.emit(Signal.image_progress(jobRequest.id, f'{jobRequest.id}.{s}', step, jobRequest.steps, ProgressType.GENERATION, hasProgressImage)) - def __generate(self, dreamRequest: DreamRequest): + def __generate(self, jobRequest: JobRequest): try: - initimgfile = None - if dreamRequest.initimg is not None: - with open("./img2img-tmp.png", "wb") as f: - initimg = dreamRequest.initimg.split(",")[1] # Ignore mime type - f.write(base64.b64decode(initimg)) - initimgfile = "./img2img-tmp.png" + # TODO: handle this file a file service for init images + initimgfile = None # TODO: support this on the model directly? + if (jobRequest.enable_init_image): + if jobRequest.initimg is not None: + with open("./img2img-tmp.png", "wb") as f: + initimg = jobRequest.initimg.split(",")[1] # Ignore mime type + f.write(base64.b64decode(initimg)) + initimgfile = "./img2img-tmp.png" - # Get a random seed if we don't have one yet - # TODO: handle "previous" seed usage? - if dreamRequest.seed == -1: - dreamRequest.seed = self.__model.seed + # Use previous seed if set to -1 + initSeed = jobRequest.seed + if initSeed == -1: + initSeed = self.__model.seed # Zero gfpgan strength if the model doesn't exist # TODO: determine if this could be at the top now? Used to cause circular import from ldm.gfpgan.gfpgan_tools import gfpgan_model_exists if not gfpgan_model_exists: - dreamRequest.gfpgan_strength = 0 + jobRequest.enable_gfpgan = False - self.__start(dreamRequest) + # Signal start + self.__on_start(jobRequest) - self.__model.prompt2image( - prompt = dreamRequest.prompt, - init_img = initimgfile, # TODO: ensure this works - strength = None if initimgfile is None else dreamRequest.strength, - fit = None if initimgfile is None else dreamRequest.fit, - iterations = dreamRequest.iterations, - cfg_scale = dreamRequest.cfgscale, - width = dreamRequest.width, - height = dreamRequest.height, - seed = dreamRequest.seed, - steps = dreamRequest.steps, - variation_amount = dreamRequest.variation_amount, - with_variations = dreamRequest.with_variations, - gfpgan_strength = dreamRequest.gfpgan_strength, - upscale = dreamRequest.upscale, - sampler_name = dreamRequest.sampler_name, - seamless = dreamRequest.seamless, - step_callback = lambda sample, step: self.__progress(dreamRequest, sample, step), - image_callback = lambda image, seed, upscaled=False: self.__done(dreamRequest, image, seed, upscaled)) + # Generate in model + # TODO: Split job generation requests instead of fitting all parameters here + # TODO: Support no generation (just upscaling/gfpgan) + + upscale = None if not jobRequest.enable_upscale else jobRequest.upscale + gfpgan_strength = 0 if not jobRequest.enable_gfpgan else jobRequest.gfpgan_strength + + if not jobRequest.enable_generate: + # If not generating, check if we're upscaling or running gfpgan + if not upscale and not gfpgan_strength: + # Invalid settings (TODO: Add message to help user) + raise CanceledException() + + image = Image.open(initimgfile) + # TODO: support progress for upscale? + self.__model.upscale_and_reconstruct( + image_list = [[image,0]], + upscale = upscale, + strength = gfpgan_strength, + save_original = False, + image_callback = lambda image, seed, upscaled=False: self.__on_image_result(jobRequest, image, seed, upscaled)) + + else: + # Generating - run the generation + init_img = None if (not jobRequest.enable_img2img or jobRequest.strength == 0) else initimgfile + + + self.__model.prompt2image( + prompt = jobRequest.prompt, + init_img = init_img, # TODO: ensure this works + strength = None if init_img is None else jobRequest.strength, + fit = None if init_img is None else jobRequest.fit, + iterations = jobRequest.iterations, + cfg_scale = jobRequest.cfg_scale, + width = jobRequest.width, + height = jobRequest.height, + seed = jobRequest.seed, + steps = jobRequest.steps, + variation_amount = jobRequest.variation_amount, + with_variations = jobRequest.with_variations, + gfpgan_strength = gfpgan_strength, + upscale = upscale, + sampler_name = jobRequest.sampler_name, + seamless = jobRequest.seamless, + embiggen = jobRequest.embiggen, + embiggen_tiles = jobRequest.embiggen_tiles, + step_callback = lambda sample, step: self.__on_progress(jobRequest, sample, step), + image_callback = lambda image, seed, upscaled=False: self.__on_image_result(jobRequest, image, seed, upscaled)) except CanceledException: - if dreamRequest.cancelled_callback: - dreamRequest.cancelled_callback() - - self.__signal_service.emit(Signal.job_canceled(dreamRequest.id())) + self.__signal_service.emit(Signal.job_canceled(jobRequest.id)) finally: - if dreamRequest.done_callback: - dreamRequest.done_callback() - self.__signal_service.emit(Signal.job_done(dreamRequest.id())) + self.__signal_service.emit(Signal.job_done(jobRequest.id)) # Remove the temp file if (initimgfile is not None): diff --git a/server/views.py b/server/views.py index 590adc6532..db4857d14f 100644 --- a/server/views.py +++ b/server/views.py @@ -8,7 +8,7 @@ from flask import current_app, jsonify, request, Response, send_from_directory, from flask.views import MethodView from dependency_injector.wiring import inject, Provide -from server.models import DreamRequest +from server.models import DreamResult, JobRequest from server.services import GeneratorService, ImageStorageService, JobQueueService from server.containers import Container @@ -16,23 +16,14 @@ class ApiJobs(MethodView): @inject def post(self, job_queue_service: JobQueueService = Provide[Container.generation_queue_service]): - dreamRequest = DreamRequest.from_json(request.json, newTime = True) + jobRequest = JobRequest.from_json(request.json) - #self.canceled.clear() - print(f">> Request to generate with prompt: {dreamRequest.prompt}") - - q = Queue() - - dreamRequest.start_callback = None - dreamRequest.image_callback = None - dreamRequest.progress_callback = None - dreamRequest.cancelled_callback = None - dreamRequest.done_callback = None + print(f">> Request to generate with prompt: {jobRequest.prompt}") # Push the request - job_queue_service.push(dreamRequest) + job_queue_service.push(jobRequest) - return { 'dreamId': dreamRequest.id() } + return { 'jobId': jobRequest.id } class WebIndex(MethodView): @@ -68,6 +59,7 @@ class ApiCancel(MethodView): return Response(status=204) +# TODO: Combine all image storage access class ApiImages(MethodView): init_every_request = False __pathRoot = None @@ -82,6 +74,27 @@ class ApiImages(MethodView): name = self.__storage.path(dreamId) fullpath=os.path.join(self.__pathRoot, name) return send_from_directory(os.path.dirname(fullpath), os.path.basename(fullpath)) + + def delete(self, dreamId): + result = self.__storage.delete(dreamId) + return Response(status=204) if result else Response(status=404) + + +class ApiImagesMetadata(MethodView): + init_every_request = False + __pathRoot = None + __storage: ImageStorageService + + @inject + def __init__(self, pathBase, storage: ImageStorageService = Provide[Container.image_storage_service]): + self.__pathRoot = os.path.abspath(os.path.join(os.path.dirname(__file__), pathBase)) + self.__storage = storage + + def get(self, dreamId): + meta = self.__storage.getMetadata(dreamId) + j = {} if meta is None else meta.__dict__ + return j + class ApiIntermediates(MethodView): init_every_request = False @@ -97,3 +110,23 @@ class ApiIntermediates(MethodView): name = self.__storage.path(dreamId, postfix=f'.{step}') fullpath=os.path.join(self.__pathRoot, name) return send_from_directory(os.path.dirname(fullpath), os.path.basename(fullpath)) + + def delete(self, dreamId): + result = self.__storage.delete(dreamId) + return Response(status=204) if result else Response(status=404) + + +class ApiImagesList(MethodView): + init_every_request = False + __storage: ImageStorageService + + @inject + def __init__(self, storage: ImageStorageService = Provide[Container.image_storage_service]): + self.__storage = storage + + def get(self): + page = request.args.get("page", default=0, type=int) + perPage = request.args.get("per_page", default=10, type=int) + + result = self.__storage.list_files(page, perPage) + return result.__dict__ diff --git a/static/dream_web/index.css b/static/dream_web/index.css index 51f0f267c3..25a0994a3d 100644 --- a/static/dream_web/index.css +++ b/static/dream_web/index.css @@ -1,3 +1,8 @@ +:root { + --fields-dark:#DCDCDC; + --fields-light:#F5F5F5; +} + * { font-family: 'Arial'; font-size: 100%; @@ -18,15 +23,26 @@ fieldset { border: none; line-height: 2.2em; } +fieldset > legend { + width: auto; + margin-left: 0; + margin-right: auto; + font-weight:bold; +} select, input { margin-right: 10px; padding: 2px; } +input:disabled { + cursor:auto; +} input[type=submit] { + cursor: pointer; background-color: #666; color: white; } input[type=checkbox] { + cursor: pointer; margin-right: 0px; width: 20px; height: 20px; @@ -87,11 +103,11 @@ header h1 { } #results img { border-radius: 5px; - object-fit: cover; + object-fit: contain; + background-color: var(--fields-dark); } #fieldset-config { line-height:2em; - background-color: #F0F0F0; } input[type="number"] { width: 60px; @@ -118,35 +134,46 @@ label { #progress-image { width: 30vh; height: 30vh; + object-fit: contain; + background-color: var(--fields-dark); } #cancel-button { cursor: pointer; color: red; } -#basic-parameters { - background-color: #EEEEEE; -} #txt2img { - background-color: #DCDCDC; + background-color: var(--fields-dark); } #variations { - background-color: #EEEEEE; + background-color: var(--fields-light); +} +#initimg { + background-color: var(--fields-dark); } #img2img { - background-color: #DCDCDC; + background-color: var(--fields-light); } -#gfpgan { - background-color: #EEEEEE; +#initimg > :not(legend) { + background-color: var(--fields-light); + margin: .5em; +} + +#postprocess, #initimg { + display:flex; + flex-wrap:wrap; + padding: 0; + margin-top: 1em; + background-color: var(--fields-dark); +} +#postprocess > fieldset, #initimg > * { + flex-grow: 1; +} +#postprocess > fieldset { + background-color: var(--fields-dark); } #progress-section { - background-color: #F5F5F5; -} -.section-header { - text-align: left; - font-weight: bold; - padding: 0 0 0 0; + background-color: var(--fields-light); } #no-results-message:not(:only-child) { display: none; } - diff --git a/static/dream_web/index.html b/static/dream_web/index.html index b8c80fe838..9dbd213669 100644 --- a/static/dream_web/index.html +++ b/static/dream_web/index.html @@ -1,104 +1,152 @@ - - Stable Diffusion Dream Server - - - - - - - - - -
-

Stable Diffusion Dream Server

-
- For news and support for this web service, visit our GitHub site + + Stable Diffusion Dream Server + + + + + + + + + + + +
+

Stable Diffusion Dream Server

+
+ For news and support for this web service, visit our GitHub + site +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + + + +
+
+
+ + + + +
-
- - - - -
+
+
+ + + + + + + + +
+
+
-
Post-processing options
- - - + + + + + + +
+
+ + + + +
- -
-
-
- - -
- -
- Postprocessing...1/3 -
- -
- -
-
-

No results...

+
+ + +
+
+
+ + +
+ +
+ Postprocessing...1/3
- - +
+ +
+
+ + + diff --git a/static/dream_web/index.js b/static/dream_web/index.js index 3af0308fb5..5de690297d 100644 --- a/static/dream_web/index.js +++ b/static/dream_web/index.js @@ -1,5 +1,73 @@ const socket = io(); +var priorResultsLoadState = { + page: 0, + pages: 1, + per_page: 10, + total: 20, + offset: 0, // number of items generated since last load + loading: false, + initialized: false +}; + +function loadPriorResults() { + // Fix next page by offset + let offsetPages = priorResultsLoadState.offset / priorResultsLoadState.per_page; + priorResultsLoadState.page += offsetPages; + priorResultsLoadState.pages += offsetPages; + priorResultsLoadState.total += priorResultsLoadState.offset; + priorResultsLoadState.offset = 0; + + if (priorResultsLoadState.loading) { + return; + } + + if (priorResultsLoadState.page >= priorResultsLoadState.pages) { + return; // Nothing more to load + } + + // Load + priorResultsLoadState.loading = true + let url = new URL('/api/images', document.baseURI); + url.searchParams.append('page', priorResultsLoadState.initialized ? priorResultsLoadState.page + 1 : priorResultsLoadState.page); + url.searchParams.append('per_page', priorResultsLoadState.per_page); + fetch(url.href, { + method: 'GET', + headers: new Headers({'content-type': 'application/json'}) + }) + .then(response => response.json()) + .then(data => { + priorResultsLoadState.page = data.page; + priorResultsLoadState.pages = data.pages; + priorResultsLoadState.per_page = data.per_page; + priorResultsLoadState.total = data.total; + + data.items.forEach(function(dreamId, index) { + let src = 'api/images/' + dreamId; + fetch('/api/images/' + dreamId + '/metadata', { + method: 'GET', + headers: new Headers({'content-type': 'application/json'}) + }) + .then(response => response.json()) + .then(metadata => { + let seed = metadata.seed || 0; // TODO: Parse old metadata + appendOutput(src, seed, metadata, true); + }); + }); + + // Load until page is full + if (!priorResultsLoadState.initialized) { + if (document.body.scrollHeight <= window.innerHeight) { + loadPriorResults(); + } + } + }) + .finally(() => { + priorResultsLoadState.loading = false; + priorResultsLoadState.initialized = true; + }); +} + function resetForm() { var form = document.getElementById('generate-form'); form.querySelector('fieldset').removeAttribute('disabled'); @@ -45,48 +113,64 @@ function toBase64(file) { }); } -function appendOutput(src, seed, config) { +function ondragdream(event) { + let dream = event.target.dataset.dream; + event.dataTransfer.setData("dream", dream); +} + +function seedClick(event) { + // Get element + var image = event.target.closest('figure').querySelector('img'); + var dream = JSON.parse(decodeURIComponent(image.dataset.dream)); + + let form = document.querySelector("#generate-form"); + for (const [k, v] of new FormData(form)) { + if (k == 'initimg') { continue; } + let formElem = form.querySelector(`*[name=${k}]`); + formElem.value = dream[k] !== undefined ? dream[k] : formElem.defaultValue; + } + + document.querySelector("#seed").value = dream.seed; + document.querySelector('#iterations').value = 1; // Reset to 1 iteration since we clicked a single image (not a full job) + + // NOTE: leaving this manual for the user for now - it was very confusing with this behavior + // document.querySelector("#with_variations").value = variations || ''; + // if (document.querySelector("#variation_amount").value <= 0) { + // document.querySelector("#variation_amount").value = 0.2; + // } + + saveFields(document.querySelector("#generate-form")); +} + +function appendOutput(src, seed, config, toEnd=false) { let outputNode = document.createElement("figure"); let altText = seed.toString() + " | " + config.prompt; + // img needs width and height for lazy loading to work + // TODO: store the full config in a data attribute on the image? const figureContents = ` - ${altText} + ${altText} -
${seed}
+
${seed}
`; outputNode.innerHTML = figureContents; - let figcaption = outputNode.querySelector('figcaption') - // Reload image config - figcaption.addEventListener('click', () => { - let form = document.querySelector("#generate-form"); - for (const [k, v] of new FormData(form)) { - if (k == 'initimg') { continue; } - form.querySelector(`*[name=${k}]`).value = config[k]; - } - if (config.variation_amount > 0 || config.with_variations != '') { - document.querySelector("#seed").value = config.seed; - } else { - document.querySelector("#seed").value = seed; - } - - if (config.variation_amount > 0) { - let oldVarAmt = document.querySelector("#variation_amount").value - let oldVariations = document.querySelector("#with_variations").value - let varSep = '' - document.querySelector("#variation_amount").value = 0; - if (document.querySelector("#with_variations").value != '') { - varSep = "," - } - document.querySelector("#with_variations").value = oldVariations + varSep + seed + ':' + config.variation_amount - } - - saveFields(document.querySelector("#generate-form")); - }); - - document.querySelector("#results").prepend(outputNode); + if (toEnd) { + document.querySelector("#results").append(outputNode); + } else { + document.querySelector("#results").prepend(outputNode); + } document.querySelector("#no-results-message")?.remove(); } @@ -119,14 +203,33 @@ async function generateSubmit(form) { // Convert file data to base64 // TODO: Should probably uplaod files with formdata or something, and store them in the backend? let formData = Object.fromEntries(new FormData(form)); + if (!formData.enable_generate && !formData.enable_init_image) { + gen_label = document.querySelector("label[for=enable_generate]").innerHTML; + initimg_label = document.querySelector("label[for=enable_init_image]").innerHTML; + alert(`Error: one of "${gen_label}" or "${initimg_label}" must be set`); + } + + formData.initimg_name = formData.initimg.name formData.initimg = formData.initimg.name !== '' ? await toBase64(formData.initimg) : null; + // Evaluate all checkboxes + let checkboxes = form.querySelectorAll('input[type=checkbox]'); + checkboxes.forEach(function (checkbox) { + if (checkbox.checked) { + formData[checkbox.name] = 'true'; + } + }); + let strength = formData.strength; let totalSteps = formData.initimg ? Math.floor(strength * formData.steps) : formData.steps; + let showProgressImages = formData.progress_images; + + // Set enabling flags + // Initialize the progress bar - initProgress(totalSteps); + initProgress(totalSteps, showProgressImages); // POST, use response to listen for events fetch(form.action, { @@ -136,13 +239,19 @@ async function generateSubmit(form) { }) .then(response => response.json()) .then(data => { - var dreamId = data.dreamId; - socket.emit('join_room', { 'room': dreamId }); + var jobId = data.jobId; + socket.emit('join_room', { 'room': jobId }); }); form.querySelector('fieldset').setAttribute('disabled',''); } +function fieldSetEnableChecked(event) { + cb = event.target; + fields = cb.closest('fieldset'); + fields.disabled = !cb.checked; +} + // Socket listeners socket.on('job_started', (data) => {}) @@ -152,6 +261,7 @@ socket.on('dream_result', (data) => { var dreamRequest = data.dreamRequest; var src = 'api/images/' + dreamId; + priorResultsLoadState.offset += 1; appendOutput(src, dreamRequest.seed, dreamRequest); resetProgress(false); @@ -193,7 +303,13 @@ socket.on('job_done', (data) => { resetProgress(); }) -window.onload = () => { +window.onload = async () => { + document.querySelector("#prompt").addEventListener("keydown", (e) => { + if (e.key === "Enter" && !e.shiftKey) { + const form = e.target.form; + generateSubmit(form); + } + }); document.querySelector("#generate-form").addEventListener('submit', (e) => { e.preventDefault(); const form = e.target; @@ -216,12 +332,65 @@ window.onload = () => { loadFields(document.querySelector("#generate-form")); document.querySelector('#cancel-button').addEventListener('click', () => { - fetch('/cancel').catch(e => { + fetch('/api/cancel').catch(e => { console.error(e); }); }); + document.documentElement.addEventListener('keydown', (e) => { + if (e.key === "Escape") + fetch('/api/cancel').catch(err => { + console.error(err); + }); + }); if (!config.gfpgan_model_exists) { document.querySelector("#gfpgan").style.display = 'none'; } + + window.addEventListener("scroll", () => { + if ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) { + loadPriorResults(); + } + }); + + + + // Enable/disable forms by checkboxes + document.querySelectorAll("legend > input[type=checkbox]").forEach(function(cb) { + cb.addEventListener('change', fieldSetEnableChecked); + fieldSetEnableChecked({ target: cb}) + }); + + + // Load some of the previous results + loadPriorResults(); + + // Image drop/upload WIP + /* + let drop = document.getElementById('dropper'); + function ondrop(event) { + let dreamData = event.dataTransfer.getData('dream'); + if (dreamData) { + var dream = JSON.parse(decodeURIComponent(dreamData)); + alert(dream.dreamId); + } + }; + + function ondragenter(event) { + event.preventDefault(); + }; + + function ondragover(event) { + event.preventDefault(); + }; + + function ondragleave(event) { + + } + + drop.addEventListener('drop', ondrop); + drop.addEventListener('dragenter', ondragenter); + drop.addEventListener('dragover', ondragover); + drop.addEventListener('dragleave', ondragleave); + */ }; From fe12c6c099ddbc1939a06d37b9b63ed58566ef15 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 16 Sep 2022 16:58:16 -0400 Subject: [PATCH 15/19] Squashed commit of the following: commit 67fbaa7c3106ee1c6461048cbff9111df5af0abc Author: Lincoln Stein Date: Fri Sep 16 16:57:54 2022 -0400 reconciled conflicting changes to pngwriter call commit ddc68b01f7a50901ef8d7ceb250ce4a337762819 Merge: f9feaac cbac95b Author: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat Sep 17 06:39:22 2022 +1000 Merge remote-tracking branch 'upstream/development' into development commit f9feaac8c728162f43f917364b96203fee8071c9 Author: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat Sep 17 06:16:16 2022 +1000 Fixes metadata related to new args commit d1de1e357a889a350d682b03842ffca633bbf359 Author: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat Sep 17 06:15:55 2022 +1000 Fixes PromptFormatter import bug --- backend/server.py | 20 +++++++++----------- ldm/dream/pngwriter.py | 9 ++++++--- scripts/sd-metadata.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/backend/server.py b/backend/server.py index 8dbbc65cb1..ef93c5b0d7 100644 --- a/backend/server.py +++ b/backend/server.py @@ -19,7 +19,8 @@ from uuid import uuid4 from ldm.gfpgan.gfpgan_tools import real_esrgan_upscale from ldm.gfpgan.gfpgan_tools import run_gfpgan from ldm.generate import Generate -from ldm.dream.pngwriter import PngWriter +from ldm.dream.pngwriter import PngWriter, retrieve_metadata + from modules.parameters import parameters_to_command, create_cmd_parser @@ -146,15 +147,12 @@ def handle_request_all_images(): paths.sort(key=lambda x: os.path.getmtime(x)) image_array = [] for path in paths: - image = Image.open(path) - metadata = {} - if 'Dream' in image.info: - try: - metadata = vars(parser.parse_args(shlex.split(image.info['Dream']))) - except SystemExit: - # TODO: Unable to parse metadata, ignore it for now, - # this can happen when metadata is missing a prompt - pass + # image = Image.open(path) + all_metadata = retrieve_metadata(path) + if 'Dream' in all_metadata and not all_metadata['sd-metadata']: + metadata = vars(parser.parse_args(shlex.split(all_metadata['Dream']))) + else: + metadata = all_metadata['sd-metadata'] image_array.append({'path': path, 'metadata': metadata}) return make_response("OK", data=image_array) @@ -307,7 +305,7 @@ def save_image(image, parameters, output_dir, step_index=None, postprocessing=Fa command = parameters_to_command(parameters) - path = pngwriter.save_image_and_prompt_to_png(image, command, filename) + path = pngwriter.save_image_and_prompt_to_png(image, command, metadata=parameters, name=filename) return path diff --git a/ldm/dream/pngwriter.py b/ldm/dream/pngwriter.py index 6291059585..9a2a8bc816 100644 --- a/ldm/dream/pngwriter.py +++ b/ldm/dream/pngwriter.py @@ -34,6 +34,7 @@ class PngWriter: # saves image named _image_ to outdir/name, writing metadata from prompt # returns full path of output def save_image_and_prompt_to_png(self, image, dream_prompt, name, metadata=None): + print(f'self.outdir={self.outdir}, name={name}') path = os.path.join(self.outdir, name) info = PngImagePlugin.PngInfo() info.add_text('Dream', dream_prompt) @@ -48,7 +49,8 @@ class PngWriter: metadata stored there, as a dict ''' path = os.path.join(self.outdir,img_basename) - return retrieve_metadata(path) + all_metadata = retrieve_metadata(path) + return all_metadata['sd-metadata'] def retrieve_metadata(img_path): ''' @@ -56,6 +58,7 @@ def retrieve_metadata(img_path): metadata stored there, as a dict ''' im = Image.open(img_path) - md = im.text.get('sd-metadata',{}) - return json.loads(md) + md = im.text.get('sd-metadata', '{}') + dream_prompt = im.text.get('Dream', '') + return {'sd-metadata': json.loads(md), 'Dream': dream_prompt} diff --git a/scripts/sd-metadata.py b/scripts/sd-metadata.py index a3438fa078..02d5002d60 100644 --- a/scripts/sd-metadata.py +++ b/scripts/sd-metadata.py @@ -13,7 +13,7 @@ filenames = sys.argv[1:] for f in filenames: try: metadata = retrieve_metadata(f) - print(f'{f}:\n',json.dumps(metadata, indent=4)) + print(f'{f}:\n',json.dumps(metadata['sd-metadata'], indent=4)) except FileNotFoundError: sys.stderr.write(f'{f} not found\n') continue From 6cb6c4a9114d8f6420fbc674f37fb22806a263be Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 16 Sep 2022 17:27:08 -0400 Subject: [PATCH 16/19] restore static files for old web server --- static/dream_web/index.css | 61 ++---- static/dream_web/index.html | 274 +++++++++++--------------- static/dream_web/index.js | 383 ++++++++++-------------------------- 3 files changed, 229 insertions(+), 489 deletions(-) diff --git a/static/dream_web/index.css b/static/dream_web/index.css index 25a0994a3d..51f0f267c3 100644 --- a/static/dream_web/index.css +++ b/static/dream_web/index.css @@ -1,8 +1,3 @@ -:root { - --fields-dark:#DCDCDC; - --fields-light:#F5F5F5; -} - * { font-family: 'Arial'; font-size: 100%; @@ -23,26 +18,15 @@ fieldset { border: none; line-height: 2.2em; } -fieldset > legend { - width: auto; - margin-left: 0; - margin-right: auto; - font-weight:bold; -} select, input { margin-right: 10px; padding: 2px; } -input:disabled { - cursor:auto; -} input[type=submit] { - cursor: pointer; background-color: #666; color: white; } input[type=checkbox] { - cursor: pointer; margin-right: 0px; width: 20px; height: 20px; @@ -103,11 +87,11 @@ header h1 { } #results img { border-radius: 5px; - object-fit: contain; - background-color: var(--fields-dark); + object-fit: cover; } #fieldset-config { line-height:2em; + background-color: #F0F0F0; } input[type="number"] { width: 60px; @@ -134,46 +118,35 @@ label { #progress-image { width: 30vh; height: 30vh; - object-fit: contain; - background-color: var(--fields-dark); } #cancel-button { cursor: pointer; color: red; } +#basic-parameters { + background-color: #EEEEEE; +} #txt2img { - background-color: var(--fields-dark); + background-color: #DCDCDC; } #variations { - background-color: var(--fields-light); -} -#initimg { - background-color: var(--fields-dark); + background-color: #EEEEEE; } #img2img { - background-color: var(--fields-light); + background-color: #DCDCDC; } -#initimg > :not(legend) { - background-color: var(--fields-light); - margin: .5em; -} - -#postprocess, #initimg { - display:flex; - flex-wrap:wrap; - padding: 0; - margin-top: 1em; - background-color: var(--fields-dark); -} -#postprocess > fieldset, #initimg > * { - flex-grow: 1; -} -#postprocess > fieldset { - background-color: var(--fields-dark); +#gfpgan { + background-color: #EEEEEE; } #progress-section { - background-color: var(--fields-light); + background-color: #F5F5F5; +} +.section-header { + text-align: left; + font-weight: bold; + padding: 0 0 0 0; } #no-results-message:not(:only-child) { display: none; } + diff --git a/static/dream_web/index.html b/static/dream_web/index.html index 9dbd213669..1e194c0205 100644 --- a/static/dream_web/index.html +++ b/static/dream_web/index.html @@ -1,152 +1,102 @@ - - - Stable Diffusion Dream Server - - - - - - - - - - - -
-

Stable Diffusion Dream Server

-
- For news and support for this web service, visit our GitHub - site -
-
- -
- -
-
- - - - - - - - - - - - - - - -
- - - - - - - - - - -
- - - - -
-
-
- - - - -
+ + +
+ +
+ +
+
+
Basic options
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+
+
Image-to-image options
-
-
- - - - - - - - -
-
-
+
+ + + + +
- - - - - - -
-
- - - - - +
Post-processing options
+ + +
-
- -
-
-
-
- - -
- -
- Postprocessing...1/3 + +
+
+
+ + +
+ +
+ Postprocessing...1/3 +
+ +
+ +
+
+

No results...

-
- -
-
-
- - + + diff --git a/static/dream_web/index.js b/static/dream_web/index.js index 5de690297d..ac68034920 100644 --- a/static/dream_web/index.js +++ b/static/dream_web/index.js @@ -1,109 +1,3 @@ -const socket = io(); - -var priorResultsLoadState = { - page: 0, - pages: 1, - per_page: 10, - total: 20, - offset: 0, // number of items generated since last load - loading: false, - initialized: false -}; - -function loadPriorResults() { - // Fix next page by offset - let offsetPages = priorResultsLoadState.offset / priorResultsLoadState.per_page; - priorResultsLoadState.page += offsetPages; - priorResultsLoadState.pages += offsetPages; - priorResultsLoadState.total += priorResultsLoadState.offset; - priorResultsLoadState.offset = 0; - - if (priorResultsLoadState.loading) { - return; - } - - if (priorResultsLoadState.page >= priorResultsLoadState.pages) { - return; // Nothing more to load - } - - // Load - priorResultsLoadState.loading = true - let url = new URL('/api/images', document.baseURI); - url.searchParams.append('page', priorResultsLoadState.initialized ? priorResultsLoadState.page + 1 : priorResultsLoadState.page); - url.searchParams.append('per_page', priorResultsLoadState.per_page); - fetch(url.href, { - method: 'GET', - headers: new Headers({'content-type': 'application/json'}) - }) - .then(response => response.json()) - .then(data => { - priorResultsLoadState.page = data.page; - priorResultsLoadState.pages = data.pages; - priorResultsLoadState.per_page = data.per_page; - priorResultsLoadState.total = data.total; - - data.items.forEach(function(dreamId, index) { - let src = 'api/images/' + dreamId; - fetch('/api/images/' + dreamId + '/metadata', { - method: 'GET', - headers: new Headers({'content-type': 'application/json'}) - }) - .then(response => response.json()) - .then(metadata => { - let seed = metadata.seed || 0; // TODO: Parse old metadata - appendOutput(src, seed, metadata, true); - }); - }); - - // Load until page is full - if (!priorResultsLoadState.initialized) { - if (document.body.scrollHeight <= window.innerHeight) { - loadPriorResults(); - } - } - }) - .finally(() => { - priorResultsLoadState.loading = false; - priorResultsLoadState.initialized = true; - }); -} - -function resetForm() { - var form = document.getElementById('generate-form'); - form.querySelector('fieldset').removeAttribute('disabled'); -} - -function initProgress(totalSteps, showProgressImages) { - // TODO: Progress could theoretically come from multiple jobs at the same time (in the future) - let progressSectionEle = document.querySelector('#progress-section'); - progressSectionEle.style.display = 'initial'; - let progressEle = document.querySelector('#progress-bar'); - progressEle.setAttribute('max', totalSteps); - - let progressImageEle = document.querySelector('#progress-image'); - progressImageEle.src = BLANK_IMAGE_URL; - progressImageEle.style.display = showProgressImages ? 'initial': 'none'; -} - -function setProgress(step, totalSteps, src) { - let progressEle = document.querySelector('#progress-bar'); - progressEle.setAttribute('value', step); - - if (src) { - let progressImageEle = document.querySelector('#progress-image'); - progressImageEle.src = src; - } -} - -function resetProgress(hide = true) { - if (hide) { - let progressSectionEle = document.querySelector('#progress-section'); - progressSectionEle.style.display = 'none'; - } - let progressEle = document.querySelector('#progress-bar'); - progressEle.setAttribute('value', 0); -} - function toBase64(file) { return new Promise((resolve, reject) => { const r = new FileReader(); @@ -113,41 +7,17 @@ function toBase64(file) { }); } -function ondragdream(event) { - let dream = event.target.dataset.dream; - event.dataTransfer.setData("dream", dream); -} - -function seedClick(event) { - // Get element - var image = event.target.closest('figure').querySelector('img'); - var dream = JSON.parse(decodeURIComponent(image.dataset.dream)); - - let form = document.querySelector("#generate-form"); - for (const [k, v] of new FormData(form)) { - if (k == 'initimg') { continue; } - let formElem = form.querySelector(`*[name=${k}]`); - formElem.value = dream[k] !== undefined ? dream[k] : formElem.defaultValue; - } - - document.querySelector("#seed").value = dream.seed; - document.querySelector('#iterations').value = 1; // Reset to 1 iteration since we clicked a single image (not a full job) - - // NOTE: leaving this manual for the user for now - it was very confusing with this behavior - // document.querySelector("#with_variations").value = variations || ''; - // if (document.querySelector("#variation_amount").value <= 0) { - // document.querySelector("#variation_amount").value = 0.2; - // } - - saveFields(document.querySelector("#generate-form")); -} - -function appendOutput(src, seed, config, toEnd=false) { +function appendOutput(src, seed, config) { let outputNode = document.createElement("figure"); - let altText = seed.toString() + " | " + config.prompt; + + let variations = config.with_variations; + if (config.variation_amount > 0) { + variations = (variations ? variations + ',' : '') + seed + ':' + config.variation_amount; + } + let baseseed = (config.with_variations || config.variation_amount > 0) ? config.seed : seed; + let altText = baseseed + ' | ' + (variations ? variations + ' | ' : '') + config.prompt; // img needs width and height for lazy loading to work - // TODO: store the full config in a data attribute on the image? const figureContents = ` + height="256"> -
${seed}
+
${seed}
`; outputNode.innerHTML = figureContents; + let figcaption = outputNode.querySelector('figcaption'); - if (toEnd) { - document.querySelector("#results").append(outputNode); - } else { - document.querySelector("#results").prepend(outputNode); - } - document.querySelector("#no-results-message")?.remove(); + // Reload image config + figcaption.addEventListener('click', () => { + let form = document.querySelector("#generate-form"); + for (const [k, v] of new FormData(form)) { + if (k == 'initimg') { continue; } + form.querySelector(`*[name=${k}]`).value = config[k]; + } + + document.querySelector("#seed").value = baseseed; + document.querySelector("#with_variations").value = variations || ''; + if (document.querySelector("#variation_amount").value <= 0) { + document.querySelector("#variation_amount").value = 0.2; + } + + saveFields(document.querySelector("#generate-form")); + }); + + document.querySelector("#results").prepend(outputNode); } function saveFields(form) { @@ -200,109 +79,93 @@ function clearFields(form) { const BLANK_IMAGE_URL = 'data:image/svg+xml,'; async function generateSubmit(form) { + const prompt = document.querySelector("#prompt").value; + // Convert file data to base64 - // TODO: Should probably uplaod files with formdata or something, and store them in the backend? let formData = Object.fromEntries(new FormData(form)); - if (!formData.enable_generate && !formData.enable_init_image) { - gen_label = document.querySelector("label[for=enable_generate]").innerHTML; - initimg_label = document.querySelector("label[for=enable_init_image]").innerHTML; - alert(`Error: one of "${gen_label}" or "${initimg_label}" must be set`); - } - - formData.initimg_name = formData.initimg.name formData.initimg = formData.initimg.name !== '' ? await toBase64(formData.initimg) : null; - // Evaluate all checkboxes - let checkboxes = form.querySelectorAll('input[type=checkbox]'); - checkboxes.forEach(function (checkbox) { - if (checkbox.checked) { - formData[checkbox.name] = 'true'; - } - }); - let strength = formData.strength; let totalSteps = formData.initimg ? Math.floor(strength * formData.steps) : formData.steps; - let showProgressImages = formData.progress_images; - // Set enabling flags + let progressSectionEle = document.querySelector('#progress-section'); + progressSectionEle.style.display = 'initial'; + let progressEle = document.querySelector('#progress-bar'); + progressEle.setAttribute('max', totalSteps); + let progressImageEle = document.querySelector('#progress-image'); + progressImageEle.src = BLANK_IMAGE_URL; + progressImageEle.style.display = {}.hasOwnProperty.call(formData, 'progress_images') ? 'initial': 'none'; - // Initialize the progress bar - initProgress(totalSteps, showProgressImages); - - // POST, use response to listen for events + // Post as JSON, using Fetch streaming to get results fetch(form.action, { method: form.method, - headers: new Headers({'content-type': 'application/json'}), body: JSON.stringify(formData), - }) - .then(response => response.json()) - .then(data => { - var jobId = data.jobId; - socket.emit('join_room', { 'room': jobId }); + }).then(async (response) => { + const reader = response.body.getReader(); + + let noOutputs = true; + while (true) { + let {value, done} = await reader.read(); + value = new TextDecoder().decode(value); + if (done) { + progressSectionEle.style.display = 'none'; + break; + } + + for (let event of value.split('\n').filter(e => e !== '')) { + const data = JSON.parse(event); + + if (data.event === 'result') { + noOutputs = false; + appendOutput(data.url, data.seed, data.config); + progressEle.setAttribute('value', 0); + progressEle.setAttribute('max', totalSteps); + } else if (data.event === 'upscaling-started') { + document.getElementById("processing_cnt").textContent=data.processed_file_cnt; + document.getElementById("scaling-inprocess-message").style.display = "block"; + } else if (data.event === 'upscaling-done') { + document.getElementById("scaling-inprocess-message").style.display = "none"; + } else if (data.event === 'step') { + progressEle.setAttribute('value', data.step); + if (data.url) { + progressImageEle.src = data.url; + } + } else if (data.event === 'canceled') { + // avoid alerting as if this were an error case + noOutputs = false; + } + } + } + + // Re-enable form, remove no-results-message + form.querySelector('fieldset').removeAttribute('disabled'); + document.querySelector("#prompt").value = prompt; + document.querySelector('progress').setAttribute('value', '0'); + + if (noOutputs) { + alert("Error occurred while generating."); + } }); + // Disable form while generating form.querySelector('fieldset').setAttribute('disabled',''); + document.querySelector("#prompt").value = `Generating: "${prompt}"`; } -function fieldSetEnableChecked(event) { - cb = event.target; - fields = cb.closest('fieldset'); - fields.disabled = !cb.checked; +async function fetchRunLog() { + try { + let response = await fetch('/run_log.json') + const data = await response.json(); + for(let item of data.run_log) { + appendOutput(item.url, item.seed, item); + } + } catch (e) { + console.error(e); + } } -// Socket listeners -socket.on('job_started', (data) => {}) - -socket.on('dream_result', (data) => { - var jobId = data.jobId; - var dreamId = data.dreamId; - var dreamRequest = data.dreamRequest; - var src = 'api/images/' + dreamId; - - priorResultsLoadState.offset += 1; - appendOutput(src, dreamRequest.seed, dreamRequest); - - resetProgress(false); -}) - -socket.on('dream_progress', (data) => { - // TODO: it'd be nice if we could get a seed reported here, but the generator would need to be updated - var step = data.step; - var totalSteps = data.totalSteps; - var jobId = data.jobId; - var dreamId = data.dreamId; - - var progressType = data.progressType - if (progressType === 'GENERATION') { - var src = data.hasProgressImage ? - 'api/intermediates/' + dreamId + '/' + step - : null; - setProgress(step, totalSteps, src); - } else if (progressType === 'UPSCALING_STARTED') { - // step and totalSteps are used for upscale count on this message - document.getElementById("processing_cnt").textContent = step; - document.getElementById("processing_total").textContent = totalSteps; - document.getElementById("scaling-inprocess-message").style.display = "block"; - } else if (progressType == 'UPSCALING_DONE') { - document.getElementById("scaling-inprocess-message").style.display = "none"; - } -}) - -socket.on('job_canceled', (data) => { - resetForm(); - resetProgress(); -}) - -socket.on('job_done', (data) => { - jobId = data.jobId - socket.emit('leave_room', { 'room': jobId }); - - resetForm(); - resetProgress(); -}) - window.onload = async () => { document.querySelector("#prompt").addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { @@ -320,7 +183,7 @@ window.onload = async () => { saveFields(e.target.form); }); document.querySelector("#reset-seed").addEventListener('click', (e) => { - document.querySelector("#seed").value = 0; + document.querySelector("#seed").value = -1; saveFields(e.target.form); }); document.querySelector("#reset-all").addEventListener('click', (e) => { @@ -332,13 +195,13 @@ window.onload = async () => { loadFields(document.querySelector("#generate-form")); document.querySelector('#cancel-button').addEventListener('click', () => { - fetch('/api/cancel').catch(e => { + fetch('/cancel').catch(e => { console.error(e); }); }); document.documentElement.addEventListener('keydown', (e) => { if (e.key === "Escape") - fetch('/api/cancel').catch(err => { + fetch('/cancel').catch(err => { console.error(err); }); }); @@ -346,51 +209,5 @@ window.onload = async () => { if (!config.gfpgan_model_exists) { document.querySelector("#gfpgan").style.display = 'none'; } - - window.addEventListener("scroll", () => { - if ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) { - loadPriorResults(); - } - }); - - - - // Enable/disable forms by checkboxes - document.querySelectorAll("legend > input[type=checkbox]").forEach(function(cb) { - cb.addEventListener('change', fieldSetEnableChecked); - fieldSetEnableChecked({ target: cb}) - }); - - - // Load some of the previous results - loadPriorResults(); - - // Image drop/upload WIP - /* - let drop = document.getElementById('dropper'); - function ondrop(event) { - let dreamData = event.dataTransfer.getData('dream'); - if (dreamData) { - var dream = JSON.parse(decodeURIComponent(dreamData)); - alert(dream.dreamId); - } - }; - - function ondragenter(event) { - event.preventDefault(); - }; - - function ondragover(event) { - event.preventDefault(); - }; - - function ondragleave(event) { - - } - - drop.addEventListener('drop', ondrop); - drop.addEventListener('dragenter', ondragenter); - drop.addEventListener('dragover', ondragover); - drop.addEventListener('dragleave', ondragleave); - */ + await fetchRunLog() }; From ba4892e03fcfab4c8dac71c453cee45f2d4f6568 Mon Sep 17 00:00:00 2001 From: Kevin Schaul Date: Fri, 16 Sep 2022 16:32:18 -0500 Subject: [PATCH 17/19] Zero-pad intermediate image file names (#616) --- ldm/dream/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ldm/dream/server.py b/ldm/dream/server.py index cde3957a1f..372d719052 100644 --- a/ldm/dream/server.py +++ b/ldm/dream/server.py @@ -228,7 +228,8 @@ class DreamServer(BaseHTTPRequestHandler): nonlocal step_index if opt.progress_images and step % 5 == 0 and step < opt.steps - 1: image = self.model.sample_to_image(sample) - name = f'{prefix}.{opt.seed}.{step_index}.png' + step_index_padded = str(step_index).rjust(len(str(opt.steps)), '0') + name = f'{prefix}.{opt.seed}.{step_index_padded}.png' metadata = f'{opt.prompt} -S{opt.seed} [intermediate]' path = step_writer.save_image_and_prompt_to_png(image, dream_prompt=metadata, name=name) step_index += 1 From 6cab2e0ca025f8541373ab374ab2881ca330ad88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E4=B8=8D=E6=B8=B8?= <71683364+mefengl@users.noreply.github.com> Date: Sat, 17 Sep 2022 05:32:52 +0800 Subject: [PATCH 18/19] refine env rebuild tip (#611) --- docs/installation/INSTALL_MAC.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/installation/INSTALL_MAC.md b/docs/installation/INSTALL_MAC.md index 39398c36ac..7ee5653c7a 100644 --- a/docs/installation/INSTALL_MAC.md +++ b/docs/installation/INSTALL_MAC.md @@ -181,7 +181,12 @@ There are several causes of these errors. - Third, if it says you're missing taming you need to rebuild your virtual environment. -`conda env remove -n ldm conda env create -f environment-mac.yaml` +````bash +conda deactivate + +conda env remove -n ldm +PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac.yaml +``` Fourth, If you have activated the ldm virtual environment and tried rebuilding it, maybe the problem could be that I have something installed that you don't From 40b61870f6e18634724cf9b14b1a3492f2ffb680 Mon Sep 17 00:00:00 2001 From: SteveCaruso Date: Fri, 16 Sep 2022 17:42:21 -0400 Subject: [PATCH 19/19] update Intel Mac instructions (#599) Co-authored-by: Lincoln Stein --- docs/installation/INSTALL_MAC.md | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/installation/INSTALL_MAC.md b/docs/installation/INSTALL_MAC.md index 7ee5653c7a..71535980f5 100644 --- a/docs/installation/INSTALL_MAC.md +++ b/docs/installation/INSTALL_MAC.md @@ -7,10 +7,7 @@ title: macOS - macOS 12.3 Monterey or later - Python - Patience -- Apple Silicon\* - -\*I haven't tested any of this on Intel Macs but I have read that one person got -it to work, so Apple Silicon might not be requried. +- Apple Silicon or Intel Mac Things have moved really fast and so these instructions change often and are often out-of-date. One of the problems is that there are so many different ways @@ -59,9 +56,13 @@ First get the weights checkpoint download started - it's big: # install python 3, git, cmake, protobuf: brew install cmake protobuf rust -# install miniconda (M1 arm64 version): - curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh -o Miniconda3-latest-MacOSX-arm64.sh - /bin/bash Miniconda3-latest-MacOSX-arm64.sh +# install miniconda for M1 arm64: +curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh -o Miniconda3-latest-MacOSX-arm64.sh +/bin/bash Miniconda3-latest-MacOSX-arm64.sh + +# OR install miniconda for Intel: +curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -o Miniconda3-latest-MacOSX-x86_64.sh +/bin/bash Miniconda3-latest-MacOSX-x86_64.sh # EITHER WAY, @@ -82,15 +83,22 @@ brew install cmake protobuf rust ln -s "$PATH_TO_CKPT/sd-v1-4.ckpt" models/ldm/stable-diffusion-v1/model.ckpt -# install packages - PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac.yaml - conda activate ldm +# install packages for arm64 +PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac.yaml +conda activate ldm + +# OR install packages for x86_64 +PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-x86_64 conda env create -f environment-mac.yaml +conda activate ldm # only need to do this once python scripts/preload_models.py # run SD! python scripts/dream.py --full_precision # half-precision requires autocast and won't work + +# or run the web interface! +python scripts/dream.py --web ``` The original scripts should work as well.