1
0
Fork 0

feat(gui): add local params and API stub so client can load without a server (#181)

This commit is contained in:
Sean Sube 2023-03-05 16:23:26 -06:00
parent 643f7bbd01
commit d5a3b0fed8
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
11 changed files with 366 additions and 126 deletions

View File

@ -1,5 +1,5 @@
{
"version": "0.7.1",
"version": "0.8.0",
"batch": {
"default": 1,
"min": 1,

View File

@ -5,8 +5,7 @@ import { copy } from 'esbuild-plugin-copy';
function envTrue(key) {
const val = (process.env[key] || '').toLowerCase();
return val == '1' || val == 't' || val == 'true' || val == 'y' || val == 'yes';
return val === '1' || val === 't' || val === 'true' || val === 'y' || val === 'yes';
}
const debug = envTrue('DEBUG');

View File

@ -3,17 +3,167 @@
"root": "http://127.0.0.1:5000"
},
"params": {
"version": "0.8.0",
"batch": {
"default": 1,
"min": 1,
"max": 5,
"step": 1
},
"bottom": {
"default": 0,
"min": 0,
"max": 512,
"step": 8
},
"cfg": {
"default": 6,
"min": 1,
"max": 30,
"step": 0.1
},
"correction": {
"default": "",
"keys": []
},
"denoise": {
"default": 0.5,
"min": 0,
"max": 1,
"step": 0.1
},
"eta": {
"default": 0.0,
"min": 0,
"max": 1,
"step": 0.01
},
"faceOutscale": {
"default": 1,
"min": 1,
"max": 4,
"step": 1
},
"faceStrength": {
"default": 0.5,
"min": 0,
"max": 1,
"step": 0.1
},
"fillColor": {
"default": "#000000",
"keys": []
},
"filter": {
"default": "none",
"keys": []
},
"height": {
"default": 512,
"min": 256,
"max": 1024,
"step": 8
},
"inversion": {
"default": "",
"keys": []
},
"left": {
"default": 0,
"min": 0,
"max": 512,
"step": 8
},
"model": {
"default": "stable-diffusion-onnx-v1-5"
"default": "stable-diffusion-onnx-v1-5",
"keys": []
},
"negativePrompt": {
"default": "",
"keys": []
},
"noise": {
"default": "histogram",
"keys": []
},
"outscale": {
"default": 1,
"min": 1,
"max": 4,
"step": 1
},
"platform": {
"default": "amd"
},
"scheduler": {
"default": "euler-a"
"default": "amd",
"keys": []
},
"prompt": {
"default": "an astronaut eating a hamburger"
"default": "an astronaut eating a hamburger",
"keys": []
},
"right": {
"default": 0,
"min": 0,
"max": 512,
"step": 8
},
"scale": {
"default": 1,
"min": 1,
"max": 4,
"step": 1
},
"scheduler": {
"default": "euler-a",
"keys": []
},
"seed": {
"default": -1,
"min": -1,
"max": 4294967295,
"step": 1
},
"steps": {
"default": 25,
"min": 1,
"max": 200,
"step": 1
},
"strength": {
"default": 0.5,
"min": 0,
"max": 1,
"step": 0.01
},
"tileOrder": {
"default": "spiral",
"keys": [
"grid",
"spiral"
]
},
"top": {
"default": 0,
"min": 0,
"max": 512,
"step": 8
},
"upscaleOrder": {
"default": "correction-first",
"keys": [
"correction-both",
"correction-first",
"correction-last"
]
},
"upscaling": {
"default": "",
"keys": []
},
"width": {
"default": 512,
"min": 256,
"max": 1024,
"step": 8
}
}
}

View File

@ -1,8 +1,8 @@
/* eslint-disable max-lines */
import { doesExist } from '@apextoaster/js-utils';
import { ServerParams } from './config.js';
import { range } from './utils.js';
import { ServerParams } from '../config.js';
import { range } from '../utils.js';
/**
* Shared parameters for anything using models, which is pretty much everything.

59
gui/src/client/local.ts Normal file
View File

@ -0,0 +1,59 @@
import { BaseError } from 'noicejs';
import { ApiClient } from './api.js';
export class NoServerError extends BaseError {
constructor() {
super('cannot connect to server');
}
}
/**
* @TODO client-side inference with https://www.npmjs.com/package/onnxruntime-web
*/
export const LOCAL_CLIENT = {
async masks() {
throw new NoServerError();
},
async blend(model, params, upscale) {
throw new NoServerError();
},
async img2img(model, params, upscale) {
throw new NoServerError();
},
async txt2img(model, params, upscale) {
throw new NoServerError();
},
async inpaint(model, params, upscale) {
throw new NoServerError();
},
async upscale(model, params, upscale) {
throw new NoServerError();
},
async outpaint(model, params, upscale) {
throw new NoServerError();
},
async noises() {
throw new NoServerError();
},
async params() {
throw new NoServerError();
},
async ready(key) {
throw new NoServerError();
},
async cancel(key) {
throw new NoServerError();
},
async models() {
throw new NoServerError();
},
async platforms() {
throw new NoServerError();
},
async schedulers() {
throw new NoServerError();
},
async strings() {
return {};
},
} as ApiClient;

View File

@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useHash } from 'react-use/lib/useHash';
import { useStore } from 'zustand';
import { ImageResponse } from '../client.js';
import { ImageResponse } from '../client/api.js';
import { BLEND_SOURCES, ConfigContext, StateContext } from '../state.js';
import { range, visibleIndex } from '../utils.js';

View File

@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query';
import { useStore } from 'zustand';
import { ImageResponse } from '../client.js';
import { ImageResponse } from '../client/api.js';
import { POLL_TIME } from '../config.js';
import { ClientContext, ConfigContext, StateContext } from '../state.js';

View File

@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useStore } from 'zustand';
import { BaseImgParams } from '../../client.js';
import { BaseImgParams } from '../../client/api.js';
import { STALE_TIME } from '../../config.js';
import { ClientContext, ConfigContext, OnnxState, StateContext } from '../../state.js';
import { NumericField } from '../input/NumericField.js';

View File

@ -1,6 +1,6 @@
import { doesExist, Maybe } from '@apextoaster/js-utils';
import { merge } from 'lodash';
import { Img2ImgParams, InpaintParams, ModelParams, OutpaintParams, STATUS_SUCCESS, Txt2ImgParams, UpscaleParams } from './client.js';
import { Img2ImgParams, InpaintParams, ModelParams, OutpaintParams, STATUS_SUCCESS, Txt2ImgParams, UpscaleParams } from './client/api.js';
export interface ConfigNumber {
default: number;
@ -113,3 +113,16 @@ export function getApiRoot(config: Config): string {
return config.api.root;
}
}
export function isDebug(): boolean {
const query = new URLSearchParams(window.location.search);
const debug = query.get('debug');
if (doesExist(debug)) {
const val = debug.toLowerCase();
// eslint-disable-next-line no-restricted-syntax
return val === '1' || val === 't' || val === 'true' || val === 'y' || val === 'yes';
} else {
return false;
}
}

View File

@ -1,5 +1,5 @@
import { mustDefault, mustExist, timeout } from '@apextoaster/js-utils';
import { createLogger } from 'browser-bunyan';
import { mustDefault, mustExist, timeout, TimeoutError } from '@apextoaster/js-utils';
import { createLogger, Logger } from 'browser-bunyan';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import * as React from 'react';
@ -10,12 +10,21 @@ import { satisfies } from 'semver';
import { createStore } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { makeClient } from './client.js';
import { ApiClient, makeClient } from './client/api.js';
import { LOCAL_CLIENT } from './client/local.js';
import { ParamsVersionError } from './components/error/ParamsVersion.js';
import { ServerParamsError } from './components/error/ServerParams.js';
import { OnnxError } from './components/OnnxError.js';
import { OnnxWeb } from './components/OnnxWeb.js';
import { getApiRoot, loadConfig, mergeConfig, PARAM_VERSION } from './config.js';
import {
Config,
getApiRoot,
isDebug,
loadConfig,
mergeConfig,
PARAM_VERSION,
ServerParams,
} from './config.js';
import {
ClientContext,
ConfigContext,
@ -30,10 +39,115 @@ import { I18N_STRINGS } from './strings/all.js';
export const INITIAL_LOAD_TIMEOUT = 5_000;
export async function renderApp(config: Config, params: ServerParams, logger: Logger, client: ApiClient) {
const completeConfig = mergeConfig(config, params);
// prep i18next
await i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
resources: I18N_STRINGS,
returnEmptyString: false,
});
logger.info('getting strings from server');
const strings = await client.strings();
for (const [lang, translation] of Object.entries(strings)) {
logger.debug({ lang, translation }, 'adding server strings');
for (const [namespace, data] of Object.entries(translation)) {
i18n.addResourceBundle(lang, namespace, data, true);
}
}
// prep zustand with a slice for each tab, using local storage
const {
createBrushSlice,
createDefaultSlice,
createHistorySlice,
createImg2ImgSlice,
createInpaintSlice,
createModelSlice,
createOutpaintSlice,
createTxt2ImgSlice,
createUpscaleSlice,
createBlendSlice,
createResetSlice,
} = createStateSlices(params);
const state = createStore<OnnxState, [['zustand/persist', OnnxState]]>(persist((...slice) => ({
...createBrushSlice(...slice),
...createDefaultSlice(...slice),
...createHistorySlice(...slice),
...createImg2ImgSlice(...slice),
...createInpaintSlice(...slice),
...createModelSlice(...slice),
...createTxt2ImgSlice(...slice),
...createOutpaintSlice(...slice),
...createUpscaleSlice(...slice),
...createBlendSlice(...slice),
...createResetSlice(...slice),
}), {
name: STATE_KEY,
partialize(s) {
return {
...s,
img2img: {
...s.img2img,
source: undefined,
},
inpaint: {
...s.inpaint,
mask: undefined,
source: undefined,
},
upscaleTab: {
...s.upscaleTab,
source: undefined,
},
blend: {
...s.blend,
mask: undefined,
sources: [],
}
};
},
storage: createJSONStorage(() => localStorage),
version: STATE_VERSION,
}));
// prep react-query client
const query = new QueryClient();
const reactLogger = logger.child({
system: 'react',
});
// go
return <QueryClientProvider client={query}>
<ClientContext.Provider value={client}>
<ConfigContext.Provider value={completeConfig}>
<LoggerContext.Provider value={reactLogger}>
<I18nextProvider i18n={i18n}>
<StateContext.Provider value={state}>
<OnnxWeb />
</StateContext.Provider>
</I18nextProvider>
</LoggerContext.Provider>
</ConfigContext.Provider>
</ClientContext.Provider>
</QueryClientProvider>;
}
export async function main() {
const debug = isDebug();
const logger = createLogger({
name: 'onnx-web',
level: 'debug',
level: debug ? 'debug' : 'info',
});
// load config from GUI server
@ -53,116 +167,21 @@ export async function main() {
const params = await timeout(INITIAL_LOAD_TIMEOUT, client.params());
const version = mustDefault(params.version, '0.0.0');
if (satisfies(version, PARAM_VERSION)) {
const completeConfig = mergeConfig(config, params);
// prep i18next
await i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
resources: I18N_STRINGS,
returnEmptyString: false,
});
logger.info('getting strings from server');
const strings = await client.strings();
for (const [lang, translation] of Object.entries(strings)) {
logger.debug({ lang, translation }, 'adding server strings');
for (const [namespace, data] of Object.entries(translation)) {
i18n.addResourceBundle(lang, namespace, data, true);
}
}
// prep zustand with a slice for each tab, using local storage
const {
createBrushSlice,
createDefaultSlice,
createHistorySlice,
createImg2ImgSlice,
createInpaintSlice,
createModelSlice,
createOutpaintSlice,
createTxt2ImgSlice,
createUpscaleSlice,
createBlendSlice,
createResetSlice,
} = createStateSlices(params);
const state = createStore<OnnxState, [['zustand/persist', OnnxState]]>(persist((...slice) => ({
...createBrushSlice(...slice),
...createDefaultSlice(...slice),
...createHistorySlice(...slice),
...createImg2ImgSlice(...slice),
...createInpaintSlice(...slice),
...createModelSlice(...slice),
...createTxt2ImgSlice(...slice),
...createOutpaintSlice(...slice),
...createUpscaleSlice(...slice),
...createBlendSlice(...slice),
...createResetSlice(...slice),
}), {
name: STATE_KEY,
partialize(s) {
return {
...s,
img2img: {
...s.img2img,
source: undefined,
},
inpaint: {
...s.inpaint,
mask: undefined,
source: undefined,
},
upscaleTab: {
...s.upscaleTab,
source: undefined,
},
blend: {
...s.blend,
mask: undefined,
sources: [],
}
};
},
storage: createJSONStorage(() => localStorage),
version: STATE_VERSION,
}));
// prep react-query client
const query = new QueryClient();
const reactLogger = logger.child({
system: 'react',
});
// go
app.render(<QueryClientProvider client={query}>
<ClientContext.Provider value={client}>
<ConfigContext.Provider value={completeConfig}>
<LoggerContext.Provider value={reactLogger}>
<I18nextProvider i18n={i18n}>
<StateContext.Provider value={state}>
<OnnxWeb />
</StateContext.Provider>
</I18nextProvider>
</LoggerContext.Provider>
</ConfigContext.Provider>
</ClientContext.Provider>
</QueryClientProvider>);
app.render(await renderApp(config, params, logger, client));
} else {
app.render(<OnnxError root={root}>
<ParamsVersionError root={root} version={version} />
</OnnxError>);
}
} catch (err) {
app.render(<OnnxError root={root}>
<ServerParamsError root={root} error={err} />
</OnnxError>);
if (err instanceof TimeoutError || (err instanceof Error && err.message.includes('Failed to fetch'))) {
// params timed out, attempt to render without a server
app.render(await renderApp(config, config.params as ServerParams, logger, LOCAL_CLIENT));
} else {
app.render(<OnnxError root={root}>
<ServerParamsError root={root} error={err} />
</OnnxError>);
}
}
}

View File

@ -1,6 +1,6 @@
/* eslint-disable max-lines */
/* eslint-disable no-null/no-null */
import { doesExist, Maybe } from '@apextoaster/js-utils';
import { Maybe } from '@apextoaster/js-utils';
import { Logger } from 'noicejs';
import { createContext } from 'react';
import { StateCreator, StoreApi } from 'zustand';
@ -19,7 +19,7 @@ import {
Txt2ImgParams,
UpscaleParams,
UpscaleReqParams,
} from './client.js';
} from './client/api.js';
import { Config, ConfigFiles, ConfigState, ServerParams } from './config.js';
/**