diff --git a/gui/examples/config.json b/gui/examples/config.json index 0f1b17a8..f40a1bf2 100644 --- a/gui/examples/config.json +++ b/gui/examples/config.json @@ -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" + } } } \ No newline at end of file diff --git a/gui/package.json b/gui/package.json index dd12f2de..2d18ce3f 100644 --- a/gui/package.json +++ b/gui/package.json @@ -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", diff --git a/gui/src/api/client.ts b/gui/src/api/client.ts index df054a7e..b5928a72 100644 --- a/gui/src/api/client.ts +++ b/gui/src/api/client.ts @@ -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>; + params(): Promise; platforms(): Promise>; schedulers(): Promise>; @@ -136,6 +138,11 @@ export function makeClient(root: string, f = fetch): ApiClient { const res = await f(path); return await res.json() as Array; }, + async params(): Promise { + const path = new URL(joinPath('settings', 'params'), root); + const res = await f(path); + return await res.json() as ConfigParams; + }, async schedulers(): Promise> { const path = new URL(joinPath('settings', 'schedulers'), root); const res = await f(path); diff --git a/gui/src/components/ImageControl.tsx b/gui/src/components/ImageControl.tsx index 64e0d629..76623b5c 100644 --- a/gui/src/components/ImageControl.tsx +++ b/gui/src/components/ImageControl.tsx @@ -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 { if (doesExist(props.onChange)) { @@ -35,9 +37,9 @@ export function ImageControl(props: ImageControlProps) { /> { if (doesExist(props.onChange)) { @@ -50,9 +52,9 @@ export function ImageControl(props: ImageControlProps) { /> { if (doesExist(props.onChange)) { @@ -63,17 +65,21 @@ export function ImageControl(props: ImageControlProps) { } }} /> - { - const seed = Math.floor(Math.random() * CONFIG_DEFAULTS.seed.max); - if (doesExist(props.onChange)) { - props.onChange({ - ...params, - seed, - }); - } - }}> - - + { if (doesExist(props.onChange)) { diff --git a/gui/src/components/Img2Img.tsx b/gui/src/components/Img2Img.tsx index 1bb1f08a..84f6d989 100644 --- a/gui/src/components/Img2Img.tsx +++ b/gui/src/components/Img2Img.tsx @@ -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(); - const [strength, setStrength] = useState(CONFIG_DEFAULTS.strength.default); + const [strength, setStrength] = useState(config.strength.default); const [params, setParams] = useState({ - 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 @@ -67,15 +67,15 @@ export function Img2Img(props: Img2ImgProps) { /> - { + { setParams(newParams); }} /> { setStrength(value); diff --git a/gui/src/components/Inpaint.tsx b/gui/src/components/Inpaint.tsx index 48172f80..d20c3e84 100644 --- a/gui/src/components/Inpaint.tsx +++ b/gui/src/components/Inpaint.tsx @@ -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(); const [source, setSource] = useState(); const [params, setParams] = useState({ - 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 { const canvas = mustExist(canvasRef.current); @@ -271,22 +271,25 @@ export function Inpaint(props: InpaintProps) { }} /> - { + { setParams(newParams); }} /> diff --git a/gui/src/components/OnnxWeb.tsx b/gui/src/components/OnnxWeb.tsx index caa91e67..24331e1b 100644 --- a/gui/src/components/OnnxWeb.tsx +++ b/gui/src/components/OnnxWeb.tsx @@ -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, diff --git a/gui/src/components/Txt2Img.tsx b/gui/src/components/Txt2Img.tsx index a299e13c..eb8f055d 100644 --- a/gui/src/components/Txt2Img.tsx +++ b/gui/src/components/Txt2Img.tsx @@ -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({ - 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 @@ -64,15 +64,15 @@ export function Txt2Img(props: Txt2ImgProps) { }} /> - { + { setParams(newParams); }} /> { setWidth(value); @@ -80,9 +80,9 @@ export function Txt2Img(props: Txt2ImgProps) { /> { setHeight(value); diff --git a/gui/src/config.ts b/gui/src/config.ts index e18f59a1..f2d7f2e7 100644 --- a/gui/src/config.ts +++ b/gui/src/config.ts @@ -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; +} + +export type KeyFilter = { + [K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never; +}[keyof T]; + +export type ConfigRanges = { + [K in KeyFilter]: T[K] extends number ? ConfigNumber : T[K] extends string ? ConfigString : never; +}; + +export type ConfigParams = ConfigRanges>; + 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 { const configPath = new URL('./config.json', window.origin); const configReq = await fetch(configPath); @@ -21,70 +49,3 @@ export async function loadConfig(): Promise { throw new Error('could not load config'); } } - -export interface ConfigRange { - default: number; - min: number; - max: number; - step: number; -} - -export type KeyFilter = { - [K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never; -}[keyof T]; - -export type ConfigRanges = { - [K in KeyFilter]: 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> = { - 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; diff --git a/gui/src/main.tsx b/gui/src/main.tsx index 32c0a63a..0a9fb32f 100644 --- a/gui/src/main.tsx +++ b/gui/src/main.tsx @@ -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( - + ); } diff --git a/gui/yarn.lock b/gui/yarn.lock index cd7d69cf..fb7596a2 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -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"