From e2114a1da5ea265a84de2131f88b68f920dbec88 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Mon, 3 Apr 2023 14:32:43 -0400 Subject: [PATCH] feat(ui): POST upload working --- invokeai/app/api/routers/images.py | 8 +-- invokeai/frontend/web/src/app/invokeai.d.ts | 14 ----- .../src/common/components/ImageUploader.tsx | 6 +-- .../gallery/store/thunks/uploadImage.ts | 54 ------------------- .../services/api/services/ImagesService.ts | 1 + .../frontend/web/src/services/apiSlice.ts | 18 ++++++- .../web/src/services/invokeMiddleware.ts | 34 +++++++++++- .../frontend/web/src/services/thunks/image.ts | 10 ++++ 8 files changed, 68 insertions(+), 77 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py index 453c114a28..8f010171f8 100644 --- a/invokeai/app/api/routers/images.py +++ b/invokeai/app/api/routers/images.py @@ -1,5 +1,5 @@ # Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) - +import io from datetime import datetime, timezone from fastapi import Path, Request, UploadFile @@ -48,19 +48,19 @@ async def upload_image(file: UploadFile, request: Request): contents = await file.read() try: - im = Image.open(contents) + im = Image.open(io.BytesIO(contents)) except: # Error opening the image return Response(status_code=415) filename = f"{str(int(datetime.now(timezone.utc).timestamp()))}.png" - ApiDependencies.invoker.services.images.save(ImageType.UPLOAD, filename, im) + ApiDependencies.invoker.services.images.save("uploads", filename, im) return Response( status_code=201, headers={ "Location": request.url_for( - "get_image", image_type=ImageType.UPLOAD, image_name=filename + "get_image", image_type="uploads", image_name=filename ) }, ) diff --git a/invokeai/frontend/web/src/app/invokeai.d.ts b/invokeai/frontend/web/src/app/invokeai.d.ts index a8d0315ad7..e01e414d03 100644 --- a/invokeai/frontend/web/src/app/invokeai.d.ts +++ b/invokeai/frontend/web/src/app/invokeai.d.ts @@ -124,20 +124,6 @@ export declare type Image = { category: GalleryCategory; isBase64?: boolean; dreamPrompt?: 'string'; - name?: string; -}; - -/** - * ResultImage - */ -export declare type ResultImage = { - name: string; - url: string; - thumbnail: string; - width: number; - height: number; - timestamp: number; - metadata?: Metadata; }; // GalleryImages is an array of Image. diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx index c4f4dca9df..c888f5f497 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx @@ -2,7 +2,6 @@ import { Box, useToast } from '@chakra-ui/react'; import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import useImageUploader from 'common/hooks/useImageUploader'; -import { uploadImage } from 'features/gallery/store/thunks/uploadImage'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { ResourceKey } from 'i18next'; import { @@ -15,6 +14,7 @@ import { } from 'react'; import { FileRejection, useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; +import { uploadImage } from 'services/thunks/image'; import ImageUploadOverlay from './ImageUploadOverlay'; type ImageUploaderProps = { @@ -49,7 +49,7 @@ const ImageUploader = (props: ImageUploaderProps) => { const fileAcceptedCallback = useCallback( async (file: File) => { - dispatch(uploadImage({ imageFile: file })); + dispatch(uploadImage({ formData: { file } })); }, [dispatch] ); @@ -124,7 +124,7 @@ const ImageUploader = (props: ImageUploaderProps) => { return; } - dispatch(uploadImage({ imageFile: file })); + dispatch(uploadImage({ formData: { file } })); }; document.addEventListener('paste', pasteImageListener); return () => { diff --git a/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts b/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts deleted file mode 100644 index 7f28928987..0000000000 --- a/invokeai/frontend/web/src/features/gallery/store/thunks/uploadImage.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { AnyAction, ThunkAction } from '@reduxjs/toolkit'; -import * as InvokeAI from 'app/invokeai'; -import { RootState } from 'app/store'; -import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; -import { setInitialImage } from 'features/parameters/store/generationSlice'; -import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; -import { v4 as uuidv4 } from 'uuid'; -import { addImage } from '../gallerySlice'; - -type UploadImageConfig = { - imageFile: File; -}; - -export const uploadImage = - ( - config: UploadImageConfig - ): ThunkAction => - async (dispatch, getState) => { - const { imageFile } = config; - - const state = getState() as RootState; - - const activeTabName = activeTabNameSelector(state); - - const formData = new FormData(); - - formData.append('file', imageFile, imageFile.name); - formData.append( - 'data', - JSON.stringify({ - kind: 'init', - }) - ); - - const response = await fetch(`${window.location.origin}/upload`, { - method: 'POST', - body: formData, - }); - - const image = (await response.json()) as InvokeAI.ImageUploadResponse; - const newImage: InvokeAI.Image = { - uuid: uuidv4(), - category: 'user', - ...image, - }; - - dispatch(addImage({ image: newImage, category: 'user' })); - - if (activeTabName === 'unifiedCanvas') { - dispatch(setInitialCanvasImage(newImage)); - } else if (activeTabName === 'img2img') { - dispatch(setInitialImage(newImage)); - } - }; diff --git a/invokeai/frontend/web/src/services/api/services/ImagesService.ts b/invokeai/frontend/web/src/services/api/services/ImagesService.ts index d0108dbce6..248adc1001 100644 --- a/invokeai/frontend/web/src/services/api/services/ImagesService.ts +++ b/invokeai/frontend/web/src/services/api/services/ImagesService.ts @@ -89,6 +89,7 @@ export class ImagesService { url: '/api/v1/images/uploads/', formData: formData, mediaType: 'multipart/form-data', + responseHeader: "location", errors: { 404: `Session not found`, 422: `Validation Error`, diff --git a/invokeai/frontend/web/src/services/apiSlice.ts b/invokeai/frontend/web/src/services/apiSlice.ts index 0eff6206c3..8798a2eb1f 100644 --- a/invokeai/frontend/web/src/services/apiSlice.ts +++ b/invokeai/frontend/web/src/services/apiSlice.ts @@ -1,8 +1,11 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import { v4 as uuidv4 } from 'uuid'; import { ProgressImage } from './events/types'; import { createSession, invokeSession } from 'services/thunks/session'; -import { getImage } from './thunks/image'; +import { getImage, uploadImage } from './thunks/image'; +import * as InvokeAI from 'app/invokeai'; +import { addImage } from 'features/gallery/store/gallerySlice'; /** * Just temp until we work out better statuses @@ -93,6 +96,19 @@ export const apiSlice = createSlice({ // !HTTP 200 // state.networkStatus = 'idle' }); + builder.addCase(uploadImage.fulfilled, (state, action) => { + // !HTTP 200 + console.log(action.payload); + // state.networkStatus = 'idle' + }); + builder.addCase(uploadImage.pending, (state, action) => { + // HTTP request pending + // state.networkStatus = 'busy' + }); + builder.addCase(uploadImage.rejected, (state, action) => { + // !HTTP 200 + // state.networkStatus = 'idle' + }); }, }); diff --git a/invokeai/frontend/web/src/services/invokeMiddleware.ts b/invokeai/frontend/web/src/services/invokeMiddleware.ts index 5579015f79..bdb6006f68 100644 --- a/invokeai/frontend/web/src/services/invokeMiddleware.ts +++ b/invokeai/frontend/web/src/services/invokeMiddleware.ts @@ -1,8 +1,16 @@ import { isFulfilled, Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; +import { v4 as uuidv4 } from 'uuid'; + import { emitSubscribe } from 'app/nodesSocketio/actions'; import { AppDispatch, RootState } from 'app/store'; import { setSessionId } from './apiSlice'; +import { uploadImage } from './thunks/image'; import { createSession, invokeSession } from './thunks/session'; +import * as InvokeAI from 'app/invokeai'; +import { addImage } from 'features/gallery/store/gallerySlice'; +import { tabMap } from 'features/ui/store/tabMap'; +import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; +import { setInitialImage } from 'features/parameters/store/generationSlice'; /** * `redux-toolkit` provides nice matching utilities, which can be used as type guards @@ -10,10 +18,11 @@ import { createSession, invokeSession } from './thunks/session'; */ const isFulfilledCreateSession = isFulfilled(createSession); +const isFulfilledUploadImage = isFulfilled(uploadImage); export const invokeMiddleware: Middleware = (store: MiddlewareAPI) => (next) => (action) => { - const { dispatch } = store; + const { dispatch, getState } = store; if (isFulfilledCreateSession(action)) { const sessionId = action.payload.id; @@ -22,6 +31,29 @@ export const invokeMiddleware: Middleware = dispatch(setSessionId(sessionId)); dispatch(emitSubscribe(sessionId)); dispatch(invokeSession({ sessionId })); + } else if (isFulfilledUploadImage(action)) { + const uploadLocation = action.payload; + console.log('uploadImage.fulfilled'); + + const newImage: InvokeAI.Image = { + uuid: uuidv4(), + category: 'user', + url: uploadLocation, + width: 512, + height: 512, + mtime: new Date().getTime(), + thumbnail: uploadLocation, + }; + + dispatch(addImage({ image: newImage, category: 'user' })); + + const { activeTab } = getState().ui; + const activeTabName = tabMap[activeTab]; + if (activeTabName === 'unifiedCanvas') { + dispatch(setInitialCanvasImage(newImage)); + } else if (activeTabName === 'img2img') { + dispatch(setInitialImage(newImage)); + } } else { next(action); } diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index b52af6f3f0..654ec5cc8d 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -11,3 +11,13 @@ export const getImage = createAppAsyncThunk( return response; } ); + +type UploadImageArg = Parameters<(typeof ImagesService)['uploadImage']>[0]; + +export const uploadImage = createAppAsyncThunk( + 'api/uploadImage', + async (arg: UploadImageArg, _thunkApi) => { + const response = await ImagesService.uploadImage(arg); + return response; + } +);