1
0
Fork 0

feat(gui): add retry function to error card

This commit is contained in:
Sean Sube 2023-03-18 18:22:41 -05:00
parent 6226778cfb
commit 89790645cb
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
14 changed files with 182 additions and 55 deletions

View File

@ -1,5 +1,5 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { doesExist } from '@apextoaster/js-utils'; import { doesExist, InvalidArgumentError } from '@apextoaster/js-utils';
import { ServerParams } from '../config.js'; import { ServerParams } from '../config.js';
import { range } from '../utils.js'; import { range } from '../utils.js';
@ -191,6 +191,43 @@ export interface ModelsResponse {
upscaling: Array<string>; upscaling: Array<string>;
} }
export type RetryParams = {
type: 'txt2img';
model: ModelParams;
params: Txt2ImgParams;
upscale?: UpscaleParams;
} | {
type: 'img2img';
model: ModelParams;
params: Img2ImgParams;
upscale?: UpscaleParams;
} | {
type: 'inpaint';
model: ModelParams;
params: InpaintParams;
upscale?: UpscaleParams;
} | {
type: 'outpaint';
model: ModelParams;
params: OutpaintParams;
upscale?: UpscaleParams;
} | {
type: 'upscale';
model: ModelParams;
params: UpscaleReqParams;
upscale?: UpscaleParams;
} | {
type: 'blend';
model: ModelParams;
params: BlendParams;
upscale?: UpscaleParams;
};
export interface ImageResponseWithRetry {
image: ImageResponse;
retry: RetryParams;
}
export interface ApiClient { export interface ApiClient {
/** /**
* List the available filter masks for inpaint. * List the available filter masks for inpaint.
@ -232,32 +269,32 @@ export interface ApiClient {
/** /**
* Start a txt2img pipeline. * Start a txt2img pipeline.
*/ */
txt2img(model: ModelParams, params: Txt2ImgParams, upscale?: UpscaleParams): Promise<ImageResponse>; txt2img(model: ModelParams, params: Txt2ImgParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry>;
/** /**
* Start an im2img pipeline. * Start an im2img pipeline.
*/ */
img2img(model: ModelParams, params: Img2ImgParams, upscale?: UpscaleParams): Promise<ImageResponse>; img2img(model: ModelParams, params: Img2ImgParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry>;
/** /**
* Start an inpaint pipeline. * Start an inpaint pipeline.
*/ */
inpaint(model: ModelParams, params: InpaintParams, upscale?: UpscaleParams): Promise<ImageResponse>; inpaint(model: ModelParams, params: InpaintParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry>;
/** /**
* Start an outpaint pipeline. * Start an outpaint pipeline.
*/ */
outpaint(model: ModelParams, params: OutpaintParams, upscale?: UpscaleParams): Promise<ImageResponse>; outpaint(model: ModelParams, params: OutpaintParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry>;
/** /**
* Start an upscale pipeline. * Start an upscale pipeline.
*/ */
upscale(model: ModelParams, params: UpscaleReqParams, upscale?: UpscaleParams): Promise<ImageResponse>; upscale(model: ModelParams, params: UpscaleReqParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry>;
/** /**
* Start a blending pipeline. * Start a blending pipeline.
*/ */
blend(model: ModelParams, params: BlendParams, upscale?: UpscaleParams): Promise<ImageResponse>; blend(model: ModelParams, params: BlendParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry>;
/** /**
* Check whether some pipeline's output is ready yet. * Check whether some pipeline's output is ready yet.
@ -265,6 +302,8 @@ export interface ApiClient {
ready(key: string): Promise<ReadyResponse>; ready(key: string): Promise<ReadyResponse>;
cancel(key: string): Promise<boolean>; cancel(key: string): Promise<boolean>;
retry(params: RetryParams): Promise<ImageResponseWithRetry>;
} }
/** /**
@ -363,7 +402,7 @@ export function appendUpscaleToURL(url: URL, upscale: UpscaleParams) {
* Make an API client using the given API root and fetch client. * Make an API client using the given API root and fetch client.
*/ */
export function makeClient(root: string, f = fetch): ApiClient { export function makeClient(root: string, f = fetch): ApiClient {
function throttleRequest(url: URL, options: RequestInit): Promise<ImageResponse> { function parseRequest(url: URL, options: RequestInit): Promise<ImageResponse> {
return f(url, options).then((res) => parseApiResponse(root, res)); return f(url, options).then((res) => parseApiResponse(root, res));
} }
@ -407,7 +446,7 @@ export function makeClient(root: string, f = fetch): ApiClient {
translation: Record<string, string>; translation: Record<string, string>;
}>; }>;
}, },
async img2img(model: ModelParams, params: Img2ImgParams, upscale?: UpscaleParams): Promise<ImageResponse> { async img2img(model: ModelParams, params: Img2ImgParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry> {
const url = makeImageURL(root, 'img2img', params); const url = makeImageURL(root, 'img2img', params);
appendModelToURL(url, model); appendModelToURL(url, model);
@ -420,13 +459,21 @@ export function makeClient(root: string, f = fetch): ApiClient {
const body = new FormData(); const body = new FormData();
body.append('source', params.source, 'source'); body.append('source', params.source, 'source');
// eslint-disable-next-line no-return-await const image = await parseRequest(url, {
return await throttleRequest(url, {
body, body,
method: 'POST', method: 'POST',
}); });
return {
image,
retry: {
type: 'img2img',
model,
params,
upscale,
},
};
}, },
async txt2img(model: ModelParams, params: Txt2ImgParams, upscale?: UpscaleParams): Promise<ImageResponse> { async txt2img(model: ModelParams, params: Txt2ImgParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry> {
const url = makeImageURL(root, 'txt2img', params); const url = makeImageURL(root, 'txt2img', params);
appendModelToURL(url, model); appendModelToURL(url, model);
@ -442,12 +489,20 @@ export function makeClient(root: string, f = fetch): ApiClient {
appendUpscaleToURL(url, upscale); appendUpscaleToURL(url, upscale);
} }
// eslint-disable-next-line no-return-await const image = await parseRequest(url, {
return await throttleRequest(url, {
method: 'POST', method: 'POST',
}); });
return {
image,
retry: {
type: 'txt2img',
model,
params,
upscale,
},
};
}, },
async inpaint(model: ModelParams, params: InpaintParams, upscale?: UpscaleParams) { async inpaint(model: ModelParams, params: InpaintParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry> {
const url = makeImageURL(root, 'inpaint', params); const url = makeImageURL(root, 'inpaint', params);
appendModelToURL(url, model); appendModelToURL(url, model);
@ -464,13 +519,21 @@ export function makeClient(root: string, f = fetch): ApiClient {
body.append('mask', params.mask, 'mask'); body.append('mask', params.mask, 'mask');
body.append('source', params.source, 'source'); body.append('source', params.source, 'source');
// eslint-disable-next-line no-return-await const image = await parseRequest(url, {
return await throttleRequest(url, {
body, body,
method: 'POST', method: 'POST',
}); });
return {
image,
retry: {
type: 'inpaint',
model,
params,
upscale,
},
};
}, },
async outpaint(model: ModelParams, params: OutpaintParams, upscale?: UpscaleParams) { async outpaint(model: ModelParams, params: OutpaintParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry> {
const url = makeImageURL(root, 'inpaint', params); const url = makeImageURL(root, 'inpaint', params);
appendModelToURL(url, model); appendModelToURL(url, model);
@ -504,13 +567,21 @@ export function makeClient(root: string, f = fetch): ApiClient {
body.append('mask', params.mask, 'mask'); body.append('mask', params.mask, 'mask');
body.append('source', params.source, 'source'); body.append('source', params.source, 'source');
// eslint-disable-next-line no-return-await const image = await parseRequest(url, {
return await throttleRequest(url, {
body, body,
method: 'POST', method: 'POST',
}); });
return {
image,
retry: {
type: 'outpaint',
model,
params,
upscale,
},
};
}, },
async upscale(model: ModelParams, params: UpscaleReqParams, upscale: UpscaleParams): Promise<ImageResponse> { async upscale(model: ModelParams, params: UpscaleReqParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry> {
const url = makeApiUrl(root, 'upscale'); const url = makeApiUrl(root, 'upscale');
appendModelToURL(url, model); appendModelToURL(url, model);
@ -527,13 +598,21 @@ export function makeClient(root: string, f = fetch): ApiClient {
const body = new FormData(); const body = new FormData();
body.append('source', params.source, 'source'); body.append('source', params.source, 'source');
// eslint-disable-next-line no-return-await const image = await parseRequest(url, {
return await throttleRequest(url, {
body, body,
method: 'POST', method: 'POST',
}); });
return {
image,
retry: {
type: 'upscale',
model,
params,
upscale,
},
};
}, },
async blend(model: ModelParams, params: BlendParams, upscale: UpscaleParams): Promise<ImageResponse> { async blend(model: ModelParams, params: BlendParams, upscale?: UpscaleParams): Promise<ImageResponseWithRetry> {
const url = makeApiUrl(root, 'blend'); const url = makeApiUrl(root, 'blend');
appendModelToURL(url, model); appendModelToURL(url, model);
@ -549,11 +628,19 @@ export function makeClient(root: string, f = fetch): ApiClient {
body.append(name, params.sources[i], name); body.append(name, params.sources[i], name);
} }
// eslint-disable-next-line no-return-await const image = await parseRequest(url, {
return await throttleRequest(url, {
body, body,
method: 'POST', method: 'POST',
}); });
return {
image,
retry: {
type: 'blend',
model,
params,
upscale,
}
};
}, },
async ready(key: string): Promise<ReadyResponse> { async ready(key: string): Promise<ReadyResponse> {
const path = makeApiUrl(root, 'ready'); const path = makeApiUrl(root, 'ready');
@ -571,6 +658,24 @@ export function makeClient(root: string, f = fetch): ApiClient {
}); });
return res.status === STATUS_SUCCESS; return res.status === STATUS_SUCCESS;
}, },
async retry(retry: RetryParams): Promise<ImageResponseWithRetry> {
switch (retry.type) {
case 'blend':
return this.blend(retry.model, retry.params, retry.upscale);
case 'img2img':
return this.img2img(retry.model, retry.params, retry.upscale);
case 'inpaint':
return this.inpaint(retry.model, retry.params, retry.upscale);
case 'outpaint':
return this.outpaint(retry.model, retry.params, retry.upscale);
case 'txt2img':
return this.txt2img(retry.model, retry.params, retry.upscale);
case 'upscale':
return this.upscale(retry.model, retry.params, retry.upscale);
default:
throw new InvalidArgumentError('unknown request type');
}
}
}; };
} }

View File

@ -44,6 +44,9 @@ export const LOCAL_CLIENT = {
async cancel(key) { async cancel(key) {
throw new NoServerError(); throw new NoServerError();
}, },
async retry(params) {
throw new NoServerError();
},
async models() { async models() {
throw new NoServerError(); throw new NoServerError();
}, },

View File

@ -30,7 +30,7 @@ export function ImageHistory() {
if (doesExist(item.ready) && item.ready.ready) { if (doesExist(item.ready) && item.ready.ready) {
if (item.ready.cancelled || item.ready.failed) { if (item.ready.cancelled || item.ready.failed) {
children.push([key, <ErrorCard key={`history-${key}`} image={item.image} ready={item.ready} />]); children.push([key, <ErrorCard key={`history-${key}`} image={item.image} ready={item.ready} retry={item.retry} />]);
continue; continue;
} }

View File

@ -1,5 +1,6 @@
import { mustExist } from '@apextoaster/js-utils'; import { mustExist } from '@apextoaster/js-utils';
import { Box, Button, Card, CardContent, Typography } from '@mui/material'; import { Delete, Replay } from '@mui/icons-material';
import { Box, Card, CardContent, IconButton, Tooltip, Typography } from '@mui/material';
import { Stack } from '@mui/system'; import { Stack } from '@mui/system';
import * as React from 'react'; import * as React from 'react';
import { useContext } from 'react'; import { useContext } from 'react';
@ -7,31 +8,35 @@ import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { ImageResponse, ReadyResponse } from '../../client/api.js'; import { ImageResponse, ReadyResponse, RetryParams } from '../../client/api.js';
import { ClientContext, ConfigContext, StateContext } from '../../state.js'; import { ClientContext, ConfigContext, StateContext } from '../../state.js';
export interface ErrorCardProps { export interface ErrorCardProps {
image: ImageResponse; image: ImageResponse;
ready: ReadyResponse; ready: ReadyResponse;
retry: RetryParams;
} }
export function ErrorCard(props: ErrorCardProps) { export function ErrorCard(props: ErrorCardProps) {
const { image, ready } = props; const { image, ready, retry: retryParams } = props;
const client = mustExist(React.useContext(ClientContext)); const client = mustExist(React.useContext(ClientContext));
const { params } = mustExist(useContext(ConfigContext)); const { params } = mustExist(useContext(ConfigContext));
const state = mustExist(useContext(StateContext)); const state = mustExist(useContext(StateContext));
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const pushHistory = useStore(state, (s) => s.pushHistory);
// eslint-disable-next-line @typescript-eslint/unbound-method
const removeHistory = useStore(state, (s) => s.removeHistory); const removeHistory = useStore(state, (s) => s.removeHistory);
const { t } = useTranslation(); const { t } = useTranslation();
// TODO: actually retry async function retryImage() {
const retry = useMutation(() => { removeHistory(image);
// eslint-disable-next-line no-console const { image: nextImage, retry: nextRetry } = await client.retry(retryParams);
console.log('retry', image); pushHistory(nextImage, nextRetry);
return Promise.resolve(true); }
});
const retry = useMutation(retryImage);
return <Card sx={{ maxWidth: params.width.default }}> return <Card sx={{ maxWidth: params.width.default }}>
<CardContent sx={{ height: params.height.default }}> <CardContent sx={{ height: params.height.default }}>
@ -50,8 +55,18 @@ export function ErrorCard(props: ErrorCardProps) {
current: ready.progress, current: ready.progress,
total: image.params.steps, total: image.params.steps,
})}</Typography> })}</Typography>
<Button onClick={() => retry.mutate()}>{t('loading.retry')}</Button> <Stack direction='row' spacing={2}>
<Button onClick={() => removeHistory(image)}>{t('loading.remove')}</Button> <Tooltip title={t('tooltip.retry')}>
<IconButton onClick={() => retry.mutate()}>
<Replay />
</IconButton>
</Tooltip>
<Tooltip title={t('tooltip.delete')}>
<IconButton onClick={() => removeHistory(image)}>
<Delete />
</IconButton>
</Tooltip>
</Stack>
</Stack> </Stack>
</Box> </Box>
</CardContent> </CardContent>

View File

@ -16,14 +16,13 @@ import { MaskCanvas } from '../input/MaskCanvas.js';
export function Blend() { export function Blend() {
async function uploadSource() { async function uploadSource() {
const { model, blend, upscale } = state.getState(); const { model, blend, upscale } = state.getState();
const { image, retry } = await client.blend(model, {
const output = await client.blend(model, {
...blend, ...blend,
mask: mustExist(blend.mask), mask: mustExist(blend.mask),
sources: mustExist(blend.sources), // TODO: show an error if this doesn't exist sources: mustExist(blend.sources), // TODO: show an error if this doesn't exist
}, upscale); }, upscale);
pushHistory(output); pushHistory(image, retry);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));

View File

@ -18,13 +18,12 @@ export function Img2Img() {
async function uploadSource() { async function uploadSource() {
const { model, img2img, upscale } = state.getState(); const { model, img2img, upscale } = state.getState();
const { image, retry } = await client.img2img(model, {
const output = await client.img2img(model, {
...img2img, ...img2img,
source: mustExist(img2img.source), // TODO: show an error if this doesn't exist source: mustExist(img2img.source), // TODO: show an error if this doesn't exist
}, upscale); }, upscale);
pushHistory(output); pushHistory(image, retry);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));

View File

@ -32,22 +32,22 @@ export function Inpaint() {
const { model, inpaint, outpaint, upscale } = state.getState(); const { model, inpaint, outpaint, upscale } = state.getState();
if (outpaint.enabled) { if (outpaint.enabled) {
const output = await client.outpaint(model, { const { image, retry } = await client.outpaint(model, {
...inpaint, ...inpaint,
...outpaint, ...outpaint,
mask: mustExist(mask), mask: mustExist(mask),
source: mustExist(source), source: mustExist(source),
}, upscale); }, upscale);
pushHistory(output); pushHistory(image, retry);
} else { } else {
const output = await client.inpaint(model, { const { image, retry } = await client.inpaint(model, {
...inpaint, ...inpaint,
mask: mustExist(mask), mask: mustExist(mask),
source: mustExist(source), source: mustExist(source),
}, upscale); }, upscale);
pushHistory(output); pushHistory(image, retry);
} }
} }

View File

@ -16,9 +16,9 @@ export function Txt2Img() {
async function generateImage() { async function generateImage() {
const { model, txt2img, upscale } = state.getState(); const { model, txt2img, upscale } = state.getState();
const output = await client.txt2img(model, txt2img, upscale); const { image, retry } = await client.txt2img(model, txt2img, upscale);
pushHistory(output); pushHistory(image, retry);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));

View File

@ -15,13 +15,12 @@ import { PromptInput } from '../input/PromptInput.js';
export function Upscale() { export function Upscale() {
async function uploadSource() { async function uploadSource() {
const { model, upscale } = state.getState(); const { model, upscale } = state.getState();
const { image, retry } = await client.upscale(model, {
const output = await client.upscale(model, {
...params, ...params,
source: mustExist(params.source), // TODO: show an error if this doesn't exist source: mustExist(params.source), // TODO: show an error if this doesn't exist
}, upscale); }, upscale);
pushHistory(output); pushHistory(image, retry);
} }
const client = mustExist(useContext(ClientContext)); const client = mustExist(useContext(ClientContext));

View File

@ -16,6 +16,7 @@ import {
ModelParams, ModelParams,
OutpaintPixels, OutpaintPixels,
ReadyResponse, ReadyResponse,
RetryParams,
Txt2ImgParams, Txt2ImgParams,
UpscaleParams, UpscaleParams,
UpscaleReqParams, UpscaleReqParams,
@ -30,6 +31,7 @@ type TabState<TabParams> = ConfigFiles<Required<TabParams>> & ConfigState<Requir
interface HistoryItem { interface HistoryItem {
image: ImageResponse; image: ImageResponse;
ready: Maybe<ReadyResponse>; ready: Maybe<ReadyResponse>;
retry: RetryParams;
} }
interface BrushSlice { interface BrushSlice {
@ -48,7 +50,7 @@ interface HistorySlice {
history: Array<HistoryItem>; history: Array<HistoryItem>;
limit: number; limit: number;
pushHistory(image: ImageResponse): void; pushHistory(image: ImageResponse, retry: RetryParams): void;
removeHistory(image: ImageResponse): void; removeHistory(image: ImageResponse): void;
setLimit(limit: number): void; setLimit(limit: number): void;
setReady(image: ImageResponse, ready: ReadyResponse): void; setReady(image: ImageResponse, ready: ReadyResponse): void;
@ -301,7 +303,7 @@ export function createStateSlices(server: ServerParams) {
const createHistorySlice: Slice<HistorySlice> = (set) => ({ const createHistorySlice: Slice<HistorySlice> = (set) => ({
history: [], history: [],
limit: DEFAULT_HISTORY.limit, limit: DEFAULT_HISTORY.limit,
pushHistory(image) { pushHistory(image, retry) {
set((prev) => ({ set((prev) => ({
...prev, ...prev,
history: [ history: [
@ -313,6 +315,7 @@ export function createStateSlices(server: ServerParams) {
progress: 0, progress: 0,
ready: false, ready: false,
}, },
retry,
}, },
...prev.history, ...prev.history,
], ],

View File

@ -154,6 +154,7 @@ export const I18N_STRINGS_DE = {
delete: 'Löschen', delete: 'Löschen',
next: 'Nächste', next: 'Nächste',
previous: 'Vorherige', previous: 'Vorherige',
retry: '',
save: 'Speichern', save: 'Speichern',
}, },
upscaleOrder: { upscaleOrder: {

View File

@ -217,6 +217,7 @@ export const I18N_STRINGS_EN = {
delete: 'Delete', delete: 'Delete',
next: 'Next', next: 'Next',
previous: 'Previous', previous: 'Previous',
retry: 'Retry',
save: 'Save', save: 'Save',
}, },
upscaleOrder: { upscaleOrder: {

View File

@ -154,6 +154,7 @@ export const I18N_STRINGS_ES = {
delete: 'Borrar', delete: 'Borrar',
next: 'Próximo', next: 'Próximo',
previous: 'Anterior', previous: 'Anterior',
retry: '',
save: 'Ahorrar', save: 'Ahorrar',
}, },
upscaleOrder: { upscaleOrder: {

View File

@ -154,6 +154,7 @@ export const I18N_STRINGS_FR = {
delete: '', delete: '',
next: '', next: '',
previous: '', previous: '',
retry: '',
save: '', save: '',
}, },
upscaleOrder: { upscaleOrder: {