├── 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 |
6 |
7 |
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 |
7 | Button(@click="cancelGeneration")
8 | | Cancel
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/AboutView.vue:
--------------------------------------------------------------------------------
1 |
2 | div(class="about")
3 | h1 This is an about page with pug
4 |
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 |
9 | .flex.flex-column.align-items-center
10 | Button.p-button-secondary.p-button-outlined.p-button-sm.p-button-text(@click="backend.loadGradioConfig")
11 | font-awesome-icon(icon="fa-solid fa-arrows-rotate")
12 | |
13 | | Load config
14 |
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 |
11 | .flex.flex-row.gap-1.grow
12 | template(v-for="(image, index) in images" :key="index")
13 | Image(:src="image", width="64", height="64")
14 |
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 |
11 | Message(severity="error" :closable="false")
12 | div(v-if="output.error_message.startsWith('<')")
13 | div(v-html="output.error_message")
14 | div(v-else)
15 | span {{ output.error_message }}
16 | p(v-if="backend.doc_url")
17 | | Please check the
18 | a(:href="backend.doc_url" target="_blank")
19 | font-awesome-icon(icon="fa-solid fa-book")
20 | | Documentation
21 |
22 |
--------------------------------------------------------------------------------
/src/views/ResultView.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | .flex.flex-row.justify-content-center
17 | Button(v-show="!output.loading_images", @click="goBackToInput")
18 | font-awesome-icon(icon="fa-solid fa-left-long")
19 | | Try again
20 | ResultImages
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/src/components/dialogs/LicenseDialog.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | Dialog(:modal="true" :visible="backend.show_license", :breakpoints="{'960px': '75vw', '640px': '100vw'}" :style="{width: '50vw'}", :closable="false")
11 | div(v-html="backend.license_html")
12 |
13 | template(#header)
14 | h3
15 | | LICENSE
16 |
17 | template(#footer)
18 | Button(label='Accept', @click="backend.acceptLicense")
19 |
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 |
10 | template(v-if="output.loading_progress")
11 | .flex.flex-column.w-full.align-items-center
12 | ProgressBar(:value="output.loading_progress" :showValue="false" style="width: 100%")
13 | template(v-if="output.loading_message")
14 | div(v-html="output.loading_message")
15 | template(v-else)
16 | ProgressSpinner(style="width: 50px; height: 50px", strokeWidth="3", fill="var(--surface-ground)" animeDuration=".5s")
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/LayoutContainer.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | Card(style="margin-bottom: 2em")
15 | template(#title)
16 | | {{ label }}
17 | template(#content)
18 | template(v-for="component in components" :key="component.id")
19 | LayoutComponent(:component="component")
20 |
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 |
9 | .flex.flex-column.align-items-center(v-if="input.seed_is_set")
10 | .p-chips.p-component(@click="resetSeeds")
11 | ul.p-chips-multiple-container
12 | li.p-chips-token
13 | span.p-chips-token-label
14 | span Seed:
15 | span {{ input.seed }}
16 | span.p-chips-token-icon.pi.pi-times-circle
17 |
18 |
30 |
--------------------------------------------------------------------------------
/src/components/InputSlider.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | Slider(@change="onChanged" :modelValue="modelValue" :min="min" :max="max" :step="step")
23 |
24 |
25 |
34 |
--------------------------------------------------------------------------------
/src/components/editor/InputCanvas.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | canvas#canvas.main-canvas(:width="editor.width", :height="editor.height", style="{max-width: 100vw}")
16 |
17 |
18 |
36 |
--------------------------------------------------------------------------------
/src/components/ModelParameterSlider.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | label(:for="'input_' + input.id" class="col-10 lg:col-4 justify-content-center lg:justify-content-end")
12 | | {{ input.label }}
13 | span(class="inline lg:hidden") : {{input.value}}
14 | label(class="col-1 col-offset-1 hidden lg:block")
15 | | {{input.value}}
16 | div(class="col-12 lg:col-5")
17 | InputSlider(:label="input.label" v-model="input.value" :min="input.validation.min" :max="input.validation.max" :step="input.step ? input.step: 1" :title="input.description")
18 |
19 |
20 |
28 |
--------------------------------------------------------------------------------
/src/components/LayoutComponent.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | template(v-if="isVisible")
22 | template(v-if="component.type==='input'")
23 | ModelParameter(:input="backend.findInput(component.id)")
24 | template(v-if="component.type==='container'")
25 | LayoutContainer(:label="component.label" :id="component.id" :components="component.components")
26 |
27 |
--------------------------------------------------------------------------------
/src/components/editor/ImageEditor.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | .flex.flex-column
14 | .flex.flex-row.justify-content-center
15 | .toolbar.align-items-center
16 | ImageEditorToolbar
17 | .flex.flex-row.justify-content-center
18 | div(v-show="ui.editor_view=='composite'")
19 | InputCanvas(v-show="ui.editor_view=='composite'")
20 | div(v-show="ui.editor_view=='mask'")
21 | template(v-if="editor.mask_image_b64")
22 | Image(:src="editor.mask_image_b64")
23 |
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 |
21 | FileUpload(name="image_upload", url="false", mode="basic", :customUpload="true", @uploader="fileUploaded", accept="image/*", :auto="true", chooseLabel="Upload an image", showUploadButton=false, class="p-button-secondary p-button-outlined p-button-sm p-button-text")
22 |
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 |
9 | Button.panel-toggler.left-panel-toggler(@click="ui.showLeftPanel")
10 | font-awesome-icon(icon="fa-solid fa-gears")
11 | Button.panel-toggler.right-panel-toggler(@click="ui.showRightPanel")
12 | font-awesome-icon(icon="fa-solid fa-images")
13 |
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 |
18 | main.main.flex.flex-column.justify-content-center(:class="{verticalcenter: !ui.show_results && !editor.has_image}")
19 | PanelHeader
20 | div(v-show="!ui.show_results")
21 | InputView
22 | div(v-show="ui.show_results")
23 | ResultView
24 | LicenseDialog
25 | EditURLDialog
26 | ConfirmDialog
27 | Toast(:autoZIndex="true")
28 |
29 |
30 |
50 |
--------------------------------------------------------------------------------
/src/components/dialogs/EditURLDialog.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 | Dialog(:modal="true" :visible="ui.edit_url_visible", :breakpoints="{'960px': '75vw', '640px': '100vw'}" :style="{width: '50vw'}", :closable="false")
26 | InputText.w-full(type="text", v-model="ui.edit_url_new_url")
27 |
28 | template(#header)
29 | h3
30 | | Edit API URL
31 |
32 | template(#footer)
33 | Button.p-button-text.p-button-warning(label="Reset original URL", @click="resetURL" v-if="ui.edit_url_new_url !== backend.original.base_url")
34 | Button.p-button-text(label="Cancel", @click="ui.hideEditURL")
35 | Button.p-button-primary(label="Modify API URL", @click="modifyURL")
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/BackendSelector.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | .flex.flex-column.align-items-center
14 | Dropdown#backend(optionLabel="label", optionValue="id", optionGroupLabel="label" optionGroupChildren="backends" v-model="backend.backend_id", :options="backend.backend_options" title="Selection of the server backend" v-if="backend.backend_id !== 'automatic1111'")
15 |
16 | template(v-if="backend.models_input && backend.models_input.validation.options.length > 1")
17 | Dropdown#model(:loading="output.loading_model" @change="changeModel" v-model="backend.models_input.value" :options="backend.models_input.validation.options" :title="backend.models_input.description" :disabled="output.loading" class="w-full lg:w-min")
18 |
19 |
20 |
32 |
33 |
39 |
--------------------------------------------------------------------------------
/src/views/HomeView.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 | template(v-if="valid_backend_id")
43 | MainView
44 | LeftPanelView
45 | RightPanelView
46 | template(v-else)
47 | Message(severity="error" :closable="false")
48 | | Invalid backend: {{ backend_id }}
49 |
50 |
--------------------------------------------------------------------------------
/src/components/PromptInput.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | div
16 | SeedChip
17 | template(v-if="backend.hasInput('prompt')")
18 | .p-inputgroup.shadow-3
19 | InputText(type="text", v-model="backend.findInput('prompt').value", @keyup.enter="generate")
20 | Button(@click="generate", :disabled="(output.loading || backend.show_license) ? 'disabled' : null")
21 | span.hide-sm Generate image
22 | span.show-sm
23 | font-awesome-icon(icon="fa-solid fa-angles-right")
24 | template(v-else)
25 | .flex.justify-content-center
26 | Button(@click="generate", :disabled="(output.loading || backend.show_license) ? 'disabled' : null")
27 | span.hide-sm Generate image
28 | span.show-sm
29 | font-awesome-icon(icon="fa-solid fa-angles-right")
30 |
31 |
32 |
43 |
--------------------------------------------------------------------------------
/src/components/ModelParameters.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | template(v-if="backend.current")
14 | template(v-if="backend.needs_gradio_config")
15 | LoadConfigButton
16 | template(v-else)
17 | template(v-if="backend.has_multiple_functions && backend.function_options.length > 1")
18 | .flex.flex-column.align-items-center
19 | Dropdown#fn_dropdown(optionLabel="label", optionValue="id", v-model="backend.fn_id", :options="backend.function_options")
20 | Divider
21 | template(v-if="backend.current_function.layout !== undefined")
22 | template(v-for="component in backend.current_function.layout" :key="component.id")
23 | LayoutComponent(:component="component")
24 | template(v-else)
25 | template(v-for="input in backend.inputs" :key="input.id")
26 | ModelParameter(:input="input")
27 |
28 |
29 |
43 |
--------------------------------------------------------------------------------
/src/views/RightPanelView.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 | Sidebar.p-sidebar-md(:visible="ui.right_panel_visible", :showCloseIcon="false" position="right" :modal="false")
31 |
32 | template(#header)
33 | Button.p-button-secondary.toggler.r(label="Close", @click="ui.hideRightPanel")
34 | font-awesome-icon(icon="fa-solid fa-angle-right")
35 |
36 | .flex.flex-column.thumbnails
37 | template(v-for="(images, index) in output.gallery" :key="index")
38 | ImageThumbnails(:images="images.content" @click="thumbnailClick(index)")
39 | .thumbnails
40 | ProgressIndicator.cursor-pointer(v-if="output.loading_images" @click="progressIndicatorClick")
41 |
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 |
16 | Sidebar.p-sidebar-md(:visible="ui.left_panel_visible", :showCloseIcon="false" :modal="false")
17 | template(#header)
18 | Button.p-button-secondary.toggler(label="Close", @click="ui.hideLeftPanel")
19 | font-awesome-icon(icon="fa-solid fa-angle-left")
20 | BackendSelector
21 | TabView
22 | TabPanel
23 | template(#header)
24 | .text-center.w-full
25 | font-awesome-icon(icon="fa-solid fa-sliders")
26 | ModelParameters
27 | TabPanel
28 | template(#header)
29 | .text-center.w-full
30 | font-awesome-icon(icon="fa-solid fa-circle-info")
31 | ModelInfo
32 |
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 |
23 | .field.grid(v-if="input && isVisible", :title="input.description" class="justify-content-center")
24 | template(v-if="input.type === 'int' || input.type === 'float'")
25 | ModelParameterSlider(:input="input")
26 | template(v-if="input.type == 'text' || input.type == 'choice' || input.type == 'bigint'")
27 | label(:for="'input_' + input.id" class="col-12 lg:col-4 justify-content-center lg:justify-content-end")
28 | | {{ input.label }}
29 | div(class="col-12 lg:col-6 lg:col-offset-1")
30 | template(v-if="input.type == 'text'")
31 | InputText.min-w-full(type="text", :id="'input_' + input.id", v-model="input.value")
32 | template(v-if="input.type == 'choice'")
33 | Dropdown(v-model="input.value" :options="input.validation.options" :optionLabel="input.validation.optionLabel" :optionValue="input.validation.optionValue" class="w-full lg:w-min")
34 | template(v-if="input.type == 'bigint'")
35 | InputNumber.flex(mode="decimal" showButtons v-model="input.value" :min="input.validation.min" :max="input.validation.max" :step="input.step ? input.step: 1" :title="input.description" :useGrouping="false")
36 | template(v-if="input.type == 'boolean'")
37 | label(:for="'input_' + input.id" class="col-6 lg:col-4 justify-content-center lg:justify-content-end")
38 | | {{ input.label }}
39 | div(class="col-6 lg:col-6 lg:col-offset-1")
40 | template(v-if="input.type == 'boolean'")
41 | InputSwitch(v-model="input.value")
42 |
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 |
72 | .result-image
73 | Image(:src="props.src" imageStyle="max-width: min(100vw, 512px);")
74 | SpeedDial(:model="buttons", direction="down", showIcon="pi pi-bars", hideIcon="pi pi-times", :tooltipOptions="{position: 'left'}")
75 |
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 |
15 | div
16 | .sidebar-section
17 | h3.sidebar-section-title Model
18 | p.model-description(v-if="backend.description")
19 | span(v-html="backend.description")
20 | p(v-if="backend.doc_url")
21 | a(:href="backend.doc_url" target="_blank")
22 | font-awesome-icon(icon="fa-solid fa-book")
23 | | Documentation
24 | p(v-if="backend.license")
25 | strong License:
26 | a.cursor-pointer(@click="backend.showLicense") {{ backend.license }}
27 | Divider(type="dashed")
28 | Card(v-if="backend.model_info_inputs" style="margin-bottom: 2em")
29 | template(#title)
30 | | Global parameters
31 | template(#content)
32 | .flex.flex-column.align-items-center
33 | .api-url(v-if="backend.api_url" title="the URL used to generate the images")
34 | .field.grid
35 | label.col-fixed(for="api-url" style="margin:auto")
36 | font-awesome-icon(icon="fa-solid fa-link")
37 | span API url
38 | .col
39 | #api-url.api-url-value.cursor-pointer(@click="ui.showEditURL") {{ backend.base_url }}
40 | template(v-if="backend.has_access_code")
41 | .field.grid(:title="backend.access_code_input.description")
42 | label.col-fixed(style="min-width: 150px; margin:auto")
43 | span Access code
44 | .col
45 | Password.min-w-full(v-model="backend.access_code_input.value" :feedback="false" inputStyle="margin: auto;")
46 | template(v-for="input in backend.model_info_inputs" :key="input.id")
47 | ModelParameter(:input="input")
48 | .flex.flex-column.align-items-center
49 | Button.p-button-outlined.p-button-danger(@click="backend.resetCurrentBackendToDefaults")
50 | span Reset to default values
51 |
52 |
53 |
54 |
80 |
--------------------------------------------------------------------------------
/src/components/ResultImages.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 | .result-images
33 | template(v-if="output.loading_images && ui.show_latest_result")
34 | .flex.flex-column.w-full.align-items-center
35 | template(v-if="output.image_preview")
36 | Image.image-preview(:src="output.image_preview" imageStyle="max-width: min(100vw, 512px);")
37 | ProgressIndicator(:class="{narrow: output.image_preview}")
38 | template(v-if="backend.cancellable")
39 | CancelButton
40 | template(v-else)
41 | template(v-if="output.error_message")
42 | ErrorMessage
43 | template(v-else)
44 | template(v-if="output.images")
45 | template(v-if="output.images.content.length == 1")
46 | ResultImage(v-touch:swipe.top="output.goDown" v-touch:swipe.bottom="output.goUp" :src="output.images.content[0]", :index="0")
47 | template(v-else)
48 | Galleria(v-touch:swipe.left="output.goRight" v-touch:swipe.right="output.goLeft" v-touch:swipe.top="output.goDown" v-touch:swipe.bottom="output.goUp" v-model:activeIndex="output.image_index.current" :value="output.gallery_images", :numVisible="Math.min(output.gallery_images.length, 4)", :responsiveOptions="responsiveOptions")
49 | template(#item="slotProps")
50 | template(v-if="slotProps.item")
51 | ResultImage(:src="slotProps.item.itemImageSrc", :index="slotProps.item.index")
52 | template(#thumbnail="slotProps")
53 | template(v-if="slotProps.item")
54 | img(:src="slotProps.item.thumbnailImageSrc", style="width: 70px; height: 70px;")
55 |
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 |
30 | .flex.flex-row.justify-content-center
31 | .toolbar-left
32 | ColorPicker(v-model="editor.chosen_color", format="hex" :style="{visibility: ui.show_color_picker ? 'visible' : 'hidden'}")
33 | Button.toolbar-button.brush-circle(:style="{visibility: ui.show_brush ? 'visible' : 'hidden'}", label="Primary", @click="brushSizeButton", class="p-button-raised p-button-rounded p-button-outlined", v-tooltip.bottom="{ value: 'Brush size'}")
34 | font-awesome-icon(icon="fa-solid fa-circle")
35 | OverlayPanel(ref="op", :showCloseIcon="false", :dismissable="true" class="brush-size-overlay")
36 | Slider(@change="updateBrushSize" v-model="editor.brush_size.slider" :min="1" :max="150" :step="2")
37 |
38 | Button.toolbar-button(:style="{ visibility: ui.show_eraser ? 'visible' : 'hidden'}", label="Primary", @click="toggleEraser", class="p-button-raised p-button-rounded p-button-outlined", :class="{ active: ui.cursor_mode === 'eraser'}", v-tooltip.bottom="{ value: 'Draw zone to modify'}")
39 | font-awesome-icon(icon="fa-solid fa-eraser")
40 | Button.toolbar-button(:style="{visibility: ui.show_pencil ? 'visible' : 'hidden'}", label="Primary", @click="toggleDraw", class="p-button-raised p-button-rounded p-button-outlined", :class="{ active: ui.cursor_mode === 'draw'}", v-tooltip.bottom="{ value: 'Draw'}")
41 | font-awesome-icon(icon="fa-solid fa-pencil")
42 | .toolbar-center
43 | Button.toolbar-button(:style="{visibility: ui.show_undo ? 'visible' : 'hidden'}", label="Primary", @click="undo", class="p-button-raised p-button-rounded p-button-outlined", v-tooltip.bottom="{ value: 'undo'}")
44 | font-awesome-icon(icon="fa-solid fa-rotate-left")
45 | Button.toolbar-button(:style="{visibility: ui.show_redo ? 'visible' : 'hidden'}", label="Primary", @click="redo", class="p-button-raised p-button-rounded p-button-outlined", v-tooltip.bottom="{ value: 'redo'}")
46 | font-awesome-icon(icon="fa-solid fa-rotate-right")
47 | .toolbar-right
48 | Button.toolbar-button(:style="{visibility: ui.show_mask_button ? 'visible' : 'hidden'}", label="Primary", @click="toggleMaskView", class="p-button-raised p-button-rounded p-button-outlined", :class="{ active: ui.editor_view === 'mask'}", v-tooltip.bottom="{ value: 'Show mask'}")
49 | font-awesome-icon(icon="fa-solid fa-image")
50 | Button.toolbar-button.close-button(label="Primary", @click="closeImage", class="p-button-text", v-tooltip.bottom="{ value: 'Close'}", style="margin-left: 60px")
51 | font-awesome-icon(icon="fa-solid fa-xmark")
52 |
53 |
54 |
59 |
60 |
89 |
--------------------------------------------------------------------------------
/src/views/InputView.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 | .flex.flex-column.gap-3
42 | template(v-if="backend.needs_gradio_config")
43 | LoadConfigButton
44 | template(v-else)
45 | template(v-if="!editor.has_image && !input.seed_is_set && backend.hasInput('prompt')")
46 | .flex.flex-column.align-items-center
47 | .enter-a-prompt
48 | | Enter a prompt:
49 | PromptInput
50 | div(v-show="backend.has_img2img_mode")
51 | div(v-show="editor.has_image")
52 | ImageEditor
53 | .main-slider(v-if="backend.hasInput('strength')", :class="{visible: ui.show_strength_slider}")
54 | .flex.flex-row.justify-content-center
55 | .slider-label.align-items-left(title="At low strengths, the initial image is not modified much")
56 | | Low variations
57 | Slider.align-items-center(v-model="backend.strength_input.value" :min="0" :max="1" :step="0.02" v-tooltip.bottom="{ value: 'Strength:' + backend.strength}")
58 | .slider-label.align-items-left(title="At a strength of 1, what was previously in the zone is ignored")
59 | | Ignore previous
60 | template(v-if="!editor.has_image")
61 | .flex.flex-column.align-items-center
62 | .or(v-if="backend.hasInput('prompt')") OR
63 | FileUploadButton.main-slider
64 | .flex.flex-column.align-items-center
65 | .or OR
66 | Button.p-button-secondary.p-button-outlined.p-button-sm.p-button-text(@click="newDrawing")
67 | font-awesome-icon(icon="fa-solid fa-paintbrush")
68 | | draw something
69 |
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 |
--------------------------------------------------------------------------------