├── public ├── _redirects ├── favicon.ico └── kudos_100.webp ├── src ├── version.js ├── actions │ ├── sleep.js │ ├── init.js │ ├── events.js │ ├── watch.js │ ├── save.js │ ├── generate.js │ ├── output.js │ ├── generate_gradio.js │ └── editor.js ├── App.vue ├── components │ ├── CancelButton.vue │ ├── LoadConfigButton.vue │ ├── ImageThumbnails.vue │ ├── ErrorMessage.vue │ ├── dialogs │ │ ├── LicenseDialog.vue │ │ └── EditURLDialog.vue │ ├── ProgressIndicator.vue │ ├── LayoutContainer.vue │ ├── SeedChip.vue │ ├── InputSlider.vue │ ├── editor │ │ ├── InputCanvas.vue │ │ ├── ImageEditor.vue │ │ └── ImageEditorToolbar.vue │ ├── ModelParameterSlider.vue │ ├── LayoutComponent.vue │ ├── FileUploadButton.vue │ ├── PanelHeader.vue │ ├── BackendSelector.vue │ ├── PromptInput.vue │ ├── ModelParameters.vue │ ├── ModelParameter.vue │ ├── ResultImage.vue │ ├── ModelInfo.vue │ └── ResultImages.vue ├── views │ ├── AboutView.vue │ ├── ResultView.vue │ ├── MainView.vue │ ├── HomeView.vue │ ├── RightPanelView.vue │ ├── LeftPanelView.vue │ └── InputView.vue ├── stores │ ├── input.js │ ├── editor.js │ ├── ui.js │ ├── output.js │ └── backend.js ├── router │ └── index.js ├── assets │ ├── main.css │ └── base.css ├── backends │ └── gradio │ │ ├── latent-diffusion.json │ │ ├── stable-diffusion.json │ │ └── stable-diffusion-automatic1111.json └── main.js ├── doc ├── cute_bunny.gif └── automatic1111_fullscreen.png ├── .vscode └── extensions.json ├── .github └── workflows │ └── lint.yml ├── vite.config.js ├── index.html ├── .gitignore ├── .eslintrc.cjs ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | const version = "0.10"; 2 | 3 | export { version }; 4 | -------------------------------------------------------------------------------- /doc/cute_bunny.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leszekhanusz/diffusion-ui/HEAD/doc/cute_bunny.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leszekhanusz/diffusion-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/kudos_100.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leszekhanusz/diffusion-ui/HEAD/public/kudos_100.webp -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/actions/sleep.js: -------------------------------------------------------------------------------- 1 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 2 | 3 | export { sleep }; 4 | -------------------------------------------------------------------------------- /doc/automatic1111_fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leszekhanusz/diffusion-ui/HEAD/doc/automatic1111_fullscreen.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/actions/init.js: -------------------------------------------------------------------------------- 1 | import { onKeyUp } from "@/actions/events"; 2 | import { setupWatchers } from "@/actions/watch"; 3 | 4 | function onHomeMounted() { 5 | document.addEventListener("keyup", onKeyUp); 6 | setupWatchers(); 7 | } 8 | 9 | export { onHomeMounted }; 10 | -------------------------------------------------------------------------------- /src/components/CancelButton.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: eslint 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install modules 13 | run: npm install 14 | - name: Run ESLint 15 | run: npm run lintcheck 16 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | "@": fileURLToPath(new URL("./src", import.meta.url)), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DiffusionUI 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /src/actions/events.js: -------------------------------------------------------------------------------- 1 | import { useUIStore } from "@/stores/ui"; 2 | import { useOutputStore } from "@/stores/output"; 3 | import { onKeyUp as onEditorKeyUp } from "@/actions/editor"; 4 | 5 | function onKeyUp(e) { 6 | const ui = useUIStore(); 7 | 8 | if (ui.show_results) { 9 | const output = useOutputStore(); 10 | output.onKeyUp(e); 11 | } else { 12 | onEditorKeyUp(e); 13 | } 14 | } 15 | 16 | export { onKeyUp }; 17 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-prettier", 10 | "plugin:vue-pug/vue3-recommended", 11 | ], 12 | parserOptions: { 13 | ecmaVersion: "latest", 14 | }, 15 | rules: { 16 | "vue/no-mutating-props": "off", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/LoadConfigButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/actions/watch.js: -------------------------------------------------------------------------------- 1 | import { toRef, watch } from "vue"; 2 | import { useBackendStore } from "@/stores/backend"; 3 | import { useOutputStore } from "@/stores/output"; 4 | 5 | function setupWatchers() { 6 | const backend = useBackendStore(); 7 | const output = useOutputStore(); 8 | 9 | watch(toRef(output.image_index, "current"), function () { 10 | output.imageIndexUpdated(); 11 | }); 12 | 13 | watch(toRef(backend, "backend_id"), function () { 14 | backend.selectedBackendUpdated(); 15 | }); 16 | } 17 | 18 | export { setupWatchers }; 19 | -------------------------------------------------------------------------------- /src/components/ImageThumbnails.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /src/stores/input.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useBackendStore } from "@/stores/backend"; 3 | 4 | export const useInputStore = defineStore({ 5 | id: "input", 6 | state: () => ({}), 7 | getters: { 8 | seed: function () { 9 | const backend = useBackendStore(); 10 | 11 | if (backend.hasInput("seeds")) { 12 | return backend.getInput("seeds"); 13 | } 14 | 15 | if (backend.hasInput("seed")) { 16 | return backend.getInput("seed"); 17 | } 18 | 19 | return ""; 20 | }, 21 | seed_is_set: function (state) { 22 | const seed = state.seed; 23 | 24 | return seed !== -1 && seed !== ""; 25 | }, 26 | }, 27 | actions: {}, 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /src/views/ResultView.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/components/dialogs/LicenseDialog.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 27 | -------------------------------------------------------------------------------- /src/actions/save.js: -------------------------------------------------------------------------------- 1 | import FileSaver from "file-saver"; 2 | import { useOutputStore } from "@/stores/output"; 3 | 4 | async function saveResultImage(image_index) { 5 | const output = useOutputStore(); 6 | 7 | let image_data = output.images.content[image_index]; 8 | const fileextension = image_data.startsWith("data:image/webp;") 9 | ? ".webp" 10 | : ".png"; 11 | const filename = output.images.metadata.input.prompt + fileextension; 12 | 13 | // If the image_data is a URL instead of a blob, we first need to download it 14 | if (image_data.startsWith("http")) { 15 | const result = await fetch(image_data); 16 | image_data = await result.blob(); 17 | } 18 | 19 | FileSaver.saveAs(image_data, filename); 20 | } 21 | 22 | export { saveResultImage }; 23 | -------------------------------------------------------------------------------- /src/components/ProgressIndicator.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/LayoutContainer.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import HomeView from "../views/HomeView.vue"; 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: "/", 9 | name: "home", 10 | component: HomeView, 11 | }, 12 | { 13 | path: "/b/:backend_id", 14 | name: "backend", 15 | component: HomeView, 16 | }, 17 | { 18 | path: "/about", 19 | name: "about", 20 | // route level code-splitting 21 | // this generates a separate chunk (About.[hash].js) for this route 22 | // which is lazy-loaded when the route is visited. 23 | component: () => import("../views/AboutView.vue"), 24 | }, 25 | ], 26 | }); 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /src/components/SeedChip.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 30 | -------------------------------------------------------------------------------- /src/components/InputSlider.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /src/components/editor/InputCanvas.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /src/components/ModelParameterSlider.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /src/components/LayoutComponent.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/components/editor/ImageEditor.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hanusz Leszek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/FileUploadButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diffusion-ui", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview --port 4173", 8 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 9 | "lintcheck": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --max-warnings 0 --ignore-path .gitignore" 10 | }, 11 | "dependencies": { 12 | "@fortawesome/fontawesome-svg-core": "^6.2.0", 13 | "@fortawesome/free-solid-svg-icons": "^6.2.0", 14 | "@fortawesome/vue-fontawesome": "^3.0.1", 15 | "@vueuse/core": "^9.2.0", 16 | "deepmerge": "^4.2.2", 17 | "fabric": "^5.2.4", 18 | "file-saver": "^2.0.5", 19 | "pinia": "^2.0.17", 20 | "primeflex": "^3.2.1", 21 | "primeicons": "^5.0.0", 22 | "primevue": "^3.16.2", 23 | "pug": "^3.0.2", 24 | "vue": "^3.2.37", 25 | "vue-router": "^4.1.3", 26 | "vue3-touch-events": "^4.1.0" 27 | }, 28 | "devDependencies": { 29 | "@rushstack/eslint-patch": "^1.1.4", 30 | "@vitejs/plugin-vue": "^3.0.1", 31 | "@vue/eslint-config-prettier": "^7.0.0", 32 | "eslint": "^8.21.0", 33 | "eslint-plugin-vue": "^9.4.0", 34 | "eslint-plugin-vue-pug": "^0.5.4", 35 | "prettier": "^2.7.1", 36 | "vite": "^3.0.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/PanelHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | 56 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | 4 | body { 5 | overscroll-behavior: contain; 6 | background-color: whitesmoke; 7 | } 8 | 9 | #app { 10 | max-width: 1280px; 11 | margin: 0 auto; 12 | 13 | font-weight: normal; 14 | } 15 | 16 | a, 17 | .green { 18 | text-decoration: none; 19 | color: hsla(160, 100%, 37%, 1); 20 | transition: 0.4s; 21 | } 22 | 23 | h2, h3 { 24 | font-weight: 600; 25 | } 26 | 27 | @media (hover: hover) { 28 | a:hover { 29 | background-color: hsla(160, 100%, 37%, 0.2); 30 | } 31 | } 32 | 33 | .hide-sm { 34 | display: initial; 35 | } 36 | 37 | .show-sm { 38 | display: none; 39 | } 40 | 41 | @media (max-width: 600px) { 42 | .hide-sm { 43 | display: none; 44 | } 45 | 46 | .show-sm { 47 | display: initial; 48 | } 49 | } 50 | 51 | /* Fixing breakpoint different from primevue and primeflex */ 52 | @media screen and (max-width: 64em) { 53 | .p-sidebar-left.p-sidebar-lg, 54 | .p-sidebar-left.p-sidebar-md, 55 | .p-sidebar-right.p-sidebar-lg, 56 | .p-sidebar-right.p-sidebar-md { 57 | width: 40rem; 58 | } 59 | } 60 | 61 | @media screen and (max-width: 991px) { 62 | .p-sidebar-left.p-sidebar-lg, 63 | .p-sidebar-left.p-sidebar-md, 64 | .p-sidebar-right.p-sidebar-lg, 65 | .p-sidebar-right.p-sidebar-md { 66 | width: 20rem; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/views/MainView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | 30 | 50 | -------------------------------------------------------------------------------- /src/components/dialogs/EditURLDialog.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/BackendSelector.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | 32 | 33 | 39 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 50 | -------------------------------------------------------------------------------- /src/components/PromptInput.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 43 | -------------------------------------------------------------------------------- /src/components/ModelParameters.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | 43 | -------------------------------------------------------------------------------- /src/views/RightPanelView.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 42 | 43 | 58 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | *, 40 | *::before, 41 | *::after { 42 | box-sizing: border-box; 43 | margin: 0; 44 | position: relative; 45 | font-weight: normal; 46 | } 47 | 48 | body { 49 | min-height: 100vh; 50 | color: var(--color-text); 51 | background: var(--color-background); 52 | transition: color 0.5s, background-color 0.5s; 53 | line-height: 1.6; 54 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 55 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 56 | font-size: 15px; 57 | text-rendering: optimizeLegibility; 58 | -webkit-font-smoothing: antialiased; 59 | -moz-osx-font-smoothing: grayscale; 60 | } 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for helping to make Diffusion-UI awesome! 4 | 5 | We welcome all kinds of contributions: 6 | 7 | - Bug fixes 8 | - Documentation improvements 9 | - New features 10 | - Refactoring & tidying 11 | 12 | ## Getting started 13 | 14 | If you have a specific contribution in mind, be sure to check the 15 | [issues](https://github.com/leszekhanusz/diffusion-ui/issues) 16 | and [pull requests](https://github.com/leszekhanusz/diffusion-ui/pulls) 17 | in progress - someone could already be working on something similar 18 | and you can help out. 19 | 20 | ## Coding guidelines 21 | 22 | Some tools are used to ensure a coherent coding style. 23 | You need to make sure that your code satisfy those requirements 24 | or the automated tests will fail. 25 | 26 | - eslint 27 | 28 | To use the linter to fix your code, run: 29 | ```sh 30 | npm run lint 31 | ``` 32 | 33 | ## Pre-commit hook 34 | 35 | If you want to automatically verify that the linting is respected 36 | at every commit, you can put this at the end of a `.git/hooks/pre-commit` file: 37 | 38 | ```sh 39 | exec npm run lintcheck 40 | ``` 41 | 42 | ## How to create a good Pull Request 43 | 44 | 1. Make a fork of the main branch on github 45 | 2. Clone your forked repo on your computer 46 | 3. Create a feature branch `git checkout -b feature_my_awesome_feature` 47 | 4. Modify the code 48 | 5. Verify that the [Coding guidelines](#coding-guidelines) are respected 49 | 7. Make a commit and push it to your fork 50 | 8. From github, create the pull request. Automated tests from GitHub actions 51 | will then automatically check the code 52 | 9. If other modifications are needed, you are free to create more commits and 53 | push them on your branch. They'll get added to the PR automatically. 54 | 55 | Once the Pull Request is accepted and merged, you can safely 56 | delete the branch (and the forked repo if no more development is needed). 57 | -------------------------------------------------------------------------------- /src/backends/gradio/latent-diffusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "latent_diffusion", 3 | "name": "Latent Diffusion", 4 | "description": "Generate images from text with Latent Diffusion LAION-400M", 5 | "base_url": "https://huggingface.co/gradioiframe/multimodalart/latentdiffusion", 6 | "api_path": "api/predict/", 7 | "type": "gradio", 8 | "inputs": [ 9 | { 10 | "label": "Prompt", 11 | "id": "prompt", 12 | "description": "try adding increments to your prompt such as 'oil on canvas', 'a painting', 'a book cover'", 13 | "type": "text", 14 | "default": "" 15 | }, { 16 | "label": "Steps", 17 | "id": "nb_steps", 18 | "description": "More steps can increase quality but will take longer to generate", 19 | "type": "int", 20 | "default": 50, 21 | "validation": { 22 | "min": 1, 23 | "max": 50 24 | } 25 | }, { 26 | "label": "Width", 27 | "id": "width", 28 | "type": "int", 29 | "default": 256, 30 | "visible": false 31 | }, { 32 | "label": "Height", 33 | "id": "height", 34 | "type": "int", 35 | "default": 256, 36 | "visible": false 37 | }, { 38 | "label": "Number of Images", 39 | "id": "nb_images", 40 | "description": "How many images you wish to generate", 41 | "type": "int", 42 | "default": 1, 43 | "validation": { 44 | "min": 1, 45 | "max": 4 46 | } 47 | }, { 48 | "label": "Diversity scale", 49 | "id": "diversity_scale", 50 | "description": "How different from one another you wish the images to be", 51 | "type": "float", 52 | "default": 5.0, 53 | "step": 0.1, 54 | "validation": { 55 | "min": 1.0, 56 | "max": 15.0 57 | } 58 | } 59 | ], 60 | "outputs": [ 61 | { 62 | "label": "Result", 63 | "type": "image" 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/views/LeftPanelView.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 39 | 40 | 80 | -------------------------------------------------------------------------------- /src/stores/editor.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | 3 | export const useEditorStore = defineStore({ 4 | id: "editor", 5 | state: () => ({ 6 | has_image: false, // false = prompt only, true = image or drawing 7 | uploaded_image_b64: null, // original image uploaded 8 | init_image_b64: null, // final generated image from canvas 9 | mask_image_b64: null, // final generated mask from canvas 10 | canvas: null, // fabric.js main canvas 11 | canvas_mask: null, // generated fabric.js canvas for the mask 12 | layers: { 13 | // layers from top to bottom 14 | brush_outline: null, // Circle outline used to show a cursor for erasing or drawing 15 | emphasize: null, // fabric.js Group layer above the image to emphasize the masked areas 16 | image: null, // image layer with holes defined in image_clip 17 | draw: null /* fabric.js Group containing: 18 | - for images: 19 | * the original image 20 | * all the drawn strokes 21 | - for drawings: 22 | * a white rectangle 23 | * all the drawn strokes 24 | This layer opacity will change depending on strength*/, 25 | // Below is a simulated transparent pattern 26 | }, 27 | image_clip: null, // holes defined in the image layer (same as mask) 28 | brush: null, // fabric.PencilBrush 29 | chosen_color: "0000ff", // value returned from the color picker component 30 | brush_size: { 31 | // brush size, we're keeping different size for drawing or erasing 32 | eraser: 60, 33 | draw: 10, 34 | slider: 60, 35 | }, 36 | history: { 37 | // complete history of the strokes (erasing and drawing) 38 | undo: [], 39 | redo: [], 40 | }, 41 | mode: "txt2img", // "txt2img", "img2img" or "inpainting" 42 | width: 512, // canvas width 43 | height: 512, // canvas height 44 | zoom_max: 5, 45 | zoom_min: 0.4, 46 | }), 47 | getters: { 48 | color: function (state) { 49 | return "#" + state.chosen_color; 50 | }, 51 | is_drawing: (state) => state.uploaded_image_b64 === null, 52 | img_format: function () { 53 | return "png"; 54 | }, 55 | }, 56 | actions: {}, 57 | }); 58 | -------------------------------------------------------------------------------- /src/components/ModelParameter.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 43 | 44 | 52 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | 4 | import App from "./App.vue"; 5 | import PrimeVue from "primevue/config"; 6 | import router from "./router"; 7 | 8 | import "./assets/main.css"; 9 | 10 | import "primevue/resources/themes/lara-light-indigo/theme.css"; 11 | import "primevue/resources/primevue.min.css"; 12 | import "primeflex/primeflex.css"; 13 | import "primeicons/primeicons.css"; 14 | import ToastService from "primevue/toastservice"; 15 | import Tooltip from "primevue/tooltip"; 16 | import ConfirmationService from "primevue/confirmationservice"; 17 | import { useConfirm } from "primevue/useconfirm"; 18 | import { useToast } from "primevue/usetoast"; 19 | 20 | import Vue3TouchEvents from "vue3-touch-events"; 21 | 22 | /* import the fontawesome core */ 23 | import { library } from "@fortawesome/fontawesome-svg-core"; 24 | 25 | /* import font awesome icon component */ 26 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 27 | 28 | /* import specific icons */ 29 | import { 30 | faAngleLeft, 31 | faAngleRight, 32 | faAnglesRight, 33 | faArrowsRotate, 34 | faBook, 35 | faCircle, 36 | faCircleInfo, 37 | faEraser, 38 | faGears, 39 | faImage, 40 | faImages, 41 | faLeftLong, 42 | faLink, 43 | faPaintbrush, 44 | faPencil, 45 | faRotateLeft, 46 | faRotateRight, 47 | faSliders, 48 | faXmark, 49 | } from "@fortawesome/free-solid-svg-icons"; 50 | 51 | /* add icons to the library */ 52 | library.add( 53 | faAngleLeft, 54 | faAngleRight, 55 | faAnglesRight, 56 | faArrowsRotate, 57 | faBook, 58 | faCircle, 59 | faCircleInfo, 60 | faEraser, 61 | faGears, 62 | faImage, 63 | faImages, 64 | faLeftLong, 65 | faLink, 66 | faPaintbrush, 67 | faPencil, 68 | faRotateLeft, 69 | faRotateRight, 70 | faSliders, 71 | faXmark 72 | ); 73 | 74 | const app = createApp(App); 75 | 76 | const pinia = createPinia(); 77 | app.use(pinia); 78 | app.use(router); 79 | app.use(PrimeVue); 80 | app.use(ConfirmationService); 81 | app.use(ToastService); 82 | app.use(Vue3TouchEvents); 83 | 84 | function confirmDialogPiniaPlugin() { 85 | return { 86 | $confirm: useConfirm(), 87 | $toast: useToast(), 88 | }; 89 | } 90 | pinia.use(confirmDialogPiniaPlugin); 91 | 92 | app.component("font-awesome-icon", FontAwesomeIcon); 93 | 94 | app.directive("tooltip", Tooltip); 95 | 96 | app.mount("#app"); 97 | -------------------------------------------------------------------------------- /src/components/ResultImage.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 76 | 77 | 99 | -------------------------------------------------------------------------------- /src/stores/ui.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useEditorStore } from "@/stores/editor"; 3 | import { useBackendStore } from "@/stores/backend"; 4 | 5 | export const useUIStore = defineStore({ 6 | id: "ui", 7 | state: () => ({ 8 | left_panel_visible: false, 9 | right_panel_visible: false, 10 | edit_url_visible: false, 11 | edit_url_new_url: "", 12 | cursor_mode: "idle", // idle, eraser or draw 13 | editor_view: "composite", // "composite" for the normal view or "mask" to show the mask 14 | show_results: false, 15 | show_latest_result: true, 16 | }), 17 | getters: { 18 | show_brush: (state) => state.cursor_mode !== "idle", 19 | show_color_picker: (state) => state.cursor_mode === "draw", 20 | show_eraser: function (state) { 21 | const editor = useEditorStore(); 22 | const backend = useBackendStore(); 23 | return ( 24 | state.editor_view === "composite" && 25 | !editor.is_drawing && 26 | backend.has_inpaint_mode 27 | ); 28 | }, 29 | show_pencil: function (state) { 30 | const editor = useEditorStore(); 31 | if (editor.has_image) { 32 | if (editor.is_drawing) { 33 | return state.editor_view === "composite"; 34 | } else { 35 | return ( 36 | state.editor_view === "composite" && editor.mode === "inpainting" 37 | ); 38 | } 39 | } else { 40 | return false; 41 | } 42 | }, 43 | show_undo: function () { 44 | const editor = useEditorStore(); 45 | return editor.history.undo.length > 0; 46 | }, 47 | show_redo: function () { 48 | const editor = useEditorStore(); 49 | return editor.history.redo.length > 0; 50 | }, 51 | show_mask_button: function (state) { 52 | const editor = useEditorStore(); 53 | return state.cursor_mode === "idle" && editor.mode === "inpainting"; 54 | }, 55 | show_strength_slider: function (state) { 56 | const editor = useEditorStore(); 57 | return editor.has_image && state.editor_view === "composite"; 58 | }, 59 | }, 60 | actions: { 61 | showEditURL() { 62 | const backend = useBackendStore(); 63 | this.edit_url_new_url = backend.current.base_url; 64 | this.edit_url_visible = true; 65 | }, 66 | hideEditURL() { 67 | this.edit_url_visible = false; 68 | }, 69 | showLeftPanel() { 70 | this.left_panel_visible = true; 71 | }, 72 | hideLeftPanel() { 73 | this.left_panel_visible = false; 74 | }, 75 | showRightPanel() { 76 | this.right_panel_visible = true; 77 | }, 78 | hideRightPanel() { 79 | this.right_panel_visible = false; 80 | }, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /src/components/ModelInfo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 53 | 54 | 80 | -------------------------------------------------------------------------------- /src/components/ResultImages.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 56 | 57 | 92 | -------------------------------------------------------------------------------- /src/stores/output.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { nextTick } from "vue"; 3 | 4 | export const useOutputStore = defineStore({ 5 | id: "output", 6 | state: () => ({ 7 | loading_images: false, 8 | loading_model: false, 9 | loading_user_info: true, 10 | loading_models: true, 11 | loading_progress: null, 12 | loading_message: null, 13 | request_uuid: null, 14 | image_preview: null, 15 | image_index: { 16 | current: 0, 17 | saved: 0, 18 | }, 19 | gallery: [], 20 | gallery_index: 0, 21 | error_message: null, 22 | }), 23 | getters: { 24 | loading: (state) => state.loading_images || state.loading_model, 25 | nb_images: (state) => state.images?.content.length, 26 | nb_gallery: (state) => state.gallery.length, 27 | images: (state) => state.gallery[state.gallery_index], 28 | gallery_images: function (state) { 29 | return state.images?.content.map(function (image, idx) { 30 | return { 31 | itemImageSrc: image, 32 | thumbnailImageSrc: image, 33 | index: idx, 34 | }; 35 | }); 36 | }, 37 | }, 38 | actions: { 39 | goLeft() { 40 | if (this.image_index.current > 0) { 41 | this.image_index.current--; 42 | this.image_index.saved = this.image_index.current; 43 | } 44 | }, 45 | goRight() { 46 | if (this.image_index.current < this.nb_images - 1) { 47 | this.image_index.current++; 48 | this.image_index.saved = this.image_index.current; 49 | } 50 | }, 51 | async restoreImageIndex() { 52 | const saved = this.image_index.saved; 53 | if (saved >= this.nb_images) { 54 | this.image_index.current = this.nb_images - 1; 55 | } else { 56 | this.image_index.current = this.image_index.saved; 57 | } 58 | await nextTick(); 59 | this.image_index.saved = saved; 60 | }, 61 | goUp() { 62 | if (this.gallery_index > 0) { 63 | this.gallery_index--; 64 | this.restoreImageIndex(); 65 | } 66 | }, 67 | goDown() { 68 | if (this.gallery_index < this.nb_gallery - 1) { 69 | this.gallery_index++; 70 | this.restoreImageIndex(); 71 | } 72 | }, 73 | goToFirst() { 74 | this.image_index.current = 0; 75 | }, 76 | imageIndexUpdated() { 77 | this.image_index.saved = this.image_index.current; 78 | }, 79 | onKeyUp(e) { 80 | switch (e.key) { 81 | case "ArrowDown": 82 | this.goDown(); 83 | e.preventDefault(); 84 | break; 85 | case "ArrowUp": 86 | this.goUp(); 87 | e.preventDefault(); 88 | break; 89 | case "ArrowLeft": 90 | this.goLeft(); 91 | e.preventDefault(); 92 | break; 93 | case "ArrowRight": 94 | this.goRight(); 95 | e.preventDefault(); 96 | break; 97 | case "Home": 98 | this.goToFirst(); 99 | e.preventDefault(); 100 | break; 101 | } 102 | }, 103 | }, 104 | }); 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diffusion-ui 2 | 3 | This is a web interface frontend for generation of images using the 4 | [Automatic1111 fork](https://github.com/AUTOMATIC1111/stable-diffusion-webui) of 5 | [Stable Diffusion](https://github.com/CompVis/stable-diffusion). 6 | 7 |

8 | 9 |

10 | 11 |

12 | 13 |

14 | 15 | ## Documentation 16 | 17 | The documentation is available [here](https://diffusionui.readthedocs.io) 18 | 19 | ## Technologies 20 | 21 | Diffusion UI was made using: 22 | 23 | * [Vue 3](https://vuejs.org/) with [Pinia](https://pinia.vuejs.org/) 24 | * [PrimeVue components](https://www.primefaces.org/primevue/) 25 | * [Fabric.js](http://fabricjs.com/) 26 | * [The Pug templating language](https://pugjs.org) 27 | * [Font Awesome icons](https://fontawesome.com/) 28 | 29 | ## Features 30 | 31 | * Text-to-image 32 | * Image-to-Image: 33 | * from an uploaded image 34 | * from a drawing made on the interface 35 | * Inpainting 36 | * Including the possibility to draw inside an inpainting region 37 | * Outpainting (using mouse to scroll out) 38 | * Modification of model parameters in left tab 39 | * Image gallery of previous image in the right tab 40 | * Allow to do variations and inpainting edits to previously generated images 41 | 42 | ## Tips 43 | 44 | * Use the mouse wheel to zoom in or zoom out in a provided image 45 | * Use the shift key to make straight lines for drawing or for making inpainting zones 46 | * Use Control-z to cancel an action in the image editor 47 | * Use the arrow keys (left,right,up and down) to move inside the image gallery. 48 | The Home key will allow you to go back to the first image of the batch. 49 | 50 | ## Frontend 51 | 52 | The frontend is available at [diffusionui.com](http://diffusionui.com) 53 | (**Note:** You still need to have a local backend to make it work with Stable diffusion) 54 | 55 | Or alternatively you can [run it locally](https://diffusionui.readthedocs.io/en/latest/frontend.html). 56 | 57 | ## Backends 58 | 59 | ### Automatic1111 Stable Diffusion 60 | 61 | #### local backend 62 | 63 | To be able to connect diffusion-ui to the Automatic1111 fork of Stable Diffusion from your own pc, you need to 64 | run it with the following parameters: `--no-gradio-queue --cors-allow-origins=http://localhost:5173,https://diffusionui.com`. 65 | 66 | See the instructions [here](https://diffusionui.readthedocs.io/en/latest/backends/automatic1111.html). 67 | 68 | #### online colab backend 69 | 70 | If you can't run it locally, it is also possible to use the automatic1111 fork of Stable Diffusion with diffusion-ui online for free with this [Google Colab notebook](https://colab.research.google.com/github/leszekhanusz/diffusion-ui/blob/main/src/backends/colab/automatic1111.ipynb) 71 | 72 | ## License 73 | [MIT License](https://github.com/leszekhanusz/diffusion-ui/blob/main/LICENSE) for the code here. 74 | 75 | [CreativeML Open RAIL-M license](https://huggingface.co/spaces/CompVis/stable-diffusion-license) 76 | for the Stable Diffusion model. 77 | -------------------------------------------------------------------------------- /src/backends/gradio/stable-diffusion.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "stable_diffusion", 3 | "name": "Stable Diffusion", 4 | "description": "Generate images from text, init image and mask image with Latent Diffusion LAION-400M.", 5 | "base_url": "http://127.0.0.1:7860", 6 | "config_path": "config", 7 | "api_path": "api/predict", 8 | "doc_url": "https://diffusionui.readthedocs.io/en/latest/backends/stable-diffusion.html", 9 | "license": "CreativeML Open RAIL-M", 10 | "license_html": "The model is licensed with a CreativeML Open RAIL-M license. The authors claim no rights on the outputs you generate, you are free to use them and are accountable for their use which must not go against the provisions set in this license. The license forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm, spread misinformation and target vulnerable groups. For the full list of restrictions please read the license

Biases and content acknowledgment

Despite how impressive being able to turn text into image is, beware to the fact that this model may output content that reinforces or exacerbates societal biases, as well as realistic faces, pornography and violence. The model was trained on the LAION-5B dataset, which scraped non-curated image-text-pairs from the internet (the exception being the removal of illegal content) and is meant for research purposes. You can read more in the model card", 11 | "type": "gradio", 12 | "functions": [ 13 | { 14 | "id": "txt2img", 15 | "label": "Unified pipeline", 16 | "fn_index": 0, 17 | "inputs": "auto", 18 | "auto_inputs": { 19 | "api_version": { 20 | "visible": false 21 | }, 22 | "prompt": { 23 | "default": "Doll from a horror movie" 24 | }, 25 | "negative_prompt": { 26 | "description": "What should not appear in the image" 27 | }, 28 | "number_of_images": { 29 | "description": "How many images you wish to generate" 30 | }, 31 | "number_of_steps": { 32 | "description": "More steps can increase quality but will take longer to generate" 33 | }, 34 | "strength": { 35 | "description": "How close we should follow the initial image" 36 | }, 37 | "guidance_scale": { 38 | "description": "How closely we should follow the prompt" 39 | }, 40 | "seeds": { 41 | "description": "Comma separated list of numbers to generate initial latent images. Random if empty" 42 | }, 43 | "access_code": { 44 | "description": "Optional access code for the backend" 45 | } 46 | }, 47 | "outputs": [ 48 | { 49 | "label": "Result", 50 | "type": "image" 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /src/components/editor/ImageEditorToolbar.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 53 | 54 | 59 | 60 | 89 | -------------------------------------------------------------------------------- /src/views/InputView.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 70 | 71 | 132 | -------------------------------------------------------------------------------- /src/actions/generate.js: -------------------------------------------------------------------------------- 1 | import { useUIStore } from "@/stores/ui"; 2 | import { useEditorStore } from "@/stores/editor"; 3 | import { useOutputStore } from "@/stores/output"; 4 | import { useBackendStore } from "@/stores/backend"; 5 | import { resetEditorButtons } from "@/actions/editor"; 6 | import { 7 | generateImageGradio, 8 | cancelGenerationGradio, 9 | changeModelGradio, 10 | } from "@/actions/generate_gradio"; 11 | 12 | async function getJson(response) { 13 | if (!response.ok) { 14 | var json_error = null; 15 | try { 16 | json_error = await response.json(); 17 | } catch (e) { 18 | // Ignore here, error thrown below 19 | } 20 | 21 | if (json_error) { 22 | if (json_error.error) { 23 | throw new Error(json_error.error); 24 | } 25 | } 26 | 27 | const error = new Error( 28 | `Error! The backend returned the http code: ${response.status}` 29 | ); 30 | error.code = response.status; 31 | throw error; 32 | } 33 | 34 | return await response.json(); 35 | } 36 | 37 | async function generateImages() { 38 | const backend = useBackendStore(); 39 | 40 | const backend_type = backend.current["type"]; 41 | 42 | switch (backend_type) { 43 | case "gradio": 44 | await generateImageGradio(); 45 | break; 46 | default: 47 | console.error(`backend type '${backend_type}' not valid`); 48 | } 49 | } 50 | 51 | function checkEditorMode() { 52 | const backend = useBackendStore(); 53 | const editor = useEditorStore(); 54 | 55 | const backend_mode = backend.mode; 56 | 57 | // Special case, reset editor mode to img2img if we are in inpainting mode 58 | // and backend is in img2img mode 59 | if (backend_mode === "img2img" && editor.mode === "inpainting") { 60 | editor.mode = "img2img"; 61 | } 62 | 63 | // Some backends have a single mode for everything 64 | if (!backend_mode) { 65 | return true; 66 | } 67 | 68 | const allowed_modes = backend.getAllowedModes(editor.mode); 69 | 70 | const allowed = allowed_modes.includes(backend_mode); 71 | 72 | if (!allowed) { 73 | var message; 74 | switch (backend_mode) { 75 | case "txt2img": 76 | message = "Text to Image mode cannot use an image!"; 77 | break; 78 | case "img2img": 79 | case "inpainting": 80 | message = "You need an image!"; 81 | break; 82 | default: 83 | message = "Invalid backend mode!"; 84 | } 85 | 86 | backend.$toast.add({ 87 | severity: "warn", 88 | detail: message, 89 | life: 3000, 90 | closable: false, 91 | }); 92 | } 93 | return allowed; 94 | } 95 | 96 | async function generate() { 97 | const output = useOutputStore(); 98 | const backend = useBackendStore(); 99 | const ui = useUIStore(); 100 | 101 | if (!checkEditorMode()) { 102 | return; 103 | } 104 | 105 | ui.show_results = true; 106 | ui.show_latest_result = true; 107 | 108 | resetEditorButtons(); 109 | 110 | if (!output.loading && !backend.show_license) { 111 | output.loading_images = true; 112 | output.loading_progress = null; 113 | output.loading_message = null; 114 | output.image_preview = null; 115 | output.request_uuid = null; 116 | 117 | try { 118 | await generateImages(); 119 | output.error_message = null; 120 | } catch (error) { 121 | ui.show_latest_result = true; 122 | output.error_message = error.message; 123 | console.error(error); 124 | } finally { 125 | output.loading_images = false; 126 | } 127 | } 128 | } 129 | 130 | async function cancelGeneration() { 131 | const backend = useBackendStore(); 132 | 133 | const backend_type = backend.current["type"]; 134 | 135 | switch (backend_type) { 136 | case "gradio": 137 | await cancelGenerationGradio(); 138 | break; 139 | default: 140 | console.error(`backend type '${backend_type}' not valid`); 141 | } 142 | } 143 | 144 | async function changeModel() { 145 | const backend = useBackendStore(); 146 | const output = useOutputStore(); 147 | 148 | try { 149 | output.loading_model = true; 150 | const backend_type = backend.current["type"]; 151 | 152 | switch (backend_type) { 153 | case "gradio": 154 | await changeModelGradio(); 155 | break; 156 | default: 157 | console.error(`backend type '${backend_type}' not valid`); 158 | } 159 | } catch (error) { 160 | console.error(error); 161 | backend.$toast.add({ 162 | severity: "error", 163 | detail: "Loading new model failed", 164 | life: 3000, 165 | closable: false, 166 | }); 167 | } finally { 168 | output.loading_model = false; 169 | } 170 | } 171 | 172 | export { cancelGeneration, changeModel, generate, getJson }; 173 | -------------------------------------------------------------------------------- /src/actions/output.js: -------------------------------------------------------------------------------- 1 | import { useBackendStore } from "@/stores/backend"; 2 | import { useOutputStore } from "@/stores/output"; 3 | import { useUIStore } from "@/stores/ui"; 4 | import { setViewportTransform } from "@/actions/editor"; 5 | 6 | function handleOutputAutomatic1111( 7 | json_result, 8 | images_with_metadata, 9 | backend_function 10 | ) { 11 | const data_field = json_result["data"]; 12 | const data_images = data_field[0]; 13 | 14 | if (backend_function.outputs[1].type === "json") { 15 | const metadata_json = data_field[1]; 16 | 17 | if (!metadata_json) { 18 | // On errors, the backend will send an empty string here 19 | throw new Error(data_field[2]); 20 | } 21 | 22 | const output_metadata = JSON.parse(metadata_json); 23 | console.log("output metadata", output_metadata); 24 | 25 | images_with_metadata.metadata.output = output_metadata; 26 | 27 | if (data_images.length > 1) { 28 | // If there is more than one image, a grid is inserted as a first image 29 | const first_seed = images_with_metadata.metadata.output.all_seeds[0]; 30 | images_with_metadata.metadata.output.all_seeds.unshift(first_seed); 31 | } 32 | } 33 | } 34 | 35 | function handleOutputDefault(json_result, images_with_metadata) { 36 | const data_field = json_result["data"]; 37 | const data_seeds = data_field[1]; 38 | 39 | // Save the generated seeds in the image metadata 40 | const metadata = images_with_metadata.metadata; 41 | 42 | if ("seeds" in metadata.input) { 43 | metadata.input["seeds"] = data_seeds; 44 | console.log(`Images received with seeds: ${data_seeds}`); 45 | } 46 | } 47 | 48 | function handleOutputGradio( 49 | backend_id, 50 | function_id, 51 | input_data, 52 | original_image, 53 | history, 54 | json_result, 55 | canvas_viewport 56 | ) { 57 | const backend = useBackendStore(); 58 | const output = useOutputStore(); 59 | const ui = useUIStore(); 60 | 61 | const data_field = json_result["data"]; 62 | const data_images = data_field[0]; 63 | 64 | // We receive either a single image or a list of images 65 | let images; 66 | if (typeof data_images == "object") { 67 | images = data_images; 68 | } else { 69 | images = [data_images]; 70 | } 71 | 72 | // Fix gradio 3.5 gallery base64 representation changed to local url 73 | // See: https://github.com/gradio-app/gradio/pull/2265 74 | images = images.map(function (image) { 75 | if (typeof image === "string") { 76 | return image; 77 | } else { 78 | if (image.is_file) { 79 | return backend.base_url + "/file=" + image.name; 80 | } else { 81 | return image.data; 82 | } 83 | } 84 | }); 85 | 86 | const images_with_metadata = { 87 | content: images, 88 | metadata: { 89 | input: input_data, 90 | backend_id: backend_id, 91 | function_id: function_id, 92 | canvas_viewport: canvas_viewport, 93 | }, 94 | original_image: original_image, 95 | history: history, 96 | }; 97 | 98 | if (backend.current_function.handle_output) { 99 | if (backend.current_function.handle_output === "automatic1111") { 100 | const backend_function = backend.getFunction(backend_id, function_id); 101 | handleOutputAutomatic1111( 102 | json_result, 103 | images_with_metadata, 104 | backend_function 105 | ); 106 | } 107 | } else { 108 | handleOutputDefault(json_result, images_with_metadata); 109 | } 110 | 111 | // Saving the latest images in the gallery 112 | output.gallery.push(images_with_metadata); 113 | 114 | if (ui.show_latest_result) { 115 | output.gallery_index = output.nb_gallery - 1; 116 | } 117 | } 118 | 119 | function handleOutput(backend_type, ...args) { 120 | switch (backend_type) { 121 | case "gradio": 122 | handleOutputGradio(...args); 123 | break; 124 | default: 125 | console.warn(`Invalid backend type: ${backend_type}`); 126 | } 127 | } 128 | 129 | function resetInputsFromResultImage(image_index) { 130 | const backend = useBackendStore(); 131 | const output = useOutputStore(); 132 | 133 | const input_metadata = output.images.metadata.input; 134 | const output_metadata = output.images.metadata.output; 135 | 136 | const backend_id = output.images.metadata.backend_id; 137 | const function_id = output.images.metadata.function_id; 138 | const canvas_viewport = output.images.metadata.canvas_viewport; 139 | 140 | backend.changeBackend(backend_id); 141 | 142 | if (function_id) { 143 | backend.changeFunction(function_id); 144 | } 145 | 146 | if (canvas_viewport) { 147 | setViewportTransform(canvas_viewport); 148 | } 149 | 150 | let new_batch_count = null; 151 | let new_batch_size = null; 152 | 153 | Object.entries(input_metadata).forEach(function (entry) { 154 | const [data_id, data_value] = entry; 155 | 156 | if (data_id === "seeds") { 157 | const seeds = data_value; 158 | const seed = seeds.split(",")[image_index]; 159 | 160 | backend.setInput("seeds", seed); 161 | } else if (data_id === "seed") { 162 | if (output_metadata) { 163 | if (output_metadata.all_seeds) { 164 | const all_seeds = output_metadata.all_seeds; 165 | 166 | const seed = output_metadata.all_seeds[image_index]; 167 | backend.setInput(data_id, seed); 168 | 169 | if ( 170 | all_seeds.length > 1 && 171 | output_metadata.index_of_first_image && 172 | image_index < output_metadata.index_of_first_image 173 | ) { 174 | // Image grid selected 175 | new_batch_count = input_metadata.batch_count; 176 | new_batch_size = input_metadata.batch_size; 177 | } else { 178 | // Requesting only one image if it's not the grid which is selected 179 | 180 | new_batch_count = 1; 181 | new_batch_size = 1; 182 | } 183 | } 184 | } 185 | } else { 186 | backend.setInput(data_id, data_value); 187 | } 188 | }); 189 | 190 | if (new_batch_count) { 191 | backend.setInput("batch_count", new_batch_count); 192 | } 193 | if (new_batch_size) { 194 | backend.setInput("batch_size", new_batch_size); 195 | } 196 | } 197 | 198 | function resetSeeds() { 199 | const backend = useBackendStore(); 200 | 201 | const without_toast = false; 202 | 203 | if (backend.hasInput("seeds")) { 204 | backend.setInput("seeds", "", without_toast); 205 | return; 206 | } 207 | 208 | const input = backend.findInput("seed"); 209 | 210 | if (input) { 211 | let value; 212 | if (input.type === "text") { 213 | value = ""; 214 | } else { 215 | value = -1; 216 | } 217 | input.value = value; 218 | } 219 | } 220 | 221 | export { handleOutput, resetInputsFromResultImage, resetSeeds }; 222 | -------------------------------------------------------------------------------- /src/actions/generate_gradio.js: -------------------------------------------------------------------------------- 1 | import { getJson } from "@/actions/generate"; 2 | import { useEditorStore } from "@/stores/editor"; 3 | import { useBackendStore } from "@/stores/backend"; 4 | import { useOutputStore } from "@/stores/output"; 5 | import { renderImage } from "@/actions/editor"; 6 | import { handleOutput } from "@/actions/output"; 7 | import { sleep } from "@/actions/sleep"; 8 | 9 | async function check_progress(progress_status) { 10 | const backend = useBackendStore(); 11 | const output = useOutputStore(); 12 | 13 | const payload = { 14 | id_task: progress_status.id_task, 15 | id_live_preview: progress_status.id_live_preview, 16 | }; 17 | 18 | const body = JSON.stringify(payload); 19 | 20 | try { 21 | const result = await fetch(backend.progress_url, { 22 | method: "POST", 23 | body: body, 24 | headers: { "Content-Type": "application/json" }, 25 | }); 26 | 27 | if (progress_status.cancelled) { 28 | return; 29 | } 30 | 31 | const json_result = await getJson(result); 32 | 33 | //const active = json_result.active; 34 | //const queued = json_result.queued; 35 | const completed = json_result.completed; 36 | const progress = json_result.progress; 37 | //const eta = json_result.eta; 38 | const id_live_preview = json_result.id_live_preview; 39 | 40 | progress_status.id_live_preview = id_live_preview; 41 | 42 | if (progress) { 43 | output.loading_progress = progress * 100; 44 | } 45 | 46 | if (completed) { 47 | progress_status.cancelled = true; 48 | } 49 | 50 | const image_preview_data = json_result.live_preview; 51 | 52 | if (image_preview_data) { 53 | output.image_preview = image_preview_data; 54 | } 55 | } catch (e) { 56 | // Nothing here, error simply ignored 57 | } 58 | } 59 | 60 | async function check_progress_loop(progress_status) { 61 | const backend = useBackendStore(); 62 | 63 | if (!backend.current.progress_path) { 64 | return; 65 | } 66 | 67 | progress_status.cancel = function () { 68 | progress_status.cancelled = true; 69 | }; 70 | 71 | while (!progress_status.cancelled) { 72 | await sleep(500); 73 | await check_progress(progress_status); 74 | } 75 | } 76 | 77 | async function generateImageGradio() { 78 | const editor = useEditorStore(); 79 | const backend = useBackendStore(); 80 | 81 | const inputs_config = backend.inputs; 82 | 83 | const backend_id = backend.backend_id; 84 | const function_id = backend.has_multiple_functions 85 | ? backend.current_function.id 86 | : null; 87 | 88 | const original_image = editor.uploaded_image_b64; 89 | const history = editor.has_image ? editor.history : null; 90 | 91 | let canvas_viewport = null; 92 | 93 | if (backend.has_image_input) { 94 | let image_input = backend.getImageInput(); 95 | let mask_image_input = backend.getImageMaskInput(); 96 | 97 | backend.image_inputs.forEach((input) => (input.value = null)); 98 | 99 | if (image_input) { 100 | if (editor.has_image) { 101 | // Create final image in input.init_image_b64 102 | renderImage(); 103 | 104 | image_input.value = editor.init_image_b64; 105 | } 106 | } 107 | 108 | if (mask_image_input) { 109 | mask_image_input.value = editor.mask_image_b64; 110 | } 111 | 112 | canvas_viewport = editor.canvas.viewportTransform; 113 | } 114 | 115 | const input_data = Object.assign( 116 | {}, 117 | ...inputs_config.map((x) => ({ [x["id"]]: x["value"] })) 118 | ); 119 | 120 | const id_task = "task(" + Math.random().toString(36).slice(2) + ")"; 121 | input_data["label"] = id_task; 122 | 123 | // selected tab in img2img tag: 124 | // img2img = 0, sketch, inpaint, inpaint sketch, inpaint upload = 4, batch) 125 | const img2img_type = editor.mode === "inpainting" ? 4 : 0; 126 | input_data["label_0"] = img2img_type; 127 | 128 | console.log("input_data", input_data); 129 | 130 | const input_data_values = Object.values(input_data); 131 | 132 | // Add dummy data at the end to support using the web ui with a newer backend version 133 | for (let i = 0; i < 10; i++) { 134 | input_data_values.push(null); 135 | } 136 | 137 | const payload = { 138 | data: input_data_values, 139 | }; 140 | 141 | if (backend.fn_index) { 142 | // Some gradio interfaces need a function index 143 | payload["fn_index"] = backend.fn_index; 144 | } 145 | 146 | const body = JSON.stringify(payload); 147 | 148 | let progress_status = { 149 | id_task: id_task, 150 | id_live_preview: -1, 151 | cancelled: false, 152 | }; 153 | 154 | const responses = await Promise.all([ 155 | check_progress_loop(progress_status), 156 | (async function () { 157 | try { 158 | const result = await fetch(backend.api_url, { 159 | method: "POST", 160 | body: body, 161 | headers: { "Content-Type": "application/json" }, 162 | }); 163 | return result; 164 | } finally { 165 | if (progress_status.cancel) { 166 | progress_status.cancel(); 167 | } 168 | } 169 | })(), 170 | ]); 171 | 172 | const response = responses[1]; 173 | const json_result = await getJson(response); 174 | 175 | handleOutput( 176 | "gradio", 177 | backend_id, 178 | function_id, 179 | input_data, 180 | original_image, 181 | history, 182 | json_result, 183 | canvas_viewport 184 | ); 185 | } 186 | 187 | async function cancelGenerationGradio() { 188 | const backend = useBackendStore(); 189 | 190 | const fn_index = backend.cancel_fn_index; 191 | 192 | if (!fn_index) { 193 | return; 194 | } 195 | 196 | const payload = { 197 | data: [], 198 | fn_index: fn_index, 199 | }; 200 | 201 | const body = JSON.stringify(payload); 202 | 203 | await fetch(backend.api_url, { 204 | method: "POST", 205 | body: body, 206 | headers: { "Content-Type": "application/json" }, 207 | }); 208 | } 209 | 210 | async function changeModelGradio() { 211 | const backend = useBackendStore(); 212 | 213 | const models_input = backend.models_input; 214 | if (!models_input) { 215 | return; 216 | } 217 | 218 | const fn_index = backend.model_change_fn_index; 219 | 220 | if (!fn_index) { 221 | return; 222 | } 223 | 224 | const requested_model = models_input.value; 225 | console.log(`Requesting changing model to ${requested_model}`); 226 | 227 | const payload = { 228 | data: [requested_model], 229 | fn_index: fn_index, 230 | }; 231 | 232 | const body = JSON.stringify(payload); 233 | 234 | await fetch(backend.api_url, { 235 | method: "POST", 236 | body: body, 237 | headers: { "Content-Type": "application/json" }, 238 | }); 239 | } 240 | 241 | async function initModelDropdown() { 242 | const backend = useBackendStore(); 243 | 244 | const fn_index = backend.model_change_load_fn_index; 245 | 246 | if (!fn_index) { 247 | return; 248 | } 249 | 250 | const payload = { 251 | data: [], 252 | fn_index: fn_index, 253 | }; 254 | 255 | const body = JSON.stringify(payload); 256 | 257 | const response = await fetch(backend.api_url, { 258 | method: "POST", 259 | body: body, 260 | headers: { "Content-Type": "application/json" }, 261 | }); 262 | 263 | const json_result = await getJson(response); 264 | 265 | const model = json_result.data[0]; 266 | console.log(`Current model: ${model}`); 267 | 268 | backend.models_input.value = model; 269 | } 270 | 271 | async function initGradio() { 272 | await initModelDropdown(); 273 | } 274 | 275 | export { 276 | cancelGenerationGradio, 277 | changeModelGradio, 278 | generateImageGradio, 279 | initGradio, 280 | }; 281 | -------------------------------------------------------------------------------- /src/backends/gradio/stable-diffusion-automatic1111.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "automatic1111", 3 | "name": "Stable Diffusion Automatic1111", 4 | "description": "Automatic1111 fork of Stable Diffusion", 5 | "base_url": "http://127.0.0.1:7860", 6 | "config_path": "config", 7 | "api_path": "api/predict", 8 | "progress_path": "internal/progress", 9 | "doc_url": "https://diffusionui.readthedocs.io/en/latest/backends/automatic1111.html", 10 | "license": "CreativeML Open RAIL-M", 11 | "license_accepted": true, 12 | "license_html": "The model is licensed with a CreativeML Open RAIL-M license. The authors claim no rights on the outputs you generate, you are free to use them and are accountable for their use which must not go against the provisions set in this license. The license forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm, spread misinformation and target vulnerable groups. For the full list of restrictions please read the license

Biases and content acknowledgment

Despite how impressive being able to turn text into image is, beware to the fact that this model may output content that reinforces or exacerbates societal biases, as well as realistic faces, pornography and violence. The model was trained on the LAION-5B dataset, which scraped non-curated image-text-pairs from the internet (the exception being the removal of illegal content) and is meant for research purposes. You can read more in the model card", 13 | "type": "gradio", 14 | "common_inputs": [ 15 | { 16 | "label": "Prompt", 17 | "id": "prompt", 18 | "description": "Description of the image to generate", 19 | "type": "text", 20 | "default": "Bouquet of roses" 21 | }, { 22 | "label": "Reverse prompt", 23 | "id": "reverse_prompt", 24 | "description": "What not to generate", 25 | "type": "text", 26 | "default": "" 27 | }, { 28 | "label": "Width", 29 | "id": "width", 30 | "description": "", 31 | "type": "int", 32 | "default": 512, 33 | "step": 64, 34 | "validation": { 35 | "min": 0, 36 | "max": 2048 37 | } 38 | }, { 39 | "label": "Height", 40 | "id": "height", 41 | "description": "", 42 | "type": "int", 43 | "default": 512, 44 | "step": 64, 45 | "validation": { 46 | "min": 0, 47 | "max": 2048 48 | } 49 | } 50 | ], 51 | "model_change": { 52 | "change": { 53 | "fn_index": { 54 | "conditions": { 55 | "trigger": "change", 56 | "targets": { 57 | "0": { 58 | "conditions": { 59 | "label": "Stable Diffusion checkpoint" 60 | } 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | "load": { 67 | "fn_index": { 68 | "conditions": { 69 | "trigger": "load", 70 | "outputs": { 71 | "0": { 72 | "conditions": { 73 | "label": "Stable Diffusion checkpoint" 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | }, 81 | "functions": [ 82 | { 83 | "id": "txt2img", 84 | "mode": "txt2img", 85 | "label": "Text to Image", 86 | "fn_index": { 87 | "conditions": { 88 | "js": "submit", 89 | "trigger": "click" 90 | } 91 | }, 92 | "cancel": { 93 | "fn_index": { 94 | "conditions": { 95 | "trigger": "click", 96 | "targets": { 97 | "0": { 98 | "conditions": { 99 | "elem_id": "txt2img_interrupt" 100 | } 101 | } 102 | } 103 | } 104 | } 105 | }, 106 | "handle_output": "automatic1111", 107 | "layout": [ 108 | { 109 | "type": "container", 110 | "id": "sampling_options", 111 | "label": "Sampling", 112 | "components": [ 113 | { 114 | "type": "input", 115 | "id": "batch_count" 116 | }, 117 | { 118 | "type": "input", 119 | "id": "batch_size" 120 | }, 121 | { 122 | "type": "input", 123 | "id": "sampling_method" 124 | }, 125 | { 126 | "type": "input", 127 | "id": "nb_steps" 128 | } 129 | ] 130 | }, 131 | { 132 | "type": "container", 133 | "id": "prompt_options", 134 | "label": "Prompt options", 135 | "components": [ 136 | { 137 | "type": "input", 138 | "id": "reverse_prompt" 139 | }, 140 | { 141 | "type": "input", 142 | "id": "guidance_scale" 143 | } 144 | ] 145 | }, 146 | { 147 | "type": "container", 148 | "id": "image_options", 149 | "label": "Image size", 150 | "components": [ 151 | { 152 | "type": "input", 153 | "id": "width" 154 | }, 155 | { 156 | "type": "input", 157 | "id": "height" 158 | } 159 | ] 160 | }, 161 | { 162 | "type": "input", 163 | "id": "seed" 164 | }, 165 | { 166 | "type": "container", 167 | "id": "extra_options", 168 | "label": "Extra", 169 | "components": [ 170 | { 171 | "type": "input", 172 | "id": "restore_faces" 173 | }, 174 | { 175 | "type": "input", 176 | "id": "tiling" 177 | }, 178 | { 179 | "type": "input", 180 | "id": "enable_hr" 181 | }, 182 | { 183 | "type": "container", 184 | "id": "hires_options", 185 | "label": "High Resolution options", 186 | "visible": { 187 | "input_id": "enable_hr", 188 | "condition": "===", 189 | "value": true 190 | }, 191 | "components": [ 192 | { 193 | "type": "input", 194 | "id": "strength" 195 | }, 196 | { 197 | "type": "input", 198 | "id": "upscaler" 199 | }, 200 | { 201 | "type": "input", 202 | "id": "hires_steps" 203 | }, 204 | { 205 | "type": "input", 206 | "id": "upscale_by" 207 | }, 208 | { 209 | "type": "input", 210 | "id": "resize_width_to" 211 | }, 212 | { 213 | "type": "input", 214 | "id": "resize_height_to" 215 | } 216 | ] 217 | } 218 | ] 219 | } 220 | ], 221 | "auto_inputs": { 222 | "upscaler": { 223 | "value": "ESRGAN_4x" 224 | }, 225 | "denoising_strength": { 226 | "id": "strength" 227 | }, 228 | "sampling_steps": { 229 | "id": "nb_steps" 230 | }, 231 | "negative_prompt": { 232 | "id": "reverse_prompt", 233 | "type": "common_input" 234 | }, 235 | "prompt": { 236 | "id": "prompt", 237 | "type": "common_input" 238 | }, 239 | "width": { 240 | "id": "width", 241 | "type": "common_input" 242 | }, 243 | "height": { 244 | "id": "height", 245 | "type": "common_input" 246 | }, 247 | "cfg_scale": { 248 | "id": "guidance_scale" 249 | }, 250 | "hires._fix": { 251 | "id": "enable_hr" 252 | } 253 | }, 254 | "inputs": "auto", 255 | "outputs": [ 256 | { 257 | "label": "Result", 258 | "type": "image" 259 | }, { 260 | "label": "JSON metadata", 261 | "type": "json" 262 | }, { 263 | "label": "HTML metadata", 264 | "type": "html" 265 | } 266 | ] 267 | }, 268 | { 269 | "id": "img2img", 270 | "mode": "img2img", 271 | "label": "Image to Image", 272 | "cancel": { 273 | "fn_index": { 274 | "conditions": { 275 | "trigger": "click", 276 | "targets": { 277 | "0": { 278 | "conditions": { 279 | "elem_id": "img2img_interrupt" 280 | } 281 | } 282 | } 283 | } 284 | } 285 | }, 286 | "fn_index": { 287 | "conditions": { 288 | "js": "submit_img2img", 289 | "trigger": "click" 290 | } 291 | }, 292 | "handle_output": "automatic1111", 293 | "layout": [ 294 | { 295 | "type": "container", 296 | "id": "img2img", 297 | "label": "Image to Image", 298 | "components": [ 299 | { 300 | "type": "input", 301 | "id": "strength" 302 | }, 303 | { 304 | "type": "input", 305 | "id": "resize_options" 306 | } 307 | ] 308 | }, 309 | { 310 | "type": "container", 311 | "id": "sampling_options", 312 | "label": "Sampling", 313 | "components": [ 314 | { 315 | "type": "input", 316 | "id": "batch_count" 317 | }, 318 | { 319 | "type": "input", 320 | "id": "batch_size" 321 | }, 322 | { 323 | "type": "input", 324 | "id": "sampling_method" 325 | }, 326 | { 327 | "type": "input", 328 | "id": "nb_steps" 329 | } 330 | ] 331 | }, 332 | { 333 | "type": "container", 334 | "id": "prompt_options", 335 | "label": "Prompt options", 336 | "components": [ 337 | { 338 | "type": "input", 339 | "id": "reverse_prompt" 340 | }, 341 | { 342 | "type": "input", 343 | "id": "guidance_scale" 344 | } 345 | ] 346 | }, 347 | { 348 | "type": "container", 349 | "id": "image_options", 350 | "label": "Image size", 351 | "components": [ 352 | { 353 | "type": "input", 354 | "id": "width" 355 | }, 356 | { 357 | "type": "input", 358 | "id": "height" 359 | } 360 | ] 361 | }, 362 | { 363 | "type": "input", 364 | "id": "seed" 365 | }, 366 | { 367 | "type": "container", 368 | "id": "extra_options", 369 | "label": "Extra", 370 | "components": [ 371 | { 372 | "type": "input", 373 | "id": "restore_faces" 374 | }, 375 | { 376 | "type": "input", 377 | "id": "tiling" 378 | } 379 | ] 380 | } 381 | ], 382 | "auto_inputs": { 383 | "denoising_strength": { 384 | "id": "strength" 385 | }, 386 | "sampling_steps": { 387 | "id": "nb_steps" 388 | }, 389 | "negative_prompt": { 390 | "id": "reverse_prompt", 391 | "type": "common_input" 392 | }, 393 | "prompt": { 394 | "id": "prompt", 395 | "type": "common_input" 396 | }, 397 | "width": { 398 | "id": "width", 399 | "type": "common_input" 400 | }, 401 | "height": { 402 | "id": "height", 403 | "type": "common_input" 404 | }, 405 | "cfg_scale": { 406 | "id": "guidance_scale" 407 | }, 408 | "resize_mode": { 409 | "id": "resize_options" 410 | } 411 | }, 412 | "inputs": "auto", 413 | "outputs": [ 414 | { 415 | "label": "Result", 416 | "type": "image" 417 | }, { 418 | "label": "JSON metadata", 419 | "type": "json" 420 | }, { 421 | "label": "HTML metadata", 422 | "type": "html" 423 | } 424 | ] 425 | }, 426 | { 427 | "id": "inpainting", 428 | "mode": "inpainting", 429 | "label": "Inpainting", 430 | "cancel": { 431 | "fn_index": { 432 | "conditions": { 433 | "trigger": "click", 434 | "targets": { 435 | "0": { 436 | "conditions": { 437 | "elem_id": "img2img_interrupt" 438 | } 439 | } 440 | } 441 | } 442 | } 443 | }, 444 | "fn_index": { 445 | "conditions": { 446 | "js": "submit_img2img", 447 | "trigger": "click" 448 | } 449 | }, 450 | "handle_output": "automatic1111", 451 | "layout": [ 452 | { 453 | "type": "container", 454 | "id": "inpainting", 455 | "label": "Inpainting", 456 | "components": [ 457 | { 458 | "type": "input", 459 | "id": "strength" 460 | }, 461 | { 462 | "type": "input", 463 | "id": "resize_options" 464 | }, 465 | { 466 | "type": "input", 467 | "id": "masked_content" 468 | }, 469 | { 470 | "type": "input", 471 | "id": "mask_blur" 472 | }, 473 | { 474 | "type": "input", 475 | "id": "mask_mode" 476 | }, 477 | { 478 | "type": "input", 479 | "id": "inpaint_area" 480 | }, 481 | { 482 | "type": "input", 483 | "id": "only_masked_padding,_pixels" 484 | } 485 | ] 486 | }, 487 | { 488 | "type": "container", 489 | "id": "sampling_options", 490 | "label": "Sampling", 491 | "components": [ 492 | { 493 | "type": "input", 494 | "id": "batch_count" 495 | }, 496 | { 497 | "type": "input", 498 | "id": "batch_size" 499 | }, 500 | { 501 | "type": "input", 502 | "id": "sampling_method" 503 | }, 504 | { 505 | "type": "input", 506 | "id": "nb_steps" 507 | } 508 | ] 509 | }, 510 | { 511 | "type": "container", 512 | "id": "prompt_options", 513 | "label": "Prompt options", 514 | "components": [ 515 | { 516 | "type": "input", 517 | "id": "reverse_prompt" 518 | }, 519 | { 520 | "type": "input", 521 | "id": "guidance_scale" 522 | } 523 | ] 524 | }, 525 | { 526 | "type": "container", 527 | "id": "image_options", 528 | "label": "Image size", 529 | "components": [ 530 | { 531 | "type": "input", 532 | "id": "width" 533 | }, 534 | { 535 | "type": "input", 536 | "id": "height" 537 | } 538 | ] 539 | }, 540 | { 541 | "type": "input", 542 | "id": "seed" 543 | }, 544 | { 545 | "type": "container", 546 | "id": "extra_options", 547 | "label": "Extra", 548 | "components": [ 549 | { 550 | "type": "input", 551 | "id": "restore_faces" 552 | } 553 | ] 554 | } 555 | ], 556 | "image": { 557 | "conditions": { 558 | "label": "Image for img2img", 559 | "elem_id": "img_inpaint_base" 560 | } 561 | }, 562 | "image_mask": { 563 | "conditions": { 564 | "label": "Mask" 565 | } 566 | }, 567 | "auto_inputs": { 568 | "only_masked_padding,_pixels": { 569 | "visible": { 570 | "input_id": "inpaint_area", 571 | "condition": "===", 572 | "value": "Only masked" 573 | } 574 | }, 575 | "masked_content": { 576 | "value": "original" 577 | }, 578 | "denoising_strength": { 579 | "id": "strength" 580 | }, 581 | "sampling_steps": { 582 | "id": "nb_steps" 583 | }, 584 | "negative_prompt": { 585 | "id": "reverse_prompt", 586 | "type": "common_input" 587 | }, 588 | "prompt": { 589 | "id": "prompt", 590 | "type": "common_input" 591 | }, 592 | "width": { 593 | "id": "width", 594 | "type": "common_input" 595 | }, 596 | "height": { 597 | "id": "height", 598 | "type": "common_input" 599 | }, 600 | "cfg_scale": { 601 | "id": "guidance_scale" 602 | }, 603 | "resize_mode": { 604 | "id": "resize_options" 605 | }, 606 | "label": { 607 | "value": 1 608 | } 609 | }, 610 | "inputs": "auto", 611 | "outputs": [ 612 | { 613 | "label": "Result", 614 | "type": "image" 615 | }, { 616 | "label": "JSON metadata", 617 | "type": "json" 618 | }, { 619 | "label": "HTML metadata", 620 | "type": "html" 621 | } 622 | ] 623 | }, 624 | { 625 | "id": "upscaling", 626 | "mode": "img2img", 627 | "label": "Upscaling", 628 | "fn_index": { 629 | "conditions": { 630 | "js": null, 631 | "trigger": "click", 632 | "targets": { 633 | "0": { 634 | "conditions": { 635 | "elem_id": "extras_generate" 636 | } 637 | } 638 | } 639 | } 640 | }, 641 | "handle_output": "automatic1111", 642 | "inputs": "auto", 643 | "auto_inputs": { 644 | "input_directory": { 645 | "visible": false 646 | }, 647 | "output_directory": { 648 | "visible": false 649 | }, 650 | "show_result_images": { 651 | "visible": false 652 | }, 653 | "width": { 654 | "visible": false 655 | }, 656 | "height": { 657 | "visible": false 658 | }, 659 | "crop_to_fit": { 660 | "visible": false 661 | }, 662 | "upscaler_1": { 663 | "value": "ESRGAN_4x" 664 | } 665 | }, 666 | "outputs": [ 667 | { 668 | "label": "Result", 669 | "type": "image" 670 | }, { 671 | "label": "HTML info x", 672 | "type": "html" 673 | }, { 674 | "label": "HTML info", 675 | "type": "html" 676 | } 677 | ] 678 | } 679 | ] 680 | } 681 | -------------------------------------------------------------------------------- /src/actions/editor.js: -------------------------------------------------------------------------------- 1 | import { fabric } from "fabric"; 2 | import { nextTick } from "vue"; 3 | import { useBackendStore } from "@/stores/backend"; 4 | import { useOutputStore } from "@/stores/output"; 5 | import { useEditorStore } from "@/stores/editor"; 6 | import { useUIStore } from "@/stores/ui"; 7 | import { resetInputsFromResultImage } from "@/actions/output"; 8 | 9 | function undo({ save_redo = true } = {}) { 10 | const editor = useEditorStore(); 11 | 12 | const undo_action = editor.history.undo.pop(); 13 | 14 | if (undo_action) { 15 | if (save_redo) { 16 | editor.history.redo.push(undo_action); 17 | } 18 | 19 | switch (undo_action.type) { 20 | case "erase": 21 | editor.image_clip.remove(undo_action.clip_path); 22 | editor.canvas_mask.remove(undo_action.mask_path); 23 | editor.layers.emphasize.remove(undo_action.emphasize_path); 24 | 25 | delete undo_action.clip_path; 26 | delete undo_action.mask_path; 27 | delete undo_action.emphasize_path; 28 | 29 | // Rerender Canvas mask and possibly switch between img2img and inpainting 30 | renderCanvasMask(); 31 | break; 32 | 33 | case "draw": 34 | editor.layers.draw.remove(undo_action.draw_path); 35 | 36 | delete undo_action.draw_path; 37 | break; 38 | } 39 | 40 | editor.canvas.renderAll(); 41 | } 42 | } 43 | 44 | async function doAction(action) { 45 | const editor = useEditorStore(); 46 | 47 | switch (action.type) { 48 | case "erase": 49 | action.clip_path = await asyncClone(action.path); 50 | action.mask_path = await asyncClone(action.path); 51 | action.emphasize_path = await asyncClone(action.path); 52 | 53 | action.emphasize_path.stroke = "lightgrey"; 54 | 55 | editor.image_clip.addWithUpdate(action.clip_path); 56 | editor.canvas_mask.add(action.mask_path); 57 | editor.layers.emphasize.addWithUpdate(action.emphasize_path); 58 | 59 | // Rerender Canvas mask and possibly switch between img2img and inpainting 60 | renderCanvasMask(); 61 | 62 | // will allow the inputs to be changed to inpainting 63 | await nextTick(); 64 | 65 | // Modify masked content param to original if we're not doing outpainting 66 | if (editor.canvas.getZoom() >= 1) { 67 | const backend = useBackendStore(); 68 | backend.setInput("masked_content", "original"); 69 | } 70 | 71 | break; 72 | 73 | case "draw": 74 | action.draw_path = await asyncClone(action.path); 75 | 76 | editor.layers.draw.addWithUpdate(action.draw_path); 77 | break; 78 | } 79 | } 80 | 81 | async function redo() { 82 | const editor = useEditorStore(); 83 | 84 | const action = editor.history.redo.pop(); 85 | 86 | if (action) { 87 | await doAction(action); 88 | editor.canvas.renderAll(); 89 | 90 | editor.history.undo.push(action); 91 | } 92 | } 93 | 94 | async function redoWholeHistory(undo) { 95 | const nb_undo = undo.length; 96 | for (let i = 0; i < nb_undo; i++) { 97 | const action = undo[i]; 98 | await doAction(action); 99 | } 100 | } 101 | 102 | function resetEditorActions() { 103 | const editor = useEditorStore(); 104 | 105 | // Remove existing layers 106 | if (editor.layers.image) { 107 | editor.canvas.remove(editor.layers.image); 108 | editor.layers.image = null; 109 | } 110 | 111 | if (editor.image_clip) { 112 | editor.image_clip = null; 113 | } 114 | 115 | if (editor.layers.draw) { 116 | editor.canvas.remove(editor.layers.draw); 117 | editor.layers.draw = null; 118 | } 119 | 120 | if (editor.layers.emphasize) { 121 | editor.canvas.remove(editor.layers.emphasize); 122 | editor.layers.emphasize = null; 123 | } 124 | 125 | editor.canvas_mask = null; 126 | editor.uploaded_image_b64 = null; 127 | editor.mask_image_b64 = null; 128 | 129 | editor.history = { 130 | undo: [], 131 | redo: [], 132 | }; 133 | } 134 | 135 | async function onKeyUp(event) { 136 | if (event.ctrlKey) { 137 | switch (event.key) { 138 | case "z": 139 | undo(); 140 | break; 141 | case "y": 142 | await redo(); 143 | break; 144 | } 145 | } else { 146 | switch (event.key) { 147 | case "Escape": 148 | resetEditorButtons(); 149 | 150 | break; 151 | } 152 | } 153 | } 154 | 155 | /* 156 | DEBUG 157 | function print_objects() { 158 | const editor = useEditorStore(); 159 | 160 | const objects = editor.canvas.getObjects(); 161 | 162 | console.log("PRINT"); 163 | objects.forEach(object => console.log(object.nam)); 164 | } 165 | */ 166 | 167 | function addToCanvas(name, item) { 168 | const editor = useEditorStore(); 169 | 170 | item.nam = name; 171 | 172 | editor.canvas.add(item); 173 | 174 | console.log(`Adding ${name} to canvas.`); 175 | } 176 | 177 | function updateDrawLayerOpacity() { 178 | const backend = useBackendStore(); 179 | const editor = useEditorStore(); 180 | 181 | if (editor.layers.draw) { 182 | const opacity = 1 - backend.getInput("strength", 0); 183 | 184 | editor.layers.draw.set("opacity", opacity); 185 | } 186 | } 187 | 188 | function makeNewCanvasMask() { 189 | const editor = useEditorStore(); 190 | 191 | editor.canvas_mask = new fabric.Canvas(); 192 | editor.canvas_mask.selection = false; 193 | editor.canvas_mask.setBackgroundColor("white"); 194 | editor.canvas_mask.setHeight(editor.height); 195 | editor.canvas_mask.setWidth(editor.width); 196 | 197 | let canvas_mask_background = new fabric.Rect({ 198 | width: editor.width, 199 | height: editor.height, 200 | left: 0, 201 | top: 0, 202 | fill: "black", 203 | absolutePositioned: true, 204 | selectable: false, 205 | }); 206 | 207 | editor.canvas_mask.add(canvas_mask_background); 208 | } 209 | 210 | function makeNewImageClip() { 211 | const editor = useEditorStore(); 212 | 213 | editor.image_clip = new fabric.Group([], { absolutePositioned: true }); 214 | editor.image_clip.inverted = true; 215 | } 216 | 217 | function makeNewLayerEmphasize() { 218 | const editor = useEditorStore(); 219 | 220 | editor.layers.emphasize = new fabric.Group([], { 221 | absolutePositioned: true, 222 | opacity: 0.2, 223 | selectable: false, 224 | }); 225 | } 226 | 227 | async function makeNewLayerImage(image) { 228 | const editor = useEditorStore(); 229 | 230 | image.selectable = false; 231 | 232 | let width = image.width; 233 | let height = image.height; 234 | 235 | console.log(`Uploaded image with resolution: ${width}x${height}`); 236 | 237 | if (width !== 512 || height !== 512) { 238 | if (width > height) { 239 | image.scaleToWidth(512); 240 | 241 | height = Math.floor(512 * (height / width)); 242 | width = 512; 243 | } else { 244 | image.scaleToHeight(512); 245 | 246 | width = Math.floor(512 * (width / height)); 247 | height = 512; 248 | } 249 | console.log(`Scaled resolution: ${width}x${height}`); 250 | } 251 | 252 | // new editor.image_clip 253 | makeNewImageClip(); 254 | 255 | image.clipPath = editor.image_clip; 256 | editor.layers.image = image; 257 | 258 | return { width, height }; 259 | } 260 | 261 | async function makeNewLayerDraw(image) { 262 | const backend = useBackendStore(); 263 | const editor = useEditorStore(); 264 | 265 | let draw_background; 266 | 267 | if (image) { 268 | draw_background = await asyncClone(image); 269 | } else { 270 | backend.setInput("strength", 0, false); 271 | 272 | draw_background = new fabric.Rect({ 273 | width: editor.width, 274 | height: editor.height, 275 | left: 0, 276 | top: 0, 277 | fill: "white", 278 | absolutePositioned: true, 279 | selectable: false, 280 | }); 281 | } 282 | 283 | editor.layers.draw = new fabric.Group([draw_background], { 284 | selectable: false, 285 | absolutePositioned: true, 286 | }); 287 | } 288 | 289 | function makeNewEditorBrush() { 290 | const editor = useEditorStore(); 291 | 292 | editor.brush = new fabric.PencilBrush(); 293 | editor.brush.color = "white"; 294 | editor.brush.width = editor.brush_size.eraser; 295 | editor.canvas.freeDrawingBrush = editor.brush; 296 | editor.brush.initialize(editor.canvas); 297 | } 298 | 299 | function makeNewTransparentBackground() { 300 | const editor = useEditorStore(); 301 | 302 | const transparentBackground = function () { 303 | const chess_canvas = new fabric.StaticCanvas(null, { 304 | enableRetinaScaling: false, 305 | }); 306 | chess_canvas.setHeight(10); 307 | chess_canvas.setWidth(10); 308 | chess_canvas.setBackgroundColor("lightgrey"); 309 | 310 | const rect1 = new fabric.Rect({ 311 | width: 5, 312 | height: 5, 313 | left: 0, 314 | top: 0, 315 | fill: "rgba(150, 150, 150, 1)", 316 | }); 317 | 318 | const rect2 = new fabric.Rect({ 319 | width: 5, 320 | height: 5, 321 | left: 5, 322 | top: 5, 323 | fill: "rgba(150, 150, 150, 1)", 324 | }); 325 | 326 | chess_canvas.add(rect1); 327 | chess_canvas.add(rect2); 328 | 329 | const transparent_pattern = new fabric.Pattern({ 330 | source: chess_canvas.getElement(), 331 | repeat: "repeat", 332 | }); 333 | return transparent_pattern; 334 | }; 335 | 336 | editor.canvas.setBackgroundColor(transparentBackground()); 337 | } 338 | 339 | function makeNewLayerBrushOutline() { 340 | const editor = useEditorStore(); 341 | 342 | editor.layers.brush_outline = new fabric.Circle({ 343 | left: 0, 344 | right: 0, 345 | originX: "center", 346 | originY: "center", 347 | radius: editor.brush_size.eraser, 348 | angle: 0, 349 | fill: "", 350 | stroke: "#b25d5d", 351 | strokeDashArray: [4, 6, 1], 352 | strokeWidth: 3, 353 | opacity: 0, 354 | }); 355 | } 356 | 357 | function onMouseMove(opt) { 358 | const editor = useEditorStore(); 359 | const ui = useUIStore(); 360 | const evt = opt.e; 361 | 362 | if (ui.cursor_mode === "idle") { 363 | if (this.isDragging) { 364 | let vpt = this.viewportTransform; 365 | vpt[4] += evt.clientX - this.lastPosX; 366 | vpt[5] += evt.clientY - this.lastPosY; 367 | this.requestRenderAll(); 368 | this.lastPosX = evt.clientX; 369 | this.lastPosY = evt.clientY; 370 | 371 | // Rerender Canvas mask and possibly switch between img2img and inpainting 372 | renderCanvasMask(); 373 | } 374 | } else { 375 | var pointer = editor.canvas.getPointer(evt); 376 | editor.layers.brush_outline.left = pointer.x; 377 | editor.layers.brush_outline.top = pointer.y; 378 | editor.layers.brush_outline.opacity = 0.9; 379 | 380 | switch (ui.cursor_mode) { 381 | case "eraser": 382 | editor.layers.brush_outline.set("strokeWidth", 3); 383 | editor.layers.brush_outline.set("fill", ""); 384 | editor.layers.brush_outline.set("radius", editor.brush_size.eraser / 2); 385 | break; 386 | 387 | case "draw": 388 | editor.layers.brush_outline.set("strokeWidth", 0); 389 | editor.layers.brush_outline.set("fill", editor.color); 390 | editor.layers.brush_outline.set("radius", editor.brush_size.draw / 2); 391 | editor.brush.color = editor.color; 392 | break; 393 | } 394 | 395 | editor.canvas.renderAll(); 396 | } 397 | } 398 | 399 | function onMouseOut() { 400 | const editor = useEditorStore(); 401 | 402 | editor.layers.brush_outline.opacity = 0; 403 | 404 | editor.canvas.renderAll(); 405 | } 406 | 407 | function onMouseDown(opt) { 408 | const evt = opt.e; 409 | 410 | this.isDragging = true; 411 | this.lastPosX = evt.clientX; 412 | this.lastPosY = evt.clientY; 413 | } 414 | 415 | function onMouseUp() { 416 | this.setViewportTransform(this.viewportTransform); 417 | this.isDragging = false; 418 | } 419 | 420 | async function onMouseWheel(opt) { 421 | const editor = useEditorStore(); 422 | 423 | const canvas = editor.canvas; 424 | const zoom_min = editor.zoom_min; 425 | const zoom_max = editor.zoom_max; 426 | 427 | const evt = opt.e; 428 | const delta = evt.deltaY; 429 | 430 | let previous_zoom = canvas.getZoom(); 431 | let zoom = previous_zoom * 0.999 ** delta; 432 | 433 | if (zoom > zoom_max) { 434 | zoom = zoom_max; 435 | } else if (zoom < zoom_min) { 436 | zoom = zoom_min; 437 | } 438 | 439 | if ((zoom >= 1 && previous_zoom < 1) || (zoom <= 1 && previous_zoom > 1)) { 440 | // Reset the zoom and panning of the canvas 441 | canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; 442 | editor.canvas.renderAll(); 443 | } else { 444 | canvas.zoomToPoint( 445 | { 446 | x: evt.offsetX, 447 | y: evt.offsetY, 448 | }, 449 | zoom 450 | ); 451 | } 452 | 453 | // Rerender Canvas mask and possibly switch between img2img and inpainting 454 | renderCanvasMask(); 455 | 456 | // will allow the inputs to be changed to inpainting 457 | await nextTick(); 458 | 459 | if (zoom < 1) { 460 | const backend = useBackendStore(); 461 | backend.setInput("masked_content", "fill"); 462 | } 463 | 464 | evt.preventDefault(); 465 | evt.stopPropagation(); 466 | } 467 | 468 | async function onPathCreated(e) { 469 | const editor = useEditorStore(); 470 | const ui = useUIStore(); 471 | 472 | const path = e.path; 473 | path.selectable = false; 474 | 475 | path.opacity = 1; 476 | 477 | editor.history.redo.length = 0; 478 | editor.canvas.remove(path); 479 | 480 | switch (ui.cursor_mode) { 481 | case "eraser": 482 | { 483 | path.color = "white"; 484 | 485 | const eraser_action = { 486 | type: "erase", 487 | path: path, 488 | }; 489 | 490 | await doAction(eraser_action); 491 | 492 | editor.history.undo.push(eraser_action); 493 | 494 | // Update the opacity depending on the strength 495 | updateDrawLayerOpacity(); 496 | 497 | // Change the mode to inpainting if needed 498 | editor.mode = "inpainting"; 499 | } 500 | break; 501 | 502 | case "draw": 503 | { 504 | path.stroke = editor.color; 505 | 506 | const draw_action = { 507 | type: "draw", 508 | path: path, 509 | }; 510 | 511 | await doAction(draw_action); 512 | 513 | editor.history.undo.push(draw_action); 514 | } 515 | break; 516 | } 517 | 518 | editor.canvas.renderAll(); 519 | } 520 | 521 | function setupEventListeners() { 522 | const editor = useEditorStore(); 523 | 524 | editor.canvas.on("mouse:down", onMouseDown); 525 | editor.canvas.on("mouse:up", onMouseUp); 526 | editor.canvas.on("mouse:move", onMouseMove); 527 | editor.canvas.on("mouse:out", onMouseOut); 528 | editor.canvas.on("mouse:wheel", onMouseWheel); 529 | editor.canvas.on("path:created", onPathCreated); 530 | } 531 | 532 | function initCanvas(canvas_id) { 533 | const editor = useEditorStore(); 534 | 535 | console.log("Init canvas!"); 536 | 537 | // new fabric canvas 538 | editor.canvas = new fabric.Canvas(canvas_id); 539 | editor.canvas.selection = false; 540 | editor.canvas.freeDrawingCursor = "none"; 541 | 542 | // put a transparent pattern as the editor.canvas background 543 | makeNewTransparentBackground(); 544 | 545 | // new editor.brush (PencilBrush) associated to editor.canvas 546 | makeNewEditorBrush(); 547 | 548 | // new editor.canvas_mask 549 | makeNewCanvasMask(); 550 | 551 | // new editor.layers.brush_outline 552 | makeNewLayerBrushOutline(); 553 | 554 | // Add to layers to canvas 555 | addToCanvas("brush_outline", editor.layers.brush_outline); 556 | 557 | // listen to canvas events 558 | setupEventListeners(); 559 | } 560 | 561 | function updateBrushSize() { 562 | const editor = useEditorStore(); 563 | const ui = useUIStore(); 564 | 565 | editor.brush.width = editor.brush_size.slider; 566 | 567 | if (ui.cursor_mode === "eraser") { 568 | editor.brush_size.eraser = editor.brush_size.slider; 569 | } else if (ui.cursor_mode === "draw") { 570 | editor.brush_size.draw = editor.brush_size.slider; 571 | } 572 | } 573 | 574 | function isCanvasMaskEmpty() { 575 | // Ignoring pixels at the edge of the mask because apparently 576 | // after zooming in and out, the black rectangle in the canvas mask 577 | // is not reset completely right, leaving a grey row and column of 1 pixel 578 | const editor = useEditorStore(); 579 | 580 | const canvas_mask = editor.canvas_mask; 581 | const context = canvas_mask.getContext(); 582 | const width = canvas_mask.width; 583 | const height = canvas_mask.height; 584 | const imageData = context.getImageData(0, 0, width, height); 585 | const pixelData = new Uint32Array(imageData.data.buffer); 586 | 587 | const blackColor = 0xff000000; 588 | 589 | let isBlack = true; 590 | 591 | // Iterate through the pixel data and check if any pixel is not the specified color 592 | for (let x = 1; x < width - 1; x++) { 593 | for (let y = 1; y < height - 1; y++) { 594 | const pixelColor = pixelData[y * width + x]; 595 | if (pixelColor !== blackColor) { 596 | isBlack = false; 597 | break; 598 | } 599 | } 600 | if (!isBlack) { 601 | break; 602 | } 603 | } 604 | 605 | return isBlack; 606 | } 607 | 608 | function setModeToImg2ImgOrInpainting() { 609 | const editor = useEditorStore(); 610 | 611 | if (isCanvasMaskEmpty()) { 612 | editor.mode = "img2img"; 613 | } else { 614 | editor.mode = "inpainting"; 615 | } 616 | } 617 | 618 | function setViewportTransform(viewportTransform) { 619 | const editor = useEditorStore(); 620 | 621 | editor.canvas.setViewportTransform(viewportTransform); 622 | 623 | // Change the mode to inpainting or img2img depending on the mask 624 | renderCanvasMask(); 625 | } 626 | 627 | function renderCanvasMask() { 628 | const editor = useEditorStore(); 629 | 630 | // Set the viewport of the mask same as the canvas 631 | // This will reset the zoom level and panning of the canvas mask 632 | editor.canvas_mask.setViewportTransform(editor.canvas.viewportTransform); 633 | 634 | editor.canvas_mask.renderAll(); 635 | 636 | // Regenerate canvas mask image 637 | editor.mask_image_b64 = editor.canvas_mask.toDataURL({ 638 | format: editor.img_format, 639 | }); 640 | 641 | // Change the mode to inpainting or img2img depending on the mask 642 | setModeToImg2ImgOrInpainting(); 643 | } 644 | 645 | function renderImage() { 646 | const editor = useEditorStore(); 647 | const img_format = editor.img_format; 648 | 649 | console.log(`Rendering image with format: ${img_format}`); 650 | 651 | let emphasize_opacity = 0; 652 | 653 | // Save opacity of ignored layers 654 | if (editor.layers.emphasize) { 655 | emphasize_opacity = editor.layers.emphasize.opacity; 656 | editor.layers.emphasize.set("opacity", 0); 657 | } 658 | 659 | const draw_opacity = editor.layers.draw.opacity; 660 | // Set the opacity to capture final image 661 | editor.layers.draw.set("opacity", 1); 662 | 663 | // render the image in the store 664 | editor.init_image_b64 = editor.canvas.toDataURL({ format: img_format }); 665 | 666 | // Restore the initial opacity 667 | if (editor.layers.emphasize) { 668 | editor.layers.emphasize.set("opacity", emphasize_opacity); 669 | } 670 | editor.layers.draw.set("opacity", draw_opacity); 671 | } 672 | 673 | async function fabricImageFromURL(image_url) { 674 | return new Promise(function (resolve, reject) { 675 | try { 676 | fabric.Image.fromURL( 677 | image_url, 678 | function (image) { 679 | resolve(image); 680 | }.bind(this), 681 | { 682 | crossOrigin: "anonymous", 683 | } 684 | ); 685 | } catch (error) { 686 | reject(error); 687 | } 688 | }); 689 | } 690 | 691 | async function asyncClone(object) { 692 | return new Promise(function (resolve, reject) { 693 | try { 694 | object.clone(function (cloned_object) { 695 | resolve(cloned_object); 696 | }); 697 | } catch (error) { 698 | reject(error); 699 | } 700 | }); 701 | } 702 | 703 | async function editNewImage(image_b64) { 704 | const editor = useEditorStore(); 705 | 706 | // Remove existing layers 707 | resetEditorActions(); 708 | 709 | editor.has_image = true; 710 | editor.mode = "img2img"; 711 | 712 | // will allow the inputs to be changed to img2img (useful for strength input change) 713 | await nextTick(); 714 | 715 | // Waiting that the canvas has been created asynchronously by Vue 716 | while (editor.canvas === null) { 717 | console.log("."); 718 | await nextTick(); 719 | } 720 | 721 | let image = null; 722 | let width = 512; 723 | let height = 512; 724 | 725 | // Reset the zoom and viewport of the canvas 726 | editor.canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; 727 | 728 | if (image_b64) { 729 | editor.uploaded_image_b64 = image_b64; 730 | 731 | image = await fabricImageFromURL(image_b64); 732 | 733 | // make editor.layers.image with new editor.image_clip 734 | ({ width, height } = await makeNewLayerImage(image)); 735 | } 736 | 737 | // Save new image aspect ratio and modify canvas size if needed 738 | if (editor.width !== width) { 739 | editor.width = width; 740 | editor.canvas.setWidth(editor.width); 741 | } 742 | if (editor.height !== height) { 743 | editor.height = height; 744 | editor.canvas.setHeight(editor.height); 745 | } 746 | 747 | // new editor.layers.draw with either white rect or image 748 | await makeNewLayerDraw(image); 749 | 750 | // Update the opacity of the draw layer depending on the strength 751 | updateDrawLayerOpacity(); 752 | 753 | addToCanvas("draw", editor.layers.draw); 754 | 755 | if (image) { 756 | // new editor.layers.emphasize 757 | makeNewLayerEmphasize(); 758 | 759 | addToCanvas("image", editor.layers.image); 760 | addToCanvas("emphasize", editor.layers.emphasize); 761 | } 762 | 763 | // Keep the brush at the front 764 | editor.canvas.bringToFront(editor.layers.brush_outline); 765 | 766 | // new editor.canvas_mask 767 | makeNewCanvasMask(); 768 | } 769 | 770 | function resetEditorButtons() { 771 | const ui = useUIStore(); 772 | const editor = useEditorStore(); 773 | 774 | ui.cursor_mode = "idle"; 775 | ui.editor_view = "composite"; 776 | if (editor.canvas) { 777 | editor.canvas.isDrawingMode = false; 778 | editor.layers.brush_outline.opacity = 0; 779 | editor.canvas.renderAll(); 780 | } 781 | } 782 | 783 | function editResultImage(image_index) { 784 | const output = useOutputStore(); 785 | editNewImage(output.images.content[image_index]); 786 | } 787 | 788 | async function generateAgainResultImage(image_index) { 789 | const editor = useEditorStore(); 790 | const output = useOutputStore(); 791 | 792 | if (output.images.history) { 793 | // If the output was made using an uploaded image or a drawing 794 | await editNewImage(output.images.original_image); 795 | await redoWholeHistory(output.images.history.undo); 796 | editor.history.undo = [...output.images.history.undo]; 797 | } else { 798 | closeImage(); 799 | } 800 | 801 | resetInputsFromResultImage(image_index); 802 | } 803 | 804 | function newDrawing() { 805 | console.log("New drawing requested"); 806 | editNewImage(); 807 | } 808 | 809 | function closeImage() { 810 | const editor = useEditorStore(); 811 | 812 | resetEditorActions(); 813 | resetEditorButtons(); 814 | editor.has_image = false; 815 | editor.mode = "txt2img"; 816 | } 817 | 818 | function setCursorMode(cursor_mode) { 819 | const ui = useUIStore(); 820 | const editor = useEditorStore(); 821 | 822 | ui.cursor_mode = cursor_mode; 823 | 824 | if (ui.cursor_mode === "idle") { 825 | editor.canvas.isDrawingMode = false; 826 | } else { 827 | editor.canvas.isDrawingMode = true; 828 | 829 | if (ui.cursor_mode === "eraser") { 830 | editor.brush_size.slider = editor.brush_size.eraser; 831 | editor.brush.color = "white"; 832 | } else if (ui.cursor_mode === "draw") { 833 | editor.brush_size.slider = editor.brush_size.draw; 834 | editor.brush.color = editor.color; 835 | } 836 | 837 | editor.brush.width = editor.brush_size.slider; 838 | editor.canvas.renderAll(); 839 | } 840 | 841 | console.log(`UI cursor mode set to ${ui.cursor_mode}`); 842 | } 843 | 844 | function toggleEraser() { 845 | const backend = useBackendStore(); 846 | const ui = useUIStore(); 847 | 848 | if (ui.cursor_mode !== "eraser") { 849 | if (backend.inpainting_allowed) { 850 | setCursorMode("eraser"); 851 | } else { 852 | backend.$toast.add({ 853 | severity: "warn", 854 | detail: "Inpainting is not allowed with this model", 855 | life: 3000, 856 | closable: false, 857 | }); 858 | } 859 | } else { 860 | setCursorMode("idle"); 861 | } 862 | } 863 | 864 | function toggleDraw() { 865 | const ui = useUIStore(); 866 | 867 | if (ui.cursor_mode !== "draw") { 868 | setCursorMode("draw"); 869 | } else { 870 | setCursorMode("idle"); 871 | } 872 | } 873 | 874 | function toggleMaskView() { 875 | const ui = useUIStore(); 876 | 877 | if (ui.editor_view != "composite") { 878 | ui.editor_view = "composite"; 879 | } else { 880 | ui.editor_view = "mask"; 881 | } 882 | console.log(`UI editor mode set to ${ui.editor_view}`); 883 | } 884 | 885 | function renderCanvas() { 886 | const editor = useEditorStore(); 887 | 888 | if (editor.canvas) { 889 | editor.canvas.renderAll(); 890 | } 891 | } 892 | 893 | export { 894 | closeImage, 895 | editNewImage, 896 | editResultImage, 897 | generateAgainResultImage, 898 | initCanvas, 899 | newDrawing, 900 | redo, 901 | renderCanvas, 902 | renderImage, 903 | resetEditorButtons, 904 | setViewportTransform, 905 | toggleDraw, 906 | toggleEraser, 907 | toggleMaskView, 908 | undo, 909 | updateBrushSize, 910 | updateDrawLayerOpacity, 911 | onKeyUp, 912 | }; 913 | -------------------------------------------------------------------------------- /src/stores/backend.js: -------------------------------------------------------------------------------- 1 | import { version } from "@/version"; 2 | import { defineStore } from "pinia"; 3 | import { useStorage } from "@vueuse/core"; 4 | import { reactive, toRaw } from "vue"; 5 | import { useUIStore } from "@/stores/ui"; 6 | import { useOutputStore } from "@/stores/output"; 7 | import { initGradio } from "@/actions/generate_gradio"; 8 | import deepmerge from "deepmerge"; 9 | import backend_latent_diffusion from "@/backends/gradio/latent-diffusion.json"; 10 | import backend_stable_diffusion from "@/backends/gradio/stable-diffusion.json"; 11 | import backend_stable_diffusion_automatic1111 from "@/backends/gradio/stable-diffusion-automatic1111.json"; 12 | 13 | const backends_json = [ 14 | backend_latent_diffusion, 15 | backend_stable_diffusion, 16 | backend_stable_diffusion_automatic1111, 17 | ]; 18 | 19 | backends_json.forEach(function (backend) { 20 | if (backend.inputs) { 21 | backend.inputs.forEach(function (input) { 22 | input.value = input.default; 23 | }); 24 | } else { 25 | backend.functions.forEach(function (fn) { 26 | if (fn.inputs !== "auto") { 27 | fn.inputs.forEach(function (input) { 28 | input.value = input.default; 29 | }); 30 | } 31 | }); 32 | } 33 | }); 34 | 35 | function mergeBackend(storageValue, defaults) { 36 | // Merging the backend data from the config json file 37 | // and from the saved values in local storage 38 | 39 | let merged = defaults; 40 | 41 | if ( 42 | !storageValue.diffusionui_version || 43 | storageValue.diffusionui_version !== version 44 | ) { 45 | console.log( 46 | `New version of diffusion-ui detected for ${defaults.id}: (${storageValue.diffusionui_version} instead of ${version}) ==> Reloading new config` 47 | ); 48 | } else { 49 | merged = deepmerge(defaults, storageValue, { 50 | arrayMerge: function (defaultArray, storageArray) { 51 | // If the saved array does not have the same number of items 52 | // than the config (after an update probably) 53 | // then reset the array to the default value 54 | if (defaultArray.length === storageArray.length) { 55 | return storageArray; 56 | } else { 57 | return defaultArray; 58 | } 59 | }, 60 | }); 61 | } 62 | 63 | return merged; 64 | } 65 | 66 | const backends = backends_json.map(function (backend) { 67 | const backend_original = JSON.parse(JSON.stringify(backend)); 68 | 69 | let backend_config = { 70 | original: backend_original, 71 | current: useStorage("backend_" + backend.id, backend, localStorage, { 72 | mergeDefaults: mergeBackend, 73 | }), 74 | gradio_config: "config_path" in backend_original ? null : undefined, 75 | }; 76 | 77 | backend_config.current.value["diffusionui_version"] = version; 78 | 79 | return backend_config; 80 | }); 81 | 82 | const backend_options = [ 83 | { 84 | label: "Online", 85 | id: "online", 86 | backends: [{ label: "Latent Diffusion", id: "latent_diffusion" }], 87 | }, 88 | { 89 | label: "Local", 90 | id: "local", 91 | backends: [ 92 | { 93 | label: "Automatic1111", 94 | id: "automatic1111", 95 | }, 96 | { label: "Stable Diffusion", id: "stable_diffusion" }, 97 | ], 98 | }, 99 | ]; 100 | 101 | const default_backend = backends.find( 102 | (backend) => backend.original.id === "automatic1111" 103 | ); 104 | const default_backend_id = default_backend.original.id; 105 | 106 | export const useBackendStore = defineStore({ 107 | id: "backends", 108 | state: () => ({ 109 | backends: backends, 110 | backend_options: backend_options, 111 | backend_id: useStorage("backend_id", default_backend_id), 112 | loading_config: false, 113 | fn_id: null, // txt2img, img2img, inpainting 114 | }), 115 | getters: { 116 | backend_ids: (state) => 117 | state.backends.map((backend) => backend.original.id), 118 | selected_backend: function (state) { 119 | var backend_found = state.backends.find( 120 | (backend) => backend.original.id === state.backend_id 121 | ); 122 | if (!backend_found) { 123 | // Use default backend 124 | state.backend_id = default_backend_id; 125 | backend_found = state.backends.find( 126 | (backend) => backend.original.id === state.backend_id 127 | ); 128 | } 129 | return backend_found; 130 | }, 131 | current: (state) => state.selected_backend.current, 132 | original: (state) => state.selected_backend.original, 133 | base_url: (state) => state.current.base_url, 134 | api_url: (state) => state.base_url + "/" + state.current.api_path, 135 | progress_url: (state) => state.base_url + "/" + state.current.progress_path, 136 | config_url: function (state) { 137 | if (state.current.config_path) { 138 | return state.base_url + "/" + state.current.config_path; 139 | } else { 140 | return null; 141 | } 142 | }, 143 | cancellable: (state) => state.cancel_fn_index, 144 | use_gradio_config: (state) => !!state.selected_backend.original.config_path, 145 | needs_gradio_config: (state) => 146 | state.use_gradio_config && !state.selected_backend.gradio_config, 147 | gradio_config: function (state) { 148 | if (state.needs_gradio_config) { 149 | this.loadGradioConfig(); 150 | } 151 | return state.selected_backend.gradio_config; 152 | }, 153 | has_multiple_functions: (state) => !!state.current.functions, 154 | current_function: function (state) { 155 | if (state.has_multiple_functions) { 156 | var current_fn = state.current.functions.find( 157 | (func) => func.id == state.fn_id 158 | ); 159 | if (!current_fn) { 160 | state.fn_id = state.current.functions[0].id; 161 | current_fn = state.current.functions.find( 162 | (func) => func.id == state.fn_id 163 | ); 164 | } 165 | return current_fn; 166 | } else { 167 | return state.current; 168 | } 169 | }, 170 | fn_index: function (state) { 171 | const fn_index_config = state.current_function.fn_index; 172 | if (!fn_index_config || typeof fn_index_config === "number") { 173 | return fn_index_config; 174 | } else { 175 | return this.getGradioConfigFunctionIndex(fn_index_config.conditions); 176 | } 177 | }, 178 | cancel_fn_index: function (state) { 179 | const cancel_info = state.current_function.cancel; 180 | 181 | if (cancel_info) { 182 | return this.getGradioConfigFunctionIndex( 183 | cancel_info.fn_index.conditions 184 | ); 185 | } 186 | 187 | return null; 188 | }, 189 | progress_initial_fn_index: function (state) { 190 | const progress_info = state.current_function.progress; 191 | 192 | if (progress_info?.fn_index_initial) { 193 | return this.getGradioConfigFunctionIndex( 194 | progress_info.fn_index_initial.conditions 195 | ); 196 | } 197 | 198 | return null; 199 | }, 200 | progress_fn_index: function (state) { 201 | const progress_info = state.current_function.progress; 202 | 203 | if (progress_info) { 204 | return this.getGradioConfigFunctionIndex( 205 | progress_info.fn_index.conditions 206 | ); 207 | } 208 | 209 | return null; 210 | }, 211 | model_change_fn_index: function (state) { 212 | const model_change_info = state.current.model_change; 213 | if (model_change_info) { 214 | return this.getGradioConfigFunctionIndex( 215 | model_change_info.change.fn_index.conditions 216 | ); 217 | } 218 | }, 219 | model_change_load_fn_index: function (state) { 220 | const model_change_info = state.current.model_change; 221 | if (model_change_info) { 222 | return this.getGradioConfigFunctionIndex( 223 | model_change_info.load.fn_index.conditions 224 | ); 225 | } 226 | }, 227 | models_input: function (state) { 228 | if (state.model_change_fn_index) { 229 | const dep = 230 | state.gradio_config.dependencies[state.model_change_fn_index]; 231 | const model_input_id = dep.targets[0]; 232 | const gradio_input = state.gradio_config.components.find( 233 | (component) => component.id === model_input_id 234 | ); 235 | let input = reactive(state.convertGradioInput(gradio_input)); 236 | input.id = "models_change"; 237 | input.description = "Model checkpoint"; 238 | input.value = input.default; 239 | return input; 240 | } else { 241 | return null; 242 | } 243 | }, 244 | gradio_function: function (state) { 245 | return state.gradio_config?.dependencies[state.fn_index]; 246 | }, 247 | gradio_input_ids: function (state) { 248 | return state.gradio_function?.inputs; 249 | }, 250 | gradio_inputs: function (state) { 251 | return state.gradio_input_ids?.map((id) => 252 | state.gradio_config.components.find((component) => component.id === id) 253 | ); 254 | }, 255 | mode: (state) => state.current_function.mode, 256 | common_inputs: (state) => state.current.common_inputs, 257 | inputs: function (state) { 258 | const inputs_json = state.current_function.inputs; 259 | console.log("Computing inputs"); 260 | 261 | if (inputs_json === "auto") { 262 | if (!state.current_function.auto_inputs) { 263 | state.current_function.auto_inputs = {}; 264 | } 265 | const auto_inputs = state.current_function.auto_inputs; 266 | if (state.gradio_inputs) { 267 | const convert_context = { 268 | used_ids: [], 269 | images_found: 0, 270 | }; 271 | return state.gradio_inputs.map(function (gradio_input) { 272 | var auto_input = state.convertGradioInput( 273 | gradio_input, 274 | convert_context 275 | ); 276 | 277 | const auto_id = auto_input.auto_id; 278 | if (!(auto_id in auto_inputs)) { 279 | auto_inputs[auto_id] = {}; 280 | } 281 | 282 | const input_def_reactive = auto_inputs[auto_id]; 283 | const input_def = toRaw(input_def_reactive); 284 | 285 | if (input_def.type === "common_input") { 286 | const common_input_found_reactive = state.common_inputs.find( 287 | (common_input) => common_input.id === input_def.id 288 | ); 289 | 290 | if (common_input_found_reactive) { 291 | const common_input_found = toRaw(common_input_found_reactive); 292 | if (!("value" in common_input_found)) { 293 | common_input_found.value = common_input_found.default; 294 | } 295 | return reactive(common_input_found); 296 | } else { 297 | console.warn(`common_input ${auto_id} not found`); 298 | } 299 | } 300 | 301 | if (!("value" in input_def)) { 302 | input_def.value = auto_input.default; 303 | } 304 | 305 | Object.assign(auto_input, input_def); 306 | 307 | Object.defineProperty(auto_input, "value", { 308 | get() { 309 | return input_def.value; 310 | }, 311 | set(value) { 312 | input_def_reactive.value = value; 313 | }, 314 | }); 315 | 316 | return reactive(auto_input); 317 | }); 318 | } 319 | } else { 320 | return inputs_json.map(function (input) { 321 | if (input.type === "common_input") { 322 | return state.common_inputs.find( 323 | (common_input) => common_input.id === input.id 324 | ); 325 | } 326 | return input; 327 | }); 328 | } 329 | return []; 330 | }, 331 | image_inputs: (state) => 332 | state.inputs.filter( 333 | (input) => input.type === "image" || input.type === "image_mask" 334 | ), 335 | model_info_inputs: (state) => 336 | state.inputs.filter((input) => input.on_model_info_tab), 337 | outputs: (state) => state.current_function.outputs, 338 | function_options: function (state) { 339 | if (!state.current.functions) { 340 | return []; 341 | } 342 | const opts = state.current.functions.map((fn) => ({ 343 | label: fn.label, 344 | id: fn.id, 345 | })); 346 | return opts; 347 | }, 348 | show_license(state) { 349 | if (state.current.license_accepted) { 350 | return false; 351 | } else { 352 | if (state.license_html) { 353 | return true; 354 | } else { 355 | return false; 356 | } 357 | } 358 | }, 359 | has_image_input: (state) => 360 | state.inputs.some((input) => input.type === "image"), 361 | has_img2img_mode: function (state) { 362 | if (state.has_image_input) { 363 | return true; 364 | } 365 | 366 | if (state.current.functions) { 367 | return state.current.functions.some((func) => func.mode === "img2img"); 368 | } 369 | 370 | return false; 371 | }, 372 | has_inpaint_mode: function (state) { 373 | if (state.has_img2img_mode) { 374 | return true; 375 | } 376 | return false; 377 | }, 378 | inpainting_allowed: function () { 379 | return true; 380 | }, 381 | strength_input: (state) => state.findInput("strength"), 382 | strength: (state) => state.getInput("strength", 0), 383 | access_code_input: (state) => state.findInput("access_code", false), 384 | has_access_code: (state) => !!state.access_code_input, 385 | license: (state) => state.getBackendField("license"), 386 | license_html: (state) => state.getBackendField("license_html"), 387 | description: (state) => state.getBackendField("description"), 388 | doc_url: (state) => state.getBackendField("doc_url"), 389 | has_seed: (state) => state.hasInput("seeds") || state.hasInput("seed"), 390 | }, 391 | actions: { 392 | acceptLicense() { 393 | this.current.license_accepted = true; 394 | }, 395 | findInput(input_id, warn) { 396 | if (warn === undefined) { 397 | warn = true; 398 | } 399 | if (this.current) { 400 | const input = this.inputs.find((input) => input.id === input_id); 401 | 402 | if (!input && warn) { 403 | console.warn(`input ${input_id} not found`); 404 | } 405 | return input; 406 | } else { 407 | return null; 408 | } 409 | }, 410 | hasInput: function (input_id) { 411 | const warn = false; 412 | return this.findInput(input_id, warn) !== undefined; 413 | }, 414 | getInput: function (input_id, default_value) { 415 | const warn = false; 416 | const input_found = this.findInput(input_id, warn); 417 | 418 | if (input_found) { 419 | return input_found.value; 420 | } 421 | 422 | return default_value; 423 | }, 424 | setInput(input_id, value, with_toast) { 425 | if (with_toast === undefined) { 426 | with_toast = true; 427 | } 428 | const input_found = this.findInput(input_id); 429 | 430 | if (input_found) { 431 | if (input_found.value !== value) { 432 | const message = `input ${input_id} set to ${value}.`; 433 | console.log(message); 434 | if (with_toast) { 435 | this.$toast.add({ 436 | severity: "info", 437 | detail: message, 438 | life: 3000, 439 | closable: false, 440 | }); 441 | } 442 | input_found.value = value; 443 | } 444 | } else { 445 | console.log(`input ${input_id} not found.`); 446 | } 447 | }, 448 | isVisible(visible_rule) { 449 | if (visible_rule === undefined) { 450 | return true; 451 | } 452 | 453 | if (visible_rule === false) { 454 | return false; 455 | } 456 | 457 | if (typeof visible_rule === "object") { 458 | const condition = visible_rule.condition; 459 | 460 | if (condition === "===") { 461 | const comparaison_input = this.findInput(visible_rule.input_id); 462 | 463 | if (comparaison_input) { 464 | const comp = comparaison_input.value === visible_rule.value; 465 | return comp; 466 | } 467 | } 468 | } 469 | 470 | return true; 471 | }, 472 | isInputVisible(input_id) { 473 | const input = this.findInput(input_id); 474 | 475 | if ( 476 | input.id === "prompt" || 477 | input.id === "access_code" || 478 | input.type === "image" || 479 | input.type === "image_mask" 480 | ) { 481 | // Some inputs are always invisible 482 | return false; 483 | } 484 | 485 | if (input) { 486 | const visible_rule = input.visible; 487 | return this.isVisible(visible_rule); 488 | } 489 | 490 | return true; 491 | }, 492 | getBackendField(field_name) { 493 | if (this.current) { 494 | if (this.current[field_name]) { 495 | return this.current[field_name]; 496 | } 497 | } 498 | return ""; 499 | }, 500 | showLicense() { 501 | this.current.license_accepted = false; 502 | }, 503 | resetCurrentBackendToDefaults() { 504 | this.$confirm.require({ 505 | message: `Reset ${this.current.name} to default values?`, 506 | header: "Confirmation", 507 | icon: "pi pi-exclamation-triangle", 508 | accept: () => { 509 | console.log( 510 | `Resetting backend ${this.current.name} to default values.` 511 | ); 512 | this.selected_backend.current = JSON.parse( 513 | JSON.stringify(this.selected_backend.original) 514 | ); 515 | }, 516 | }); 517 | }, 518 | changeBackend(backend_id) { 519 | if (this.backend_id !== backend_id) { 520 | const message = `Switching backend to ${backend_id}`; 521 | 522 | console.log(message); 523 | 524 | this.$toast.add({ 525 | severity: "info", 526 | detail: message, 527 | life: 3000, 528 | closable: false, 529 | }); 530 | 531 | this.backend_id = backend_id; 532 | } 533 | }, 534 | getBackend(backend_id) { 535 | return this.backends.find( 536 | (backend) => backend.original.id === backend_id 537 | ); 538 | }, 539 | getFunction(backend_id, function_id) { 540 | const backend = this.getBackend(backend_id); 541 | 542 | if (backend) { 543 | if (backend.current.functions) { 544 | return backend.current.functions.find( 545 | (func) => func.id === function_id 546 | ); 547 | } 548 | } 549 | return null; 550 | }, 551 | changeFunction(function_id) { 552 | if (this.fn_id !== function_id) { 553 | const message = `Switching to ${function_id}`; 554 | 555 | console.log(message); 556 | 557 | if (!this.current.functions) { 558 | console.warn(`Impossible to change function with this backend.`); 559 | return; 560 | } 561 | 562 | const new_function = this.current.functions.find( 563 | (func) => func.id === function_id 564 | ); 565 | 566 | if (!new_function) { 567 | console.warn(`Function id ${function_id} not found.`); 568 | return; 569 | } 570 | 571 | this.$toast.add({ 572 | severity: "info", 573 | detail: message, 574 | life: 3000, 575 | closable: false, 576 | }); 577 | 578 | this.fn_id = function_id; 579 | } 580 | }, 581 | getAllowedModes(editor_mode) { 582 | // Return the allow backend modes depending on the editor mode 583 | switch (editor_mode) { 584 | case "txt2img": 585 | return ["txt2img"]; 586 | case "img2img": 587 | return ["img2img"]; 588 | case "inpainting": 589 | return ["inpainting"]; 590 | } 591 | }, 592 | changeFunctionForModes(modes) { 593 | if (!this.current.functions) { 594 | return; 595 | } 596 | 597 | console.log(`Changing for modes ${modes}`); 598 | 599 | if (modes.includes(this.mode)) { 600 | console.log( 601 | `The current function '${this.fn_id}' mode: '${this.mode}' is already in ${modes}` 602 | ); 603 | return; 604 | } 605 | 606 | modes.every( 607 | function (mode) { 608 | const found_func = this.current.functions.find( 609 | (func) => func.mode === mode 610 | ); 611 | if (found_func) { 612 | this.changeFunction(found_func.id); 613 | return false; 614 | } 615 | return true; 616 | }.bind(this) 617 | ); 618 | }, 619 | async loadGradioConfig() { 620 | const config_url = this.config_url; 621 | 622 | if (config_url) { 623 | let error_message = null; 624 | 625 | try { 626 | this.loading_config = true; 627 | console.log(`Downloading gradio config from ${config_url}`); 628 | const config_response = await fetch(config_url, { 629 | method: "GET", 630 | }); 631 | 632 | const gradio_config = await config_response.json(); 633 | 634 | console.log("gradio_config", gradio_config); 635 | 636 | const gradio_config_keys = [ 637 | "version", 638 | "mode", 639 | "components", 640 | "dependencies", 641 | ]; 642 | 643 | const is_gradio_config = gradio_config_keys.every( 644 | (key) => key in gradio_config 645 | ); 646 | 647 | if (is_gradio_config) { 648 | this.selected_backend.gradio_config = gradio_config; 649 | } else { 650 | error_message = 651 | "The config json file downloaded does not seem to be a gradio config file"; 652 | console.error(error_message, gradio_config); 653 | } 654 | 655 | await initGradio(); 656 | } catch (e) { 657 | if (this.backend_id === "automatic1111") { 658 | error_message = 659 | "You need to start the automatic1111 backend on your computer with '--no-gradio-queue --cors-allow-origins=http://localhost:5173,https://diffusionui.com'."; 660 | } else { 661 | error_message = "Error trying to download the gradio config"; 662 | } 663 | console.error(error_message, e); 664 | } finally { 665 | this.loading_config = false; 666 | } 667 | 668 | if (error_message) { 669 | const ui = useUIStore(); 670 | const output = useOutputStore(); 671 | ui.show_latest_result = true; 672 | ui.show_results = true; 673 | output.error_message = error_message; 674 | } 675 | } 676 | }, 677 | getImageInput() { 678 | const conditions = this.current_function?.image?.conditions; 679 | 680 | return this.inputs.find(function (input) { 681 | if (input.type === "image") { 682 | if (conditions) { 683 | return Object.keys(conditions).every( 684 | (cond_key) => input.props[cond_key] === conditions[cond_key] 685 | ); 686 | } else { 687 | return true; 688 | } 689 | } 690 | }); 691 | }, 692 | getImageMaskInput() { 693 | const conditions = this.current_function?.image_mask?.conditions; 694 | 695 | return this.inputs.find(function (input) { 696 | if (input.type === "image_mask") { 697 | if (conditions) { 698 | return Object.keys(conditions).every( 699 | (cond_key) => input.props[cond_key] === conditions[cond_key] 700 | ); 701 | } else { 702 | return true; 703 | } 704 | } 705 | }); 706 | }, 707 | getGradioConfigFunctionIndex(conditions) { 708 | if (this.gradio_config) { 709 | const dependencies = this.gradio_config.dependencies; 710 | const components = this.gradio_config.components; 711 | const component_array_keys = ["inputs", "outputs", "targets"]; 712 | 713 | const fn_index = Object.keys(dependencies).find(function (key) { 714 | const dependency = dependencies[key]; 715 | return Object.keys(conditions).every(function (cond_key) { 716 | if (component_array_keys.includes(cond_key)) { 717 | const condition_item = conditions[cond_key]; 718 | return Object.keys(condition_item).every(function (target_key) { 719 | const target_conditions = condition_item[target_key].conditions; 720 | 721 | const component_key = Object.keys(components).find(function ( 722 | comp_key 723 | ) { 724 | let component = components[comp_key]; 725 | 726 | return Object.keys(target_conditions).every(function ( 727 | target_cond_key 728 | ) { 729 | if (target_cond_key === "type") { 730 | return target_conditions["type"] === component.type; 731 | } else { 732 | return ( 733 | target_conditions[target_cond_key] === 734 | component.props[target_cond_key] 735 | ); 736 | } 737 | }); 738 | }); 739 | 740 | const component = components[component_key]; 741 | 742 | if (component) { 743 | return dependency[cond_key][target_key] == component.id; 744 | } else { 745 | return false; 746 | } 747 | }); 748 | } else { 749 | return dependency[cond_key] === conditions[cond_key]; 750 | } 751 | }); 752 | }); 753 | 754 | return fn_index; 755 | } 756 | }, 757 | convertGradioInput(gradio_input, convert_context) { 758 | let input; 759 | 760 | const props = gradio_input.props; 761 | switch (gradio_input.type) { 762 | case "label": 763 | input = { 764 | label: props.name, 765 | visible: false, 766 | default: 0, 767 | }; 768 | break; 769 | case "textbox": 770 | input = { 771 | label: props.label, 772 | type: "text", 773 | }; 774 | break; 775 | case "dropdown": 776 | case "radio": 777 | input = { 778 | label: props.label, 779 | type: "choice", 780 | validation: { 781 | options: props.choices.map((choice) => 782 | Array.isArray(choice) ? choice[0] : choice 783 | ), 784 | }, 785 | }; 786 | break; 787 | case "slider": 788 | input = { 789 | label: props.label, 790 | type: "float", 791 | step: props.step, 792 | validation: { 793 | min: props.minimum, 794 | max: props.maximum, 795 | }, 796 | }; 797 | break; 798 | case "checkbox": 799 | input = { 800 | label: props.label, 801 | type: "boolean", 802 | }; 803 | break; 804 | case "number": 805 | input = { 806 | label: props.label, 807 | type: "bigint", 808 | validation: { 809 | min: -1, 810 | max: 4294967296, 811 | }, 812 | }; 813 | break; 814 | case "file": 815 | input = { 816 | label: props.label, 817 | default: null, 818 | visible: false, 819 | }; 820 | break; 821 | case "checkboxgroup": 822 | input = { 823 | label: props.label, 824 | id: "checkboxgroup_" + gradio_input.id, 825 | default: props.value, 826 | visible: false, 827 | }; 828 | break; 829 | case "html": 830 | input = { 831 | label: props.name, 832 | id: "html_" + gradio_input.id, 833 | default: "", 834 | visible: false, 835 | }; 836 | break; 837 | case "image": 838 | { 839 | let image_id = convert_context ? convert_context.images_found++ : 0; 840 | if (gradio_input.props.tool === "editor") { 841 | input = { 842 | label: props.label, 843 | id: "image_" + image_id, 844 | type: props.label.toLowerCase().includes("mask") 845 | ? "image_mask" 846 | : "image", 847 | props: props, 848 | default: null, 849 | visible: false, 850 | }; 851 | } else { 852 | input = { 853 | label: props.label, 854 | id: "image_" + image_id, 855 | default: null, 856 | visible: false, 857 | }; 858 | } 859 | } 860 | break; 861 | case "state": 862 | input = { 863 | label: props.name, 864 | id: "state_" + gradio_input.id, 865 | default: "", 866 | visible: false, 867 | }; 868 | break; 869 | case "markdown": 870 | input = { 871 | label: props.name, 872 | id: "markdown_" + gradio_input.id, 873 | default: "", 874 | visible: false, 875 | }; 876 | break; 877 | case "video": 878 | input = { 879 | label: props.name, 880 | id: "video_" + gradio_input.id, 881 | default: null, 882 | visible: false, 883 | }; 884 | break; 885 | default: 886 | console.log("Unsupported gradio component type: ", gradio_input.type); 887 | console.log("Unsupported gradio component type: ", gradio_input); 888 | 889 | input = { 890 | label: props.label !== undefined ? props.label : "undefined_label", 891 | id: 892 | "unsupported_" + gradio_input.id !== undefined 893 | ? gradio_input.id 894 | : "no_id", 895 | default: null, 896 | visible: false, 897 | }; 898 | break; 899 | } 900 | 901 | if (input.label === undefined) { 902 | input.label = "undefined_label"; 903 | } 904 | 905 | if (!("visible" in input)) { 906 | input.visible = gradio_input.visible; 907 | } 908 | 909 | if (!("default" in input)) { 910 | input.default = gradio_input.props.value; 911 | 912 | if (input.default === undefined) { 913 | input.default = ""; 914 | } 915 | } 916 | 917 | if (!("id" in input)) { 918 | input.id = input.label.replace(/\s+/g, "_").toLowerCase(); 919 | } 920 | 921 | const input_id = input.id; 922 | let next_id = 0; 923 | if (convert_context) { 924 | while (convert_context.used_ids.includes(input.id)) { 925 | input.id = input_id + "_" + next_id; 926 | next_id++; 927 | } 928 | convert_context.used_ids.push(input.id); 929 | } 930 | 931 | input.auto_id = input.id; 932 | input.description = ""; 933 | 934 | return input; 935 | }, 936 | selectedBackendUpdated() { 937 | console.log(`backend changed to ${this.backend_id}`); 938 | }, 939 | }, 940 | }); 941 | --------------------------------------------------------------------------------