diff --git a/.gitignore b/.gitignore index b64a0969..4e10effc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ installer.nsh *.exe *.7z *.whl +ComfyUI/ diff --git a/WebUI/.gitignore b/WebUI/.gitignore index 0a180dd7..1cdd97bd 100644 --- a/WebUI/.gitignore +++ b/WebUI/.gitignore @@ -12,6 +12,7 @@ dist/ dist-ssr *.local release/ +ComfyUI/ # Editor directories and files .vscode/* diff --git a/WebUI/electron/main.ts b/WebUI/electron/main.ts index 32be9028..25ce37cb 100644 --- a/WebUI/electron/main.ts +++ b/WebUI/electron/main.ts @@ -74,11 +74,16 @@ const settings: LocalSettings = { currentTheme:"lnl" }; +const comfyuiState = { + currentVersion: null, + port: 0, +} + let webContentsFinishedLoad = false; -const startupMessageCache: {message: string, source: 'electron-backend' | 'ai-backend', level: 'error' | 'info'}[] = [] +const startupMessageCache: {message: string, source: 'electron-backend' | 'ai-backend' | 'comfyui-backend', level: 'error' | 'info'}[] = [] const logger = { - info: (message: string, source: 'electron-backend' | 'ai-backend' = 'electron-backend') => { + info: (message: string, source: 'electron-backend' | 'ai-backend' | 'comfyui-backend' = 'electron-backend') => { console.info(`[${source}]: ${message}`); if (webContentsFinishedLoad) { try { @@ -90,7 +95,7 @@ const logger = { startupMessageCache.push({ level: 'info', source, message }) } }, - error: (message: string, source: 'electron-backend' | 'ai-backend' = 'electron-backend') => { + error: (message: string, source: 'electron-backend' | 'ai-backend' | 'comfyui-backend' = 'electron-backend') => { console.error(`[${source}]: ${message}`); if (webContentsFinishedLoad) { try { @@ -120,6 +125,7 @@ async function loadSettings() { }); } settings.port = await getPort({ port: portNumbers(59000, 59999) }); + comfyuiState.port = await getPort({ port: portNumbers(59000, 59999) }); settings.apiHost = `http://127.0.0.1:${settings.port}`; } @@ -131,8 +137,10 @@ async function createWindow() { resizable: true, frame: false, // fullscreen: true, - width: 1440, - height: 951, + x: 2800, + y: 600, + width: 2440, + height: 1400, webPreferences: { preload: path.join(__dirname, "../preload/preload.js"), contextIsolation: true @@ -475,6 +483,25 @@ function initEventHandle() { win?.webContents.openDevTools({ mode: "detach", activate: true }); }); + ipcMain.handle("getComfyuiState", () => { + return comfyuiState; + }); + + ipcMain.handle("updateComfyui", () => { + return; + }); + + ipcMain.handle("reloadImageWorkflows", () => { + const files = fs.readdirSync(path.join(externalRes, "workflows")); + const workflows = files.map((file) => fs.readFileSync(path.join(externalRes, "workflows", file), { encoding: "utf-8" })); + return workflows; + }); + + ipcMain.handle("startComfyui", () => { + console.log('startComfyui') + return; + }); + ipcMain.on("openImageWithSystem", (event, url: string) => { // Assuming 'settings' and 'externalRes' are properly defined let imagePath = url.replace(settings.apiHost + "/", ""); // Remove the API host part @@ -537,6 +564,7 @@ function isProcessRunning(pid: number) { function wakeupApiService() { const wordkDir = path.resolve(app.isPackaged ? path.join(process.resourcesPath, "service") : path.join(__dirname, "../../../service")); + const comfyWordkDir = path.resolve(app.isPackaged ? path.join(process.resourcesPath, "ComfyUI") : path.join(__dirname, "../../../ComfyUI")); const baseDir = app.isPackaged ? process.resourcesPath : path.join(__dirname, "../../../"); const pythonExe = path.resolve(path.join(baseDir, "env/python.exe")); const additionalEnvVariables = { @@ -546,6 +574,7 @@ function wakeupApiService() { }; spawnAPI(pythonExe, wordkDir, additionalEnvVariables); + spawnComfy(pythonExe, comfyWordkDir, additionalEnvVariables); } function spawnAPI(pythonExe: string, wordkDir: string, additionalEnvVariables: Record, tries = 0) { @@ -595,6 +624,23 @@ function spawnAPI(pythonExe: string, wordkDir: string, additionalEnvVariables: R }) } +function spawnComfy(pythonExe: string, wordkDir: string, additionalEnvVariables: Record, tries = 0) { + logger.info(`#1 try to start ComfyUI API`) + + const webProcess = spawn(pythonExe, ["main.py", "--port", comfyuiState.port.toString(), "--preview-method", "auto", "--bf16-unet", "--lowvram"], { + cwd: wordkDir, + windowsHide: true, + env: Object.assign(process.env, additionalEnvVariables) + }); + + webProcess.stdout.on('data', (message) => { + logger.info(`${message}`, 'comfyui-backend') + }) + webProcess.stderr.on('data', (message) => { + logger.info(`${message}`, 'comfyui-backend') + }) +} + function closeApiService() { apiService.normalExit = true; diff --git a/WebUI/electron/preload.ts b/WebUI/electron/preload.ts index f5226fa9..3aa9220a 100644 --- a/WebUI/electron/preload.ts +++ b/WebUI/electron/preload.ts @@ -107,6 +107,10 @@ contextBridge.exposeInMainWorld("envVars", { productVersion: pkg.version, }); contextBridge.exposeInMainWorld("electronAPI", { + getComfyuiState: () => ipcRenderer.invoke("getComfyuiState"), + updateComfyui: () => ipcRenderer.invoke("updateComfyui"), + startComfyui: () => ipcRenderer.invoke("startComfyui"), + reloadImageWorkflows: () => ipcRenderer.invoke("reloadImageWorkflows"), openDevTools: () => ipcRenderer.send("openDevTools"), openUrl: (url: string) => ipcRenderer.send("openUrl", url), getLocalSettings: () => ipcRenderer.invoke("getLocalSettings"), diff --git a/WebUI/external/workflows/flux.json b/WebUI/external/workflows/flux.json new file mode 100644 index 00000000..29121eac --- /dev/null +++ b/WebUI/external/workflows/flux.json @@ -0,0 +1,219 @@ +{ + "name": "Flux.1 schnell", + "backend": "comfyui", + "tags": [ + "flux", + "comfyui", + "high-vram" + ], + "requirements": [ + "high-vram" + ], + "inputs": [], + "outputs": [ + { + "name": "output_image", + "type": "image" + } + ], + "defaultSettings": { + "width": 1024, + "height": 1024, + "inferenceSteps": 4 + }, + "displayedSettings": [ + "inferenceSteps", + "imageModel" + ], + "modifiableSettings": [ + "seed", + "batchSize", + "imagePreview", + "resolution" + ], + "comfyUiApiWorkflow": { + "167": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "179", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "169": { + "inputs": { + "noise": [ + "184", + 0 + ], + "guider": [ + "178", + 0 + ], + "sampler": [ + "185", + 0 + ], + "sigmas": [ + "181", + 0 + ], + "latent_image": [ + "180", + 0 + ] + }, + "class_type": "SamplerCustomAdvanced", + "_meta": { + "title": "SamplerCustomAdvanced" + } + }, + "170": { + "inputs": { + "unet_name": "flux1-schnell-Q2_K.gguf" + }, + "class_type": "UnetLoaderGGUF", + "_meta": { + "title": "Unet Loader (GGUF)" + } + }, + "171": { + "inputs": { + "vae_name": "diffusion_pytorch_model.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "Load VAE" + } + }, + "174": { + "inputs": { + "guidance": 1, + "conditioning": [ + "177", + 0 + ] + }, + "class_type": "FluxGuidance", + "_meta": { + "title": "FluxGuidance" + } + }, + "175": { + "inputs": { + "clip_name1": "t5xxl_fp8_e4m3fn.safetensors", + "clip_name2": "clip_l.safetensors", + "type": "flux" + }, + "class_type": "DualCLIPLoader", + "_meta": { + "title": "DualCLIPLoader" + } + }, + "177": { + "inputs": { + "text": "A cool llama wearing a pair of sunglasses, holding a blue and purple neon sign that says \"Lunar Lake\" in front, vibrant colors, blurry cyberpunk gaming background.", + "clip": [ + "188", + 0 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "prompt" + } + }, + "178": { + "inputs": { + "model": [ + "170", + 0 + ], + "conditioning": [ + "174", + 0 + ] + }, + "class_type": "BasicGuider", + "_meta": { + "title": "BasicGuider" + } + }, + "179": { + "inputs": { + "samples": [ + "169", + 1 + ], + "vae": [ + "171", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "180": { + "inputs": { + "width": 768, + "height": 768, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage", + "_meta": { + "title": "Empty Latent Image" + } + }, + "181": { + "inputs": { + "scheduler": "normal", + "steps": 4, + "denoise": 1, + "model": [ + "170", + 0 + ] + }, + "class_type": "BasicScheduler", + "_meta": { + "title": "BasicScheduler" + } + }, + "184": { + "inputs": { + "noise_seed": 508274201813129 + }, + "class_type": "RandomNoise", + "_meta": { + "title": "RandomNoise" + } + }, + "185": { + "inputs": { + "sampler_name": "euler" + }, + "class_type": "KSamplerSelect", + "_meta": { + "title": "KSamplerSelect" + } + }, + "188": { + "inputs": { + "clip_name1": "t5-v1_1-xxl-encoder-Q3_K_M.gguf", + "clip_name2": "clip_l.safetensors", + "type": "flux" + }, + "class_type": "DualCLIPLoaderGGUF", + "_meta": { + "title": "DualCLIPLoader (GGUF)" + } + } + } +} \ No newline at end of file diff --git a/WebUI/external/workflows/sd15.json b/WebUI/external/workflows/sd15.json new file mode 100644 index 00000000..f4c7abee --- /dev/null +++ b/WebUI/external/workflows/sd15.json @@ -0,0 +1,139 @@ +{ + "name": "SD1.5", + "backend": "comfyui", + "tags": [ + "comfyui", + "sd1.5" + ], + "requirements": [], + "inputs": [ + { + "name": "positive_prompt", + "type": "text" + } + ], + "outputs": [ + { + "name": "output_image", + "type": "image" + } + ], + "displayedSettings": [], + "modifiableSettings": [ + "seed", + "negativePrompt", + "batchSize", + "imagePreview", + "resolution", + "guidanceScale", + "scheduler", + "inferenceSteps" + ], + "comfyUiApiWorkflow": { + "3": { + "inputs": { + "seed": 1111600661499464, + "steps": 20, + "cfg": 8, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1, + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + }, + "4": { + "inputs": { + "ckpt_name": "v1-5-pruned-emaonly.ckpt" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "5": { + "inputs": { + "width": 512, + "height": 512, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage", + "_meta": { + "title": "Empty Latent Image" + } + }, + "6": { + "inputs": { + "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "prompt" + } + }, + "7": { + "inputs": { + "text": "text, watermark", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "negativePrompt" + } + }, + "8": { + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "9": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + } + } +} \ No newline at end of file diff --git a/WebUI/package-lock.json b/WebUI/package-lock.json index f617524b..bf3f8824 100644 --- a/WebUI/package-lock.json +++ b/WebUI/package-lock.json @@ -19,6 +19,7 @@ "koffi": "^2.9.1", "marked": "^14.1.3", "marked-highlight": "^2.1.4", + "partysocket": "^1.0.2", "pinia": "^2.2.4", "pinia-plugin-persistedstate": "^4.1.1", "radix-vue": "^1.9.7", @@ -26,7 +27,8 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "unplugin-auto-import": "^0.18.3", - "vue": "^3.5.12" + "vue": "^3.5.12", + "zod": "^3.23.8" }, "devDependencies": { "@types/exif": "^0.6.5", @@ -4672,6 +4674,18 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/event-target-shim": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -6874,6 +6888,15 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/partysocket": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.0.2.tgz", + "integrity": "sha512-rAFOUKImaq+VBk2B+2RTBsWEvlnarEP53nchoUHzpVs8V6fG2/estihOTslTQUWHVuHEKDL5k8htG8K3TngyFA==", + "license": "ISC", + "dependencies": { + "event-target-shim": "^6.0.2" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -9293,6 +9316,15 @@ "engines": { "node": ">= 10" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/WebUI/package.json b/WebUI/package.json index 30cb2044..e9a68b65 100644 --- a/WebUI/package.json +++ b/WebUI/package.json @@ -26,6 +26,7 @@ "koffi": "^2.9.1", "marked": "^14.1.3", "marked-highlight": "^2.1.4", + "partysocket": "^1.0.2", "pinia": "^2.2.4", "pinia-plugin-persistedstate": "^4.1.1", "radix-vue": "^1.9.7", @@ -33,7 +34,8 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "unplugin-auto-import": "^0.18.3", - "vue": "^3.5.12" + "vue": "^3.5.12", + "zod": "^3.23.8" }, "devDependencies": { "@types/exif": "^0.6.5", diff --git a/WebUI/src/App.vue b/WebUI/src/App.vue index 1856323d..8cce1c57 100644 --- a/WebUI/src/App.vue +++ b/WebUI/src/App.vue @@ -119,8 +119,6 @@ import { useTheme } from "./assets/js/store/theme.ts"; const isOpen = ref(false); -const i18n = useI18N(); - const theme = useTheme(); const activeTabIdx = ref(0); diff --git a/WebUI/src/assets/css/main.css b/WebUI/src/assets/css/main.css index 40867a9e..641b72d6 100644 --- a/WebUI/src/assets/css/main.css +++ b/WebUI/src/assets/css/main.css @@ -328,7 +328,7 @@ textarea { z-index: 99; .panel-tab { - width: 138px; + width: 110px; height: 30px; font-size: 14px; border-radius: 8px; diff --git a/WebUI/src/assets/js/store/comfyUi.ts b/WebUI/src/assets/js/store/comfyUi.ts new file mode 100644 index 00000000..9340407c --- /dev/null +++ b/WebUI/src/assets/js/store/comfyUi.ts @@ -0,0 +1,216 @@ +import { defineStore } from "pinia"; +import { WebSocket } from "partysocket"; +import { ComfyUIApiWorkflow, Setting, useImageGeneration } from "./imageGeneration"; +import { useI18N } from "./i18n"; + +const WEBSOCKET_OPEN = 1; + +export const useComfyUi = defineStore("comfyUi", () => { + + const comfyUiState = ref(null); + const imageGeneration = useImageGeneration(); + const i18nState = useI18N().state; + const comfyHostAndPort = computed(() => { + return `localhost:${comfyUiState.value?.port}`; + }); + const websocket = ref(null); + const clientId = '12345'; + + window.electronAPI.getComfyuiState().then((stateFromBackend) => { + comfyUiState.value = stateFromBackend; + console.log('comfyUiState from backend', comfyUiState.value); + }); + + function connectToComfyUi() { + if (!comfyUiState.value) { + console.warn('ComfyUI backend not running, cannot start websocket'); + return; + } + + websocket.value = new WebSocket(`ws://localhost:${comfyUiState.value.port}/ws?clientId=${clientId}`); + websocket.value.binaryType = 'arraybuffer' + websocket.value.addEventListener('message', (event) => { + try { + if (event.data instanceof ArrayBuffer) { + const view = new DataView(event.data) + const eventType = view.getUint32(0) + const buffer = event.data.slice(4) + switch (eventType) { + case 1: + const view2 = new DataView(event.data) + const imageType = view2.getUint32(0) + let imageMime + switch (imageType) { + case 1: + default: + imageMime = 'image/jpeg' + break + case 2: + imageMime = 'image/png' + } + const imageBlob = new Blob([buffer.slice(4)], { + type: imageMime + }) + console.log('got image blob') + const imageUrl = URL.createObjectURL(imageBlob) + console.log('image url', imageUrl) + if (imageBlob) { + imageGeneration.updateDestImage(0, imageUrl); + } + break + default: + throw new Error( + `Unknown binary websocket message of type ${eventType}` + ) + } + } else { + const msg = JSON.parse(event.data) + switch (msg.type) { + case 'status': + break + case 'progress': + imageGeneration.currentState = "generating"; + imageGeneration.stepText = `${i18nState.COM_GENERATING} ${msg.data.value}/${msg.data.max}`; + console.log('progress', { data: msg.data }) + break + case 'executing': + console.log('executing', { + detail: msg.data.display_node || msg.data.node + }) + break + case 'executed': + const images: { filename: string, type: string, subfolder: string }[] = msg.data?.output?.images?.filter((i: { type: string }) => i.type === 'output'); + images.forEach((image, i) => { + imageGeneration.updateDestImage(i, `http://${comfyHostAndPort.value}/view?filename=${image.filename}&type=${image.type}&subfolder=${image.subfolder ?? ''}`); + imageGeneration.generateIdx++; + }); + console.log('executed', { detail: msg.data }) + break + case 'execution_start': + console.log('execution_start', { detail: msg.data }) + break + case 'execution_success': + console.log('execution_success', { detail: msg.data }) + break + case 'execution_error': + break + case 'execution_cached': + break + } + } + } catch (error) { + console.warn('Unhandled message:', event.data, error) + } + }) + } + + watchEffect(() => { + if (comfyUiState.value?.port) { + connectToComfyUi(); + } + }); + + + async function generate() { + console.log('generateWithComfy') + if (!imageGeneration.activeWorkflow.comfyUiApiWorkflow) { + console.warn('No comfyUiApiWorkflow found in activeWorkflow'); + return; + } + if (imageGeneration.processing) { + console.warn('Already processing'); + return; + } + if (websocket.value?.readyState !== WEBSOCKET_OPEN) { + console.warn('Websocket not open'); + return; + } + try { + imageGeneration.processing = true; + + const mutableWorkflow: ComfyUIApiWorkflow = JSON.parse(JSON.stringify(imageGeneration.activeWorkflow.comfyUiApiWorkflow)) + const seed = imageGeneration.seed === -1 ? (Math.random()*1000000).toFixed(0) : imageGeneration.seed; + + modifySettingInWorkflow(mutableWorkflow, 'seed', seed); + modifySettingInWorkflow(mutableWorkflow, 'inferenceSteps', imageGeneration.inferenceSteps); + modifySettingInWorkflow(mutableWorkflow, 'height', imageGeneration.height); + modifySettingInWorkflow(mutableWorkflow, 'width', imageGeneration.width); + modifySettingInWorkflow(mutableWorkflow, 'prompt', imageGeneration.prompt); + modifySettingInWorkflow(mutableWorkflow, 'negativePrompt', imageGeneration.negativePrompt); + modifySettingInWorkflow(mutableWorkflow, 'batchSize', imageGeneration.batchSize); + + fetch(`http://${comfyHostAndPort.value}/prompt`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + mode: 'no-cors', + body: JSON.stringify({ + prompt: mutableWorkflow, + client_id: clientId + }) + }) + } catch (ex) { + console.error('Error generating image', ex); + } finally { + imageGeneration.processing = false; + } + } + + function stop() { + console.log('stop comfyui ##### NOT IMPLEMENTED') + } + + return { + comfyUiState, + generate, + stop + } +}, { + persist: { + pick: ['backend'] + } +}); + +const settingToComfyInputsName = { + 'seed': ['seed', 'noise_seed'], + 'inferenceSteps': ['steps'], + 'height': ['height'], + 'width': ['width'], + 'prompt': ['text'], + 'negativePrompt': ['text'], + 'guidanceScale': ['cfg'], + 'scheduler': ['scheduler'], + 'batchSize': ['batch_size'], +} satisfies Partial>; +type ComfySetting = keyof typeof settingToComfyInputsName; +const findKeysByTitle = (workflow: ComfyUIApiWorkflow, setting: ComfySetting) => + Object.entries(workflow).filter(([_key, value]) => (value as any)?.['_meta']?.title === setting).map(([key, _value]) => key); +const findKeysByInputsName = (workflow: ComfyUIApiWorkflow, setting: ComfySetting) => { + for (const inputName of settingToComfyInputsName[setting]) { + if (inputName === 'text') continue; + const keys = Object.entries(workflow).filter(([_key, value]) => (value as any)?.['inputs']?.[inputName ?? ''] !== undefined).map(([key, _value]) => key) + if (keys.length > 0) return keys; + } + return []; +}; +const getInputNameBySettingAndKey = (workflow: ComfyUIApiWorkflow, key: string, setting: ComfySetting) => { + for (const inputName of settingToComfyInputsName[setting]) { + if (workflow[key]?.inputs?.[inputName ?? '']) return inputName; + } + return ''; +} +function modifySettingInWorkflow(workflow: ComfyUIApiWorkflow, setting: ComfySetting, value: any) { + const keys = findKeysByTitle(workflow, setting).length > 0 ? findKeysByTitle(workflow, setting) : findKeysByInputsName(workflow, setting); + if (keys.length === 0) { + console.error(`No key found for setting ${setting}. Stopping generation`); + return; + } + if (keys.length > 1) { + console.warn(`Multiple keys found for setting ${setting}. Using first one`); + } + const key = keys[0]; + if (workflow[key]?.inputs?.[getInputNameBySettingAndKey(workflow, key, setting)] !== undefined) { + workflow[key].inputs[getInputNameBySettingAndKey(workflow, key, setting)] = value; + } +} \ No newline at end of file diff --git a/WebUI/src/assets/js/store/imageGeneration.ts b/WebUI/src/assets/js/store/imageGeneration.ts new file mode 100644 index 00000000..01c130c7 --- /dev/null +++ b/WebUI/src/assets/js/store/imageGeneration.ts @@ -0,0 +1,560 @@ +import { defineStore } from "pinia"; +import { effect } from "vue"; +import z from "zod"; +import { useComfyUi } from "./comfyUi"; +import { useStableDiffusion } from "./stableDiffusion"; +import { useI18N } from "./i18n"; +import { Const } from "../const"; +import { useGlobalSetup } from "./globalSetup"; + +export type StableDiffusionSettings = { + resolution: 'standard' | 'hd' | 'manual', // ~ modelSettings.resolution 0, 1, 3 + quality: 'standard' | 'high' | 'fast', // ~ modelSettings.quality 0, 1, 2 + imageModel: string, + inpaintModel: string, + negativePrompt: string, + batchSize: number, // ~ modelSettings.generateNumber + pickerResolution?: string, + width: number, + height: number, + guidanceScale: number, + inferenceSteps: number, + seed: number, + lora: string | null, + scheduler: string | null, + imagePreview: boolean, + safetyCheck: boolean +} + +const SettingsSchema = z.object({ + imageModel: z.string(), + inpaintModel: z.string(), + negativePrompt: z.string(), + batchSize: z.number(), + width: z.number(), + height: z.number(), + prompt: z.string(), + resolution: z.string(), + guidanceScale: z.number(), + inferenceSteps: z.number(), + seed: z.number(), + lora: z.string().nullable(), + scheduler: z.string().nullable(), + imagePreview: z.boolean(), + safetyCheck: z.boolean() +}) + +const SettingSchema = SettingsSchema.keyof(); + +export type Setting = z.infer + +// const SettingsSchema = z.enum([ +// 'imageModel', +// 'inpaintModel', +// 'guidanceScale', +// 'inferenceSteps', +// 'seed', +// 'negativePrompt', +// 'batchSize', +// 'imagePreview', +// 'safetyCheck', +// 'width', +// 'height', +// 'scheduler', +// 'lora', +// ]); + +const WorkflowRequirementSchema = z.enum(['high-vram']) +export type WorkflowRequirement = z.infer +const ComfyUIApiWorkflowSchema = z.record(z.string(), z.object({ + inputs: z.object({ + text: z.string().optional(), + }).passthrough().optional(), +}).passthrough()); +export type ComfyUIApiWorkflow = z.infer; +const WorkflowSchema = z.object({ + name: z.string(), + backend: z.enum(['default', 'comfyui']), + tags: z.array(z.string()), + requirements: z.array(WorkflowRequirementSchema), + inputs: z.array(z.object({ + name: z.string(), + type: z.enum(['image', 'mask', 'text']) + })), + outputs: z.array(z.object({ + name: z.string(), + type: z.literal('image') + })), + defaultSettings: SettingsSchema.partial().optional(), + displayedSettings: z.array(SettingsSchema.keyof()), + modifiableSettings: z.array(SettingsSchema.keyof()), + dependencies: z.array(z.unknown()).optional(), + comfyUiApiWorkflow: ComfyUIApiWorkflowSchema.optional() +}) +export type Workflow = z.infer; + +const globalDefaultSettings = { + width: 512, + height: 512, + inferenceSteps: 20, + resolution: '512x512', + batchSize: 1, + negativePrompt: 'nsfw', + imageModel: 'Lykon/dreamshaper-8', + inpaintModel: 'Lykon/dreamshaper-8-inpainting', + guidanceScale: 7, + lora: "None", + scheduler: 'DPM++ SDE Karras', +} + +export const useImageGeneration = defineStore("imageGeneration", () => { + + const comfyUi = useComfyUi(); + const stableDiffusion = useStableDiffusion(); + const globalSetup = useGlobalSetup(); + const i18nState = useI18N().state; + + const workflows = ref(predefinedWorkflows); + const activeWorkflowName = ref('Standard'); + const activeWorkflow = computed(() => { + console.log('### activeWorkflowName', activeWorkflowName.value) + return workflows.value.find(w => w.name === activeWorkflowName.value) ?? predefinedWorkflows[0] + }); + const processing = ref(false); + const stopping = ref(false); + + const prompt = ref(''); + const negativePrompt = ref('nsfw'); + const seed = ref(-1); + const width = ref(512); + const height = ref(512); + const batchSize = ref(1); + const imagePreview = ref(true); + const safeCheck = ref(true); // TODO wire up to settings + const scheduler = ref("None"); + const imageModel = ref(activeWorkflow.value.defaultSettings?.imageModel ?? globalDefaultSettings.imageModel); + const lora = ref("None"); + const guidanceScale = ref(7.5); + + const backend = computed({ + get() { + return activeWorkflow.value.backend; + }, + set(newValue) { + activeWorkflowName.value = workflows.value.find(w => w.backend === newValue)?.name ?? activeWorkflowName.value; + } + }); + + const resolution = computed({ + get() { + return `${width.value}x${height.value}` + }, + set(newValue) { + [width.value, height.value] = newValue.split('x').map(Number); + } + }) + + const inferenceSteps = ref(20); + + const settings = { inferenceSteps, width, height, resolution, batchSize, negativePrompt, lora, scheduler, guidanceScale, imageModel }; + type ModifiableSettings = keyof typeof settings; + + const settingsPerWorkflow = ref>({}); + + const isModifiable = (settingName: ModifiableSettings) => activeWorkflow.value.modifiableSettings.includes(settingName); + + watch([activeWorkflowName, workflows], () => { + console.log('loading settings'); + const getSavedOrDefault = (settingName: ModifiableSettings) => { + if (!activeWorkflowName.value) return; + let saved = undefined; + if (isModifiable(settingName)) { + saved = settingsPerWorkflow.value[activeWorkflowName.value]?.[settingName]; + console.log('got saved', {settingName, saved}); + } + settings[settingName].value = saved ?? activeWorkflow.value?.defaultSettings?.[settingName] ?? globalDefaultSettings[settingName]; + } + + getSavedOrDefault('inferenceSteps'); + getSavedOrDefault('width'); + getSavedOrDefault('height'); + getSavedOrDefault('resolution'); + getSavedOrDefault('batchSize'); + getSavedOrDefault('negativePrompt'); + getSavedOrDefault('lora'); + getSavedOrDefault('scheduler'); + getSavedOrDefault('guidanceScale'); + getSavedOrDefault('imageModel'); + + }, {}); + + watch(resolution, () => { + const [width, height] = resolution.value.split('x').map(Number); + settings.width.value = width; + settings.height.value = height; + }); + + watch([inferenceSteps, width, height], () => { + console.log('saving to settingsPerWorkflow'); + const saveToSettingsPerWorkflow = (settingName: ModifiableSettings) => { + if (!activeWorkflowName.value) return; + if (isModifiable(settingName)) { + settingsPerWorkflow.value[activeWorkflowName.value] = { + ...settingsPerWorkflow.value[activeWorkflowName.value], + [settingName]: settings[settingName].value + } + console.log('saving', {settingName, value: settings[settingName].value}); + } + } + saveToSettingsPerWorkflow('inferenceSteps'); + saveToSettingsPerWorkflow('width'); + saveToSettingsPerWorkflow('height'); + saveToSettingsPerWorkflow('resolution'); + saveToSettingsPerWorkflow('batchSize'); + saveToSettingsPerWorkflow('negativePrompt'); + saveToSettingsPerWorkflow('lora'); + saveToSettingsPerWorkflow('scheduler'); + saveToSettingsPerWorkflow('guidanceScale'); + saveToSettingsPerWorkflow('imageModel'); + }); + + + const imageUrls = ref([]); + const currentState = ref("no_start"); + const stepText = ref(""); + const previewIdx = ref(0); + const generateIdx = ref(-999); + + async function updateDestImage(index: number, image: string) { + if (index + 1 > imageUrls.value.length) { + imageUrls.value.push(image); + } else { + imageUrls.value.splice(index, 1, image); + } + } + + async function loadWorkflowsFromJson() { + const workflowsFromFiles = await window.electronAPI.reloadImageWorkflows(); + const parsedWorkflows = workflowsFromFiles.map((workflow) => { + try { + return WorkflowSchema.parse(JSON.parse(workflow)); + } catch (error) { + console.error('Failed to parse workflow', {error, workflow}); + return undefined; + } + }).filter((wf) => wf !== undefined); + workflows.value = [...predefinedWorkflows, ...parsedWorkflows]; + } + + async function getMissingModels() { + const checkList: CheckModelExistParam[] = [{ repo_id: imageModel.value, type: Const.MODEL_TYPE_STABLE_DIFFUSION }]; + if (lora.value !== "None") { + checkList.push({ repo_id: lora.value, type: Const.MODEL_TYPE_LORA }) + } + if (imagePreview.value) { + checkList.push({ repo_id: "madebyollin/taesd", type: Const.MODEL_TYPE_PREVIEW }) + checkList.push({ repo_id: "madebyollin/taesdxl", type: Const.MODEL_TYPE_PREVIEW }) + } + const result = await globalSetup.checkModelExists(checkList); + const downloadList: CheckModelExistParam[] = []; + for (const item of result) { + if (!item.exist) { + downloadList.push({ repo_id: item.repo_id, type: item.type }) + } + } + return downloadList; + } + + function generate() { + generateIdx.value = 0; + previewIdx.value = 0; + stepText.value = i18nState.COM_GENERATING; + if (activeWorkflow.value.backend === 'default') { + stableDiffusion.generate(); + } else { + comfyUi.generate(); + } + } + + function stop() { + if (activeWorkflow.value.backend === 'default') { + stableDiffusion.stop(); + } else { + comfyUi.stop(); + } + } + + function reset() { + currentState.value = "no_start"; + stableDiffusion.generateParams.length = 0; + imageUrls.value.length = 0; + generateIdx.value = -999; + previewIdx.value = -1; + } + + loadWorkflowsFromJson(); + + return { + backend, + workflows, + activeWorkflowName, + activeWorkflow, + processing, + prompt, + imageUrls, + currentState, + stepText, + stopping, + previewIdx, + generateIdx, + imageModel, + lora, + scheduler, + guidanceScale, + imagePreview, + safeCheck, + inferenceSteps, + seed, + width, + height, + batchSize, + negativePrompt, + settingsPerWorkflow, + loadWorkflowsFromJson, + getMissingModels, + updateDestImage, + generate, + stop, + reset + } +}, { + persist: { + debug: true, + pick: ['backend', 'activeWorkflowName', 'settingsPerWorkflow'] + } +}); + +const predefinedWorkflows: Workflow[] = [ + { + name: 'Standard', + backend: 'default', + tags: ['sd1.5'], + requirements: [], + inputs: [], + outputs: [{ name: 'output_image', type: 'image' }], + defaultSettings: { + imageModel: 'Lykon/dreamshaper-8', + inpaintModel: 'Lykon/dreamshaper-8-inpainting', + width: 512, + height: 512, + guidanceScale: 7, + inferenceSteps: 20, + scheduler: "DPM++ SDE Karras" + }, + displayedSettings: [ + 'imageModel', + 'inpaintModel', + 'guidanceScale', + 'inferenceSteps', + 'scheduler', + ], + modifiableSettings: [ + 'resolution', + 'seed', + 'negativePrompt', + 'batchSize', + 'imagePreview', + 'safetyCheck', + ] + }, + { + name: 'Standard - High Quality', + backend: 'default', + tags: ['sd1.5', 'hq'], + requirements: [], + inputs: [], + outputs: [{ name: 'output_image', type: 'image' }], + defaultSettings: { + imageModel: 'Lykon/dreamshaper-8', + inpaintModel: 'Lykon/dreamshaper-8-inpainting', + width: 512, + height: 512, + guidanceScale: 7, + inferenceSteps: 50, + scheduler: "DPM++ SDE Karras" + }, + displayedSettings: [ + 'imageModel', + 'inpaintModel', + 'guidanceScale', + 'inferenceSteps', + 'scheduler', + ], + modifiableSettings: [ + 'resolution', + 'seed', + 'negativePrompt', + 'batchSize', + 'imagePreview', + 'safetyCheck', + ] + }, + { + name: 'Standard - Fast', + backend: 'default', + tags: ['sd1.5', 'fast'], + requirements: [], + inputs: [], + outputs: [{ name: 'output_image', type: 'image' }], + defaultSettings: { + imageModel: 'Lykon/dreamshaper-8', + inpaintModel: 'Lykon/dreamshaper-8-inpainting', + width: 512, + height: 512, + guidanceScale: 1, + inferenceSteps: 6, + scheduler: "LCM", + lora: "latent-consistency/lcm-lora-sdv1-5" + }, + displayedSettings: [ + 'imageModel', + 'inpaintModel', + 'guidanceScale', + 'inferenceSteps', + 'scheduler', + 'lora' + ], + modifiableSettings: [ + 'resolution', + 'seed', + 'negativePrompt', + 'batchSize', + 'imagePreview', + 'safetyCheck', + ] + }, + { + name: 'HD', + backend: 'default', + tags: ['sdxl', 'high-vram'], + requirements: [], + inputs: [], + outputs: [{ name: 'output_image', type: 'image' }], + defaultSettings: { + imageModel: 'RunDiffusion/Juggernaut-XL-v9', + inpaintModel: 'RunDiffusion/Juggernaut-XL-v9', + width: 1024, + height: 1024, + guidanceScale: 7, + inferenceSteps: 20, + scheduler: "DPM++ SDE", + lora: "None" + }, + displayedSettings: [ + 'imageModel', + 'inpaintModel', + 'guidanceScale', + 'inferenceSteps', + 'scheduler', + ], + modifiableSettings: [ + 'resolution', + 'seed', + 'negativePrompt', + 'batchSize', + 'imagePreview', + 'safetyCheck', + ] + }, + { + name: 'HD - High Quality', + backend: 'default', + tags: ['sdxl', 'high-vram', 'hq'], + requirements: [], + inputs: [], + outputs: [{ name: 'output_image', type: 'image' }], + defaultSettings: { + imageModel: 'RunDiffusion/Juggernaut-XL-v9', + inpaintModel: 'RunDiffusion/Juggernaut-XL-v9', + width: 1024, + height: 1024, + guidanceScale: 7, + inferenceSteps: 50, + scheduler: "DPM++ SDE", + lora: "None" + }, + displayedSettings: [ + 'imageModel', + 'inpaintModel', + 'guidanceScale', + 'inferenceSteps', + 'scheduler', + ], + modifiableSettings: [ + 'resolution', + 'seed', + 'negativePrompt', + 'batchSize', + 'imagePreview', + 'safetyCheck', + ] + }, + { + name: 'HD - Fast', + backend: 'default', + tags: ['sdxl', 'high-vram', 'fast'], + requirements: [], + inputs: [], + outputs: [{ name: 'output_image', type: 'image' }], + defaultSettings: { + imageModel: 'RunDiffusion/Juggernaut-XL-v9', + inpaintModel: 'RunDiffusion/Juggernaut-XL-v9', + width: 1024, + height: 1024, + guidanceScale: 1, + inferenceSteps: 6, + scheduler: "LCM", + lora: "latent-consistency/lcm-lora-sdxl" + }, + displayedSettings: [ + 'imageModel', + 'inpaintModel', + 'guidanceScale', + 'inferenceSteps', + 'scheduler', + ], + modifiableSettings: [ + 'resolution', + 'seed', + 'negativePrompt', + 'batchSize', + 'imagePreview', + 'safetyCheck', + ] + }, + { + name: 'Manual', + backend: 'default', + tags: ['sd1.5', 'sdxl'], + requirements: [], + inputs: [], + outputs: [{ name: 'output_image', type: 'image' }], + displayedSettings: [ + ], + modifiableSettings: [ + 'seed', + 'negativePrompt', + 'batchSize', + 'imagePreview', + 'safetyCheck', + 'width', + 'height', + 'imageModel', + 'inpaintModel', + 'inferenceSteps', + 'guidanceScale', + 'scheduler', + 'lora', + ] + }, +] \ No newline at end of file diff --git a/WebUI/src/assets/js/store/models.ts b/WebUI/src/assets/js/store/models.ts index 6f23d886..7782acc1 100644 --- a/WebUI/src/assets/js/store/models.ts +++ b/WebUI/src/assets/js/store/models.ts @@ -23,6 +23,8 @@ export const useModels = defineStore("models", () => { const models = ref(predefinedModels); const llms = computed(() => models.value.filter(m => m.type === 'llm')); + const downloadList = ref([]); + async function refreshModels() { const sdModels = await window.electronAPI.getDownloadedDiffusionModels(); const llmModels = await window.electronAPI.getDownloadedLLMs(); @@ -44,6 +46,8 @@ export const useModels = defineStore("models", () => { } + async function download(models: DownloadModelParam[]) { + }; refreshModels() return { @@ -51,7 +55,9 @@ export const useModels = defineStore("models", () => { llms, hfToken, hfTokenIsValid: computed(() => hfToken.value?.startsWith('hf_')), + downloadList, refreshModels, + download, } }, { persist: { diff --git a/WebUI/src/assets/js/store/stableDiffusion.ts b/WebUI/src/assets/js/store/stableDiffusion.ts new file mode 100644 index 00000000..747fabd8 --- /dev/null +++ b/WebUI/src/assets/js/store/stableDiffusion.ts @@ -0,0 +1,244 @@ +import { defineStore } from "pinia"; +import { StableDiffusionSettings, useImageGeneration } from "./imageGeneration"; +import { useGlobalSetup } from "./globalSetup"; +import { Const } from "../const"; +import { useModels } from "./models"; +import { util } from "../util"; +import { SSEProcessor } from "../sseProcessor"; +import { useI18N } from "./i18n"; +import { toast } from "../toast"; + +type BackendParams = { + mode: number, + device: string, + prompt: string, + model_repo_id: string, + negative_prompt: string, + generate_number: number, + inference_steps: number, + guidance_scale: number, + seed: number, + height: number, + width: number, + lora: string, + scheduler: string, + image_preview: boolean, + safe_check: boolean +} + +export const useStableDiffusion = defineStore("stableDiffusion", () => { + const settings = ref({ + resolution: 'standard', + quality: 'standard', + negativePrompt: "bad hands, nsfw", + batchSize: 1, + seed: -1, + width: 512, + height: 512, + imageModel: "Lykon/dreamshaper-8", + inpaintModel: "Lykon/dreamshaper-8-inpainting", + guidanceScale: 7.5, + inferenceSteps: 20, + lora: null, + scheduler: null, + imagePreview: true, + safetyCheck: true + }); + + const manualModeSettings = ref({ + resolution: 'standard', + quality: 'standard', + imageModel: "Lykon/dreamshaper-8", + inpaintModel: "Lykon/dreamshaper-8-inpainting", + negativePrompt: "bad hands, nsfw", + batchSize: 1, + width: 512, + height: 512, + guidanceScale: 7.5, + inferenceSteps: 20, + seed: -1, + lora: null, + scheduler: null, + imagePreview: true, + safetyCheck: true + }); + + const imageGeneration = useImageGeneration(); + const globalSetup = useGlobalSetup(); + const i18nState = useI18N().state; + const hdWarningDismissed = ref(false); + const hdWarningModalActive = ref(false); + const models = useModels(); + + let abortContooler: AbortController | null; + const generateParams = ref(new Array()); + + const setModel = (model: 'sd' | 'inpaint', modelId: string) => { + if (model === 'sd') settings.value.imageModel = modelId; + if (model === 'inpaint') settings.value.inpaintModel = modelId; + } + + const setResolution = (resolution?: StableDiffusionSettings['resolution'], confirm = false) => { + if (resolution === 'hd' && !hdWarningDismissed.value && !confirm) { + hdWarningModalActive.value = true; + return; + } + if (resolution) settings.value.resolution = resolution; + hdWarningModalActive.value = false; + } + + async function generate() { + if (imageGeneration.processing) { return; } + try { + imageGeneration.processing = true; + await checkModel(); + const defaultBackendParams = { + mode: 0, + device: globalSetup.modelSettings.graphics, + prompt: imageGeneration.prompt, + model_repo_id: `stableDiffusion:${imageGeneration.imageModel}`, + negative_prompt: imageGeneration.negativePrompt, + generate_number: imageGeneration.batchSize, + inference_steps: imageGeneration.inferenceSteps, + guidance_scale: imageGeneration.guidanceScale, + seed: imageGeneration.seed, + height: imageGeneration.height, + width: imageGeneration.width, + lora: imageGeneration.lora, + scheduler: imageGeneration.scheduler, + image_preview: imageGeneration.imagePreview, + safe_check: imageGeneration.safeCheck + }; + + await sendGenerate(defaultBackendParams); + } catch (ex) { + } finally { + imageGeneration.processing = false; + } + } + + async function checkModel() { + return new Promise(async (resolve, reject) => { + const checkList: CheckModelExistParam[] = [{ repo_id: globalSetup.modelSettings.sd_model, type: Const.MODEL_TYPE_STABLE_DIFFUSION }]; + if (globalSetup.modelSettings.lora != "None") { + checkList.push({ repo_id: globalSetup.modelSettings.lora, type: Const.MODEL_TYPE_LORA }) + } + if (globalSetup.modelSettings.imagePreview) { + checkList.push({ repo_id: "madebyollin/taesd", type: Const.MODEL_TYPE_PREVIEW }) + checkList.push({ repo_id: "madebyollin/taesdxl", type: Const.MODEL_TYPE_PREVIEW }) + } + const result = await globalSetup.checkModelExists(checkList); + const downloadList: CheckModelExistParam[] = []; + for (const item of result) { + if (!item.exist) { + downloadList.push({ repo_id: item.repo_id, type: item.type }) + } + } + await models.download(downloadList); + resolve(); + }); + } + + + function finishGenerate() { + imageGeneration.processing = false; + } + + async function dataProcess(line: string) { + util.log(`SD data: ${line}`); + const dataJson = line.slice(5); + const data = JSON.parse(dataJson) as SDOutCallback; + switch (data.type) { + case "image_out": + imageGeneration.currentState = "image_out"; + if (!data.safe_check_pass) { + data.image = '/src/assets/image/nsfw_result_detected.png' + } + await imageGeneration.updateDestImage(data.index, data.image); + generateParams.value.push(data.params); + imageGeneration.generateIdx++; + break; + case "step_end": + imageGeneration.currentState = "generating"; + imageGeneration.stepText = `${i18nState.COM_GENERATING} ${data.step}/${data.total_step}`; + if (data.image) { + await imageGeneration.updateDestImage(data.index, data.image); + } + if (data.step == 0) { + imageGeneration.previewIdx = data.index; + } + break; + case "load_model": + imageGeneration.currentState = "load_model"; + break; + case "load_model_components": + imageGeneration.currentState = data.event == "finish" ? "generating" : "load_model_components"; + break; + case "error": + imageGeneration.processing = false; + imageGeneration.currentState = "error"; + switch (data.err_type) { + case "not_enough_disk_space": + toast.error(i18nState.ERR_NOT_ENOUGH_DISK_SPACE.replace("{requires_space}", data.requires_space).replace("{free_space}", data.free_space)); + break; + case "download_exception": + toast.error(i18nState.ERR_DOWNLOAD_FAILED); + break; + case "runtime_error": + toast.error(i18nState.ERROR_RUNTIME_ERROR); + break; + case "unknow_exception": + toast.error(i18nState.ERROR_GENERATE_UNKONW_EXCEPTION); + break; + } + break; + } + } + + async function sendGenerate(defaultBackendParams: BackendParams) { + try { + imageGeneration.processing = true; + if (!abortContooler) { + abortContooler = new AbortController() + } + const response = await fetch(`${useGlobalSetup().apiHost}/api/sd/generate`, { + method: "POST", + body: util.convertToFormData(defaultBackendParams), + signal: abortContooler.signal + }) + const reader = response.body!.getReader(); + await new SSEProcessor(reader, dataProcess, finishGenerate).start(); + } finally { + imageGeneration.processing = false; + } + } + + async function stop() { + if (imageGeneration.processing && !imageGeneration.stopping) { + imageGeneration.stopping = true; + await fetch(`${globalSetup.apiHost}/api/sd/stopGenerate`); + if (abortContooler) { + abortContooler.abort(); + abortContooler = null; + } + imageGeneration.processing = false; + imageGeneration.stopping = false; + } + } + + + return { + settings, + hdWarningDismissed, + hdWarningModalActive, + generateParams, + setResolution, + setModel, + generate, + stop, + } +}, { + persist: { + pick: ['settings', 'hdWarningDismissed'] + } +}); diff --git a/WebUI/src/assets/js/util.ts b/WebUI/src/assets/js/util.ts index 92a040ce..bf67c0b1 100644 --- a/WebUI/src/assets/js/util.ts +++ b/WebUI/src/assets/js/util.ts @@ -126,7 +126,7 @@ export module util { console.log(`[${util.dateFormat(new Date(), "hh:mm:ss:fff")}] ${message}`); } - export function convertToFromData(data: any) { + export function convertToFormData(data: any) { const formData = new FormData(); for (const key in data) { const val = data[key]; diff --git a/WebUI/src/components/SettingsImageGeneration.vue b/WebUI/src/components/SettingsImageGeneration.vue new file mode 100644 index 00000000..a44ab4ef --- /dev/null +++ b/WebUI/src/components/SettingsImageGeneration.vue @@ -0,0 +1,144 @@ + + \ No newline at end of file diff --git a/WebUI/src/components/SettingsImageWorkflowSelector.vue b/WebUI/src/components/SettingsImageWorkflowSelector.vue new file mode 100644 index 00000000..87a28ec7 --- /dev/null +++ b/WebUI/src/components/SettingsImageWorkflowSelector.vue @@ -0,0 +1,147 @@ + + \ No newline at end of file diff --git a/WebUI/src/components/SettingsUi.vue b/WebUI/src/components/SettingsUi.vue new file mode 100644 index 00000000..650f90bd --- /dev/null +++ b/WebUI/src/components/SettingsUi.vue @@ -0,0 +1,69 @@ + + \ No newline at end of file diff --git a/WebUI/src/components/ui/slider/ResolutionPicker.vue b/WebUI/src/components/ui/slider/ResolutionPicker.vue index 9174a4bb..e41bed77 100644 --- a/WebUI/src/components/ui/slider/ResolutionPicker.vue +++ b/WebUI/src/components/ui/slider/ResolutionPicker.vue @@ -1,142 +1,157 @@ \ No newline at end of file diff --git a/WebUI/src/views/Enhance.vue b/WebUI/src/views/Enhance.vue index 3723c7ab..5a4171fd 100644 --- a/WebUI/src/views/Enhance.vue +++ b/WebUI/src/views/Enhance.vue @@ -479,7 +479,7 @@ async function sendGenerate() { } const response = await fetch(`${globalSetup.apiHost}/api/sd/generate`, { method: "POST", - body: util.convertToFromData(lastPostParams), + body: util.convertToFormData(lastPostParams), signal: abortContooler.signal }) const reader = response.body!.getReader();