mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): POST upload working
This commit is contained in:
parent
3722f055fb
commit
e2114a1da5
@ -1,5 +1,5 @@
|
|||||||
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
|
# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
|
||||||
|
import io
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import Path, Request, UploadFile
|
from fastapi import Path, Request, UploadFile
|
||||||
@ -48,19 +48,19 @@ async def upload_image(file: UploadFile, request: Request):
|
|||||||
|
|
||||||
contents = await file.read()
|
contents = await file.read()
|
||||||
try:
|
try:
|
||||||
im = Image.open(contents)
|
im = Image.open(io.BytesIO(contents))
|
||||||
except:
|
except:
|
||||||
# Error opening the image
|
# Error opening the image
|
||||||
return Response(status_code=415)
|
return Response(status_code=415)
|
||||||
|
|
||||||
filename = f"{str(int(datetime.now(timezone.utc).timestamp()))}.png"
|
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(
|
return Response(
|
||||||
status_code=201,
|
status_code=201,
|
||||||
headers={
|
headers={
|
||||||
"Location": request.url_for(
|
"Location": request.url_for(
|
||||||
"get_image", image_type=ImageType.UPLOAD, image_name=filename
|
"get_image", image_type="uploads", image_name=filename
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
14
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
14
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
@ -124,20 +124,6 @@ export declare type Image = {
|
|||||||
category: GalleryCategory;
|
category: GalleryCategory;
|
||||||
isBase64?: boolean;
|
isBase64?: boolean;
|
||||||
dreamPrompt?: 'string';
|
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.
|
// GalleryImages is an array of Image.
|
||||||
|
@ -2,7 +2,6 @@ import { Box, useToast } from '@chakra-ui/react';
|
|||||||
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from 'app/contexts/ImageUploaderTriggerContext';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import useImageUploader from 'common/hooks/useImageUploader';
|
import useImageUploader from 'common/hooks/useImageUploader';
|
||||||
import { uploadImage } from 'features/gallery/store/thunks/uploadImage';
|
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { ResourceKey } from 'i18next';
|
import { ResourceKey } from 'i18next';
|
||||||
import {
|
import {
|
||||||
@ -15,6 +14,7 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { uploadImage } from 'services/thunks/image';
|
||||||
import ImageUploadOverlay from './ImageUploadOverlay';
|
import ImageUploadOverlay from './ImageUploadOverlay';
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
@ -49,7 +49,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
|
|
||||||
const fileAcceptedCallback = useCallback(
|
const fileAcceptedCallback = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
dispatch(uploadImage({ imageFile: file }));
|
dispatch(uploadImage({ formData: { file } }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -124,7 +124,7 @@ const ImageUploader = (props: ImageUploaderProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(uploadImage({ imageFile: file }));
|
dispatch(uploadImage({ formData: { file } }));
|
||||||
};
|
};
|
||||||
document.addEventListener('paste', pasteImageListener);
|
document.addEventListener('paste', pasteImageListener);
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -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<void, RootState, unknown, AnyAction> =>
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
};
|
|
@ -89,6 +89,7 @@ export class ImagesService {
|
|||||||
url: '/api/v1/images/uploads/',
|
url: '/api/v1/images/uploads/',
|
||||||
formData: formData,
|
formData: formData,
|
||||||
mediaType: 'multipart/form-data',
|
mediaType: 'multipart/form-data',
|
||||||
|
responseHeader: "location",
|
||||||
errors: {
|
errors: {
|
||||||
404: `Session not found`,
|
404: `Session not found`,
|
||||||
422: `Validation Error`,
|
422: `Validation Error`,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { ProgressImage } from './events/types';
|
import { ProgressImage } from './events/types';
|
||||||
import { createSession, invokeSession } from 'services/thunks/session';
|
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
|
* Just temp until we work out better statuses
|
||||||
@ -93,6 +96,19 @@ export const apiSlice = createSlice({
|
|||||||
// !HTTP 200
|
// !HTTP 200
|
||||||
// state.networkStatus = 'idle'
|
// 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'
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import { isFulfilled, Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
|
import { isFulfilled, Middleware, MiddlewareAPI } from '@reduxjs/toolkit';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { emitSubscribe } from 'app/nodesSocketio/actions';
|
import { emitSubscribe } from 'app/nodesSocketio/actions';
|
||||||
import { AppDispatch, RootState } from 'app/store';
|
import { AppDispatch, RootState } from 'app/store';
|
||||||
import { setSessionId } from './apiSlice';
|
import { setSessionId } from './apiSlice';
|
||||||
|
import { uploadImage } from './thunks/image';
|
||||||
import { createSession, invokeSession } from './thunks/session';
|
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
|
* `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 isFulfilledCreateSession = isFulfilled(createSession);
|
||||||
|
const isFulfilledUploadImage = isFulfilled(uploadImage);
|
||||||
|
|
||||||
export const invokeMiddleware: Middleware =
|
export const invokeMiddleware: Middleware =
|
||||||
(store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => {
|
(store: MiddlewareAPI<AppDispatch, RootState>) => (next) => (action) => {
|
||||||
const { dispatch } = store;
|
const { dispatch, getState } = store;
|
||||||
|
|
||||||
if (isFulfilledCreateSession(action)) {
|
if (isFulfilledCreateSession(action)) {
|
||||||
const sessionId = action.payload.id;
|
const sessionId = action.payload.id;
|
||||||
@ -22,6 +31,29 @@ export const invokeMiddleware: Middleware =
|
|||||||
dispatch(setSessionId(sessionId));
|
dispatch(setSessionId(sessionId));
|
||||||
dispatch(emitSubscribe(sessionId));
|
dispatch(emitSubscribe(sessionId));
|
||||||
dispatch(invokeSession({ 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 {
|
} else {
|
||||||
next(action);
|
next(action);
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,13 @@ export const getImage = createAppAsyncThunk(
|
|||||||
return response;
|
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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user