diff --git a/api/params.json b/api/params.json index f53c9014..bb5f47f7 100644 --- a/api/params.json +++ b/api/params.json @@ -1,5 +1,11 @@ { "version": "0.5.0", + "bottom": { + "default": 0, + "min": 0, + "max": 512, + "step": 8 + }, "cfg": { "default": 6, "min": 1, @@ -18,12 +24,26 @@ "max": 1, "step": 0.1 }, + "fillColor": { + "default": "#000000", + "keys": [] + }, + "filter": { + "default": "none", + "keys": [] + }, "height": { "default": 512, "min": 64, "max": 1024, "step": 8 }, + "left": { + "default": 0, + "min": 0, + "max": 512, + "step": 8 + }, "model": { "default": "stable-diffusion-onnx-v1-5", "keys": [] @@ -32,6 +52,10 @@ "default": "", "keys": [] }, + "noise": { + "default": "histogram", + "keys": [] + }, "outscale": { "default": 1, "min": 1, @@ -46,6 +70,12 @@ "default": "an astronaut eating a hamburger", "keys": [] }, + "right": { + "default": 0, + "min": 0, + "max": 512, + "step": 8 + }, "scale": { "default": 1, "min": 1, @@ -74,6 +104,12 @@ "max": 1, "step": 0.01 }, + "top": { + "default": 0, + "min": 0, + "max": 512, + "step": 8 + }, "width": { "default": 512, "min": 64, diff --git a/gui/src/client.ts b/gui/src/client.ts index 54bd49fb..5af6dedf 100644 --- a/gui/src/client.ts +++ b/gui/src/client.ts @@ -1,22 +1,36 @@ +/* eslint-disable max-lines */ import { doesExist } from '@apextoaster/js-utils'; import { ServerParams } from './config.js'; +/** + * Shared parameters for anything using models, which is pretty much everything. + */ export interface ModelParams { /** - * Which ONNX model to use. + * The diffusion model to use. */ model: string; /** - * Hardware accelerator or CPU mode. + * The hardware acceleration platform to use. */ platform: string; + /** + * The upscaling model to use. + */ upscaling: string; + + /** + * The correction model to use. + */ correction: string; } +/** + * Shared parameters for most of the image requests. + */ export interface BaseImgParams { scheduler: string; prompt: string; @@ -27,20 +41,25 @@ export interface BaseImgParams { seed: number; } -export interface Img2ImgParams extends BaseImgParams { - source: Blob; - strength: number; -} - -export type Img2ImgResponse = Required>; - +/** + * Parameters for txt2img requests. + */ export interface Txt2ImgParams extends BaseImgParams { width?: number; height?: number; } -export type Txt2ImgResponse = Required; +/** + * Parameters for img2img requests. + */ +export interface Img2ImgParams extends BaseImgParams { + source: Blob; + strength: number; +} +/** + * Parameters for inpaint requests. + */ export interface InpaintParams extends BaseImgParams { mask: Blob; source: Blob; @@ -51,6 +70,11 @@ export interface InpaintParams extends BaseImgParams { fillColor: string; } +/** + * Additional parameters for outpaint border. + * + * @todo should be nested under inpaint/outpaint params + */ export interface OutpaintPixels { enabled: boolean; @@ -60,14 +84,27 @@ export interface OutpaintPixels { bottom: number; } +/** + * Parameters for outpaint requests. + */ export type OutpaintParams = InpaintParams & OutpaintPixels; +/** + * Additional parameters for the inpaint brush. + * + * These are not currently sent to the server and only stored in state. + * + * @todo move to state + */ export interface BrushParams { color: number; size: number; strength: number; } +/** + * Additional parameters for upscaling. + */ export interface UpscaleParams { enabled: boolean; @@ -78,10 +115,16 @@ export interface UpscaleParams { faceStrength: number; } +/** + * Parameters for upscale requests. + */ export interface UpscaleReqParams { source: Blob; } +/** + * General response for most image requests. + */ export interface ImageResponse { output: { key: string; @@ -94,10 +137,16 @@ export interface ImageResponse { }; } +/** + * Status response from the ready endpoint. + */ export interface ReadyResponse { ready: boolean; } +/** + * List of available models. + */ export interface ModelsResponse { diffusion: Array; correction: Array; @@ -105,50 +154,103 @@ export interface ModelsResponse { } export interface ApiClient { + /** + * List the available filter masks for inpaint. + */ masks(): Promise>; + + /** + * List the available models. + */ models(): Promise; + + /** + * List the available noise sources for inpaint. + */ noises(): Promise>; + + /** + * Get the valid server parameters to validate image parameters. + */ params(): Promise; + + /** + * Get the available hardware acceleration platforms. + */ platforms(): Promise>; + + /** + * List the available pipeline schedulers. + */ schedulers(): Promise>; - img2img(model: ModelParams, params: Img2ImgParams, upscale?: UpscaleParams): Promise; + /** + * Start a txt2img pipeline. + */ txt2img(model: ModelParams, params: Txt2ImgParams, upscale?: UpscaleParams): Promise; + + /** + * Start an im2img pipeline. + */ + img2img(model: ModelParams, params: Img2ImgParams, upscale?: UpscaleParams): Promise; + + /** + * Start an inpaint pipeline. + */ inpaint(model: ModelParams, params: InpaintParams, upscale?: UpscaleParams): Promise; + + /** + * Start an outpaint pipeline. + */ outpaint(model: ModelParams, params: OutpaintParams, upscale?: UpscaleParams): Promise; + + /** + * Start an upscale pipeline. + */ upscale(model: ModelParams, params: UpscaleReqParams, upscale?: UpscaleParams): Promise; + /** + * Check whether some pipeline's output is ready yet. + */ ready(params: ImageResponse): Promise; } -export const STATUS_SUCCESS = 200; - -export function paramsFromConfig(defaults: ServerParams): Required { - return { - cfg: defaults.cfg.default, - negativePrompt: defaults.negativePrompt.default, - prompt: defaults.prompt.default, - scheduler: defaults.scheduler.default, - steps: defaults.steps.default, - seed: defaults.seed.default, - }; -} - +/** + * Fixed precision for integer parameters. + */ export const FIXED_INTEGER = 0; + +/** + * Fixed precision for float parameters. + * + * The GUI limits the input steps based on the server parameters, but this does limit + * the maximum precision that can be sent back to the server, and may have to be + * increased in the future. + */ export const FIXED_FLOAT = 2; +export const STATUS_SUCCESS = 200; export function equalResponse(a: ImageResponse, b: ImageResponse): boolean { return a.output === b.output; } +/** + * Join URL path segments, which always use a forward slash per https://www.rfc-editor.org/rfc/rfc1738 + */ export function joinPath(...parts: Array): string { return parts.join('/'); } +/** + * Build the URL to an API endpoint, given the API root and a list of segments. + */ export function makeApiUrl(root: string, ...path: Array) { return new URL(joinPath('api', ...path), root); } +/** + * Build the URL for an image request, including all of the base image parameters. + */ export function makeImageURL(root: string, type: string, params: BaseImgParams): URL { const url = makeApiUrl(root, type); url.searchParams.append('cfg', params.cfg.toFixed(FIXED_FLOAT)); @@ -172,6 +274,9 @@ export function makeImageURL(root: string, type: string, params: BaseImgParams): return url; } +/** + * Append the model parameters to an existing URL. + */ export function appendModelToURL(url: URL, params: ModelParams) { url.searchParams.append('model', params.model); url.searchParams.append('platform', params.platform); @@ -179,6 +284,9 @@ export function appendModelToURL(url: URL, params: ModelParams) { url.searchParams.append('correction', params.correction); } +/** + * Append the upscale parameters to an existing URL. + */ export function appendUpscaleToURL(url: URL, upscale: UpscaleParams) { if (upscale.enabled) { url.searchParams.append('denoise', upscale.denoise.toFixed(FIXED_FLOAT)); @@ -189,6 +297,9 @@ export function appendUpscaleToURL(url: URL, upscale: UpscaleParams) { } } +/** + * Make an API client using the given API root and fetch client. + */ export function makeClient(root: string, f = fetch): ApiClient { let pending: Promise | undefined; @@ -388,6 +499,12 @@ export function makeClient(root: string, f = fetch): ApiClient { }; } +/** + * Parse a successful API response into the full image response record. + * + * The server sends over the output key, and the client is in the best position to turn + * that into a full URL, since it already knows the root URL of the server. + */ export async function parseApiResponse(root: string, res: Response): Promise { type LimitedResponse = Omit & { output: string }; diff --git a/gui/src/config.ts b/gui/src/config.ts index e03cc77a..0442deb1 100644 --- a/gui/src/config.ts +++ b/gui/src/config.ts @@ -14,23 +14,40 @@ export interface ConfigString { keys: Array; } +/** + * Helper type to filter keys whose value extends `TValid`. + */ export type KeyFilter = { [K in keyof T]: T[K] extends TValid ? K : never; }[keyof T]; +/** + * Keep fields with a file-like value, but make them optional. + */ export type ConfigFiles = { [K in KeyFilter]: Maybe; }; +/** + * Map numbers and strings to their corresponding config types and drop the rest of the fields. + */ export type ConfigRanges = { [K in KeyFilter]: T[K] extends number ? ConfigNumber : T[K] extends string ? ConfigString : never; }; +/** + * Keep fields whose value extends `TValid` and drop the rest. + */ export type ConfigState = { [K in KeyFilter]: T[K] extends TValid ? T[K] : never; }; +// eslint does not understand how to indent this and expects each line to increase /* eslint-disable */ +/** + * Combine all of the request parameter groups, make optional parameters required, then + * map them to the number/string ranges. + */ export type ServerParams = ConfigRanges { params: T; } -export const DEFAULT_BRUSH = { - color: 255, - size: 8, -}; - export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png'; export const PARAM_VERSION = '>=0.4.0'; diff --git a/gui/src/state.ts b/gui/src/state.ts index 3ef7be19..4389d9bd 100644 --- a/gui/src/state.ts +++ b/gui/src/state.ts @@ -12,15 +12,47 @@ import { InpaintParams, ModelParams, OutpaintPixels, - paramsFromConfig, Txt2ImgParams, UpscaleParams, UpscaleReqParams, } from './client.js'; import { Config, ConfigFiles, ConfigState, ServerParams } from './config.js'; +/** + * Combine optional files and required ranges. + */ type TabState = ConfigFiles> & ConfigState>; +interface BrushSlice { + brush: BrushParams; + + setBrush(brush: Partial): void; +} + +interface DefaultSlice { + defaults: TabState; + + setDefaults(param: Partial): void; +} + +interface HistorySlice { + history: Array; + limit: number; + loading: Maybe; + + pushHistory(image: ImageResponse): void; + removeHistory(image: ImageResponse): void; + setLimit(limit: number): void; + setLoading(image: Maybe): void; +} + +interface ModelSlice { + model: ModelParams; + + setModel(model: Partial): void; +} + +// #region tab slices interface Txt2ImgSlice { txt2img: TabState; @@ -42,35 +74,12 @@ interface InpaintSlice { resetInpaint(): void; } -interface HistorySlice { - history: Array; - limit: number; - loading: Maybe; - - pushHistory(image: ImageResponse): void; - removeHistory(image: ImageResponse): void; - setLimit(limit: number): void; - setLoading(image: Maybe): void; -} - -interface DefaultSlice { - defaults: TabState; - - setDefaults(param: Partial): void; -} - interface OutpaintSlice { outpaint: OutpaintPixels; setOutpaint(pixels: Partial): void; } -interface BrushSlice { - brush: BrushParams; - - setBrush(brush: Partial): void; -} - interface UpscaleSlice { upscale: UpscaleParams; upscaleTab: TabState; @@ -79,13 +88,11 @@ interface UpscaleSlice { setUpscaleTab(params: Partial): void; resetUpscaleTab(): void; } +// #endregion -interface ModelSlice { - model: ModelParams; - - setModel(model: Partial): void; -} - +/** + * Full merged state including all slices. + */ export type OnnxState = BrushSlice & DefaultSlice @@ -97,14 +104,85 @@ export type OnnxState & Txt2ImgSlice & UpscaleSlice; -export function createStateSlices(base: ServerParams) { - const defaults = paramsFromConfig(base); +/** + * Shorthand for state creator to reduce repeated arguments. + */ +export type Slice = StateCreator; - const createTxt2ImgSlice: StateCreator = (set) => ({ +/** + * React context binding for API client. + */ +export const ClientContext = createContext>(undefined); + +/** + * React context binding for merged config, including server parameters. + */ +export const ConfigContext = createContext>>(undefined); + +/** + * React context binding for zustand state store. + */ +export const StateContext = createContext>>(undefined); + +/** + * Current state version for zustand persistence. + */ +export const STATE_VERSION = 4; + +/** + * Default parameters for the inpaint brush. + * + * Not provided by the server yet. + */ +export const DEFAULT_BRUSH = { + color: 255, + size: 8, + strength: 0.5, +}; + +/** + * Default parameters for the image history. + * + * Not provided by the server yet. + */ +export const DEFAULT_HISTORY = { + /** + * The number of images to be shown. + */ + limit: 4, + + /** + * The number of additional images to be kept in history, so they can scroll + * back into view when you delete one. Does not include deleted images. + */ + scrollback: 2, +}; + +export function baseParamsFromServer(defaults: ServerParams): Required { + return { + cfg: defaults.cfg.default, + negativePrompt: defaults.negativePrompt.default, + prompt: defaults.prompt.default, + scheduler: defaults.scheduler.default, + steps: defaults.steps.default, + seed: defaults.seed.default, + }; +} + +/** + * Prepare the state slice constructors. + * + * In the default state, image sources should be null and booleans should be false. Everything + * else should be initialized from the default value in the base parameters. + */ +export function createStateSlices(server: ServerParams) { + const base = baseParamsFromServer(server); + + const createTxt2ImgSlice: Slice = (set) => ({ txt2img: { - ...defaults, - width: base.width.default, - height: base.height.default, + ...base, + width: server.width.default, + height: server.height.default, }, setTxt2Img(params) { set((prev) => ({ @@ -117,19 +195,19 @@ export function createStateSlices(base: ServerParams) { resetTxt2Img() { set({ txt2img: { - ...defaults, - width: base.width.default, - height: base.height.default, + ...base, + width: server.width.default, + height: server.height.default, }, }); }, }); - const createImg2ImgSlice: StateCreator = (set) => ({ + const createImg2ImgSlice: Slice = (set) => ({ img2img: { - ...defaults, + ...base, source: null, - strength: base.strength.default, + strength: server.strength.default, }, setImg2Img(params) { set((prev) => ({ @@ -142,23 +220,23 @@ export function createStateSlices(base: ServerParams) { resetImg2Img() { set({ img2img: { - ...defaults, + ...base, source: null, - strength: base.strength.default, + strength: server.strength.default, }, }); }, }); - const createInpaintSlice: StateCreator = (set) => ({ + const createInpaintSlice: Slice = (set) => ({ inpaint: { - ...defaults, - fillColor: '#000000', - filter: 'none', + ...base, + fillColor: server.fillColor.default, + filter: server.filter.default, mask: null, - noise: 'histogram', + noise: server.noise.default, source: null, - strength: 1.0, + strength: server.strength.default, }, setInpaint(params) { set((prev) => ({ @@ -171,21 +249,21 @@ export function createStateSlices(base: ServerParams) { resetInpaint() { set({ inpaint: { - ...defaults, - fillColor: '#000000', - filter: 'none', + ...base, + fillColor: server.fillColor.default, + filter: server.filter.default, mask: null, - noise: 'histogram', + noise: server.noise.default, source: null, - strength: 1.0, + strength: server.strength.default, }, }); }, }); - const createHistorySlice: StateCreator = (set) => ({ + const createHistorySlice: Slice = (set) => ({ history: [], - limit: 4, + limit: DEFAULT_HISTORY.limit, loading: null, pushHistory(image) { set((prev) => ({ @@ -193,7 +271,7 @@ export function createStateSlices(base: ServerParams) { history: [ image, ...prev.history, - ].slice(0, prev.limit), + ].slice(0, prev.limit + DEFAULT_HISTORY.scrollback), loading: null, })); }, @@ -217,13 +295,13 @@ export function createStateSlices(base: ServerParams) { }, }); - const createOutpaintSlice: StateCreator = (set) => ({ + const createOutpaintSlice: Slice = (set) => ({ outpaint: { enabled: false, - left: 0, - right: 0, - top: 0, - bottom: 0, + left: server.left.default, + right: server.right.default, + top: server.top.default, + bottom: server.bottom.default, }, setOutpaint(pixels) { set((prev) => ({ @@ -235,11 +313,9 @@ export function createStateSlices(base: ServerParams) { }, }); - const createBrushSlice: StateCreator = (set) => ({ + const createBrushSlice: Slice = (set) => ({ brush: { - color: 255, - size: 8, - strength: 0.5, + ...DEFAULT_BRUSH, }, setBrush(brush) { set((prev) => ({ @@ -251,14 +327,14 @@ export function createStateSlices(base: ServerParams) { }, }); - const createUpscaleSlice: StateCreator = (set) => ({ + const createUpscaleSlice: Slice = (set) => ({ upscale: { - denoise: 0.5, + denoise: server.denoise.default, enabled: false, faces: false, - scale: 1, - outscale: 1, - faceStrength: 0.5, + scale: server.scale.default, + outscale: server.outscale.default, + faceStrength: server.faceStrength.default, }, upscaleTab: { source: null, @@ -288,9 +364,9 @@ export function createStateSlices(base: ServerParams) { }, }); - const createDefaultSlice: StateCreator = (set) => ({ + const createDefaultSlice: Slice = (set) => ({ defaults: { - ...defaults, + ...base, }, setDefaults(params) { set((prev) => ({ @@ -302,12 +378,12 @@ export function createStateSlices(base: ServerParams) { }, }); - const createModelSlice: StateCreator = (set) => ({ + const createModelSlice: Slice = (set) => ({ model: { - model: '', - platform: '', - upscaling: '', - correction: '', + model: server.model.default, + platform: server.platform.default, + upscaling: server.upscaling.default, + correction: server.correction.default, }, setModel(params) { set((prev) => ({ @@ -331,8 +407,3 @@ export function createStateSlices(base: ServerParams) { createUpscaleSlice, }; } - -export const ClientContext = createContext>(undefined); -export const ConfigContext = createContext>>(undefined); -export const StateContext = createContext>>(undefined); -export const STATE_VERSION = 4;