feat(gui): persist image control state (fixes #11)
This commit is contained in:
parent
c8b2abc110
commit
07fa81a66b
|
@ -20,7 +20,8 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-query": "^3.39.2",
|
||||
"tslib": "^2.4.1"
|
||||
"tslib": "^2.4.1",
|
||||
"zustand": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mochajs/multi-reporter": "^1.1.0",
|
||||
|
|
|
@ -72,11 +72,14 @@ export interface ApiClient {
|
|||
|
||||
export const STATUS_SUCCESS = 200;
|
||||
|
||||
export function paramsFromConfig(defaults: ConfigParams): BaseImgParams {
|
||||
export function paramsFromConfig(defaults: ConfigParams): Required<BaseImgParams> {
|
||||
return {
|
||||
cfg: defaults.cfg.default,
|
||||
model: defaults.model.default,
|
||||
negativePrompt: defaults.negativePrompt.default,
|
||||
platform: defaults.platform.default,
|
||||
prompt: defaults.prompt.default,
|
||||
scheduler: defaults.scheduler.default,
|
||||
steps: defaults.steps.default,
|
||||
seed: defaults.seed.default,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { doesExist } from '@apextoaster/js-utils';
|
||||
import { doesExist, mustDefault, mustExist } from '@apextoaster/js-utils';
|
||||
import { Casino } from '@mui/icons-material';
|
||||
import { Button, Stack, TextField } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { BaseImgParams } from '../api/client.js';
|
||||
import { ConfigParams } from '../config.js';
|
||||
import { ConfigParams, STALE_TIME } from '../config.js';
|
||||
import { ClientContext } from '../main.js';
|
||||
import { SCHEDULER_LABELS } from '../strings.js';
|
||||
import { NumericField } from './NumericField.js';
|
||||
import { QueryList } from './QueryList.js';
|
||||
|
||||
const { useContext } = React;
|
||||
|
||||
export interface ImageControlProps {
|
||||
config: ConfigParams;
|
||||
|
@ -14,10 +20,33 @@ export interface ImageControlProps {
|
|||
onChange?: (params: BaseImgParams) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* doesn't need to use state, the parent component knows which params to pass
|
||||
*/
|
||||
export function ImageControl(props: ImageControlProps) {
|
||||
const { config, params } = props;
|
||||
|
||||
const client = mustExist(useContext(ClientContext));
|
||||
const schedulers = useQuery('schedulers', async () => client.schedulers(), {
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
|
||||
return <Stack spacing={2}>
|
||||
<QueryList
|
||||
id='schedulers'
|
||||
labels={SCHEDULER_LABELS}
|
||||
name='Scheduler'
|
||||
result={schedulers}
|
||||
value={mustDefault(params.scheduler, '')}
|
||||
onChange={(value) => {
|
||||
if (doesExist(props.onChange)) {
|
||||
props.onChange({
|
||||
...params,
|
||||
scheduler: value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Stack direction='row' spacing={4}>
|
||||
<NumericField
|
||||
decimal
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import { mustExist } from '@apextoaster/js-utils';
|
||||
import { Box, Button, Stack } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { ApiClient, BaseImgParams, paramsFromConfig } from '../api/client.js';
|
||||
import { ConfigParams, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||
import { SCHEDULER_LABELS } from '../strings.js';
|
||||
import { equalResponse } from '../api/client.js';
|
||||
import { ConfigParams, IMAGE_FILTER } from '../config.js';
|
||||
import { ClientContext, StateContext } from '../main.js';
|
||||
import { ImageCard } from './ImageCard.js';
|
||||
import { ImageControl } from './ImageControl.js';
|
||||
import { ImageInput } from './ImageInput.js';
|
||||
import { MutationHistory } from './MutationHistory.js';
|
||||
import { NumericField } from './NumericField.js';
|
||||
import { QueryList } from './QueryList.js';
|
||||
|
||||
const { useState } = React;
|
||||
const { useContext, useState } = React;
|
||||
|
||||
export interface Img2ImgProps {
|
||||
client: ApiClient;
|
||||
config: ConfigParams;
|
||||
|
||||
model: string;
|
||||
|
@ -24,46 +23,28 @@ export interface Img2ImgProps {
|
|||
}
|
||||
|
||||
export function Img2Img(props: Img2ImgProps) {
|
||||
const { client, config, model, platform } = props;
|
||||
const { config, model, platform } = props;
|
||||
|
||||
async function uploadSource() {
|
||||
return client.img2img({
|
||||
...params,
|
||||
...state.img2img,
|
||||
model,
|
||||
platform,
|
||||
scheduler,
|
||||
strength,
|
||||
source: mustExist(source), // TODO: show an error if this doesn't exist
|
||||
});
|
||||
}
|
||||
|
||||
const client = mustExist(useContext(ClientContext));
|
||||
const upload = useMutation(uploadSource);
|
||||
const schedulers = useQuery('schedulers', async () => client.schedulers(), {
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
const state = useStore(mustExist(useContext(StateContext)));
|
||||
|
||||
const [source, setSource] = useState<File>();
|
||||
const [strength, setStrength] = useState(config.strength.default);
|
||||
const [params, setParams] = useState<BaseImgParams>(paramsFromConfig(config));
|
||||
const [scheduler, setScheduler] = useState(config.scheduler.default);
|
||||
|
||||
return <Box>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction='row' spacing={2}>
|
||||
<QueryList
|
||||
id='schedulers'
|
||||
labels={SCHEDULER_LABELS}
|
||||
name='Scheduler'
|
||||
result={schedulers}
|
||||
value={scheduler}
|
||||
onChange={(value) => {
|
||||
setScheduler(value);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<ImageInput filter={IMAGE_FILTER} label='Source' onChange={setSource} />
|
||||
<ImageControl config={config} params={params} onChange={(newParams) => {
|
||||
setParams(newParams);
|
||||
<ImageControl config={config} params={state.img2img} onChange={(newParams) => {
|
||||
state.setImg2Img(newParams);
|
||||
}} />
|
||||
<NumericField
|
||||
decimal
|
||||
|
@ -71,14 +52,16 @@ export function Img2Img(props: Img2ImgProps) {
|
|||
min={config.strength.min}
|
||||
max={config.strength.max}
|
||||
step={config.strength.step}
|
||||
value={strength}
|
||||
value={state.img2img.strength}
|
||||
onChange={(value) => {
|
||||
setStrength(value);
|
||||
state.setImg2Img({
|
||||
strength: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => upload.mutate()}>Generate</Button>
|
||||
<MutationHistory result={upload} limit={4} element={ImageCard}
|
||||
isEqual={(a, b) => a.output === b.output}
|
||||
isEqual={equalResponse}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>;
|
||||
|
|
|
@ -2,19 +2,19 @@ import { doesExist, mustExist } from '@apextoaster/js-utils';
|
|||
import { FormatColorFill, Gradient } from '@mui/icons-material';
|
||||
import { Box, Button, Stack } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { ApiClient, ApiResponse, BaseImgParams, equalResponse, paramsFromConfig } from '../api/client.js';
|
||||
import { ConfigParams, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||
import { SCHEDULER_LABELS } from '../strings.js';
|
||||
import { ApiResponse, equalResponse } from '../api/client.js';
|
||||
import { ConfigParams, DEFAULT_BRUSH, IMAGE_FILTER } from '../config.js';
|
||||
import { ClientContext, StateContext } from '../main.js';
|
||||
import { ImageCard } from './ImageCard.js';
|
||||
import { ImageControl } from './ImageControl.js';
|
||||
import { ImageInput } from './ImageInput.js';
|
||||
import { MutationHistory } from './MutationHistory.js';
|
||||
import { NumericField } from './NumericField.js';
|
||||
import { QueryList } from './QueryList.js';
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
const { useContext, useEffect, useRef, useState } = React;
|
||||
|
||||
export const FULL_CIRCLE = 2 * Math.PI;
|
||||
export const PIXEL_SIZE = 4;
|
||||
|
@ -46,6 +46,10 @@ export function floodAbove(n: number): number {
|
|||
}
|
||||
}
|
||||
|
||||
export function floodGray(n: number): number {
|
||||
return n;
|
||||
}
|
||||
|
||||
export function grayToRGB(n: number): string {
|
||||
return `rgb(${n.toFixed(0)},${n.toFixed(0)},${n.toFixed(0)})`;
|
||||
}
|
||||
|
@ -56,7 +60,6 @@ export interface Point {
|
|||
}
|
||||
|
||||
export interface InpaintProps {
|
||||
client: ApiClient;
|
||||
config: ConfigParams;
|
||||
|
||||
model: string;
|
||||
|
@ -64,17 +67,17 @@ export interface InpaintProps {
|
|||
}
|
||||
|
||||
export function Inpaint(props: InpaintProps) {
|
||||
const { client, config, model, platform } = props;
|
||||
const { config, model, platform } = props;
|
||||
const client = mustExist(useContext(ClientContext));
|
||||
|
||||
async function uploadSource() {
|
||||
const canvas = mustExist(canvasRef.current);
|
||||
return new Promise<ApiResponse>((res, _rej) => {
|
||||
canvas.toBlob((blob) => {
|
||||
res(client.inpaint({
|
||||
...params,
|
||||
...state.inpaint,
|
||||
model,
|
||||
platform,
|
||||
scheduler,
|
||||
mask: mustExist(blob),
|
||||
source: mustExist(source),
|
||||
}));
|
||||
|
@ -84,13 +87,14 @@ export function Inpaint(props: InpaintProps) {
|
|||
|
||||
function drawSource(file: File) {
|
||||
const image = new Image();
|
||||
const src = URL.createObjectURL(file);
|
||||
image.onload = () => {
|
||||
const canvas = mustExist(canvasRef.current);
|
||||
const ctx = mustExist(canvas.getContext('2d'));
|
||||
ctx.drawImage(image, 0, 0);
|
||||
URL.revokeObjectURL(src);
|
||||
};
|
||||
|
||||
const src = URL.createObjectURL(file);
|
||||
image.src = src;
|
||||
}
|
||||
|
||||
|
@ -110,25 +114,6 @@ export function Inpaint(props: InpaintProps) {
|
|||
}
|
||||
}
|
||||
|
||||
function grayscaleMask() {
|
||||
const canvas = mustExist(canvasRef.current);
|
||||
const ctx = mustExist(canvas.getContext('2d'));
|
||||
const image = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const pixels = image.data;
|
||||
|
||||
for (let x = 0; x < canvas.width; ++x) {
|
||||
for (let y = 0; y < canvas.height; ++y) {
|
||||
const i = (y * canvas.width * PIXEL_SIZE) + (x * PIXEL_SIZE);
|
||||
const hue = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / PIXEL_WEIGHT;
|
||||
pixels[i] = hue;
|
||||
pixels[i + 1] = hue;
|
||||
pixels[i + 2] = hue;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(image, 0, 0);
|
||||
}
|
||||
|
||||
function floodMask(flooder: (n: number) => number) {
|
||||
const canvas = mustExist(canvasRef.current);
|
||||
const ctx = mustExist(canvas.getContext('2d'));
|
||||
|
@ -151,22 +136,19 @@ export function Inpaint(props: InpaintProps) {
|
|||
}
|
||||
|
||||
const upload = useMutation(uploadSource);
|
||||
const schedulers = useQuery('schedulers', async () => client.schedulers(), {
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [clicks, setClicks] = useState<Array<Point>>([]);
|
||||
const state = useStore(mustExist(useContext(StateContext)));
|
||||
|
||||
// painting state
|
||||
const [clicks, setClicks] = useState<Array<Point>>([]);
|
||||
const [painting, setPainting] = useState(false);
|
||||
const [brushColor, setBrushColor] = useState(DEFAULT_BRUSH.color);
|
||||
const [brushSize, setBrushSize] = useState(DEFAULT_BRUSH.size);
|
||||
|
||||
// image state
|
||||
const [mask, setMask] = useState<File>();
|
||||
const [source, setSource] = useState<File>();
|
||||
const [params, setParams] = useState<BaseImgParams>(paramsFromConfig(config));
|
||||
const [scheduler, setScheduler] = useState(config.scheduler.default);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = mustExist(canvasRef.current);
|
||||
|
@ -228,18 +210,6 @@ export function Inpaint(props: InpaintProps) {
|
|||
|
||||
return <Box>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction='row' spacing={2}>
|
||||
<QueryList
|
||||
id='schedulers'
|
||||
labels={SCHEDULER_LABELS}
|
||||
name='Scheduler'
|
||||
result={schedulers}
|
||||
value={scheduler}
|
||||
onChange={(value) => {
|
||||
setScheduler(value);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<ImageInput filter={IMAGE_FILTER} label='Source' onChange={changeSource} />
|
||||
<ImageInput filter={IMAGE_FILTER} label='Mask' onChange={changeMask} renderImage={renderCanvas} />
|
||||
<Stack direction='row' spacing={4}>
|
||||
|
@ -274,7 +244,7 @@ export function Inpaint(props: InpaintProps) {
|
|||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<Gradient />}
|
||||
onClick={() => grayscaleMask()}>
|
||||
onClick={() => floodMask(floodGray)}>
|
||||
Grayscale
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -284,9 +254,13 @@ export function Inpaint(props: InpaintProps) {
|
|||
Gray to white
|
||||
</Button>
|
||||
</Stack>
|
||||
<ImageControl config={config} params={params} onChange={(newParams) => {
|
||||
setParams(newParams);
|
||||
}} />
|
||||
<ImageControl
|
||||
config={config}
|
||||
params={state.inpaint}
|
||||
onChange={(newParams) => {
|
||||
state.setInpaint(newParams);
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => upload.mutate()}>Generate</Button>
|
||||
<MutationHistory result={upload} limit={4} element={ImageCard}
|
||||
isEqual={equalResponse}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { mustExist } from '@apextoaster/js-utils';
|
||||
import { TabContext, TabList, TabPanel } from '@mui/lab';
|
||||
import { Box, Container, Stack, Tab, Typography } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
|
@ -5,13 +6,15 @@ import { useQuery } from 'react-query';
|
|||
|
||||
import { ApiClient } from '../api/client.js';
|
||||
import { ConfigParams, STALE_TIME } from '../config.js';
|
||||
import { ClientContext } from '../main.js';
|
||||
import { MODEL_LABELS, PLATFORM_LABELS } from '../strings.js';
|
||||
import { Img2Img } from './Img2Img.js';
|
||||
import { Inpaint } from './Inpaint.js';
|
||||
import { QueryList } from './QueryList.js';
|
||||
import { Settings } from './Settings.js';
|
||||
import { Txt2Img } from './Txt2Img.js';
|
||||
|
||||
const { useState } = React;
|
||||
const { useContext, useState } = React;
|
||||
|
||||
export interface OnnxWebProps {
|
||||
client: ApiClient;
|
||||
|
@ -19,8 +22,9 @@ export interface OnnxWebProps {
|
|||
}
|
||||
|
||||
export function OnnxWeb(props: OnnxWebProps) {
|
||||
const { client, config } = props;
|
||||
const { config } = props;
|
||||
|
||||
const client = mustExist(useContext(ClientContext));
|
||||
const [tab, setTab] = useState('txt2img');
|
||||
const [model, setModel] = useState(config.model.default);
|
||||
const [platform, setPlatform] = useState(config.platform.default);
|
||||
|
@ -76,18 +80,16 @@ export function OnnxWeb(props: OnnxWebProps) {
|
|||
</TabList>
|
||||
</Box>
|
||||
<TabPanel value='txt2img'>
|
||||
<Txt2Img client={client} config={config} model={model} platform={platform} />
|
||||
<Txt2Img config={config} model={model} platform={platform} />
|
||||
</TabPanel>
|
||||
<TabPanel value='img2img'>
|
||||
<Img2Img client={client} config={config} model={model} platform={platform} />
|
||||
<Img2Img config={config} model={model} platform={platform} />
|
||||
</TabPanel>
|
||||
<TabPanel value='inpaint'>
|
||||
<Inpaint client={client} config={config} model={model} platform={platform} />
|
||||
<Inpaint config={config} model={model} platform={platform} />
|
||||
</TabPanel>
|
||||
<TabPanel value='settings'>
|
||||
<Box>
|
||||
settings for onnx-web
|
||||
</Box>
|
||||
<Settings config={config} />
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Container>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { mustExist } from '@apextoaster/js-utils';
|
||||
import { Button, Stack, TextField } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { ConfigParams } from '../config.js';
|
||||
import { StateContext } from '../main.js';
|
||||
|
||||
const { useContext } = React;
|
||||
|
||||
export interface SettingsProps {
|
||||
config: ConfigParams;
|
||||
}
|
||||
|
||||
export function Settings(_props: SettingsProps) {
|
||||
const state = useStore(mustExist(useContext(StateContext)));
|
||||
|
||||
return <Stack spacing={2}>
|
||||
<Stack direction='row' spacing={2}>
|
||||
<Button onClick={() => state.resetTxt2Img()}>Reset Txt2Img</Button>
|
||||
<Button onClick={() => state.resetImg2Img()}>Reset Img2Img</Button>
|
||||
<Button onClick={() => state.resetInpaint()}>Reset Inpaint</Button>
|
||||
<Button disabled>Reset All</Button>
|
||||
</Stack>
|
||||
<TextField variant='outlined' label='Default Model' value={state.defaults.model} onChange={(event) => {
|
||||
state.setDefaults({
|
||||
model: event.target.value,
|
||||
});
|
||||
}} />
|
||||
<TextField variant='outlined' label='Default Platform' value={state.defaults.platform} onChange={(event) => {
|
||||
state.setDefaults({
|
||||
platform: event.target.value,
|
||||
});
|
||||
}} />
|
||||
<TextField variant='outlined' label='Default Prompt' value={state.defaults.prompt} onChange={(event) => {
|
||||
state.setDefaults({
|
||||
prompt: event.target.value,
|
||||
});
|
||||
}} />
|
||||
<TextField variant='outlined' label='Default Scheduler' value={state.defaults.scheduler} onChange={(event) => {
|
||||
state.setDefaults({
|
||||
scheduler: event.target.value,
|
||||
});
|
||||
}} />
|
||||
</Stack>;
|
||||
}
|
|
@ -1,20 +1,20 @@
|
|||
import { mustExist } from '@apextoaster/js-utils';
|
||||
import { Box, Button, Stack } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { ApiClient, BaseImgParams, paramsFromConfig } from '../api/client.js';
|
||||
import { ConfigParams, STALE_TIME } from '../config.js';
|
||||
import { SCHEDULER_LABELS } from '../strings.js';
|
||||
import { BaseImgParams, equalResponse, paramsFromConfig } from '../api/client.js';
|
||||
import { ConfigParams } from '../config.js';
|
||||
import { ClientContext, StateContext } from '../main.js';
|
||||
import { ImageCard } from './ImageCard.js';
|
||||
import { ImageControl } from './ImageControl.js';
|
||||
import { MutationHistory } from './MutationHistory.js';
|
||||
import { NumericField } from './NumericField.js';
|
||||
import { QueryList } from './QueryList.js';
|
||||
|
||||
const { useState } = React;
|
||||
const { useContext, useState } = React;
|
||||
|
||||
export interface Txt2ImgProps {
|
||||
client: ApiClient;
|
||||
config: ConfigParams;
|
||||
|
||||
model: string;
|
||||
|
@ -22,45 +22,24 @@ export interface Txt2ImgProps {
|
|||
}
|
||||
|
||||
export function Txt2Img(props: Txt2ImgProps) {
|
||||
const { client, config, model, platform } = props;
|
||||
const { config, model, platform } = props;
|
||||
|
||||
async function generateImage() {
|
||||
return client.txt2img({
|
||||
...params,
|
||||
...state.txt2img,
|
||||
model,
|
||||
platform,
|
||||
scheduler,
|
||||
height,
|
||||
width,
|
||||
});
|
||||
}
|
||||
|
||||
const client = mustExist(useContext(ClientContext));
|
||||
const generate = useMutation(generateImage);
|
||||
const schedulers = useQuery('schedulers', async () => client.schedulers(), {
|
||||
staleTime: STALE_TIME,
|
||||
});
|
||||
|
||||
const [height, setHeight] = useState(config.height.default);
|
||||
const [width, setWidth] = useState(config.width.default);
|
||||
const [params, setParams] = useState<BaseImgParams>(paramsFromConfig(config));
|
||||
const [scheduler, setScheduler] = useState(config.scheduler.default);
|
||||
const state = useStore(mustExist(useContext(StateContext)));
|
||||
|
||||
return <Box>
|
||||
<Stack spacing={2}>
|
||||
<Stack direction='row' spacing={2}>
|
||||
<QueryList
|
||||
id='schedulers'
|
||||
labels={SCHEDULER_LABELS}
|
||||
name='Scheduler'
|
||||
result={schedulers}
|
||||
value={scheduler}
|
||||
onChange={(value) => {
|
||||
setScheduler(value);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<ImageControl config={config} params={params} onChange={(newParams) => {
|
||||
setParams(newParams);
|
||||
<ImageControl config={config} params={state.txt2img} onChange={(newParams) => {
|
||||
state.setTxt2Img(newParams);
|
||||
}} />
|
||||
<Stack direction='row' spacing={4}>
|
||||
<NumericField
|
||||
|
@ -68,9 +47,11 @@ export function Txt2Img(props: Txt2ImgProps) {
|
|||
min={config.width.min}
|
||||
max={config.width.max}
|
||||
step={config.width.step}
|
||||
value={width}
|
||||
value={state.txt2img.width}
|
||||
onChange={(value) => {
|
||||
setWidth(value);
|
||||
state.setTxt2Img({
|
||||
width: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<NumericField
|
||||
|
@ -78,15 +59,20 @@ export function Txt2Img(props: Txt2ImgProps) {
|
|||
min={config.height.min}
|
||||
max={config.height.max}
|
||||
step={config.height.step}
|
||||
value={height}
|
||||
value={state.txt2img.height}
|
||||
onChange={(value) => {
|
||||
setHeight(value);
|
||||
state.setTxt2Img({
|
||||
height: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Button onClick={() => generate.mutate()}>Generate</Button>
|
||||
<MutationHistory result={generate} limit={4} element={ImageCard}
|
||||
isEqual={(a, b) => a.output === b.output}
|
||||
<MutationHistory
|
||||
element={ImageCard}
|
||||
limit={4}
|
||||
isEqual={equalResponse}
|
||||
result={generate}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>;
|
||||
|
|
|
@ -20,6 +20,10 @@ export type ConfigRanges<T extends object> = {
|
|||
[K in KeyFilter<T>]: T[K] extends number ? ConfigNumber : T[K] extends string ? ConfigString : never;
|
||||
};
|
||||
|
||||
export type ConfigState<T extends object> = {
|
||||
[K in KeyFilter<T>]: T[K] extends number ? number : T[K] extends string ? string : never;
|
||||
};
|
||||
|
||||
export type ConfigParams = ConfigRanges<Required<Img2ImgParams & Txt2ImgParams>>;
|
||||
|
||||
export interface Config {
|
||||
|
|
118
gui/src/main.tsx
118
gui/src/main.tsx
|
@ -1,13 +1,33 @@
|
|||
/* eslint-disable no-console */
|
||||
import { mustExist } from '@apextoaster/js-utils';
|
||||
import { Maybe, mustExist } from '@apextoaster/js-utils';
|
||||
import { merge } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { createStore, StoreApi } from 'zustand';
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
|
||||
import { makeClient } from './api/client.js';
|
||||
import { ApiClient, BaseImgParams, Img2ImgParams, InpaintParams, makeClient, paramsFromConfig, Txt2ImgParams } from './api/client.js';
|
||||
import { OnnxWeb } from './components/OnnxWeb.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { ConfigState, loadConfig } from './config.js';
|
||||
|
||||
const { createContext } = React;
|
||||
|
||||
interface OnnxState {
|
||||
defaults: Required<BaseImgParams>;
|
||||
txt2img: ConfigState<Required<Txt2ImgParams>>;
|
||||
img2img: ConfigState<Required<Img2ImgParams>>;
|
||||
inpaint: ConfigState<Required<InpaintParams>>;
|
||||
|
||||
setDefaults(newParams: Partial<BaseImgParams>): void;
|
||||
setTxt2Img(newParams: Partial<ConfigState<Required<Txt2ImgParams>>>): void;
|
||||
setImg2Img(newParams: Partial<ConfigState<Required<Img2ImgParams>>>): void;
|
||||
setInpaint(newParams: Partial<ConfigState<Required<InpaintParams>>>): void;
|
||||
|
||||
resetTxt2Img(): void;
|
||||
resetImg2Img(): void;
|
||||
resetInpaint(): void;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const config = await loadConfig();
|
||||
|
@ -15,11 +35,98 @@ export async function main() {
|
|||
const params = await client.params();
|
||||
merge(params, config.params);
|
||||
|
||||
const defaults = paramsFromConfig(params);
|
||||
const state = createStore<OnnxState, [['zustand/persist', never]]>(persist((set) => ({
|
||||
defaults,
|
||||
txt2img: {
|
||||
...defaults,
|
||||
height: params.height.default,
|
||||
width: params.width.default,
|
||||
},
|
||||
img2img: {
|
||||
...defaults,
|
||||
strength: params.strength.default,
|
||||
},
|
||||
inpaint: {
|
||||
...defaults,
|
||||
},
|
||||
setDefaults(newParams) {
|
||||
set((oldState) => ({
|
||||
...oldState,
|
||||
defaults: {
|
||||
...oldState.defaults,
|
||||
...newParams,
|
||||
},
|
||||
}));
|
||||
},
|
||||
setTxt2Img(newParams) {
|
||||
set((oldState) => ({
|
||||
...oldState,
|
||||
txt2img: {
|
||||
...oldState.txt2img,
|
||||
...newParams,
|
||||
},
|
||||
}));
|
||||
},
|
||||
setImg2Img(newParams) {
|
||||
set((oldState) => ({
|
||||
...oldState,
|
||||
img2img: {
|
||||
...oldState.img2img,
|
||||
...newParams,
|
||||
},
|
||||
}));
|
||||
},
|
||||
setInpaint(newParams) {
|
||||
set((oldState) => ({
|
||||
...oldState,
|
||||
inpaint: {
|
||||
...oldState.inpaint,
|
||||
...newParams,
|
||||
},
|
||||
}));
|
||||
},
|
||||
resetTxt2Img() {
|
||||
set((oldState) => ({
|
||||
...oldState,
|
||||
txt2img: {
|
||||
...defaults,
|
||||
height: params.height.default,
|
||||
width: params.width.default,
|
||||
},
|
||||
}));
|
||||
},
|
||||
resetImg2Img() {
|
||||
set((oldState) => ({
|
||||
...oldState,
|
||||
img2img: {
|
||||
...defaults,
|
||||
strength: params.strength.default,
|
||||
},
|
||||
}));
|
||||
},
|
||||
resetInpaint() {
|
||||
set((oldState) => ({
|
||||
...oldState,
|
||||
inpaint: {
|
||||
...defaults,
|
||||
},
|
||||
}));
|
||||
},
|
||||
}), {
|
||||
name: 'onnx-web',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}));
|
||||
|
||||
const query = new QueryClient();
|
||||
const appElement = mustExist(document.getElementById('app'));
|
||||
const app = ReactDOM.createRoot(appElement);
|
||||
app.render(<QueryClientProvider client={query}>
|
||||
<OnnxWeb client={client} config={params} />
|
||||
<ClientContext.Provider value={client}>
|
||||
<StateContext.Provider value={state}>
|
||||
<OnnxWeb client={client} config={params} />
|
||||
</StateContext.Provider>
|
||||
</ClientContext.Provider>
|
||||
</QueryClientProvider>);
|
||||
}
|
||||
|
||||
|
@ -29,3 +136,6 @@ window.addEventListener('load', () => {
|
|||
console.error('error in main', err);
|
||||
});
|
||||
}, false);
|
||||
|
||||
export const ClientContext = createContext<Maybe<ApiClient>>(undefined);
|
||||
export const StateContext = createContext<Maybe<StoreApi<OnnxState>>>(undefined);
|
||||
|
|
|
@ -2799,6 +2799,11 @@ uri-js@^4.2.2:
|
|||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-sync-external-store@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
v8-to-istanbul@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4"
|
||||
|
@ -2919,3 +2924,10 @@ yocto-queue@^0.1.0:
|
|||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zustand@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.1.tgz#76c47ef713c43763953f7a9e518f89efd898e3bb"
|
||||
integrity sha512-EVyo/eLlOTcJm/X5M00rwtbYFXwRVTaRteSvhtbTZUCQFJkNfIyHPiJ6Ke68MSWzcKHpPzvqNH4gC2ZS/sbNqw==
|
||||
dependencies:
|
||||
use-sync-external-store "1.2.0"
|
||||
|
|
Loading…
Reference in New Issue