1
0
Fork 0

feat(gui): load and merge server params with config

This commit is contained in:
Sean Sube 2023-01-09 22:59:08 -06:00
parent 03fd728ab0
commit 37efd51341
11 changed files with 146 additions and 146 deletions

View File

@ -2,10 +2,18 @@
"api": {
"root": "http://127.0.0.1:5000"
},
"default": {
"model": "stable-diffusion-onnx-v1-5",
"platform": "amd",
"scheduler": "euler-a",
"prompt": "an astronaut eating a hamburger"
"params": {
"model": {
"default": "stable-diffusion-onnx-v1-5"
},
"platform": {
"default": "amd"
},
"scheduler": {
"default": "euler-a"
},
"prompt": {
"default": "an astronaut eating a hamburger"
}
}
}

View File

@ -13,7 +13,9 @@
"@mui/icons-material": "^5.11.0",
"@mui/lab": "^5.0.0-alpha.114",
"@mui/material": "^5.11.3",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"lodash": "^4.17.21",
"noicejs": "^5.0.0-3",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@ -1,4 +1,5 @@
import { doesExist, NotImplementedError } from '@apextoaster/js-utils';
import { ConfigParams } from '../config';
export interface BaseImgParams {
/**
@ -57,6 +58,7 @@ export interface ApiResponse {
export interface ApiClient {
models(): Promise<Array<string>>;
params(): Promise<ConfigParams>;
platforms(): Promise<Array<string>>;
schedulers(): Promise<Array<string>>;
@ -136,6 +138,11 @@ export function makeClient(root: string, f = fetch): ApiClient {
const res = await f(path);
return await res.json() as Array<string>;
},
async params(): Promise<ConfigParams> {
const path = new URL(joinPath('settings', 'params'), root);
const res = await f(path);
return await res.json() as ConfigParams;
},
async schedulers(): Promise<Array<string>> {
const path = new URL(joinPath('settings', 'schedulers'), root);
const res = await f(path);

View File

@ -1,28 +1,30 @@
import { doesExist } from '@apextoaster/js-utils';
import { Casino } from '@mui/icons-material';
import { IconButton, Stack, TextField } from '@mui/material';
import { Button, Stack, TextField } from '@mui/material';
import * as React from 'react';
import { BaseImgParams } from '../api/client.js';
import { CONFIG_DEFAULTS } from '../config.js';
import { ConfigParams } from '../config.js';
import { NumericField } from './NumericField.js';
export interface ImageControlProps {
config: ConfigParams;
params: BaseImgParams;
onChange?: (params: BaseImgParams) => void;
}
export function ImageControl(props: ImageControlProps) {
const { params } = props;
const { config, params } = props;
return <Stack spacing={2}>
<Stack direction='row' spacing={4}>
<NumericField
decimal
label='CFG'
min={CONFIG_DEFAULTS.cfg.min}
max={CONFIG_DEFAULTS.cfg.max}
step={CONFIG_DEFAULTS.cfg.step}
min={config.cfg.min}
max={config.cfg.max}
step={config.cfg.step}
value={params.cfg}
onChange={(cfg) => {
if (doesExist(props.onChange)) {
@ -35,9 +37,9 @@ export function ImageControl(props: ImageControlProps) {
/>
<NumericField
label='Steps'
min={CONFIG_DEFAULTS.steps.min}
max={CONFIG_DEFAULTS.steps.max}
step={CONFIG_DEFAULTS.steps.step}
min={config.steps.min}
max={config.steps.max}
step={config.steps.step}
value={params.steps}
onChange={(steps) => {
if (doesExist(props.onChange)) {
@ -50,9 +52,9 @@ export function ImageControl(props: ImageControlProps) {
/>
<NumericField
label='Seed'
min={CONFIG_DEFAULTS.seed.min}
max={CONFIG_DEFAULTS.seed.max}
step={CONFIG_DEFAULTS.seed.step}
min={config.seed.min}
max={config.seed.max}
step={config.seed.step}
value={params.seed}
onChange={(seed) => {
if (doesExist(props.onChange)) {
@ -63,17 +65,21 @@ export function ImageControl(props: ImageControlProps) {
}
}}
/>
<IconButton onClick={() => {
const seed = Math.floor(Math.random() * CONFIG_DEFAULTS.seed.max);
<Button
variant='outlined'
startIcon={<Casino />}
onClick={() => {
const seed = Math.floor(Math.random() * config.seed.max);
if (doesExist(props.onChange)) {
props.onChange({
...params,
seed,
});
}
}}>
<Casino />
</IconButton>
}}
>
New Seed
</Button>
</Stack>
<TextField label='Prompt' variant='outlined' value={params.prompt} onChange={(event) => {
if (doesExist(props.onChange)) {

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { useMutation, useQuery } from 'react-query';
import { ApiClient, BaseImgParams } from '../api/client.js';
import { Config, CONFIG_DEFAULTS, IMAGE_FILTER, STALE_TIME } from '../config.js';
import { ConfigParams, IMAGE_FILTER, STALE_TIME } from '../config.js';
import { SCHEDULER_LABELS } from '../strings.js';
import { ImageInput } from './ImageInput.js';
import { ImageCard } from './ImageCard.js';
@ -17,7 +17,7 @@ const { useState } = React;
export interface Img2ImgProps {
client: ApiClient;
config: Config;
config: ConfigParams;
model: string;
platform: string;
@ -43,14 +43,14 @@ export function Img2Img(props: Img2ImgProps) {
});
const [source, setSource] = useState<File>();
const [strength, setStrength] = useState(CONFIG_DEFAULTS.strength.default);
const [strength, setStrength] = useState(config.strength.default);
const [params, setParams] = useState<BaseImgParams>({
cfg: CONFIG_DEFAULTS.cfg.default,
seed: CONFIG_DEFAULTS.seed.default,
steps: CONFIG_DEFAULTS.steps.default,
prompt: config.default.prompt,
cfg: config.cfg.default,
seed: config.seed.default,
steps: config.steps.default,
prompt: config.prompt.default,
});
const [scheduler, setScheduler] = useState(config.default.scheduler);
const [scheduler, setScheduler] = useState(config.scheduler.default);
return <Box>
<Stack spacing={2}>
@ -67,15 +67,15 @@ export function Img2Img(props: Img2ImgProps) {
/>
</Stack>
<ImageInput filter={IMAGE_FILTER} label='Source' onChange={setSource} />
<ImageControl params={params} onChange={(newParams) => {
<ImageControl config={config} params={params} onChange={(newParams) => {
setParams(newParams);
}} />
<NumericField
decimal
label='Strength'
min={CONFIG_DEFAULTS.strength.min}
max={CONFIG_DEFAULTS.strength.max}
step={CONFIG_DEFAULTS.strength.step}
min={config.strength.min}
max={config.strength.max}
step={config.strength.step}
value={strength}
onChange={(value) => {
setStrength(value);

View File

@ -5,7 +5,7 @@ import * as React from 'react';
import { useMutation, useQuery } from 'react-query';
import { ApiClient, ApiResponse, BaseImgParams, equalResponse } from '../api/client.js';
import { Config, CONFIG_DEFAULTS, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js';
import { Config, ConfigParams, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js';
import { SCHEDULER_LABELS } from '../strings.js';
import { ImageInput } from './ImageInput.js';
import { ImageCard } from './ImageCard.js';
@ -57,7 +57,7 @@ export interface Point {
export interface InpaintProps {
client: ApiClient;
config: Config;
config: ConfigParams;
model: string;
platform: string;
@ -166,12 +166,12 @@ export function Inpaint(props: InpaintProps) {
const [mask, setMask] = useState<File>();
const [source, setSource] = useState<File>();
const [params, setParams] = useState<BaseImgParams>({
cfg: CONFIG_DEFAULTS.cfg.default,
seed: CONFIG_DEFAULTS.seed.default,
steps: CONFIG_DEFAULTS.steps.default,
prompt: config.default.prompt,
cfg: config.cfg.default,
seed: config.seed.default,
steps: config.steps.default,
prompt: config.prompt.default,
});
const [scheduler, setScheduler] = useState(config.default.scheduler);
const [scheduler, setScheduler] = useState(config.scheduler.default);
useEffect(() => {
const canvas = mustExist(canvasRef.current);
@ -190,11 +190,11 @@ export function Inpaint(props: InpaintProps) {
function renderCanvas() {
return <canvas
ref={canvasRef}
height={CONFIG_DEFAULTS.height.default}
width={CONFIG_DEFAULTS.width.default}
height={config.height.default}
width={config.width.default}
style={{
maxHeight: CONFIG_DEFAULTS.height.default,
maxWidth: CONFIG_DEFAULTS.width.default,
maxHeight: config.height.default,
maxWidth: config.width.default,
}}
onClick={(event) => {
const canvas = mustExist(canvasRef.current);
@ -271,22 +271,25 @@ export function Inpaint(props: InpaintProps) {
}}
/>
<Button
variant='outlined'
startIcon={<FormatColorFill />}
onClick={() => floodMask(floodBelow)}>
Gray to black
</Button>
<Button
variant='outlined'
startIcon={<Gradient />}
onClick={() => grayscaleMask()}>
Grayscale
</Button>
<Button
variant='outlined'
startIcon={<FormatColorFill />}
onClick={() => floodMask(floodAbove)}>
Gray to white
</Button>
</Stack>
<ImageControl params={params} onChange={(newParams) => {
<ImageControl config={config} params={params} onChange={(newParams) => {
setParams(newParams);
}} />
<Button onClick={() => upload.mutate()}>Generate</Button>

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { useQuery } from 'react-query';
import { ApiClient } from '../api/client.js';
import { Config, STALE_TIME } from '../config.js';
import { Config, ConfigParams, STALE_TIME } from '../config.js';
import { MODEL_LABELS, PLATFORM_LABELS } from '../strings.js';
import { Img2Img } from './Img2Img.js';
import { Inpaint } from './Inpaint.js';
@ -15,15 +15,15 @@ const { useState } = React;
export interface OnnxWebProps {
client: ApiClient;
config: Config;
config: ConfigParams;
}
export function OnnxWeb(props: OnnxWebProps) {
const { client, config } = props;
const [tab, setTab] = useState('txt2img');
const [model, setModel] = useState(config.default.model);
const [platform, setPlatform] = useState(config.default.platform);
const [model, setModel] = useState(config.model.default);
const [platform, setPlatform] = useState(config.platform.default);
const models = useQuery('models', async () => client.models(), {
staleTime: STALE_TIME,

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { useMutation, useQuery } from 'react-query';
import { ApiClient, BaseImgParams } from '../api/client.js';
import { Config, CONFIG_DEFAULTS, STALE_TIME } from '../config.js';
import { ConfigParams, STALE_TIME } from '../config.js';
import { SCHEDULER_LABELS } from '../strings.js';
import { ImageCard } from './ImageCard.js';
import { ImageControl } from './ImageControl.js';
@ -15,7 +15,7 @@ const { useState } = React;
export interface Txt2ImgProps {
client: ApiClient;
config: Config;
config: ConfigParams;
model: string;
platform: string;
@ -40,15 +40,15 @@ export function Txt2Img(props: Txt2ImgProps) {
staleTime: STALE_TIME,
});
const [height, setHeight] = useState(CONFIG_DEFAULTS.height.default);
const [width, setWidth] = useState(CONFIG_DEFAULTS.width.default);
const [height, setHeight] = useState(config.height.default);
const [width, setWidth] = useState(config.width.default);
const [params, setParams] = useState<BaseImgParams>({
cfg: CONFIG_DEFAULTS.cfg.default,
seed: CONFIG_DEFAULTS.seed.default,
steps: CONFIG_DEFAULTS.steps.default,
prompt: config.default.prompt,
cfg: config.cfg.default,
seed: config.seed.default,
steps: config.steps.default,
prompt: config.prompt.default,
});
const [scheduler, setScheduler] = useState(config.default.scheduler);
const [scheduler, setScheduler] = useState(config.scheduler.default);
return <Box>
<Stack spacing={2}>
@ -64,15 +64,15 @@ export function Txt2Img(props: Txt2ImgProps) {
}}
/>
</Stack>
<ImageControl params={params} onChange={(newParams) => {
<ImageControl config={config} params={params} onChange={(newParams) => {
setParams(newParams);
}} />
<Stack direction='row' spacing={4}>
<NumericField
label='Width'
min={CONFIG_DEFAULTS.width.min}
max={CONFIG_DEFAULTS.width.max}
step={CONFIG_DEFAULTS.width.step}
min={config.width.min}
max={config.width.max}
step={config.width.step}
value={width}
onChange={(value) => {
setWidth(value);
@ -80,9 +80,9 @@ export function Txt2Img(props: Txt2ImgProps) {
/>
<NumericField
label='Height'
min={CONFIG_DEFAULTS.height.min}
max={CONFIG_DEFAULTS.height.max}
step={CONFIG_DEFAULTS.height.step}
min={config.height.min}
max={config.height.max}
step={config.height.step}
value={height}
onChange={(value) => {
setHeight(value);

View File

@ -1,17 +1,45 @@
import { Img2ImgParams, STATUS_SUCCESS, Txt2ImgParams } from './api/client.js';
export interface ConfigNumber {
default: number;
min: number;
max: number;
step: number;
}
export interface ConfigString {
default: string;
keys: Array<string>;
}
export type KeyFilter<T extends object> = {
[K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never;
}[keyof T];
export type ConfigRanges<T extends object> = {
[K in KeyFilter<T>]: T[K] extends number ? ConfigNumber : T[K] extends string ? ConfigString : never;
};
export type ConfigParams = ConfigRanges<Required<Img2ImgParams & Txt2ImgParams>>;
export interface Config {
api: {
root: string;
};
default: {
model: string;
platform: string;
scheduler: string;
prompt: string;
params: {
model: ConfigString;
platform: ConfigString;
scheduler: ConfigString;
prompt: ConfigString;
};
}
export const DEFAULT_BRUSH = 8;
export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png';
export const IMAGE_STEP = 8;
export const IMAGE_MAX = 512;
export const STALE_TIME = 3_000;
export async function loadConfig(): Promise<Config> {
const configPath = new URL('./config.json', window.origin);
const configReq = await fetch(configPath);
@ -21,70 +49,3 @@ export async function loadConfig(): Promise<Config> {
throw new Error('could not load config');
}
}
export interface ConfigRange {
default: number;
min: number;
max: number;
step: number;
}
export type KeyFilter<T extends object> = {
[K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never;
}[keyof T];
export type ConfigRanges<T extends object> = {
[K in KeyFilter<T>]: T[K] extends number ? ConfigRange : T[K] extends string ? string : never;
};
export const DEFAULT_BRUSH = 8;
export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png';
export const IMAGE_STEP = 8;
export const IMAGE_MAX = 512;
export const CONFIG_DEFAULTS: ConfigRanges<Required<Img2ImgParams & Txt2ImgParams>> = {
cfg: {
default: 6,
min: 1,
max: 30,
step: 0.1,
},
height: {
default: IMAGE_MAX,
min: IMAGE_STEP,
max: IMAGE_MAX,
step: IMAGE_STEP,
},
model: '',
negativePrompt: '',
platform: '',
prompt: 'an astronaut eating a hamburger',
scheduler: '',
steps: {
default: 25,
min: 1,
max: 200,
step: 1,
},
seed: {
default: -1,
min: -1,
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
max: (2 ** 32) - 1,
step: 1,
},
strength: {
default: 0.5,
min: 0,
max: 1,
step: 0.01,
},
width: {
default: IMAGE_MAX,
min: IMAGE_STEP,
max: IMAGE_MAX,
step: IMAGE_STEP,
},
};
export const STALE_TIME = 3_000;

View File

@ -1,22 +1,25 @@
/* eslint-disable no-console */
import { 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 { makeClient } from './api/client.js';
import { OnnxWeb } from './components/OnnxWeb.js';
import { loadConfig } from './config.js';
import { ConfigParams, loadConfig } from './config.js';
export async function main() {
const config = await loadConfig();
const client = makeClient(config.api.root);
const params = await client.params();
const merged = merge(params, config.params) as ConfigParams;
const query = new QueryClient();
const appElement = mustExist(document.getElementById('app'));
const app = ReactDOM.createRoot(appElement);
app.render(<QueryClientProvider client={query}>
<OnnxWeb client={client} config={config} />
<OnnxWeb client={client} config={merged} />
</QueryClientProvider>);
}

View File

@ -541,6 +541,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@^4.14.191":
version "4.14.191"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
"@types/mocha@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b"
@ -2028,6 +2033,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"