diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 1cfd9487..00000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -extensions -extensions-disabled -repositories -venv \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 9c70eff8..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,96 +0,0 @@ -/* global module */ -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: "eslint:recommended", - parserOptions: { - ecmaVersion: "latest", - }, - rules: { - "arrow-spacing": "error", - "block-spacing": "error", - "brace-style": "error", - "comma-dangle": ["error", "only-multiline"], - "comma-spacing": "error", - "comma-style": ["error", "last"], - "curly": ["error", "multi-line", "consistent"], - "eol-last": "error", - "func-call-spacing": "error", - "function-call-argument-newline": ["error", "consistent"], - "function-paren-newline": ["error", "consistent"], - "indent": ["error", 4], - "key-spacing": "error", - "keyword-spacing": "error", - "linebreak-style": ["error", "unix"], - "no-extra-semi": "error", - "no-mixed-spaces-and-tabs": "error", - "no-multi-spaces": "error", - "no-redeclare": ["error", {builtinGlobals: false}], - "no-trailing-spaces": "error", - "no-unused-vars": "off", - "no-whitespace-before-property": "error", - "object-curly-newline": ["error", {consistent: true, multiline: true}], - "object-curly-spacing": ["error", "never"], - "operator-linebreak": ["error", "after"], - "quote-props": ["error", "consistent-as-needed"], - "semi": ["error", "always"], - "semi-spacing": "error", - "semi-style": ["error", "last"], - "space-before-blocks": "error", - "space-before-function-paren": ["error", "never"], - "space-in-parens": ["error", "never"], - "space-infix-ops": "error", - "space-unary-ops": "error", - "switch-colon-spacing": "error", - "template-curly-spacing": ["error", "never"], - "unicode-bom": "error", - }, - globals: { - //script.js - gradioApp: "readonly", - executeCallbacks: "readonly", - onAfterUiUpdate: "readonly", - onOptionsChanged: "readonly", - onUiLoaded: "readonly", - onUiUpdate: "readonly", - uiCurrentTab: "writable", - uiElementInSight: "readonly", - uiElementIsVisible: "readonly", - //ui.js - opts: "writable", - all_gallery_buttons: "readonly", - selected_gallery_button: "readonly", - selected_gallery_index: "readonly", - switch_to_txt2img: "readonly", - switch_to_img2img_tab: "readonly", - switch_to_img2img: "readonly", - switch_to_sketch: "readonly", - switch_to_inpaint: "readonly", - switch_to_inpaint_sketch: "readonly", - switch_to_extras: "readonly", - get_tab_index: "readonly", - create_submit_args: "readonly", - restart_reload: "readonly", - updateInput: "readonly", - onEdit: "readonly", - //extraNetworks.js - requestGet: "readonly", - popup: "readonly", - // from python - localization: "readonly", - // progrssbar.js - randomId: "readonly", - requestProgress: "readonly", - // imageviewer.js - modalPrevImage: "readonly", - modalNextImage: "readonly", - // localStorage.js - localSet: "readonly", - localGet: "readonly", - localRemove: "readonly", - // resizeHandle.js - setupResizeHandle: "writable" - } -}; diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5876e941..c86bd8a6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -91,7 +91,7 @@ body: id: logs attributes: label: Console logs - description: Please provide **full** cmd/terminal logs from the moment you started UI to the end of it, after the bug occured. If it's very long, provide a link to pastebin or similar service. + description: Please provide **full** cmd/terminal logs from the moment you started UI to the end of it, after the bug occurred. If it's very long, provide a link to pastebin or similar service. render: Shell validations: required: true diff --git a/.github/workflows/on_pull_request.yaml b/.github/workflows/on_pull_request.yaml index 9e44c806..9326c6a4 100644 --- a/.github/workflows/on_pull_request.yaml +++ b/.github/workflows/on_pull_request.yaml @@ -11,8 +11,8 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Checkout Code - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: 3.11 # NB: there's no cache: pip here since we're not installing anything @@ -20,7 +20,7 @@ jobs: # not to have GHA download an (at the time of writing) 4 GB cache # of PyTorch and other dependencies. - name: Install Ruff - run: pip install ruff==0.1.6 + run: pip install ruff==0.3.3 - name: Run Ruff run: ruff . lint-js: @@ -29,9 +29,9 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 - run: npm i --ci diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index f42e4758..0610f4f5 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -11,9 +11,9 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.10.6 cache: pip @@ -22,7 +22,7 @@ jobs: launch.py - name: Cache models id: cache-models - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: models key: "2023-12-30" @@ -68,13 +68,13 @@ jobs: python -m coverage report -i python -m coverage html -i - name: Upload main app output - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: output path: output.txt - name: Upload coverage HTML - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: htmlcov diff --git a/.gitignore b/.gitignore index 6790e9ee..e81ad31f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__ *.ckpt *.safetensors *.pth +.DS_Store /ESRGAN/* /SwinIR/* /repositories @@ -38,3 +39,6 @@ notification.mp3 /package-lock.json /.coverage* /test/test_outputs +/cache +trace.json +/sysinfo-????-??-??-??-??.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c65981..5f7e5935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,283 @@ -## 1.8.0-RC +## 1.10.1 + +### Bug Fixes: +* fix image upscale on cpu ([#16275](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16275)) + + +## 1.10.0 + +### Features: +* A lot of performance improvements (see below in Performance section) +* Stable Diffusion 3 support ([#16030](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16030), [#16164](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16164), [#16212](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16212)) + * Recommended Euler sampler; DDIM and other timestamp samplers currently not supported + * T5 text model is disabled by default, enable it in settings +* New schedulers: + * Align Your Steps ([#15751](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15751)) + * KL Optimal ([#15608](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15608)) + * Normal ([#16149](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16149)) + * DDIM ([#16149](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16149)) + * Simple ([#16142](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16142)) + * Beta ([#16235](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16235)) +* New sampler: DDIM CFG++ ([#16035](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16035)) + +### Minor: +* Option to skip CFG on early steps ([#15607](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15607)) +* Add --models-dir option ([#15742](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15742)) +* Allow mobile users to open context menu by using two fingers press ([#15682](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15682)) +* Infotext: add Lora name as TI hashes for bundled Textual Inversion ([#15679](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15679)) +* Check model's hash after downloading it to prevent corruped downloads ([#15602](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15602)) +* More extension tag filtering options ([#15627](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15627)) +* When saving AVIF, use JPEG's quality setting ([#15610](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15610)) +* Add filename pattern: `[basename]` ([#15978](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15978)) +* Add option to enable clip skip for clip L on SDXL ([#15992](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15992)) +* Option to prevent screen sleep during generation ([#16001](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16001)) +* ToggleLivePriview button in image viewer ([#16065](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16065)) +* Remove ui flashing on reloading and fast scrollong ([#16153](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16153)) +* option to disable save button log.csv ([#16242](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16242)) + +### Extensions and API: +* Add process_before_every_sampling hook ([#15984](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15984)) +* Return HTTP 400 instead of 404 on invalid sampler error ([#16140](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16140)) + +### Performance: +* [Performance 1/6] use_checkpoint = False ([#15803](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15803)) +* [Performance 2/6] Replace einops.rearrange with torch native ops ([#15804](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15804)) +* [Performance 4/6] Precompute is_sdxl_inpaint flag ([#15806](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15806)) +* [Performance 5/6] Prevent unnecessary extra networks bias backup ([#15816](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15816)) +* [Performance 6/6] Add --precision half option to avoid casting during inference ([#15820](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15820)) +* [Performance] LDM optimization patches ([#15824](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15824)) +* [Performance] Keep sigmas on CPU ([#15823](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15823)) +* Check for nans in unet only once, after all steps have been completed +* Added pption to run torch profiler for image generation + +### Bug Fixes: +* Fix for grids without comprehensive infotexts ([#15958](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15958)) +* feat: lora partial update precede full update ([#15943](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15943)) +* Fix bug where file extension had an extra '.' under some circumstances ([#15893](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15893)) +* Fix corrupt model initial load loop ([#15600](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15600)) +* Allow old sampler names in API ([#15656](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15656)) +* more old sampler scheduler compatibility ([#15681](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15681)) +* Fix Hypertile xyz ([#15831](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15831)) +* XYZ CSV skipinitialspace ([#15832](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15832)) +* fix soft inpainting on mps and xpu, torch_utils.float64 ([#15815](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15815)) +* fix extention update when not on main branch ([#15797](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15797)) +* update pickle safe filenames +* use relative path for webui-assets css ([#15757](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15757)) +* When creating a virtual environment, upgrade pip in webui.bat/webui.sh ([#15750](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15750)) +* Fix AttributeError ([#15738](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15738)) +* use script_path for webui root in launch_utils ([#15705](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15705)) +* fix extra batch mode P Transparency ([#15664](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15664)) +* use gradio theme colors in css ([#15680](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15680)) +* Fix dragging text within prompt input ([#15657](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15657)) +* Add correct mimetype for .mjs files ([#15654](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15654)) +* QOL Items - handle metadata issues more cleanly for SD models, Loras and embeddings ([#15632](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15632)) +* replace wsl-open with wslpath and explorer.exe ([#15968](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15968)) +* Fix SDXL Inpaint ([#15976](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15976)) +* multi size grid ([#15988](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15988)) +* fix Replace preview ([#16118](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16118)) +* Possible fix of wrong scale in weight decomposition ([#16151](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16151)) +* Ensure use of python from venv on Mac and Linux ([#16116](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16116)) +* Prioritize python3.10 over python3 if both are available on Linux and Mac (with fallback) ([#16092](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16092)) +* stoping generation extras ([#16085](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16085)) +* Fix SD2 loading ([#16078](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16078), [#16079](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16079)) +* fix infotext Lora hashes for hires fix different lora ([#16062](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16062)) +* Fix sampler scheduler autocorrection warning ([#16054](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16054)) +* fix ui flashing on reloading and fast scrollong ([#16153](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16153)) +* fix upscale logic ([#16239](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16239)) +* [bug] do not break progressbar on non-job actions (add wrap_gradio_call_no_job) ([#16202](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16202)) +* fix OSError: cannot write mode P as JPEG ([#16194](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16194)) + +### Other: +* fix changelog #15883 -> #15882 ([#15907](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15907)) +* ReloadUI backgroundColor --background-fill-primary ([#15864](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15864)) +* Use different torch versions for Intel and ARM Macs ([#15851](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15851)) +* XYZ override rework ([#15836](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15836)) +* scroll extensions table on overflow ([#15830](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15830)) +* img2img batch upload method ([#15817](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15817)) +* chore: sync v1.8.0 packages according to changelog ([#15783](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15783)) +* Add AVIF MIME type support to mimetype definitions ([#15739](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15739)) +* Update imageviewer.js ([#15730](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15730)) +* no-referrer ([#15641](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15641)) +* .gitignore trace.json ([#15980](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15980)) +* Bump spandrel to 0.3.4 ([#16144](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16144)) +* Defunct --max-batch-count ([#16119](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16119)) +* docs: update bug_report.yml ([#16102](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16102)) +* Maintaining Project Compatibility for Python 3.9 Users Without Upgrade Requirements. ([#16088](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16088), [#16169](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16169), [#16192](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16192)) +* Update torch for ARM Macs to 2.3.1 ([#16059](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16059)) +* remove deprecated setting dont_fix_second_order_samplers_schedule ([#16061](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16061)) +* chore: fix typos ([#16060](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16060)) +* shlex.join launch args in console log ([#16170](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16170)) +* activate venv .bat ([#16231](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16231)) +* add ids to the resize tabs in img2img ([#16218](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16218)) +* update installation guide linux ([#16178](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16178)) +* Robust sysinfo ([#16173](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16173)) +* do not send image size on paste inpaint ([#16180](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16180)) +* Fix noisy DS_Store files for MacOS ([#16166](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/16166)) + + +## 1.9.4 + +### Bug Fixes: +* pin setuptools version to fix the startup error ([#15882](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15882)) + +## 1.9.3 + +### Bug Fixes: +* fix get_crop_region_v2 ([#15594](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15594)) + +## 1.9.2 + +### Extensions and API: +* restore 1.8.0-style naming of scripts + +## 1.9.1 + +### Minor: +* Add avif support ([#15582](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15582)) +* Add filename patterns: `[sampler_scheduler]` and `[scheduler]` ([#15581](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15581)) + +### Extensions and API: +* undo adding scripts to sys.modules +* Add schedulers API endpoint ([#15577](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15577)) +* Remove API upscaling factor limits ([#15560](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15560)) + +### Bug Fixes: +* Fix images do not match / Coordinate 'right' is less than 'left' ([#15534](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15534)) +* fix: remove_callbacks_for_function should also remove from the ordered map ([#15533](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15533)) +* fix x1 upscalers ([#15555](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15555)) +* Fix cls.__module__ value in extension script ([#15532](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15532)) +* fix typo in function call (eror -> error) ([#15531](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15531)) + +### Other: +* Hide 'No Image data blocks found.' message ([#15567](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15567)) +* Allow webui.sh to be runnable from arbitrary directories containing a .git file ([#15561](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15561)) +* Compatibility with Debian 11, Fedora 34+ and openSUSE 15.4+ ([#15544](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15544)) +* numpy DeprecationWarning product -> prod ([#15547](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15547)) +* get_crop_region_v2 ([#15583](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15583), [#15587](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15587)) + + +## 1.9.0 + +### Features: +* Make refiner switchover based on model timesteps instead of sampling steps ([#14978](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14978)) +* add an option to have old-style directory view instead of tree view; stylistic changes for extra network sorting/search controls +* add UI for reordering callbacks, support for specifying callback order in extension metadata ([#15205](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15205)) +* Sgm uniform scheduler for SDXL-Lightning models ([#15325](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15325)) +* Scheduler selection in main UI ([#15333](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15333), [#15361](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15361), [#15394](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15394)) + +### Minor: +* "open images directory" button now opens the actual dir ([#14947](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14947)) +* Support inference with LyCORIS BOFT networks ([#14871](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14871), [#14973](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14973)) +* make extra network card description plaintext by default, with an option to re-enable HTML as it was +* resize handle for extra networks ([#15041](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15041)) +* cmd args: `--unix-filenames-sanitization` and `--filenames-max-length` ([#15031](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15031)) +* show extra networks parameters in HTML table rather than raw JSON ([#15131](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15131)) +* Add DoRA (weight-decompose) support for LoRA/LoHa/LoKr ([#15160](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15160), [#15283](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15283)) +* Add '--no-prompt-history' cmd args for disable last generation prompt history ([#15189](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15189)) +* update preview on Replace Preview ([#15201](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15201)) +* only fetch updates for extensions' active git branches ([#15233](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15233)) +* put upscale postprocessing UI into an accordion ([#15223](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15223)) +* Support dragdrop for URLs to read infotext ([#15262](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15262)) +* use diskcache library for caching ([#15287](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15287), [#15299](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15299)) +* Allow PNG-RGBA for Extras Tab ([#15334](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15334)) +* Support cover images embedded in safetensors metadata ([#15319](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15319)) +* faster interrupt when using NN upscale ([#15380](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15380)) +* Extras upscaler: an input field to limit maximul side length for the output image ([#15293](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15293), [#15415](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15415), [#15417](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15417), [#15425](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15425)) +* add an option to hide postprocessing options in Extras tab + +### Extensions and API: +* ResizeHandleRow - allow overriden column scale parametr ([#15004](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15004)) +* call script_callbacks.ui_settings_callback earlier; fix extra-options-section built-in extension killing the ui if using a setting that doesn't exist +* make it possible to use zoom.js outside webui context ([#15286](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15286), [#15288](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15288)) +* allow variants for extension name in metadata.ini ([#15290](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15290)) +* make reloading UI scripts optional when doing Reload UI, and off by default +* put request: gr.Request at start of img2img function similar to txt2img +* open_folder as util ([#15442](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15442)) +* make it possible to import extensions' script files as `import scripts.` ([#15423](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15423)) + +### Performance: +* performance optimization for extra networks HTML pages +* optimization for extra networks filtering +* optimization for extra networks sorting + +### Bug Fixes: +* prevent escape button causing an interrupt when no generation has been made yet +* [bug] avoid doble upscaling in inpaint ([#14966](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14966)) +* possible fix for reload button not appearing in some cases for extra networks. +* fix: the `split_threshold` parameter does not work when running Split oversized images ([#15006](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15006)) +* Fix resize-handle visability for vertical layout (mobile) ([#15010](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15010)) +* register_tmp_file also for mtime ([#15012](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15012)) +* Protect alphas_cumprod during refiner switchover ([#14979](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14979)) +* Fix EXIF orientation in API image loading ([#15062](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15062)) +* Only override emphasis if actually used in prompt ([#15141](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15141)) +* Fix emphasis infotext missing from `params.txt` ([#15142](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15142)) +* fix extract_style_text_from_prompt #15132 ([#15135](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15135)) +* Fix Soft Inpaint for AnimateDiff ([#15148](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15148)) +* edit-attention: deselect surrounding whitespace ([#15178](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15178)) +* chore: fix font not loaded ([#15183](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15183)) +* use natural sort in extra networks when ordering by path +* Fix built-in lora system bugs caused by torch.nn.MultiheadAttention ([#15190](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15190)) +* Avoid error from None in get_learned_conditioning ([#15191](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15191)) +* Add entry to MassFileLister after writing metadata ([#15199](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15199)) +* fix issue with Styles when Hires prompt is used ([#15269](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15269), [#15276](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15276)) +* Strip comments from hires fix prompt ([#15263](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15263)) +* Make imageviewer event listeners browser consistent ([#15261](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15261)) +* Fix AttributeError in OFT when trying to get MultiheadAttention weight ([#15260](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15260)) +* Add missing .mean() back ([#15239](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15239)) +* fix "Restore progress" button ([#15221](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15221)) +* fix ui-config for InputAccordion [custom_script_source] ([#15231](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15231)) +* handle 0 wheel deltaY ([#15268](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15268)) +* prevent alt menu for firefox ([#15267](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15267)) +* fix: fix syntax errors ([#15179](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15179)) +* restore outputs path ([#15307](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15307)) +* Escape btn_copy_path filename ([#15316](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15316)) +* Fix extra networks buttons when filename contains an apostrophe ([#15331](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15331)) +* escape brackets in lora random prompt generator ([#15343](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15343)) +* fix: Python version check for PyTorch installation compatibility ([#15390](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15390)) +* fix typo in call_queue.py ([#15386](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15386)) +* fix: when find already_loaded model, remove loaded by array index ([#15382](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15382)) +* minor bug fix of sd model memory management ([#15350](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15350)) +* Fix CodeFormer weight ([#15414](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15414)) +* Fix: Remove script callbacks in ordered_callbacks_map ([#15428](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15428)) +* fix limited file write (thanks, Sylwia) +* Fix extra-single-image API not doing upscale failed ([#15465](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15465)) +* error handling paste_field callables ([#15470](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15470)) + +### Hardware: +* Add training support and change lspci for Ascend NPU ([#14981](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14981)) +* Update to ROCm5.7 and PyTorch ([#14820](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14820)) +* Better workaround for Navi1, removing --pre for Navi3 ([#15224](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15224)) +* Ascend NPU wiki page ([#15228](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15228)) + +### Other: +* Update comment for Pad prompt/negative prompt v0 to add a warning about truncation, make it override the v1 implementation +* support resizable columns for touch (tablets) ([#15002](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15002)) +* Fix #14591 using translated content to do categories mapping ([#14995](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14995)) +* Use `absolute` path for normalized filepath ([#15035](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15035)) +* resizeHandle handle double tap ([#15065](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15065)) +* --dat-models-path cmd flag ([#15039](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15039)) +* Add a direct link to the binary release ([#15059](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15059)) +* upscaler_utils: Reduce logging ([#15084](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15084)) +* Fix various typos with crate-ci/typos ([#15116](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15116)) +* fix_jpeg_live_preview ([#15102](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15102)) +* [alternative fix] can't load webui if selected wrong extra option in ui ([#15121](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15121)) +* Error handling for unsupported transparency ([#14958](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14958)) +* Add model description to searched terms ([#15198](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15198)) +* bump action version ([#15272](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15272)) +* PEP 604 annotations ([#15259](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15259)) +* Automatically Set the Scale by value when user selects an Upscale Model ([#15244](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15244)) +* move postprocessing-for-training into builtin extensions ([#15222](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15222)) +* type hinting in shared.py ([#15211](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15211)) +* update ruff to 0.3.3 +* Update pytorch lightning utilities ([#15310](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15310)) +* Add Size as an XYZ Grid option ([#15354](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15354)) +* Use HF_ENDPOINT variable for HuggingFace domain with default ([#15443](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15443)) +* re-add update_file_entry ([#15446](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15446)) +* create_infotext allow index and callable, re-work Hires prompt infotext ([#15460](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15460)) +* update restricted_opts to include more options for --hide-ui-dir-config ([#15492](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15492)) + + +## 1.8.0 ### Features: * Update torch to version 2.1.2 @@ -14,7 +293,7 @@ * Add support for DAT upscaler models ([#14690](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14690), [#15039](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15039)) * Extra Networks Tree View ([#14588](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14588), [#14900](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14900)) * NPU Support ([#14801](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14801)) -* Propmpt comments support +* Prompt comments support ### Minor: * Allow pasting in WIDTHxHEIGHT strings into the width/height fields ([#14296](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14296)) @@ -59,9 +338,9 @@ * modules/api/api.py: add api endpoint to refresh embeddings list ([#14715](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14715)) * set_named_arg ([#14773](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14773)) * add before_token_counter callback and use it for prompt comments -* ResizeHandleRow - allow overriden column scale parameter ([#15004](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15004)) +* ResizeHandleRow - allow overridden column scale parameter ([#15004](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15004)) -### Performance +### Performance: * Massive performance improvement for extra networks directories with a huge number of files in them in an attempt to tackle #14507 ([#14528](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14528)) * Reduce unnecessary re-indexing extra networks directory ([#14512](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14512)) * Avoid unnecessary `isfile`/`exists` calls ([#14527](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14527)) @@ -101,7 +380,7 @@ * Gracefully handle mtime read exception from cache ([#14933](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14933)) * Only trigger interrupt on `Esc` when interrupt button visible ([#14932](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14932)) * Disable prompt token counters option actually disables token counting rather than just hiding results. -* avoid doble upscaling in inpaint ([#14966](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14966)) +* avoid double upscaling in inpaint ([#14966](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14966)) * Fix #14591 using translated content to do categories mapping ([#14995](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14995)) * fix: the `split_threshold` parameter does not work when running Split oversized images ([#15006](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15006)) * Fix resize-handle for mobile ([#15010](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15010), [#15065](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/15065)) @@ -171,7 +450,7 @@ * infotext updates: add option to disregard certain infotext fields, add option to not include VAE in infotext, add explanation to infotext settings page, move some options to infotext settings page * add FP32 fallback support on sd_vae_approx ([#14046](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14046)) * support XYZ scripts / split hires path from unet ([#14126](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14126)) -* allow use of mutiple styles csv files ([#14125](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14125)) +* allow use of multiple styles csv files ([#14125](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/14125)) * make extra network card description plaintext by default, with an option (Treat card description as HTML) to re-enable HTML as it was (originally by [#13241](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/13241)) ### Extensions and API: @@ -308,7 +587,7 @@ * new samplers: Restart, DPM++ 2M SDE Exponential, DPM++ 2M SDE Heun, DPM++ 2M SDE Heun Karras, DPM++ 2M SDE Heun Exponential, DPM++ 3M SDE, DPM++ 3M SDE Karras, DPM++ 3M SDE Exponential ([#12300](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/12300), [#12519](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/12519), [#12542](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/12542)) * rework DDIM, PLMS, UniPC to use CFG denoiser same as in k-diffusion samplers: * makes all of them work with img2img - * makes prompt composition posssible (AND) + * makes prompt composition possible (AND) * makes them available for SDXL * always show extra networks tabs in the UI ([#11808](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/11808)) * use less RAM when creating models ([#11958](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/11958), [#12599](https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/12599)) @@ -484,7 +763,7 @@ * user metadata system for custom networks * extended Lora metadata editor: set activation text, default weight, view tags, training info * Lora extension rework to include other types of networks (all that were previously handled by LyCORIS extension) - * show github stars for extenstions + * show github stars for extensions * img2img batch mode can read extra stuff from png info * img2img batch works with subdirectories * hotkeys to move prompt elements: alt+left/right @@ -703,7 +982,7 @@ * do not wait for Stable Diffusion model to load at startup * add filename patterns: `[denoising]` * directory hiding for extra networks: dirs starting with `.` will hide their cards on extra network tabs unless specifically searched for - * LoRA: for the `<...>` text in prompt, use name of LoRA that is in the metdata of the file, if present, instead of filename (both can be used to activate LoRA) + * LoRA: for the `<...>` text in prompt, use name of LoRA that is in the metadata of the file, if present, instead of filename (both can be used to activate LoRA) * LoRA: read infotext params from kohya-ss's extension parameters if they are present and if his extension is not active * LoRA: fix some LoRAs not working (ones that have 3x3 convolution layer) * LoRA: add an option to use old method of applying LoRAs (producing same results as with kohya-ss) @@ -733,7 +1012,7 @@ * fix gamepad navigation * make the lightbox fullscreen image function properly * fix squished thumbnails in extras tab - * keep "search" filter for extra networks when user refreshes the tab (previously it showed everthing after you refreshed) + * keep "search" filter for extra networks when user refreshes the tab (previously it showed everything after you refreshed) * fix webui showing the same image if you configure the generation to always save results into same file * fix bug with upscalers not working properly * fix MPS on PyTorch 2.0.1, Intel Macs @@ -751,7 +1030,7 @@ * switch to PyTorch 2.0.0 (except for AMD GPUs) * visual improvements to custom code scripts * add filename patterns: `[clip_skip]`, `[hasprompt<>]`, `[batch_number]`, `[generation_number]` - * add support for saving init images in img2img, and record their hashes in infotext for reproducability + * add support for saving init images in img2img, and record their hashes in infotext for reproducibility * automatically select current word when adjusting weight with ctrl+up/down * add dropdowns for X/Y/Z plot * add setting: Stable Diffusion/Random number generator source: makes it possible to make images generated from a given manual seed consistent across different GPUs diff --git a/README.md b/README.md index b819920c..54f95c8b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ Seamless integration with the [Infinite image browsing](https://github.com/zanll ![InfiniteImageBrowserIntegration](https://github.com/anapnoe/stable-diffusion-webui-ux/assets/124302297/f9048ff4-0d78-4227-8b3f-5a282d24e5cb) ## Todo -- Improve Mobile Support - Fullscreen Gallery Support @@ -110,7 +109,7 @@ Seamless integration with the [Infinite image browsing](https://github.com/zanll - Clip skip - Hypernetworks - Loras (same as Hypernetworks but more pretty) -- A separate UI where you can choose, with preview, which embeddings, hypernetworks or Loras to add to your prompt +- A separate UI where you can choose, with preview, which embeddings, hypernetworks or Loras to add to your prompt - Can select to load a different VAE from settings screen - Estimated completion time in progress bar - API @@ -129,6 +128,7 @@ Make sure the required [dependencies](https://github.com/AUTOMATIC1111/stable-di - [NVidia](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Install-and-Run-on-NVidia-GPUs) (recommended) - [AMD](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Install-and-Run-on-AMD-GPUs) GPUs. - [Intel CPUs, Intel GPUs (both integrated and discrete)](https://github.com/openvinotoolkit/stable-diffusion-webui/wiki/Installation-on-Intel-Silicon) (external wiki page) +- [Ascend NPUs](https://github.com/wangshuai09/stable-diffusion-webui/wiki/Install-and-run-on-Ascend-NPUs) (external wiki page) Alternatively, use online services (like Google Colab): @@ -146,14 +146,38 @@ Alternatively, use online services (like Google Colab): # Debian-based: sudo apt install wget git python3 python3-venv libgl1 libglib2.0-0 # Red Hat-based: -sudo dnf install wget git python3 +sudo dnf install wget git python3 gperftools-libs libglvnd-glx +# openSUSE-based: +sudo zypper install wget git python3 libtcmalloc4 libglvnd # Arch-based: sudo pacman -S wget git python3 ``` +If your system is very new, you need to install python3.11 or python3.10: +```bash +# Ubuntu 24.04 +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt update +sudo apt install python3.11 + +# Manjaro/Arch +sudo pacman -S yay +yay -S python311 # do not confuse with python3.11 package + +# Only for 3.11 +# Then set up env variable in launch script +export python_cmd="python3.11" +# or in webui-user.sh +python_cmd="python3.11" +``` 2. Navigate to the directory you would like the webui to be installed and execute the following command: ```bash wget -q https://raw.githubusercontent.com/anapnoe/stable-diffusion-webui-ux/master/webui.sh ``` +Or just clone the repo wherever you want: +```bash +git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui +``` + 3. Run `webui.sh`. 4. Check `webui-user.sh` for options. ### Installation on Apple Silicon @@ -172,7 +196,8 @@ For the purposes of getting Google and other search engines to crawl the wiki, h ## Credits Licenses for borrowed code can be found in `Settings -> Licenses` screen, and also in `html/licenses.html` file. -- Stable Diffusion - https://github.com/CompVis/stable-diffusion, https://github.com/CompVis/taming-transformers + +- Stable Diffusion - https://github.com/Stability-AI/stablediffusion, https://github.com/CompVis/taming-transformers, https://github.com/mcmonkey4eva/sd3-ref - k-diffusion - https://github.com/crowsonkb/k-diffusion.git - Spandrel - https://github.com/chaiNNer-org/spandrel implementing - GFPGAN - https://github.com/TencentARC/GFPGAN.git diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..1c63fe70 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,5 @@ +[default.extend-words] +# Part of "RGBa" (Pillow's pre-multiplied alpha RGB mode) +Ba = "Ba" +# HSA is something AMD uses for their GPUs +HSA = "HSA" diff --git a/configs/alt-diffusion-inference.yaml b/configs/alt-diffusion-inference.yaml index cfbee72d..4944ab5c 100644 --- a/configs/alt-diffusion-inference.yaml +++ b/configs/alt-diffusion-inference.yaml @@ -40,7 +40,7 @@ model: use_spatial_transformer: True transformer_depth: 1 context_dim: 768 - use_checkpoint: True + use_checkpoint: False legacy: False first_stage_config: diff --git a/configs/alt-diffusion-m18-inference.yaml b/configs/alt-diffusion-m18-inference.yaml index 41a031d5..c60dca8c 100644 --- a/configs/alt-diffusion-m18-inference.yaml +++ b/configs/alt-diffusion-m18-inference.yaml @@ -41,7 +41,7 @@ model: use_linear_in_transformer: True transformer_depth: 1 context_dim: 1024 - use_checkpoint: True + use_checkpoint: False legacy: False first_stage_config: diff --git a/configs/instruct-pix2pix.yaml b/configs/instruct-pix2pix.yaml index 4e896879..564e50ae 100644 --- a/configs/instruct-pix2pix.yaml +++ b/configs/instruct-pix2pix.yaml @@ -45,7 +45,7 @@ model: use_spatial_transformer: True transformer_depth: 1 context_dim: 768 - use_checkpoint: True + use_checkpoint: False legacy: False first_stage_config: diff --git a/configs/sd3-inference.yaml b/configs/sd3-inference.yaml new file mode 100644 index 00000000..bccb69d2 --- /dev/null +++ b/configs/sd3-inference.yaml @@ -0,0 +1,5 @@ +model: + target: modules.models.sd3.sd3_model.SD3Inferencer + params: + shift: 3 + state_dict: null diff --git a/configs/sd_xl_inpaint.yaml b/configs/sd_xl_inpaint.yaml index 3bad3721..f40f45e3 100644 --- a/configs/sd_xl_inpaint.yaml +++ b/configs/sd_xl_inpaint.yaml @@ -21,7 +21,7 @@ model: params: adm_in_channels: 2816 num_classes: sequential - use_checkpoint: True + use_checkpoint: False in_channels: 9 out_channels: 4 model_channels: 320 diff --git a/configs/v1-inference.yaml b/configs/v1-inference.yaml index d4effe56..25c4d9ed 100644 --- a/configs/v1-inference.yaml +++ b/configs/v1-inference.yaml @@ -40,7 +40,7 @@ model: use_spatial_transformer: True transformer_depth: 1 context_dim: 768 - use_checkpoint: True + use_checkpoint: False legacy: False first_stage_config: diff --git a/configs/v1-inpainting-inference.yaml b/configs/v1-inpainting-inference.yaml index f9eec37d..68c199f9 100644 --- a/configs/v1-inpainting-inference.yaml +++ b/configs/v1-inpainting-inference.yaml @@ -40,7 +40,7 @@ model: use_spatial_transformer: True transformer_depth: 1 context_dim: 768 - use_checkpoint: True + use_checkpoint: False legacy: False first_stage_config: diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..9de86903 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,94 @@ +/* global module */ +module.exports = { + ignores: [ + "extensions", + "extensions-disabled", + "repositories", + "venv" + ], + languageOptions: { + globals: { + gradioApp: "readonly", + executeCallbacks: "readonly", + onAfterUiUpdate: "readonly", + onOptionsChanged: "readonly", + onUiLoaded: "readonly", + onUiUpdate: "readonly", + uiCurrentTab: "writable", + uiElementInSight: "readonly", + uiElementIsVisible: "readonly", + opts: "writable", + all_gallery_buttons: "readonly", + selected_gallery_button: "readonly", + selected_gallery_index: "readonly", + switch_to_txt2img: "readonly", + switch_to_img2img_tab: "readonly", + switch_to_img2img: "readonly", + switch_to_sketch: "readonly", + switch_to_inpaint: "readonly", + switch_to_inpaint_sketch: "readonly", + switch_to_extras: "readonly", + get_tab_index: "readonly", + create_submit_args: "readonly", + restart_reload: "readonly", + updateInput: "readonly", + onEdit: "readonly", + requestGet: "readonly", + popup: "readonly", + createVisualizationTable: "readonly", + localization: "readonly", + randomId: "readonly", + requestProgress: "readonly", + modalPrevImage: "readonly", + modalNextImage: "readonly", + localSet: "readonly", + localGet: "readonly", + localRemove: "readonly", + setupResizeHandle: "writable" + }, + parserOptions: { + ecmaVersion: "latest", + }, + }, + rules: { + "arrow-spacing": "error", + "block-spacing": "error", + "brace-style": "error", + "comma-dangle": ["error", "only-multiline"], + "comma-spacing": "error", + "comma-style": ["error", "last"], + "curly": ["error", "multi-line", "consistent"], + "eol-last": "error", + "func-call-spacing": "error", + "function-call-argument-newline": ["error", "consistent"], + "function-paren-newline": ["error", "consistent"], + "indent": ["error", 4], + "key-spacing": "error", + "keyword-spacing": "error", + "linebreak-style": ["error", "unix"], + "no-extra-semi": "error", + "no-mixed-spaces-and-tabs": "error", + "no-multi-spaces": "error", + "no-redeclare": ["error", {builtinGlobals: false}], + "no-trailing-spaces": "error", + "no-unused-vars": "off", + "no-whitespace-before-property": "error", + "object-curly-newline": ["error", {consistent: true, multiline: true}], + "object-curly-spacing": ["error", "never"], + "operator-linebreak": ["error", "after"], + "quote-props": ["error", "consistent-as-needed"], + "semi": ["error", "always"], + "semi-spacing": "error", + "semi-style": ["error", "last"], + "space-before-blocks": "error", + "space-before-function-paren": ["error", "never"], + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": "error", + "switch-colon-spacing": "error", + "template-curly-spacing": ["error", "never"], + "unicode-bom": "error", + }, + // Add any additional configurations you need directly here, + // or extend from other configurations (if needed) +}; diff --git a/extensions-builtin/LDSR/sd_hijack_ddpm_v1.py b/extensions-builtin/LDSR/sd_hijack_ddpm_v1.py index 04adc5eb..51ab1821 100644 --- a/extensions-builtin/LDSR/sd_hijack_ddpm_v1.py +++ b/extensions-builtin/LDSR/sd_hijack_ddpm_v1.py @@ -301,7 +301,7 @@ class DDPMV1(pl.LightningModule): elif self.parameterization == "x0": target = x_start else: - raise NotImplementedError(f"Paramterization {self.parameterization} not yet supported") + raise NotImplementedError(f"Parameterization {self.parameterization} not yet supported") loss = self.get_loss(model_out, target, mean=False).mean(dim=[1, 2, 3]) @@ -572,7 +572,7 @@ class LatentDiffusionV1(DDPMV1): :param h: height :param w: width :return: normalized distance to image border, - wtith min distance = 0 at border and max dist = 0.5 at image center + with min distance = 0 at border and max dist = 0.5 at image center """ lower_right_corner = torch.tensor([h - 1, w - 1]).view(1, 1, 2) arr = self.meshgrid(h, w) / lower_right_corner @@ -880,7 +880,7 @@ class LatentDiffusionV1(DDPMV1): def apply_model(self, x_noisy, t, cond, return_ids=False): if isinstance(cond, dict): - # hybrid case, cond is exptected to be a dict + # hybrid case, cond is expected to be a dict pass else: if not isinstance(cond, list): @@ -916,7 +916,7 @@ class LatentDiffusionV1(DDPMV1): cond_list = [{c_key: [c[:, :, :, :, i]]} for i in range(c.shape[-1])] elif self.cond_stage_key == 'coordinates_bbox': - assert 'original_image_size' in self.split_input_params, 'BoudingBoxRescaling is missing original_image_size' + assert 'original_image_size' in self.split_input_params, 'BoundingBoxRescaling is missing original_image_size' # assuming padding of unfold is always 0 and its dilation is always 1 n_patches_per_row = int((w - ks[0]) / stride[0] + 1) @@ -926,7 +926,7 @@ class LatentDiffusionV1(DDPMV1): num_downs = self.first_stage_model.encoder.num_resolutions - 1 rescale_latent = 2 ** (num_downs) - # get top left postions of patches as conforming for the bbbox tokenizer, therefore we + # get top left positions of patches as conforming for the bbbox tokenizer, therefore we # need to rescale the tl patch coordinates to be in between (0,1) tl_patch_coordinates = [(rescale_latent * stride[0] * (patch_nr % n_patches_per_row) / full_img_w, rescale_latent * stride[1] * (patch_nr // n_patches_per_row) / full_img_h) diff --git a/extensions-builtin/Lora/extra_networks_lora.py b/extensions-builtin/Lora/extra_networks_lora.py index 005ff32c..17a620f7 100644 --- a/extensions-builtin/Lora/extra_networks_lora.py +++ b/extensions-builtin/Lora/extra_networks_lora.py @@ -9,6 +9,8 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): self.errors = {} """mapping of network names to the number of errors the network had during operation""" + remove_symbols = str.maketrans('', '', ":,") + def activate(self, p, params_list): additional = shared.opts.sd_lora @@ -43,22 +45,15 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork): networks.load_networks(names, te_multipliers, unet_multipliers, dyn_dims) if shared.opts.lora_add_hashes_to_infotext: - network_hashes = [] + if not getattr(p, "is_hr_pass", False) or not hasattr(p, "lora_hashes"): + p.lora_hashes = {} + for item in networks.loaded_networks: - shorthash = item.network_on_disk.shorthash - if not shorthash: - continue + if item.network_on_disk.shorthash and item.mentioned_name: + p.lora_hashes[item.mentioned_name.translate(self.remove_symbols)] = item.network_on_disk.shorthash - alias = item.mentioned_name - if not alias: - continue - - alias = alias.replace(":", "").replace(",", "") - - network_hashes.append(f"{alias}: {shorthash}") - - if network_hashes: - p.extra_generation_params["Lora hashes"] = ", ".join(network_hashes) + if p.lora_hashes: + p.extra_generation_params["Lora hashes"] = ', '.join(f'{k}: {v}' for k, v in p.lora_hashes.items()) def deactivate(self, p): if self.errors: diff --git a/extensions-builtin/Lora/lyco_helpers.py b/extensions-builtin/Lora/lyco_helpers.py index 1679a0ce..6f134d54 100644 --- a/extensions-builtin/Lora/lyco_helpers.py +++ b/extensions-builtin/Lora/lyco_helpers.py @@ -30,7 +30,7 @@ def factorization(dimension: int, factor:int=-1) -> tuple[int, int]: In LoRA with Kroneckor Product, first value is a value for weight scale. secon value is a value for weight. - Becuase of non-commutative property, A⊗B ≠ B⊗A. Meaning of two matrices is slightly different. + Because of non-commutative property, A⊗B ≠ B⊗A. Meaning of two matrices is slightly different. examples) factor diff --git a/extensions-builtin/Lora/network.py b/extensions-builtin/Lora/network.py index b8fd9194..98ff367f 100644 --- a/extensions-builtin/Lora/network.py +++ b/extensions-builtin/Lora/network.py @@ -7,6 +7,7 @@ import torch.nn as nn import torch.nn.functional as F from modules import sd_models, cache, errors, hashes, shared +import modules.models.sd3.mmdit NetworkWeights = namedtuple('NetworkWeights', ['network_key', 'sd_key', 'w', 'sd_module']) @@ -29,7 +30,6 @@ class NetworkOnDisk: def read_metadata(): metadata = sd_models.read_metadata_from_safetensors(filename) - metadata.pop('ssmd_cover_images', None) # those are cover images, and they are too big to display in UI as text return metadata @@ -115,8 +115,17 @@ class NetworkModule: self.sd_key = weights.sd_key self.sd_module = weights.sd_module - if hasattr(self.sd_module, 'weight'): + if isinstance(self.sd_module, modules.models.sd3.mmdit.QkvLinear): + s = self.sd_module.weight.shape + self.shape = (s[0] // 3, s[1]) + elif hasattr(self.sd_module, 'weight'): self.shape = self.sd_module.weight.shape + elif isinstance(self.sd_module, nn.MultiheadAttention): + # For now, only self-attn use Pytorch's MHA + # So assume all qkvo proj have same shape + self.shape = self.sd_module.out_proj.weight.shape + else: + self.shape = None self.ops = None self.extra_kwargs = {} @@ -146,6 +155,9 @@ class NetworkModule: self.alpha = weights.w["alpha"].item() if "alpha" in weights.w else None self.scale = weights.w["scale"].item() if "scale" in weights.w else None + self.dora_scale = weights.w.get("dora_scale", None) + self.dora_norm_dims = len(self.shape) - 1 + def multiplier(self): if 'transformer' in self.sd_key[:20]: return self.network.te_multiplier @@ -160,6 +172,27 @@ class NetworkModule: return 1.0 + def apply_weight_decompose(self, updown, orig_weight): + # Match the device/dtype + orig_weight = orig_weight.to(updown.dtype) + dora_scale = self.dora_scale.to(device=orig_weight.device, dtype=updown.dtype) + updown = updown.to(orig_weight.device) + + merged_scale1 = updown + orig_weight + merged_scale1_norm = ( + merged_scale1.transpose(0, 1) + .reshape(merged_scale1.shape[1], -1) + .norm(dim=1, keepdim=True) + .reshape(merged_scale1.shape[1], *[1] * self.dora_norm_dims) + .transpose(0, 1) + ) + + dora_merged = ( + merged_scale1 * (dora_scale / merged_scale1_norm) + ) + final_updown = dora_merged - orig_weight + return final_updown + def finalize_updown(self, updown, orig_weight, output_shape, ex_bias=None): if self.bias is not None: updown = updown.reshape(self.bias.shape) @@ -175,7 +208,12 @@ class NetworkModule: if ex_bias is not None: ex_bias = ex_bias * self.multiplier() - return updown * self.calc_scale() * self.multiplier(), ex_bias + updown = updown * self.calc_scale() + + if self.dora_scale is not None: + updown = self.apply_weight_decompose(updown, orig_weight) + + return updown * self.multiplier(), ex_bias def calc_updown(self, target): raise NotImplementedError() diff --git a/extensions-builtin/Lora/network_lora.py b/extensions-builtin/Lora/network_lora.py index 4cc40295..a7a08894 100644 --- a/extensions-builtin/Lora/network_lora.py +++ b/extensions-builtin/Lora/network_lora.py @@ -1,6 +1,7 @@ import torch import lyco_helpers +import modules.models.sd3.mmdit import network from modules import devices @@ -10,6 +11,13 @@ class ModuleTypeLora(network.ModuleType): if all(x in weights.w for x in ["lora_up.weight", "lora_down.weight"]): return NetworkModuleLora(net, weights) + if all(x in weights.w for x in ["lora_A.weight", "lora_B.weight"]): + w = weights.w.copy() + weights.w.clear() + weights.w.update({"lora_up.weight": w["lora_B.weight"], "lora_down.weight": w["lora_A.weight"]}) + + return NetworkModuleLora(net, weights) + return None @@ -29,7 +37,7 @@ class NetworkModuleLora(network.NetworkModule): if weight is None and none_ok: return None - is_linear = type(self.sd_module) in [torch.nn.Linear, torch.nn.modules.linear.NonDynamicallyQuantizableLinear, torch.nn.MultiheadAttention] + is_linear = type(self.sd_module) in [torch.nn.Linear, torch.nn.modules.linear.NonDynamicallyQuantizableLinear, torch.nn.MultiheadAttention, modules.models.sd3.mmdit.QkvLinear] is_conv = type(self.sd_module) in [torch.nn.Conv2d] if is_linear: diff --git a/extensions-builtin/Lora/network_oft.py b/extensions-builtin/Lora/network_oft.py index 7821a8a7..1c515ebb 100644 --- a/extensions-builtin/Lora/network_oft.py +++ b/extensions-builtin/Lora/network_oft.py @@ -36,13 +36,6 @@ class NetworkModuleOFT(network.NetworkModule): # self.alpha is unused self.dim = self.oft_blocks.shape[1] # (num_blocks, block_size, block_size) - # LyCORIS BOFT - if self.oft_blocks.dim() == 4: - self.is_boft = True - self.rescale = weights.w.get('rescale', None) - if self.rescale is not None: - self.rescale = self.rescale.reshape(-1, *[1]*(self.org_module[0].weight.dim() - 1)) - is_linear = type(self.sd_module) in [torch.nn.Linear, torch.nn.modules.linear.NonDynamicallyQuantizableLinear] is_conv = type(self.sd_module) in [torch.nn.Conv2d] is_other_linear = type(self.sd_module) in [torch.nn.MultiheadAttention] # unsupported @@ -54,6 +47,13 @@ class NetworkModuleOFT(network.NetworkModule): elif is_other_linear: self.out_dim = self.sd_module.embed_dim + # LyCORIS BOFT + if self.oft_blocks.dim() == 4: + self.is_boft = True + self.rescale = weights.w.get('rescale', None) + if self.rescale is not None and not is_other_linear: + self.rescale = self.rescale.reshape(-1, *[1]*(self.org_module[0].weight.dim() - 1)) + self.num_blocks = self.dim self.block_size = self.out_dim // self.dim self.constraint = (0 if self.alpha is None else self.alpha) * self.out_dim diff --git a/extensions-builtin/Lora/networks.py b/extensions-builtin/Lora/networks.py index 83ea2802..67f9abe2 100644 --- a/extensions-builtin/Lora/networks.py +++ b/extensions-builtin/Lora/networks.py @@ -1,3 +1,4 @@ +from __future__ import annotations import gradio as gr import logging import os @@ -19,6 +20,7 @@ from typing import Union from modules import shared, devices, sd_models, errors, scripts, sd_hijack import modules.textual_inversion.textual_inversion as textual_inversion +import modules.models.sd3.mmdit from lora_logger import logger @@ -130,7 +132,9 @@ def assign_network_names_to_compvis_modules(sd_model): network_layer_mapping[network_name] = module module.network_layer_name = network_name else: - for name, module in shared.sd_model.cond_stage_model.wrapped.named_modules(): + cond_stage_model = getattr(shared.sd_model.cond_stage_model, 'wrapped', shared.sd_model.cond_stage_model) + + for name, module in cond_stage_model.named_modules(): network_name = name.replace(".", "_") network_layer_mapping[network_name] = module module.network_layer_name = network_name @@ -143,6 +147,14 @@ def assign_network_names_to_compvis_modules(sd_model): sd_model.network_layer_mapping = network_layer_mapping +class BundledTIHash(str): + def __init__(self, hash_str): + self.hash = hash_str + + def __str__(self): + return self.hash if shared.opts.lora_bundled_ti_to_infotext else '' + + def load_network(name, network_on_disk): net = network.Network(name, network_on_disk) net.mtime = os.path.getmtime(network_on_disk.filename) @@ -155,12 +167,26 @@ def load_network(name, network_on_disk): keys_failed_to_match = {} is_sd2 = 'model_transformer_resblocks' in shared.sd_model.network_layer_mapping + if hasattr(shared.sd_model, 'diffusers_weight_map'): + diffusers_weight_map = shared.sd_model.diffusers_weight_map + elif hasattr(shared.sd_model, 'diffusers_weight_mapping'): + diffusers_weight_map = {} + for k, v in shared.sd_model.diffusers_weight_mapping(): + diffusers_weight_map[k] = v + shared.sd_model.diffusers_weight_map = diffusers_weight_map + else: + diffusers_weight_map = None matched_networks = {} bundle_embeddings = {} for key_network, weight in sd.items(): - key_network_without_network_parts, _, network_part = key_network.partition(".") + + if diffusers_weight_map: + key_network_without_network_parts, network_name, network_weight = key_network.rsplit(".", 2) + network_part = network_name + '.' + network_weight + else: + key_network_without_network_parts, _, network_part = key_network.partition(".") if key_network_without_network_parts == "bundle_emb": emb_name, vec_name = network_part.split(".", 1) @@ -172,7 +198,11 @@ def load_network(name, network_on_disk): emb_dict[vec_name] = weight bundle_embeddings[emb_name] = emb_dict - key = convert_diffusers_name_to_compvis(key_network_without_network_parts, is_sd2) + if diffusers_weight_map: + key = diffusers_weight_map.get(key_network_without_network_parts, key_network_without_network_parts) + else: + key = convert_diffusers_name_to_compvis(key_network_without_network_parts, is_sd2) + sd_module = shared.sd_model.network_layer_mapping.get(key, None) if sd_module is None: @@ -229,6 +259,7 @@ def load_network(name, network_on_disk): for emb_name, data in bundle_embeddings.items(): embedding = textual_inversion.create_embedding_from_data(data, emb_name, filename=network_on_disk.filename + "/" + emb_name) embedding.loaded = None + embedding.shorthash = BundledTIHash(name) embeddings[emb_name] = embedding net.bundle_embeddings = embeddings @@ -260,6 +291,16 @@ def load_networks(names, te_multipliers=None, unet_multipliers=None, dyn_dims=No loaded_networks.clear() + unavailable_networks = [] + for name in names: + if name.lower() in forbidden_network_aliases and available_networks.get(name) is None: + unavailable_networks.append(name) + elif available_network_aliases.get(name) is None: + unavailable_networks.append(name) + + if unavailable_networks: + update_available_networks_by_names(unavailable_networks) + networks_on_disk = [available_networks.get(name, None) if name.lower() in forbidden_network_aliases else available_network_aliases.get(name, None) for name in names] if any(x is None for x in networks_on_disk): list_available_networks() @@ -325,6 +366,28 @@ def load_networks(names, te_multipliers=None, unet_multipliers=None, dyn_dims=No purge_networks_from_memory() +def allowed_layer_without_weight(layer): + if isinstance(layer, torch.nn.LayerNorm) and not layer.elementwise_affine: + return True + + return False + + +def store_weights_backup(weight): + if weight is None: + return None + + return weight.to(devices.cpu, copy=True) + + +def restore_weights_backup(obj, field, weight): + if weight is None: + setattr(obj, field, None) + return + + getattr(obj, field).copy_(weight) + + def network_restore_weights_from_backup(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn.GroupNorm, torch.nn.LayerNorm, torch.nn.MultiheadAttention]): weights_backup = getattr(self, "network_weights_backup", None) bias_backup = getattr(self, "network_bias_backup", None) @@ -334,28 +397,22 @@ def network_restore_weights_from_backup(self: Union[torch.nn.Conv2d, torch.nn.Li if weights_backup is not None: if isinstance(self, torch.nn.MultiheadAttention): - self.in_proj_weight.copy_(weights_backup[0]) - self.out_proj.weight.copy_(weights_backup[1]) + restore_weights_backup(self, 'in_proj_weight', weights_backup[0]) + restore_weights_backup(self.out_proj, 'weight', weights_backup[1]) else: - self.weight.copy_(weights_backup) + restore_weights_backup(self, 'weight', weights_backup) - if bias_backup is not None: - if isinstance(self, torch.nn.MultiheadAttention): - self.out_proj.bias.copy_(bias_backup) - else: - self.bias.copy_(bias_backup) + if isinstance(self, torch.nn.MultiheadAttention): + restore_weights_backup(self.out_proj, 'bias', bias_backup) else: - if isinstance(self, torch.nn.MultiheadAttention): - self.out_proj.bias = None - else: - self.bias = None + restore_weights_backup(self, 'bias', bias_backup) def network_apply_weights(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn.GroupNorm, torch.nn.LayerNorm, torch.nn.MultiheadAttention]): """ Applies the currently selected set of networks to the weights of torch layer self. If weights already have this particular set of networks applied, does nothing. - If not, restores orginal weights from backup and alters weights according to networks. + If not, restores original weights from backup and alters weights according to networks. """ network_layer_name = getattr(self, 'network_layer_name', None) @@ -367,24 +424,30 @@ def network_apply_weights(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn weights_backup = getattr(self, "network_weights_backup", None) if weights_backup is None and wanted_names != (): - if current_names != (): - raise RuntimeError("no backup weights found and current weights are not unchanged") + if current_names != () and not allowed_layer_without_weight(self): + raise RuntimeError(f"{network_layer_name} - no backup weights found and current weights are not unchanged") if isinstance(self, torch.nn.MultiheadAttention): - weights_backup = (self.in_proj_weight.to(devices.cpu, copy=True), self.out_proj.weight.to(devices.cpu, copy=True)) + weights_backup = (store_weights_backup(self.in_proj_weight), store_weights_backup(self.out_proj.weight)) else: - weights_backup = self.weight.to(devices.cpu, copy=True) + weights_backup = store_weights_backup(self.weight) self.network_weights_backup = weights_backup bias_backup = getattr(self, "network_bias_backup", None) - if bias_backup is None: + if bias_backup is None and wanted_names != (): if isinstance(self, torch.nn.MultiheadAttention) and self.out_proj.bias is not None: - bias_backup = self.out_proj.bias.to(devices.cpu, copy=True) + bias_backup = store_weights_backup(self.out_proj.bias) elif getattr(self, 'bias', None) is not None: - bias_backup = self.bias.to(devices.cpu, copy=True) + bias_backup = store_weights_backup(self.bias) else: bias_backup = None + + # Unlike weight which always has value, some modules don't have bias. + # Only report if bias is not None and current bias are not unchanged. + if bias_backup is not None and current_names != (): + raise RuntimeError("no backup bias found and current bias are not unchanged") + self.network_bias_backup = bias_backup if current_names != wanted_names: @@ -392,7 +455,7 @@ def network_apply_weights(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn for net in loaded_networks: module = net.modules.get(network_layer_name, None) - if module is not None and hasattr(self, 'weight'): + if module is not None and hasattr(self, 'weight') and not isinstance(module, modules.models.sd3.mmdit.QkvLinear): try: with torch.no_grad(): if getattr(self, 'fp16_weight', None) is None: @@ -429,9 +492,12 @@ def network_apply_weights(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn if isinstance(self, torch.nn.MultiheadAttention) and module_q and module_k and module_v and module_out: try: with torch.no_grad(): - updown_q, _ = module_q.calc_updown(self.in_proj_weight) - updown_k, _ = module_k.calc_updown(self.in_proj_weight) - updown_v, _ = module_v.calc_updown(self.in_proj_weight) + # Send "real" orig_weight into MHA's lora module + qw, kw, vw = self.in_proj_weight.chunk(3, 0) + updown_q, _ = module_q.calc_updown(qw) + updown_k, _ = module_k.calc_updown(kw) + updown_v, _ = module_v.calc_updown(vw) + del qw, kw, vw updown_qkv = torch.vstack([updown_q, updown_k, updown_v]) updown_out, ex_bias = module_out.calc_updown(self.out_proj.weight) @@ -449,6 +515,24 @@ def network_apply_weights(self: Union[torch.nn.Conv2d, torch.nn.Linear, torch.nn continue + if isinstance(self, modules.models.sd3.mmdit.QkvLinear) and module_q and module_k and module_v: + try: + with torch.no_grad(): + # Send "real" orig_weight into MHA's lora module + qw, kw, vw = self.weight.chunk(3, 0) + updown_q, _ = module_q.calc_updown(qw) + updown_k, _ = module_k.calc_updown(kw) + updown_v, _ = module_v.calc_updown(vw) + del qw, kw, vw + updown_qkv = torch.vstack([updown_q, updown_k, updown_v]) + self.weight += updown_qkv + + except RuntimeError as e: + logging.debug(f"Network {net.name} layer {network_layer_name}: {e}") + extra_network_lora.errors[net.name] = extra_network_lora.errors.get(net.name, 0) + 1 + + continue + if module is None: continue @@ -563,22 +647,16 @@ def network_MultiheadAttention_load_state_dict(self, *args, **kwargs): return originals.MultiheadAttention_load_state_dict(self, *args, **kwargs) -def list_available_networks(): - available_networks.clear() - available_network_aliases.clear() - forbidden_network_aliases.clear() - available_network_hash_lookup.clear() - forbidden_network_aliases.update({"none": 1, "Addams": 1}) - - os.makedirs(shared.cmd_opts.lora_dir, exist_ok=True) - +def process_network_files(names: list[str] | None = None): candidates = list(shared.walk_files(shared.cmd_opts.lora_dir, allowed_extensions=[".pt", ".ckpt", ".safetensors"])) candidates += list(shared.walk_files(shared.cmd_opts.lyco_dir_backcompat, allowed_extensions=[".pt", ".ckpt", ".safetensors"])) for filename in candidates: if os.path.isdir(filename): continue - name = os.path.splitext(os.path.basename(filename))[0] + # if names is provided, only load networks with names in the list + if names and name not in names: + continue try: entry = network.NetworkOnDisk(name, filename) except OSError: # should catch FileNotFoundError and PermissionError etc. @@ -594,6 +672,22 @@ def list_available_networks(): available_network_aliases[entry.alias] = entry +def update_available_networks_by_names(names: list[str]): + process_network_files(names) + + +def list_available_networks(): + available_networks.clear() + available_network_aliases.clear() + forbidden_network_aliases.clear() + available_network_hash_lookup.clear() + forbidden_network_aliases.update({"none": 1, "Addams": 1}) + + os.makedirs(shared.cmd_opts.lora_dir, exist_ok=True) + + process_network_files() + + re_network_name = re.compile(r"(.*)\s*\([0-9a-fA-F]+\)") diff --git a/extensions-builtin/Lora/scripts/lora_script.py b/extensions-builtin/Lora/scripts/lora_script.py index 1518f7e5..d3ea369a 100644 --- a/extensions-builtin/Lora/scripts/lora_script.py +++ b/extensions-builtin/Lora/scripts/lora_script.py @@ -36,6 +36,7 @@ shared.options_templates.update(shared.options_section(('extra_networks', "Extra "sd_lora": shared.OptionInfo("None", "Add network to prompt", gr.Dropdown, lambda: {"choices": ["None", *networks.available_networks]}, refresh=networks.list_available_networks), "lora_preferred_name": shared.OptionInfo("Alias from file", "When adding to prompt, refer to Lora by", gr.Radio, {"choices": ["Alias from file", "Filename"]}), "lora_add_hashes_to_infotext": shared.OptionInfo(True, "Add Lora hashes to infotext"), + "lora_bundled_ti_to_infotext": shared.OptionInfo(True, "Add Lora name as TI hashes for bundled Textual Inversion").info('"Add Textual Inversion hashes to infotext" needs to be enabled'), "lora_show_all": shared.OptionInfo(False, "Always show all networks on the Lora page").info("otherwise, those detected as for incompatible version of Stable Diffusion will be hidden"), "lora_hide_unknown_for_versions": shared.OptionInfo([], "Hide networks of unknown versions for model versions", gr.CheckboxGroup, {"choices": ["SD1", "SD2", "SDXL"]}), "lora_in_memory_limit": shared.OptionInfo(0, "Number of Lora networks to keep cached in memory", gr.Number, {"precision": 0}), diff --git a/extensions-builtin/Lora/ui_edit_user_metadata.py b/extensions-builtin/Lora/ui_edit_user_metadata.py index 3160aecf..b6c4d1c6 100644 --- a/extensions-builtin/Lora/ui_edit_user_metadata.py +++ b/extensions-builtin/Lora/ui_edit_user_metadata.py @@ -21,10 +21,12 @@ re_comma = re.compile(r" *, *") def build_tags(metadata): tags = {} - for _, tags_dict in metadata.get("ss_tag_frequency", {}).items(): - for tag, tag_count in tags_dict.items(): - tag = tag.strip() - tags[tag] = tags.get(tag, 0) + int(tag_count) + ss_tag_frequency = metadata.get("ss_tag_frequency", {}) + if ss_tag_frequency is not None and hasattr(ss_tag_frequency, 'items'): + for _, tags_dict in ss_tag_frequency.items(): + for tag, tag_count in tags_dict.items(): + tag = tag.strip() + tags[tag] = tags.get(tag, 0) + int(tag_count) if tags and is_non_comma_tagset(tags): new_tags = {} @@ -149,6 +151,8 @@ class LoraUserMetadataEditor(ui_extra_networks_user_metadata.UserMetadataEditor) v = random.random() * max_count if count > v: + for x in "({[]})": + tag = tag.replace(x, '\\' + x) res.append(tag) return ", ".join(sorted(res)) diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index 66d15dd0..3e34d69d 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -31,7 +31,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): "name": name, "filename": lora_on_disk.filename, "shorthash": lora_on_disk.shorthash, - "preview": self.find_preview(path), + "preview": self.find_preview(path) or self.find_embedded_preview(path, name, lora_on_disk.metadata), "description": self.find_description(path), "search_terms": search_terms, "local_preview": f"{path}.{shared.opts.samples_format}", @@ -60,7 +60,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): else: sd_version = lora_on_disk.sd_version - if shared.opts.lora_show_all or not enable_filter: + if shared.opts.lora_show_all or not enable_filter or not shared.sd_model: pass elif sd_version == network.SdVersion.Unknown: model_version = network.SdVersion.SDXL if shared.sd_model.is_sdxl else network.SdVersion.SD2 if shared.sd_model.is_sd2 else network.SdVersion.SD1 diff --git a/extensions-builtin/anapnoe-sd-theme-editor/javascript/ui_theme.js b/extensions-builtin/anapnoe-sd-theme-editor/javascript/ui_theme.js index eebc2d19..5d9e4dba 100644 --- a/extensions-builtin/anapnoe-sd-theme-editor/javascript/ui_theme.js +++ b/extensions-builtin/anapnoe-sd-theme-editor/javascript/ui_theme.js @@ -1,419 +1,371 @@ function hexToRgb(color) { - var hex = color[0] === "#" ? color.slice(1) : color; - var c; // expand the short hex by doubling each character, fc0 -> ffcc00 + let hex = color[0] === "#" ? color.slice(1) : color; + let c; - if (hex.length !== 6) { - hex = (function() { - var result = []; + if (hex.length !== 6) { + hex = (() => { + const result = []; + for (let i = 0; i < Array.from(hex).length; i++) { + c = Array.from(hex)[i]; + result.push(`${c}${c}`); + } + return result; + })().join(""); + } - for ( - var _i = 0, _Array$from = Array.from(hex); - _i < _Array$from.length; - _i++ - ) { - c = _Array$from[_i]; - result.push("".concat(c).concat(c)); - } - - return result; - })().join(""); - } - - var colorStr = hex.match(/#?(.{2})(.{2})(.{2})/).slice(1); - var rgb = colorStr.map(function(col) { - return parseInt(col, 16); - }); - rgb.push(1); - return rgb; + const colorStr = hex.match(/#?(.{2})(.{2})(.{2})/).slice(1); + const rgb = colorStr.map((col) => parseInt(col, 16)); + rgb.push(1); + return rgb; } function rgbToHsl(rgb) { - var r = rgb[0] / 255; - var g = rgb[1] / 255; - var b = rgb[2] / 255; - var max = Math.max(r, g, b); - var min = Math.min(r, g, b); - var diff = max - min; - var add = max + min; - var hue = - min === max - ? 0 - : r === max - ? (60 * (g - b) / diff + 360) % 360 - : g === max ? 60 * (b - r) / diff + 120 : 60 * (r - g) / diff + 240; - var lum = 0.5 * add; - var sat = - lum === 0 ? 0 : lum === 1 ? 1 : lum <= 0.5 ? diff / add : diff / (2 - add); - var h = Math.round(hue); - var s = Math.round(sat * 100); - var l = Math.round(lum * 100); - var a = rgb[3] || 1; - return [h, s, l, a]; + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const diff = max - min; + const add = max + min; + + const hue = min === max ? + 0 : + r === max ? + (60 * (g - b) / diff + 360) % 360 : + g === max ? + 60 * (b - r) / diff + 120 : + 60 * (r - g) / diff + 240; + + const lum = 0.5 * add; + const sat = lum === 0 ? + 0 : + lum === 1 ? + 1 : + lum <= 0.5 ? + diff / add : + diff / (2 - add); + + return [ + Math.round(hue), + Math.round(sat * 100), + Math.round(lum * 100), + rgb[3] || 1, + ]; } function hexToHsl(color) { - var rgb = hexToRgb(color); - var hsl = rgbToHsl(rgb); - return "hsl(" + hsl[0] + "deg " + hsl[1] + "% " + hsl[2] + "%)"; + const rgb = hexToRgb(color); + const hsl = rgbToHsl(rgb); + return `hsl(${hsl[0]}deg ${hsl[1]}% ${hsl[2]}%)`; } function hslToHex(h, s, l) { - l /= 100; - var a = s * Math.min(l, 1 - l) / 100; + l /= 100; + const a = (s * Math.min(l, 1 - l)) / 100; - var f = function f(n) { - var k = (n + h / 30) % 12; - var color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); - return Math.round(255 * Math.max(0, Math.min(color, 1))) - .toString(16) - .padStart(2, "0"); // convert to Hex and prefix "0" if needed - }; + const f = (n) => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return ( + Math.round(255 * Math.max(0, Math.min(color, 1))) + .toString(16) + .padStart(2, "0") + ); + }; - return "#".concat(f(0)).concat(f(8)).concat(f(4)); + return `#${f(0)}${f(8)}${f(4)}`; } function hsl2rgb(h, s, l) { - var a = s * Math.min(l, 1 - l); + const a = s * Math.min(l, 1 - l); - var f = function f(n) { - var k = - arguments.length > 1 && arguments[1] !== undefined - ? arguments[1] - : (n + h / 30) % 12; - return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); - }; + const f = (n) => { + return l - a * Math.max(Math.min(n - 3, 9 - n, 1), -1); + }; - return [f(0), f(8), f(4)]; + return [f(0), f(8), f(4)]; } function invertColor(hex) { - if (hex.indexOf("#") === 0) { - hex = hex.slice(1); - } // convert 3-digit hex to 6-digits. + if (hex.startsWith("#")) { + hex = hex.slice(1); + } - if (hex.length === 3) { - hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; - } + if (hex.length === 3) { + hex = hex.split("").map((c) => c + c).join(""); + } - if (hex.length !== 6) { - throw new Error("Invalid HEX color."); - } // invert color components + if (hex.length !== 6) { + throw new Error("Invalid HEX color."); + } - var r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16), - g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16), - b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16); // pad each with zeros and return + const r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16); + const g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16); + const b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16); - return "#" + padZero(r) + padZero(g) + padZero(b); + return `#${padZero(r)}${padZero(g)}${padZero(b)}`; } -function padZero(str, len) { - len = len || 2; - var zeros = new Array(len).join("0"); - return (zeros + str).slice(-len); +function padZero(str, len = 2) { + const zeros = new Array(len).join("0"); + return (zeros + str).slice(-len); } function getValsWrappedIn(str, c1, c2) { - var rg = new RegExp("(?<=\\" + c1 + ")(.*?)(?=\\" + c2 + ")", "g"); - return str.match(rg); + const rg = new RegExp(`(?<=\\${c1})(.*?)(?=\\${c2})`, "g"); + return str.match(rg); } -var styleobj = {}; -var hslobj = {}; -var isColorsInv; +const styleobj = {}; +const hslobj = {}; +let isColorsInv; -var toHSLArray = function toHSLArray(hslStr) { - return hslStr.match(/\d+/g).map(Number); -}; +const toHSLArray = (hslStr) => + hslStr.match(/\d+/g).map(Number); function offsetColorsHSV(ohsl) { - var inner_styles = ""; + let inner_styles = ""; - for (var key in styleobj) { - var keyVal = styleobj[key]; + for (const key in styleobj) { + let keyVal = styleobj[key]; - if (keyVal.indexOf("#") != -1 || keyVal.indexOf("hsl") != -1) { - var colcomp = document.body.querySelector("#" + key + " input"); + if (keyVal.includes("#") || keyVal.includes("hsl")) { + const colcomp = document.body.querySelector(`#${key} input`); - if (colcomp) { - var hsl = void 0; + if (colcomp) { + let hsl; - if (keyVal.indexOf("#") != -1) { - keyVal = keyVal.replace(/\s+/g, ""); //inv ? keyVal = invertColor(keyVal) : 0; + if (keyVal.includes("#")) { + keyVal = keyVal.replace(/\s+/g, ""); - if (isColorsInv) { - keyVal = invertColor(keyVal); - styleobj[key] = keyVal; - } + if (isColorsInv) { + keyVal = invertColor(keyVal); + styleobj[key] = keyVal; + } - hsl = rgbToHsl(hexToRgb(keyVal)); + hsl = rgbToHsl(hexToRgb(keyVal)); + } else { + if (isColorsInv) { + const c = toHSLArray(keyVal); + const _hex = hslToHex(c[0], c[1], c[2]); + styleobj[key] = invertColor(_hex); + hsl = rgbToHsl(hexToRgb(styleobj[key])); + } else { + hsl = toHSLArray(keyVal); + } + } + + const h = (parseInt(hsl[0]) + parseInt(ohsl[0])) % 360; + const s = Math.min(Math.max(parseInt(hsl[1]) + parseInt(ohsl[1]), 0), 100); + const l = Math.min(Math.max(parseInt(hsl[2]) + parseInt(ohsl[2]), 0), 100); + + const hex = hslToHex(h, s, l); + colcomp.value = hex; + hslobj[key] = `hsl(${h}deg ${s}% ${l}%)`; + inner_styles += `${key}:${hslobj[key]};`; + } } else { - if (isColorsInv) { - var c = toHSLArray(keyVal); - - var _hex = hslToHex(c[0], c[1], c[2]); - - keyVal = invertColor(_hex); - styleobj[key] = keyVal; - hsl = rgbToHsl(hexToRgb(keyVal)); - } else { - hsl = toHSLArray(keyVal); - } + inner_styles += `${key}:${styleobj[key]};`; } - - var h = (parseInt(hsl[0]) + parseInt(ohsl[0])) % 360; - var s = parseInt(hsl[1]) + parseInt(ohsl[1]); - var l = parseInt(hsl[2]) + parseInt(ohsl[2]); - var hex = hslToHex( - h, - Math.min(Math.max(s, 0), 100), - Math.min(Math.max(l, 0), 100) - ); - colcomp.value = hex; - hslobj[key] = "hsl(" + h + "deg " + s + "% " + l + "%)"; - inner_styles += key + ":" + hslobj[key] + ";"; - } - } else { - inner_styles += key + ":" + styleobj[key] + ";"; } - } - isColorsInv = false; - var preview_styles = document.body.querySelector("#preview-styles"); - preview_styles.innerHTML = ":root {" + inner_styles + "}"; - preview_styles.innerHTML += - "@media only screen and (max-width: 860px) {:root{--ae-outside-gap-size: var(--ae-mobile-outside-gap-size);--ae-inside-padding-size: var(--ae-mobile-inside-padding-size);}}"; - var vars_textarea = document.body.querySelector("#theme_vars textarea"); - vars_textarea.value = inner_styles; - window.updateInput(vars_textarea); + isColorsInv = false; + const preview_styles = document.body.querySelector("#preview-styles"); + preview_styles.innerHTML = `:root {${inner_styles}}`; + preview_styles.innerHTML += `@media only screen and (max-width: 860px) { :root { --ae-outside-gap-size: var(--ae-mobile-outside-gap-size); --ae-inside-padding-size: var(--ae-mobile-inside-padding-size); } }`; + + const vars_textarea = document.body.querySelector("#theme_vars textarea"); + vars_textarea.value = inner_styles; + window.updateInput(vars_textarea); } function updateTheme(vars) { - var inner_styles = ""; + let inner_styles = ""; - var _loop = function _loop(i) { - var key = vars[i].split(":"); - var id = key[0].replace(/\s+/g, ""); - var val = key[1].trim(); - styleobj[id] = val; - inner_styles += id + ":" + val + ";"; - document.body.querySelectorAll("#" + id + " input").forEach(function(elem) { - if (val.indexOf("hsl") != -1) { - var hsl = toHSLArray(val); - var hex = hslToHex(hsl[0], hsl[1], hsl[2]); - elem.value = hex; - } else { - elem.value = val.split("px")[0]; - } - }); - }; + for (let i = 0; i < vars.length - 1; i++) { + const [id, val] = vars[i].split(":").map((v) => v.trim()); + styleobj[id.replace(/\s+/g, "")] = val; + inner_styles += `${id}:${val};`; + const elem = document.body.querySelectorAll(`#${id} input`); + elem.forEach((el) => { + if (val.includes("hsl")) { + const hsl = toHSLArray(val); + el.value = hslToHex(hsl[0], hsl[1], hsl[2]); + } else { + el.value = val.split("px")[0]; + } + }); + } - for (var i = 0; i < vars.length - 1; i++) { - _loop(i); - } + const preview_styles = document.body.querySelector("#preview-styles"); - var preview_styles = document.body.querySelector("#preview-styles"); + if (preview_styles) { + preview_styles.innerHTML = `:root {${inner_styles}}`; + preview_styles.innerHTML += `@media only screen and (max-width: 860px) { :root { --ae-outside-gap-size: var(--ae-mobile-outside-gap-size); --ae-inside-padding-size: var(--ae-mobile-inside-padding-size); } }`; + } else { + const style = document.createElement("style"); + style.id = "preview-styles"; + style.innerHTML = `:root {${inner_styles}}`; + style.innerHTML += `@media only screen and (max-width: 860px) { :root { --ae-outside-gap-size: var(--ae-mobile-outside-gap-size); --ae-inside-padding-size: var(--ae-mobile-inside-padding-size); } }`; + document.body.appendChild(style); + } - if (preview_styles) { - preview_styles.innerHTML = ":root {" + inner_styles + "}"; - preview_styles.innerHTML += - "@media only screen and (max-width: 860px) {:root{--ae-outside-gap-size: var(--ae-mobile-outside-gap-size);--ae-inside-padding-size: var(--ae-mobile-inside-padding-size);}}"; - } else { - var r = document.body; - var style = document.createElement("style"); - style.id = "preview-styles"; - style.innerHTML = ":root {" + inner_styles + "}"; - style.innerHTML += - "@media only screen and (max-width: 860px) {:root{--ae-outside-gap-size: var(--ae-mobile-outside-gap-size);--ae-inside-padding-size: var(--ae-mobile-inside-padding-size);}}"; - r.appendChild(style); - } - - var vars_textarea = document.body.querySelector("#theme_vars textarea"); - var css_textarea = document.body.querySelector("#theme_css textarea"); - vars_textarea.value = inner_styles; - css_textarea.value = css_textarea.value; //console.log(Object); - - window.updateInput(vars_textarea); - window.updateInput(css_textarea); + const vars_textarea = document.body.querySelector("#theme_vars textarea"); + const css_textarea = document.body.querySelector("#theme_css textarea"); + vars_textarea.value = inner_styles; + window.updateInput(vars_textarea); + window.updateInput(css_textarea); } function applyTheme() { - console.log("apply"); + console.log("apply"); } function initTheme(styles) { - var css_styles = styles.split("/*BREAKPOINT_CSS_CONTENT*/"); - var init_css_vars = css_styles[0].split("}")[0].split("{")[1]; - init_css_vars = init_css_vars.replace(/\n|\r/g, ""); - var init_vars = init_css_vars.split(";"); - var vars = init_vars; //console.log(vars); + const css_styles = styles.split("/*BREAKPOINT_CSS_CONTENT*/"); + let init_css_vars = css_styles[0].split("}")[0].split("{")[1].replace(/\n|\r/g, ""); + const init_vars = init_css_vars.split(";"); - var vars_textarea = document.body.querySelector("#theme_vars textarea"); - var css_textarea = document.body.querySelector("#theme_css textarea"); - vars_textarea.value = init_css_vars; - var additional_styles = css_styles[1] !== undefined ? css_styles[1] : ""; - css_textarea.value = - "/*BREAKPOINT_CSS_CONTENT*/" + - additional_styles + - "/*BREAKPOINT_CSS_CONTENT*/"; - updateTheme(vars); - var preview_styles = document.body.querySelector("#preview-styles"); - - if (!preview_styles) { - var style = document.createElement("style"); - style.id = "preview-styles"; - style.innerHTML = styles; - document.body.appendChild(style); - } - - var intervalChange; - document.body - .querySelectorAll("#ui_theme_settings input") - .forEach(function(elem) { - elem.addEventListener("input", function(e) { - var celem = e.currentTarget; - var val = e.currentTarget.value; - var curr_val; - - switch (e.currentTarget.type) { - case "range": - celem = celem.parentElement; - val = e.currentTarget.value + "px"; - break; - - case "color": - celem = celem.parentElement.parentElement; - val = e.currentTarget.value; - break; - - case "number": - celem = celem.parentElement.parentElement.parentElement; - val = e.currentTarget.value + "px"; - break; - } - - if (celem.id === "--ae-input-slider-height") { - val = e.currentTarget.value; - } - - styleobj[celem.id] = val; //console.log(styleobj); - - if (intervalChange != null) clearInterval(intervalChange); - intervalChange = setTimeout(function() { - var inner_styles = ""; - - for (var key in styleobj) { - inner_styles += key + ":" + styleobj[key] + ";"; - } - - vars = inner_styles.split(";"); - preview_styles.innerHTML = ":root {" + inner_styles + "}"; - preview_styles.innerHTML += - "@media only screen and (max-width: 860px) {:root{--ae-outside-gap-size: var(--ae-mobile-outside-gap-size);--ae-inside-padding-size: var(--ae-mobile-inside-padding-size);}}"; - vars_textarea.value = inner_styles; - window.updateInput(vars_textarea); - - offsetColorsHSV(hsloffset); - }, 1000); - }); - }); - var reset_btn = document.getElementById("theme_reset_btn"); - reset_btn.addEventListener("click", function(e) { - e.preventDefault(); - e.stopPropagation(); - document.body - .querySelectorAll("#ui_theme_hsv input") - .forEach(function(elem) { - elem.value = 0; - }); - hsloffset = [0, 0, 0]; - updateTheme(init_vars); - }); - var intervalCheck; - - function dropDownOnChange() { - if (init_css_vars !== vars_textarea.value) { - clearInterval(intervalCheck); - init_css_vars = vars_textarea.value.replace(/\n|\r/g, ""); - vars_textarea.value = init_css_vars; - init_vars = init_css_vars.split(";"); - vars = init_vars; - updateTheme(vars); - } - } - - var drop_down = document.body.querySelector("#themes_drop_down input"); - drop_down.addEventListener("click", function(e) { - if (intervalCheck !== null) clearInterval(intervalCheck); + const vars_textarea = document.body.querySelector("#theme_vars textarea"); + const css_textarea = document.body.querySelector("#theme_css textarea"); vars_textarea.value = init_css_vars; - intervalCheck = setInterval(dropDownOnChange, 500); - }); - var hsloffset = [0, 0, 0]; - var hue = document.body - .querySelectorAll("#theme_hue input") - .forEach(function(elem) { - elem.addEventListener("change", function(e) { + const additional_styles = css_styles[1] !== undefined ? css_styles[1] : ""; + css_textarea.value = `/*BREAKPOINT_CSS_CONTENT*/${additional_styles}/*BREAKPOINT_CSS_CONTENT*/`; + updateTheme(init_vars); + + const preview_styles = document.body.querySelector("#preview-styles"); + if (!preview_styles) { + const style = document.createElement("style"); + style.id = "preview-styles"; + style.innerHTML = styles; + document.body.appendChild(style); + } + + let intervalChange; + document.body + .querySelectorAll("#ui_theme_settings input") + .forEach((elem) => { + elem.addEventListener("input", (e) => { + let val = e.currentTarget.value; + if (e.currentTarget.type === "range" || e.currentTarget.type === "number") { + val += "px"; + styleobj[e.currentTarget.parentElement.id] = val; + } + if (e.currentTarget.type === "color") { + val = e.currentTarget.value; + styleobj[e.currentTarget.parentElement.parentElement.id] = val; + } + + if (intervalChange != null) clearInterval(intervalChange); + intervalChange = setTimeout(() => { + let inner_styles = ""; + for (const key in styleobj) { + inner_styles += `${key}:${styleobj[key]};`; + } + + vars_textarea.value = inner_styles; + preview_styles.innerHTML = `:root {${inner_styles}}`; + preview_styles.innerHTML += `@media only screen and (max-width: 860px) { :root { --ae-outside-gap-size: var(--ae-mobile-outside-gap-size); --ae-inside-padding-size: var(--ae-mobile-inside-padding-size); } }`; + + window.updateInput(vars_textarea); + offsetColorsHSV(hsloffset); + }, 1000); + }); + }); + + const reset_btn = document.getElementById("theme_reset_btn"); + reset_btn.addEventListener("click", (e) => { e.preventDefault(); - e.stopPropagation(); - hsloffset[0] = e.currentTarget.value; - offsetColorsHSV(hsloffset); - }); + document.body + .querySelectorAll("#ui_theme_hsv input") + .forEach((elem) => { + elem.value = 0; + }); + hsloffset = [0, 0, 0]; + updateTheme(init_vars); }); - var sat = document.body - .querySelectorAll("#theme_sat input") - .forEach(function(elem) { - elem.addEventListener("change", function(e) { + + let intervalCheck; + + function dropDownOnChange() { + if (init_css_vars !== vars_textarea.value) { + clearInterval(intervalCheck); + init_css_vars = vars_textarea.value.replace(/\n|\r/g, ""); + vars_textarea.value = init_css_vars; + const vars = init_css_vars.split(";"); + updateTheme(vars); + } + } + + const drop_down = document.body.querySelector("#themes_drop_down input"); + drop_down.addEventListener("click", (e) => { + if (intervalCheck !== null) clearInterval(intervalCheck); + vars_textarea.value = init_css_vars; + intervalCheck = setInterval(dropDownOnChange, 500); + }); + + let hsloffset = [0, 0, 0]; + + document.body + .querySelectorAll("#theme_hue input") + .forEach((elem) => { + elem.addEventListener("change", (e) => { + e.preventDefault(); + hsloffset[0] = e.currentTarget.value; + offsetColorsHSV(hsloffset); + }); + }); + + document.body + .querySelectorAll("#theme_sat input") + .forEach((elem) => { + elem.addEventListener("change", (e) => { + e.preventDefault(); + hsloffset[1] = e.currentTarget.value; + offsetColorsHSV(hsloffset); + }); + }); + + document.body + .querySelectorAll("#theme_brt input") + .forEach((elem) => { + elem.addEventListener("change", (e) => { + e.preventDefault(); + hsloffset[2] = e.currentTarget.value; + offsetColorsHSV(hsloffset); + }); + }); + + const inv_btn = document.getElementById("theme_invert_btn"); + inv_btn.addEventListener("click", (e) => { e.preventDefault(); - e.stopPropagation(); - hsloffset[1] = e.currentTarget.value; + isColorsInv = !isColorsInv; offsetColorsHSV(hsloffset); - }); }); - var brt = document.body - .querySelectorAll("#theme_brt input") - .forEach(function(elem) { - elem.addEventListener("change", function(e) { - e.preventDefault(); - e.stopPropagation(); - hsloffset[2] = e.currentTarget.value; - offsetColorsHSV(hsloffset); - }); - }); - var inv_btn = document.getElementById("theme_invert_btn"); - inv_btn.addEventListener("click", function(e) { - e.preventDefault(); - e.stopPropagation(); - isColorsInv = !isColorsInv; - offsetColorsHSV(hsloffset); - }); } function observeGradioApp() { - var observer = new MutationObserver(function() { - var css = document.querySelector('[rel="stylesheet"][href*="user"]'); - var block = gradioApp().getElementById("tab_ui_theme"); + const observer = new MutationObserver(() => { + const css = document.querySelector('[rel="stylesheet"][href*="user"]'); + const block = gradioApp().getElementById("tab_ui_theme"); - if (css && block) { - observer.disconnect(); - setTimeout(function() { - var rootRules = Array.from(css.sheet.cssRules).filter(function( - cssRule - ) { - return ( - cssRule instanceof CSSStyleRule && cssRule.selectorText === ":root" - ); - }); - var rootCssText = rootRules[0].cssText; //console.log("Loading theme vars", rootCssText); + if (css && block) { + observer.disconnect(); + setTimeout(() => { + const rootRules = Array.from(css.sheet.cssRules).filter((cssRule) => { + return cssRule instanceof CSSStyleRule && cssRule.selectorText === ":root"; + }); + const rootCssText = rootRules[0].cssText; + initTheme(rootCssText); + }, 500); + } + }); - initTheme(rootCssText); - }, "500"); - } - }); - observer.observe(gradioApp(), { - childList: true, - subtree: true - }); + observer.observe(gradioApp(), { + childList: true, + subtree: true, + }); } -document.addEventListener("DOMContentLoaded", function() { - observeGradioApp(); -}); +document.addEventListener("DOMContentLoaded", observeGradioApp); + diff --git a/extensions-builtin/anapnoe-sd-theme-editor/scripts/ui_theme.py b/extensions-builtin/anapnoe-sd-theme-editor/scripts/ui_theme.py index 399798e1..72a48c0f 100644 --- a/extensions-builtin/anapnoe-sd-theme-editor/scripts/ui_theme.py +++ b/extensions-builtin/anapnoe-sd-theme-editor/scripts/ui_theme.py @@ -1,63 +1,63 @@ import os -import shutil from pathlib import Path import gradio as gr import modules.scripts as scripts -from modules import script_callbacks, shared +from modules import script_callbacks -basedir = scripts.basedir() +basedir = scripts.basedir() webui_dir = Path(basedir).parents[1] -themes_folder = os.path.join(basedir, "themes") -javascript_folder = os.path.join(basedir, "javascript") +themes_folder = os.path.join(basedir, "themes") webui_style_path = os.path.join(webui_dir, "user.css") -def get_files(folder, file_filter=[], file_list=[], split=False): - file_list = [file_name if not split else os.path.splitext(file_name)[0] for file_name in os.listdir(folder) if os.path.isfile(os.path.join(folder, file_name)) and file_name not in file_filter] +def get_files(folder, file_filter=None, split=False): + if file_filter is None: + file_filter = [] + file_list = [ + file_name if not split else os.path.splitext(file_name)[0] + for file_name in os.listdir(folder) + if os.path.isfile(os.path.join(folder, file_name)) and file_name not in file_filter + ] return file_list - def on_ui_tabs(): - - with gr.Blocks(analytics_enabled=False) as ui_theme: + with gr.Blocks(analytics_enabled=False) as ui_theme: with gr.Row(): - with gr.Column(): - with gr.Row(): - themes_dropdown = gr.Dropdown(label="Themes", elem_id="themes_drop_down", interactive=True, choices=get_files(themes_folder,[".css, .txt"]), type="value") + with gr.Column(): + with gr.Row(): + themes_dropdown = gr.Dropdown( + label="Themes", + elem_id="themes_drop_down", + interactive=True, + choices=get_files(themes_folder, [".css", ".txt"]), + type="value" + ) save_as_filename = gr.Text(label="Save / Save as") - with gr.Row(): - reset_button = gr.Button(elem_id="theme_reset_btn", value="Reset", variant="primary") - #apply_button = gr.Button(elem_id="theme_apply_btn", value="Apply", variant="primary") - save_button = gr.Button(value="Save", variant="primary") - #delete_button = gr.Button(value="Delete", variant="primary") - - #with gr.Accordion(label="Debug View", open=True): + with gr.Row(): + gr.Button(elem_id="theme_reset_btn", value="Reset", variant="primary") # Removed unused variable + save_button = gr.Button(value="Save", variant="primary") + with gr.Row(elem_id="theme_hidden"): - vars_text = gr.Textbox(label="Vars", elem_id="theme_vars", show_label=True, lines=7, interactive=False, visible=True) - css_text = gr.Textbox(label="Css", elem_id="theme_css", show_label=True, lines=7, interactive=False, visible=True) - #result_text = gr.Text(elem_id="theme_result", interactive=False, visible=False) - - with gr.Accordion(label="Theme Color adjustments", open=True): + vars_text = gr.Textbox(label="Vars", elem_id="theme_vars", show_label=True, lines=7, interactive=False, visible=True) + css_text = gr.Textbox(label="Css", elem_id="theme_css", show_label=True, lines=7, interactive=False, visible=True) + + with gr.Accordion(label="Theme Color adjustments", open=True): with gr.Row(): - with gr.Column(elem_id="ui_theme_hsv"): + with gr.Column(elem_id="ui_theme_hsv"): gr.Slider(elem_id="theme_hue", label='Hue', minimum=0, maximum=360, step=1) gr.Slider(elem_id="theme_sat", label='Saturation', minimum=-100, maximum=100, step=1, value=0, interactive=True) gr.Slider(elem_id="theme_brt", label='Lightness', minimum=-50, maximum=50, step=1, value=0, interactive=True) - gr.Button(elem_id="theme_invert_btn", value="Invert", variant="primary") - with gr.Column(elem_id="ui_theme_settings"): - with gr.Accordion(label="Main", open=False): + with gr.Column(elem_id="ui_theme_settings"): + with gr.Accordion(label="Main", open=False): gr.ColorPicker(elem_id="--ae-main-bg-color", interactive=True, label="Background color") gr.ColorPicker(elem_id="--ae-primary-color", label="Primary color") - gr.ColorPicker(elem_id="--ae-secondary-color", label="Secondary color") + gr.ColorPicker(elem_id="--ae-secondary-color", label="Secondary color") with gr.Accordion(label="Spacing", open=False): - gr.Slider(elem_id="--ae-gap-size-val", label='Gap size', minimum=0, maximum=8, step=1) - - """ with gr.Accordion(elem_classes="hidden", label="Spacing (Mobile)", open=False): - gr.Slider(elem_id="--ae-mobile-gap-size-val", label='Gap size', minimum=0, maximum=8, step=1) """ - + gr.Slider(elem_id="--ae-gap-size-val", label='Gap size', minimum=0, maximum=8, step=1) + with gr.Accordion(label="Panel", open=False): gr.ColorPicker(elem_id="--ae-panel-bg-color", label="Panel background color") gr.ColorPicker(elem_id="--ae-panel-border-color", label="Panel border color") @@ -65,8 +65,7 @@ def on_ui_tabs(): gr.Slider(elem_id="--ae-border-radius", label='Panel border radius', minimum=0, maximum=8, step=1) gr.Slider(elem_id="--ae-border-size", label='Panel border size', minimum=0, maximum=4, step=1) - with gr.Accordion(label="Component", open=False): - + with gr.Accordion(label="Component", open=False): gr.Slider(elem_id="--ae-input-height", label='Component size', minimum=28, maximum=45, step=1) gr.Slider(elem_id="--ae-input-slider-height", label='Slider height', minimum=0.1, maximum=1, step=0.1) gr.Slider(elem_id="--ae-input-padding", label='Padding', minimum=0, maximum=10, step=1) @@ -74,18 +73,13 @@ def on_ui_tabs(): gr.Slider(elem_id="--ae-input-line-height", label='Line height', minimum=10, maximum=40, step=1) gr.Slider(elem_id="--ae-input-border-size", label='Border size', minimum=0, maximum=4, step=1) gr.Slider(elem_id="--ae-input-border-radius", label='Border radius', minimum=0, maximum=8, step=1) - gr.ColorPicker(elem_id="--ae-input-bg-color", label="Input background color") gr.ColorPicker(elem_id="--ae-input-border-color", label="Input border color") gr.ColorPicker(elem_id="--ae-input-text-color", label="Input text color") gr.ColorPicker(elem_id="--ae-input-placeholder-color", label="Input placeholder color") - gr.ColorPicker(elem_id="--ae-label-color", label="Label color") gr.ColorPicker(elem_id="--ae-secondary-label-color", label="Secondary label color") - gr.ColorPicker(elem_id="--ae-input-hover-text-color", label="Input hover text color") - - with gr.Accordion(label="Group", open=False): gr.ColorPicker(elem_id="--ae-group-bg-color", label="Group background color") @@ -95,69 +89,31 @@ def on_ui_tabs(): gr.Slider(elem_id="--ae-group-border-size", label='Group border size', minimum=0, maximum=4, step=1) gr.Slider(elem_id="--ae-group-gap", label='Group gap size', minimum=0, maximum=8, step=1) - - - - def save_theme( vars_text, css_text, filename): - style_data= ":root{" + vars_text + "}" + css_text - with open(os.path.join(themes_folder, f"{filename}.css"), 'w', encoding="utf-8") as file: - file.write(vars_text) - file.close() - with open(webui_style_path, 'w', encoding="utf-8") as file: - file.write(style_data) - file.close() - themes_dropdown.choices=get_files(themes_folder,[".css, .txt"]) + def save_theme(vars_text, css_text, filename): + style_data = ":root{" + vars_text + "}" + css_text + with open(os.path.join(themes_folder, f"{filename}.css"), 'w', encoding="utf-8") as file: + file.write(style_data) # Updated to save complete data + themes_dropdown.choices = get_files(themes_folder, [".css", ".txt"]) return gr.update(choices=themes_dropdown.choices, value=f"{filename}.css") - - def open_theme(filename, css_text): - with open(os.path.join(themes_folder, f"{filename}"), 'r') as file: - vars_text=file.read() - no_ext=filename.rsplit('.', 1)[0] - #save_theme( vars_text, css_text, no_ext) - # shared.state.interrupt() - # shared.state.need_restart = True - return [vars_text, no_ext] - - # def delete_theme(filename): - # try: - # os.remove(os.path.join(themes_folder, filename)) - # except FileNotFoundError: - # pass - # delete_button.click( - # fn = lambda: delete_theme() - # ) - + def open_theme(filename): + with open(os.path.join(themes_folder, filename), 'r') as file: + vars_text = file.read() + no_ext = filename.rsplit('.', 1)[0] + return [vars_text, no_ext] + save_button.click( fn=save_theme, inputs=[vars_text, css_text, save_as_filename], outputs=themes_dropdown ) - + themes_dropdown.change( fn=open_theme, - #_js = "applyTheme", - inputs=[themes_dropdown, css_text], + inputs=[themes_dropdown], outputs=[vars_text, save_as_filename] ) - - # apply_button.click( - # fn=None, - # _js = "applyTheme" - # ) - - # vars_text.change( - # fn=None, - # _js = "applyTheme", - # inputs=[], - # outputs=[vars_text, css_text] - # ) - - - return (ui_theme, 'Theme', 'ui_theme'), - - -script_callbacks.on_ui_tabs(on_ui_tabs) \ No newline at end of file +script_callbacks.on_ui_tabs(on_ui_tabs) diff --git a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-app-root.html b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-app-root.html index 35437406..da44b88d 100644 --- a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-app-root.html +++ b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-app-root.html @@ -235,17 +235,9 @@
-
-
Close -
- +
- -
diff --git a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extra-networks.html b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extra-networks.html index abd9b615..7ecc1d53 100644 --- a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extra-networks.html +++ b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extra-networks.html @@ -26,9 +26,7 @@
- -
+
diff --git a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extras-params.html b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extras-params.html index b4893360..f7f44e11 100644 --- a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extras-params.html +++ b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-extras-params.html @@ -29,49 +29,54 @@
-
+
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -120,7 +120,7 @@
diff --git a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-txt2img-results.html b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-txt2img-results.html index 541f9ffe..56faf6de 100644 --- a/extensions-builtin/anapnoe-sd-uiux/html/templates/template-txt2img-results.html +++ b/extensions-builtin/anapnoe-sd-uiux/html/templates/template-txt2img-results.html @@ -49,6 +49,14 @@
+
diff --git a/extensions-builtin/anapnoe-sd-uiux/javascript/anapnoe_sd_uiux_core.js b/extensions-builtin/anapnoe-sd-uiux/javascript/anapnoe_sd_uiux_core.js index 3bdbce6d..b465614f 100644 --- a/extensions-builtin/anapnoe-sd-uiux/javascript/anapnoe_sd_uiux_core.js +++ b/extensions-builtin/anapnoe-sd-uiux/javascript/anapnoe_sd_uiux_core.js @@ -1,37 +1,38 @@ -//var orig_all_gallery_buttons = window.all_gallery_buttons; -window.all_gallery_buttons = function(){ +const anapnoe_app_id = "#anapnoe_app"; + +window.all_gallery_buttons = function() { //orig_all_gallery_buttons(); - var tabitem = gradioApp().querySelector('#main-nav-bar button.active')?.getAttribute('tabitemid'); - var visibleGalleryButtons = []; - if(tabitem){ - //console.log(tabitem, allGalleryButtons); - var allGalleryButtons = gradioApp().querySelectorAll(tabitem+' .gradio-gallery .thumbnails > .thumbnail-small'); - allGalleryButtons?.forEach(function(elem) { - if (elem.parentElement.offsetParent && elem.parentElement.offsetParent !== document.body) { - visibleGalleryButtons.push(elem); - } - }); - } - return visibleGalleryButtons; -} + var tabitem = gradioApp().querySelector('#main-nav-bar button.active')?.getAttribute('tabitemid'); + var visibleGalleryButtons = []; + if (tabitem) { + //console.log(tabitem, allGalleryButtons); + var allGalleryButtons = gradioApp().querySelectorAll(tabitem + ' .gradio-gallery .thumbnails > .thumbnail-small'); + allGalleryButtons?.forEach(function(elem) { + if (elem.parentElement.offsetParent && elem.parentElement.offsetParent !== document.body) { + visibleGalleryButtons.push(elem); + } + }); + } + return visibleGalleryButtons; +}; -window.extraNetworksSearchButton = function(tabs_id, e) { - var button = e.target; - const attr = e.target.parentElement.parentElement.getAttribute("search-field"); +window.extraNetworksSearchButton = function(tabname, extra_networks_tabname, event) { + var attr = event.target.parentElement.parentElement.getAttribute("search-field"); var searchTextarea = gradioApp().querySelector(attr); - //console.log(button, searchTextarea) - var text = button.classList.contains("search-all") ? "" : button.textContent.trim(); - searchTextarea.value = text; - window.updateInput(searchTextarea); -} + var button = event.target; + var text = button.classList.contains("search-all") ? "" : button.textContent.trim(); -window.imageMaskResize = function(){ - // override function empty we dont need any fix here -} + searchTextarea.value = text; + window.updateInput(searchTextarea); +}; -window.extraNetworksEditUserMetadata = function(event, tabname, extraPage, cardName) { - //var id = tabname + '_' + extraPage + '_edit_user_metadata'; - var tid = 'txt2img_' + extraPage + '_edit_user_metadata'; +window.imageMaskResize = function() { + // override function empty we dont need any fix here +}; + +window.extraNetworksEditUserMetadata = function(event, tabname, extraPage) { + var tid = 'txt2img_' + extraPage + '_edit_user_metadata'; + /* eslint-disable no-undef */ var editor = extraPageUserMetadataEditors[tid]; if (!editor) { editor = {}; @@ -40,27 +41,27 @@ window.extraNetworksEditUserMetadata = function(event, tabname, extraPage, cardN editor.button = gradioApp().querySelector("#" + tid + "_button"); extraPageUserMetadataEditors[tid] = editor; } - + var cardName = event.target.parentElement.parentElement.getAttribute("data-name"); + if (!cardName) { + cardName = event.target.parentElement.parentElement.parentElement.querySelector('.tree-list-item-label').innerHTML.trim(); + } editor.nameTextarea.value = cardName; updateInput(editor.nameTextarea); - editor.button.click(); - popup(editor.page); + event.stopPropagation(); +}; - //event.stopPropagation(); -} - -window.get_uiCurrentTabContent = function(){ +window.get_uiCurrentTabContent = function() { //console.log(active_main_tab); - if(active_main_tab.id === "tab_txt2img"){ + if (active_main_tab.id === "tab_txt2img") { return document.getElementById("txt2img_tabitem"); - }else if(active_main_tab.id === "tab_img2img"){ + } else if (active_main_tab.id === "tab_img2img") { return document.getElementById("img2img_tabitem"); } -} +}; -async function getContributors(repoName, page = 1) { +async function getContributors(repoName, page = 1) { let request = await fetch(`https://api.github.com/repos/${repoName}/contributors?per_page=100&page=${page}`, { method: 'GET', headers: { @@ -84,7 +85,7 @@ async function getAllContributorsRecursive(repoName, page = 1, allContributors = // The base case: when the list is empty, return allContributors return allContributors; } - + localStorage.setItem('UiUxReady', "false"); localStorage.setItem('UiUxComplete', "false"); const default_ext_path = './file=extensions-builtin/anapnoe-sd-uiux/html/templates/'; @@ -93,214 +94,225 @@ let total = 0; let active_main_tab;// = document.querySelector("#tab_txt2img");//null let loggerUiUx; const split_instances = []; -const anapnoe_app_id = "#anapnoe_app"; function detectMobile() { - return ( ( window.innerWidth <= 768 ) );//&& ( window.innerHeight <= 600 ) ); + return ((window.innerWidth <= 768));//&& ( window.innerHeight <= 600 ) ); } -/* +/* function debounce(func){ - var timer; - return function(event){ - if(timer) clearTimeout(timer); - timer = setTimeout(func,100,event); - }; -} + var timer; + return function(event){ + if(timer) clearTimeout(timer); + timer = setTimeout(func,100,event); + }; +} */ -function applyDefaultLayout(isMobile){ - anapnoe_app.querySelectorAll("[mobile]").forEach((tabItem) => { - //console.log(tabItem); - if(isMobile){ - if(tabItem.childElementCount === 0){ - const mobile_attr = tabItem.getAttribute("mobile"); - if(mobile_attr){ - const mobile_target = anapnoe_app.querySelector(mobile_attr); - if(mobile_target){ +function applyDefaultLayout(isMobile) { + const anapnoe_app = document.querySelector(anapnoe_app_id); + anapnoe_app.querySelectorAll("[mobile]").forEach((tabItem) => { + //console.log(tabItem); + if (isMobile) { + if (tabItem.childElementCount === 0) { + const mobile_attr = tabItem.getAttribute("mobile"); + if (mobile_attr) { + const mobile_target = anapnoe_app.querySelector(mobile_attr); + if (mobile_target) { tabItem.setAttribute("mobile-restore", `#${mobile_target.parentElement.id}`); tabItem.append(mobile_target); - - } + + } } } - }else{ - if(tabItem.childElementCount > 0){ - const mobile_restore_attr = tabItem.getAttribute("mobile-restore"); - if(mobile_restore_attr){ - const mobile_restore_target = anapnoe_app.querySelector(mobile_restore_attr); - if(mobile_restore_target){ + } else { + if (tabItem.childElementCount > 0) { + const mobile_restore_attr = tabItem.getAttribute("mobile-restore"); + if (mobile_restore_attr) { + const mobile_restore_target = anapnoe_app.querySelector(mobile_restore_attr); + if (mobile_restore_target) { mobile_restore_target.append(tabItem.firstElementChild); - } + } } } - } + } }); - if(isMobile){ + if (isMobile) { anapnoe_app.querySelector(".accordion-vertical.expand #mask-icon-acc-arrow")?.click(); anapnoe_app.classList.add("default-mobile"); - }else{ + } else { anapnoe_app.classList.remove("default-mobile"); } } -function switchMobile(){ +function switchMobile() { + const anapnoe_app = document.querySelector(anapnoe_app_id); const optslayout = window.opts.uiux_default_layout; //console.log(optslayout); anapnoe_app.classList.add(`default-${optslayout.toLowerCase()}`); - if(optslayout === "Auto"){ - /* - window.addEventListener("resize", debounce(function(e){ - const isMobile = detectMobile(); - applyDefaultLayout(isMobile); - })); - */ - window.addEventListener('resize', function(event){ + if (optslayout === "Auto") { + /* + window.addEventListener("resize", debounce(function(e){ + const isMobile = detectMobile(); + applyDefaultLayout(isMobile); + })); + */ + window.addEventListener('resize', function(event) { const isMobile = detectMobile(); applyDefaultLayout(isMobile); }); applyDefaultLayout(detectMobile()); - }else if(optslayout === "Mobile"){ + } else if (optslayout === "Mobile") { applyDefaultLayout(true); - }else{ + } else { applyDefaultLayout(false); - } + } } function getRootContainer() { - return document.getElementById('tab_anapnoe_sd_uiux_core'); + return document.getElementById('tab_anapnoe_sd_uiux_core'); } function mainTabs(element, tab) { - if(active_main_tab){ - active_main_tab.style.display = 'none'; - } - const ntab = document.querySelector(tab); - if(ntab){ - ntab.style.display = 'block'; - //console.log(tab, ntab); - active_main_tab = ntab; - } + if (active_main_tab) { + active_main_tab.style.display = 'none'; + } + const ntab = document.querySelector(tab); + if (ntab) { + ntab.style.display = 'block'; + //console.log(tab, ntab); + active_main_tab = ntab; + } } function setAttrSelector(parent_elem, content_div, count, index, length) { - //const t = parent_elem.getAttribute("data-timeout"); - //const delay = t ? parseInt(t) : 0; - - //setTimeout(() => { + //const t = parent_elem.getAttribute("data-timeout"); + //const delay = t ? parseInt(t) : 0; - const mcount = count % 2; - //const parent_elem = this.el; + //setTimeout(() => { - const s = parent_elem.getAttribute("data-selector"); - const sp = parent_elem.getAttribute("data-parent-selector"); - + const mcount = count % 2; + //const parent_elem = this.el; - let target_elem; - - switch (mcount) { - case 0: - target_elem = document.querySelector(`${sp} ${s}`); - break; - case 1: - target_elem = content_div.querySelector(`${s}`); - break; - } + const s = parent_elem.getAttribute("data-selector"); + const sp = parent_elem.getAttribute("data-parent-selector"); - if (target_elem && parent_elem) { - parent_elem.append(target_elem); - total += 1; - console.log("register | Ref", index, sp, s); - const d = parent_elem.getAttribute('droppable'); - if (d) { - const childs = Array.from(parent_elem.children); - //console.log("droppable", target_elem, parent_elem, childs); - childs.forEach((c) => { - if (c !== target_elem) { - if (target_elem.className.indexOf('gradio-accordion') !== -1) { - target_elem.children[2].append(c); - } else { - target_elem.append(c); - } - } - }); - } + let target_elem; - const hb = parent_elem.getAttribute("show-button"); - if(hb){document.querySelector(hb)?.classList.remove("hidden");} + switch (mcount) { + case 0: + target_elem = document.querySelector(`${sp} ${s}`); + break; + case 1: + target_elem = content_div.querySelector(`${s}`); + break; + } - } else if (count < 4) { + if (target_elem && parent_elem) { + parent_elem.append(target_elem); + total += 1; + console.log("register | Ref", index, sp, s); + const d = parent_elem.getAttribute('droppable'); + const adx = parent_elem.getAttribute('append-index'); - const t = parent_elem.getAttribute("data-timeout"); - const delay = t ? parseInt(t) : 500; - - setTimeout(() => { - console.log( count + 1, "retry | ", delay, " | Ref", index, sp, s); - setAttrSelector(parent_elem, content_div, count + 1, index, length); - }, delay); - } else { - console.log("error | Ref", index, sp, s); - total += 1; + if (d) { + const childs = Array.from(parent_elem.children); + //console.log("droppable", target_elem, parent_elem, childs); + childs.forEach((c) => { + if (c !== target_elem) { + if (target_elem.className.indexOf('gradio-accordion') !== -1) { + if (adx) { + let idx = parseInt(adx, 10); + let ebefore = target_elem.children[2].children[idx]; + target_elem.children[2].insertBefore(c, ebefore); + } else { + target_elem.children[2].append(c); + } + } else { + target_elem.append(c); + } + } + }); + } - } + const hb = parent_elem.getAttribute("show-button"); + if (hb) { + document.querySelector(hb)?.classList.remove("hidden"); + } - if(total === length){ - console.log("Runtime components initilized"); - localStorage.setItem('UiUxReady', true); - } + } else if (count < 4) { - //}, delay ); + const t = parent_elem.getAttribute("data-timeout"); + const delay = t ? parseInt(t) : 500; + + setTimeout(() => { + console.log(count + 1, "retry | ", delay, " | Ref", index, sp, s); + setAttrSelector(parent_elem, content_div, count + 1, index, length); + }, delay); + + } else { + console.log("error | Ref", index, sp, s); + total += 1; + + } + + if (total === length) { + console.log("Runtime components initilized"); + localStorage.setItem('UiUxReady', true); + } + + //}, delay ); } -function testpopup(){ - const content_div = document.querySelector('#popup_tabitem'); - //const meta_id = document.querySelector('.global-popup-inner > div')?.id; - content_div.querySelectorAll(`.portal`).forEach((el, index, array) => { - /* +function testpopup() { + const content_div = document.querySelector('#popup_tabitem'); + //const meta_id = document.querySelector('.global-popup-inner > div')?.id; + content_div.querySelectorAll(`.portal`).forEach((el, index, array) => { + /* const childs = Array.from(el.children); childs.forEach((c) => { - const ac = c.getAttribute("meta-Id"); - if(ac){ - if(ac === meta_id){ - c.style.display = ""; - el.style.display = ""; - - }else{ - c.style.display = "none"; - if(childs.length === 1){ - el.style.display = "none"; - } - } - }else{ - c.setAttribute("meta-id", meta_id); - } - }); - */ - setAttrSelector(el, content_div, 0, index, array.length); - }); + const ac = c.getAttribute("meta-Id"); + if(ac){ + if(ac === meta_id){ + c.style.display = ""; + el.style.display = ""; - /* + }else{ + c.style.display = "none"; + if(childs.length === 1){ + el.style.display = "none"; + } + } + }else{ + c.setAttribute("meta-id", meta_id); + } + }); + */ + setAttrSelector(el, content_div, 0, index, array.length); + }); + + /* content_div.querySelectorAll(`.ae-popup .edit-user-metadata-buttons button`).forEach((el) => { - } + } */ } function createButtons4Extensions() { - const other_extensions = document.querySelector(`#other_extensions`); - const other_views = document.querySelector(`#split-left`); - //const other_views = document.querySelector(`#no-split-app`); - document.querySelectorAll(`#tabs > .tabitem`).forEach((c) => { - const cid = c.id; - const nid = cid.split('tab_')[1]; - if( cid !== "tab_txt2img" && + const other_extensions = document.querySelector(`#other_extensions`); + const other_views = document.querySelector(`#split-left`); + //const other_views = document.querySelector(`#no-split-app`); + document.querySelectorAll(`#tabs > .tabitem`).forEach((c) => { + const cid = c.id; + const nid = cid.split('tab_')[1]; + if (cid !== "tab_txt2img" && cid !== "tab_img2img" && cid !== "tab_extras" && cid !== "tab_pnginfo" && @@ -310,257 +322,257 @@ function createButtons4Extensions() { cid !== "tab_extensions" && cid !== "tab_ui_theme" && cid !== "tab_anapnoe_dock" && - cid !== "tab_anapnoe_sd_uiux_core") - - { - //tab_openpose_editor - const temp = document.createElement('div'); - temp.innerHTML= ` - `; - const btn = temp.firstElementChild; - other_extensions.append(btn); - //console.log(other_extensions, btn); + cid !== "tab_anapnoe_sd_uiux_core") { + //tab_openpose_editor + const temp = document.createElement('div'); + temp.innerHTML = ` + `; + const btn = temp.firstElementChild; + other_extensions.append(btn); + //console.log(other_extensions, btn); - temp.innerHTML= ` -
-
-
-
`; + temp.innerHTML = ` +
+
+
+
`; - const view = temp.firstElementChild; - //console.log(other_views, view); - other_views.append(view); - } + const view = temp.firstElementChild; + //console.log(other_views, view); + other_views.append(view); + } - }); + }); } -function setupExtraNetworksSearchSort(){ +function setupExtraNetworksSearchSort() { - const applyFilter = function(e) { + const applyFilter = function(e) { const search_term = e.target.value.toLowerCase(); - const cards = e.target.getAttribute("data-target"); - //console.log(search_term, cards ) + const cards = e.target.getAttribute("data-target"); + //console.log(search_term, cards ) gradioApp().querySelectorAll(cards).forEach(function(elem) { //const text = elem.querySelector(".search_term").textContent;//elem.getAttribute("data-path").toLowerCase().split("\\").join("/"); - //const text = elem.querySelector('.name').textContent.toLowerCase() + " " + elem.getAttribute("data-sort-path").toLowerCase();//elem.querySelector('.search_term').textContent.toLowerCase(); - const text = elem.getAttribute("data-sort-path").toLowerCase();//elem.querySelector('.search_term').textContent.toLowerCase(); - let visible = text.indexOf(search_term) != -1; - if (search_term.length < 3) { + //const text = elem.querySelector('.name').textContent.toLowerCase() + " " + elem.getAttribute("data-sort-path").toLowerCase();//elem.querySelector('.search_term').textContent.toLowerCase(); + const text = elem.getAttribute("data-sort-path").toLowerCase();//elem.querySelector('.search_term').textContent.toLowerCase(); + let visible = text.indexOf(search_term) != -1; + if (search_term.length < 3) { visible = true; } elem.style.display = visible ? "" : "none"; }); }; - document.querySelectorAll('.search_extra_networks').forEach((el) => { - el.addEventListener("input", applyFilter); - }) - - function applySort(el, sortKey) { + document.querySelectorAll('.search_extra_networks').forEach((el) => { + el.addEventListener("input", applyFilter); + }); - function comparator(cardA, cardB) { - var a = cardA.getAttribute(sortKey); - var b = cardB.getAttribute(sortKey); - if (!isNaN(a) && !isNaN(b)) { - return parseInt(a) - parseInt(b); - } - return (a < b ? -1 : (a > b ? 1 : 0)); - } + function applySort(el, sortKey) { - const cards_selector = el.getAttribute("data-target"); - const cards = gradioApp().querySelectorAll(cards_selector); - const cards_parent = cards[0].parentElement; - const cardsArray = Array.from(cards); - let sorted = cardsArray.sort(comparator); + function comparator(cardA, cardB) { + var a = cardA.getAttribute(sortKey); + var b = cardB.getAttribute(sortKey); + if (!isNaN(a) && !isNaN(b)) { + return parseInt(a) - parseInt(b); + } + return (a < b ? -1 : (a > b ? 1 : 0)); + } - const reverse_button = el.nextElementSibling;//closest(".reverse-order"); - const reverse = reverse_button.className.indexOf("active") !== -1; - if (reverse) { + const cards_selector = el.getAttribute("data-target"); + const cards = gradioApp().querySelectorAll(cards_selector); + const cards_parent = cards[0].parentElement; + const cardsArray = Array.from(cards); + let sorted = cardsArray.sort(comparator); + + const reverse_button = el.nextElementSibling;//closest(".reverse-order"); + const reverse = reverse_button.className.indexOf("active") !== -1; + if (reverse) { sorted.reverse(); } - sorted.forEach(e => cards_parent.appendChild(e)); + sorted.forEach(e => cards_parent.appendChild(e)); - } - - document.querySelectorAll('.extra_networks_order_by').forEach((el) => { - el.addEventListener('change', function(e) { - applySort(e.target, this.value); - //console.log('You selected: ', this.value); - }); - el.nextElementSibling.addEventListener('click', function(e) { - applySort(el, el.value); - console.log('You selected: ', el.value); - }); - }) + } + + document.querySelectorAll('.extra_networks_order_by').forEach((el) => { + el.addEventListener('change', function(e) { + applySort(e.target, this.value); + //console.log('You selected: ', this.value); + }); + el.nextElementSibling.addEventListener('click', function(e) { + applySort(el, el.value); + console.log('You selected: ', el.value); + }); + }); } -function updateExtraNetworksCards(el){ +function updateExtraNetworksCards(el) { - console.log("Starting optimizations for", el.id); - el.querySelectorAll(".card, .card-button").forEach((card) => { - const onclick_data = card.getAttribute("onClick"); - card.setAttribute("data-apply", onclick_data); - card.removeAttribute("onClick"); - if(card.getAttribute("data-name")){ - console.log("Remove EventListener", card.getAttribute("data-name")) - }else{ - const card_closest = card.closest(".card"); - if(card_closest && card_closest.getAttribute("data-name")){ - console.log("Remove EventListener", `${card_closest.getAttribute("data-name")} - ${card.getAttribute("title")}`) - } - - } - - }) + console.log("Starting optimizations for", el.id); + el.querySelectorAll(".card, .card-button").forEach((card) => { + const onclick_data = card.getAttribute("onClick"); + card.setAttribute("data-apply", onclick_data); + card.removeAttribute("onClick"); + if (card.getAttribute("data-name")) { + console.log("Remove EventListener", card.getAttribute("data-name")); + } else { + const card_closest = card.closest(".card"); + if (card_closest && card_closest.getAttribute("data-name")) { + console.log("Remove EventListener", `${card_closest.getAttribute("data-name")} - ${card.getAttribute("title")}`); + } - el.querySelectorAll(".tree-list-content-file").forEach((tree) => { - const onclick_data = tree.getAttribute("onClick"); - tree.setAttribute("data-apply", onclick_data); - tree.removeAttribute("onClick"); - console.log("Remove EventListener", `${tree.getAttribute("data-path")}`) - }) - - console.log("Attach EventListener", el.id); - el.addEventListener("click", function(e) { - const ctarget = e.target; - if(ctarget && ctarget.className.indexOf("card") !== -1 || ctarget && ctarget.className.indexOf("tree-list-content") !== -1 ) { - let data_apply = ctarget.getAttribute("data-apply"); - if(!data_apply){ - const onclick_data = ctarget.getAttribute("onClick"); - if(onclick_data){ - ctarget.setAttribute("data-apply", onclick_data); - ctarget.removeAttribute("onClick"); - } - } + } - const tabkey = active_main_tab.id.split("tab_")[1]; - if(tabkey === "img2img"){ - data_apply = data_apply?.replaceAll("txt2img", "img2img"); - }else{ - data_apply = data_apply?.replaceAll("img2img", "txt2img"); - } + }); - ctarget.setAttribute("onClick", data_apply ); - ctarget.click(); - ctarget.removeAttribute("onClick"); + el.querySelectorAll(".tree-list-content-file").forEach((tree) => { + const onclick_data = tree.getAttribute("onClick"); + tree.setAttribute("data-apply", onclick_data); + tree.removeAttribute("onClick"); + console.log("Remove EventListener", `${tree.getAttribute("data-path")}`); + }); - - if(ctarget.className.indexOf("tree-list-content-dir") !== -1){ - const search_closest = el.closest(".layout-extra-networks"); - if(search_closest && ctarget.getAttribute("data-path")){ - const search_field = search_closest.querySelector(".search_extra_networks"); - search_field.value = ctarget.getAttribute("data-path"); - updateInput(search_field); - } - } + console.log("Attach EventListener", el.id); + el.addEventListener("click", function(e) { + const ctarget = e.target; + if (ctarget && ctarget.className.indexOf("card") !== -1 || ctarget && ctarget.className.indexOf("tree-list-content") !== -1) { + let data_apply = ctarget.getAttribute("data-apply"); + if (!data_apply) { + const onclick_data = ctarget.getAttribute("onClick"); + if (onclick_data) { + ctarget.setAttribute("data-apply", onclick_data); + ctarget.removeAttribute("onClick"); + } + } - if(ctarget.className.indexOf("card-button") !== -1){ - data_apply = data_apply?.replaceAll("img2img", "txt2img"); - popup_trigger.click(); - testpopup(); - } + const tabkey = active_main_tab.id.split("tab_")[1]; + if (tabkey === "img2img") { + data_apply = data_apply?.replaceAll("txt2img", "img2img"); + } else { + data_apply = data_apply?.replaceAll("img2img", "txt2img"); + } + + ctarget.setAttribute("onClick", data_apply); + ctarget.click(); + ctarget.removeAttribute("onClick"); + + + if (ctarget.className.indexOf("tree-list-content-dir") !== -1) { + const search_closest = el.closest(".layout-extra-networks"); + if (search_closest && ctarget.getAttribute("data-path")) { + const search_field = search_closest.querySelector(".search_extra_networks"); + search_field.value = ctarget.getAttribute("data-path"); + updateInput(search_field); + } + } + + if (ctarget.className.indexOf("card-button") !== -1) { + data_apply = data_apply?.replaceAll("img2img", "txt2img"); + /* eslint-disable no-undef */ + popup_trigger.click(); + testpopup(); + } + + } + }); + + document.querySelectorAll("#txt2img_styles_edit_button, #img2img_styles_edit_button").forEach((elm) => { + elm.addEventListener("click", function(e) { + /* eslint-disable no-undef */ + popup_trigger.click(); + testpopup(); + }); + }); - } - }) - document.querySelectorAll("#txt2img_styles_edit_button, #img2img_styles_edit_button").forEach((elm) => { - elm.addEventListener("click", function(e) { - popup_trigger.click(); - testpopup(); - }) - }) - - } -function setupAnimationEventListeners(){ +function setupAnimationEventListeners() { - document.addEventListener('animationend', function (e) { - if (e.animationName === 'fade-out') { - e.target.classList.add('hidden'); - } - }); + document.addEventListener('animationend', function(e) { + if (e.animationName === 'fade-out') { + e.target.classList.add('hidden'); + } + }); const notransition = window.opts.uiux_disable_transitions; - document.addEventListener('animationstart', function (e) { - if (notransition) { + document.addEventListener('animationstart', function(e) { + if (notransition) { e.target.classList.add("notransition"); } - if (e.animationName === 'fade-out') { - if (e.target.className.indexOf('notransition') !== -1) { - e.target.classList.add('hidden'); - } - } - if (e.animationName === 'fade-in') { - e.target.classList.remove('hidden'); - } - }); + if (e.animationName === 'fade-out') { + if (e.target.className.indexOf('notransition') !== -1) { + e.target.classList.add('hidden'); + } + } + if (e.animationName === 'fade-in') { + e.target.classList.remove('hidden'); + } + }); } -function showContributors(){ - const contributors_btn = document.querySelector('#contributors'); - const contributors_view = document.querySelector('#contributors_tabitem'); - const temp = document.createElement('div'); - temp.id = 'contributors_grid'; - temp.innerHTML = `Kindly allow us a moment to retrieve the contributors. - We're grateful for the many individuals who have generously put their time and effort to make this possible.`; - contributors_view.append(temp); +function showContributors() { + const contributors_btn = document.querySelector('#contributors'); + const contributors_view = document.querySelector('#contributors_tabitem'); + const temp = document.createElement('div'); + temp.id = 'contributors_grid'; + temp.innerHTML = `Kindly allow us a moment to retrieve the contributors. + We're grateful for the many individuals who have generously put their time and effort to make this possible.`; + contributors_view.append(temp); - contributors_btn.addEventListener('click', function(e) { - //console.log(getAllContributors("anapnoe/stable-diffusion-webui-ux")); - if(!contributors_btn.getAttribute("data-visited")){ - contributors_btn.setAttribute("data-visited", "true"); - const promise = getAllContributorsRecursive("anapnoe/stable-diffusion-webui-ux"); - promise.then(function (result) { - //console.log(result) - temp.innerHTML = ""; - for (let i = 0; i < result.length; i++) { - const login = result[i].login; - const html_url = result[i].html_url; - const avatar_url = result[i].avatar_url; - temp.innerHTML += ` - -
-
- ${login} -
-
`; - } - }) - } - }); + contributors_btn.addEventListener('click', function(e) { + //console.log(getAllContributors("anapnoe/stable-diffusion-webui-ux")); + if (!contributors_btn.getAttribute("data-visited")) { + contributors_btn.setAttribute("data-visited", "true"); + const promise = getAllContributorsRecursive("anapnoe/stable-diffusion-webui-ux"); + promise.then(function(result) { + //console.log(result) + temp.innerHTML = ""; + for (let i = 0; i < result.length; i++) { + const login = result[i].login; + const html_url = result[i].html_url; + const avatar_url = result[i].avatar_url; + temp.innerHTML += ` + +
+
+ ${login} +
+
`; + } + }); + } + }); } -function onUiUxReady(content_div){ +function onUiUxReady(content_div) { - const interval = setInterval(() => { - const isUiUxReady = localStorage.getItem('UiUxReady'); - if( isUiUxReady === "true"){ - clearInterval(interval); + const interval = setInterval(() => { + const isUiUxReady = localStorage.getItem('UiUxReady'); + if (isUiUxReady === "true") { + clearInterval(interval); - const logger_screen = document.querySelector("#logger_screen"); - if(logger_screen){ - const asideconsole = document.querySelector("#layout-console-log"); - asideconsole.append(loggerUiUx); - logger_screen.remove(); - } + const logger_screen = document.querySelector("#logger_screen"); + if (logger_screen) { + const asideconsole = document.querySelector("#layout-console-log"); + asideconsole.append(loggerUiUx); + logger_screen.remove(); + } - console.log("Starting optimizations for Extra Networks"); - /* content_div.querySelector("#img2img_textual_inversion_cards_html")?.remove(); + console.log("Starting optimizations for Extra Networks"); + /* content_div.querySelector("#img2img_textual_inversion_cards_html")?.remove(); content_div.querySelector("#img2img_checkpoints_cards_html")?.remove(); content_div.querySelector("#img2img_hypernetworks_cards_html")?.remove(); content_div.querySelector("#img2img_lora_cards_html")?.remove(); @@ -570,562 +582,617 @@ function onUiUxReady(content_div){ console.log("Remove element #img2img_hypernetworks_cards_html"); console.log("Remove element #img2img_lora_cards_html"); */ - content_div.querySelectorAll(".extra-network-cards, .extra-network-tree").forEach((el) => { - updateExtraNetworksCards(el); - }); + content_div.querySelectorAll(".extra-network-cards, .extra-network-tree").forEach((el) => { + updateExtraNetworksCards(el); + }); - console.log("Finishing optimizations for Extra Networks"); - - setupExtraNetworksSearchSort(); + console.log("Finishing optimizations for Extra Networks"); - const ch_input = document.querySelector ("#setting_sd_model_checkpoint .secondary-wrap input"); - //const hash_target = document.querySelector('#sd_checkpoint_hash'); - //const hash_old_value = hash_target.textContent; - const hash_old_value = ch_input.value; - let oldcard = document.querySelector(`#txt2img_checkpoints_cards .card[data-apply*="${hash_old_value}"]`); - if(oldcard){ - oldcard?.classList.add("selected"); - console.log("Checkpoint name:", oldcard.getAttribute("data-name"), "
"); - } + setupExtraNetworksSearchSort(); - const ch_footer_preload = document.querySelector("#txt2img_checkpoints_main_footer .model-preloader"); - const ch_footer_selected = document.querySelector("#txt2img_checkpoints_main_footer .model-selected"); - - const ch_preload = document.querySelector ("#setting_sd_model_checkpoint .wrap"); - ch_footer_preload.append(ch_preload); + const ch_input = document.querySelector("#setting_sd_model_checkpoint .secondary-wrap input"); + //const hash_target = document.querySelector('#sd_checkpoint_hash'); + //const hash_old_value = hash_target.textContent; + const hash_old_value = ch_input.value; + let oldcard = document.querySelector(`#txt2img_checkpoints_cards .card[data-apply*="${hash_old_value}"]`); + if (oldcard) { + oldcard?.classList.add("selected"); + console.log("Checkpoint name:", oldcard.getAttribute("data-name"), "
"); + } - const preload_model_observer = new MutationObserver(function (mutations) { - mutations.forEach(function (m) { - if(oldcard){ - const hash_target = document.querySelector('#sd_checkpoint_hash'); - const hash_value = hash_target.textContent; - const card = document.querySelector(`#txt2img_checkpoints_cards .card[data-apply*="${hash_value}"]`); - if(card){ - if(oldcard !== card){ - oldcard.classList.remove("selected"); - card.classList.add("selected"); - oldcard = card; - ch_footer_selected.textContent = ch_input.value; - console.log("Checkpoint:", ch_input.value) - } - } - } - }); - }); + const ch_footer_preload = document.querySelector("#txt2img_checkpoints_main_footer .model-preloader"); + const ch_footer_selected = document.querySelector("#txt2img_checkpoints_main_footer .model-selected"); - preload_model_observer.observe(ch_preload, { childList: true, subtree: false }); + const ch_preload = document.querySelector("#setting_sd_model_checkpoint .wrap"); + ch_footer_preload.append(ch_preload); + + const preload_model_observer = new MutationObserver(function(mutations) { + mutations.forEach(function(m) { + if (oldcard) { + const hash_target = document.querySelector('#sd_checkpoint_hash'); + const hash_value = hash_target.textContent; + const card = document.querySelector(`#txt2img_checkpoints_cards .card[data-apply*="${hash_value}"]`); + if (card) { + if (oldcard !== card) { + oldcard.classList.remove("selected"); + card.classList.add("selected"); + oldcard = card; + ch_footer_selected.textContent = ch_input.value; + console.log("Checkpoint:", ch_input.value); + } + } + } + }); + }); + + preload_model_observer.observe(ch_preload, {childList: true, subtree: false}); - setupGenerateObservers(); + setupGenerateObservers(); uiuxOptionSettings(); - //const main_nav_content = document.querySelector('#main_nav_content'); - //const sidebar_tabs = document.querySelector('#sidebar_tabs'); - //main_nav_content.append(sidebar_tabs); - showContributors() + //const main_nav_content = document.querySelector('#main_nav_content'); + //const sidebar_tabs = document.querySelector('#sidebar_tabs'); + //main_nav_content.append(sidebar_tabs); + showContributors(); switchMobile(); - - + + localStorage.setItem('UiUxComplete', true); - //controlnet copy alpha mask for inpaint - //not needed any more but i will use this in img2img inpaint roundtrip - /* + //controlnet copy alpha mask for inpaint + //not needed any more but i will use this in img2img inpaint roundtrip + /* function copyCanvas(sourceId, destId) { - let sourceImage = document.querySelector(sourceId); - let destCanvas = document.querySelector(destId); + let sourceImage = document.querySelector(sourceId); + let destCanvas = document.querySelector(destId); - let destContext = destCanvas.getContext("2d"); - - destContext.clearRect(0, 0, destCanvas.width, destCanvas.height); - destContext.drawImage(sourceImage, 0, 0, destCanvas.width, destCanvas.height); - - let imageData = destContext.getImageData(0, 0, destCanvas.width, destCanvas.height); - let data = imageData.data; - - for (var i = 0; i < data.length; i += 4) { - var alpha = data[i + 3]; - data[i] = 255; - data[i + 1] = 255; - data[i + 2] = 255; - data[i + 3] = alpha; - } - - destContext.putImageData(imageData, 0, 0); + let destContext = destCanvas.getContext("2d"); + + destContext.clearRect(0, 0, destCanvas.width, destCanvas.height); + destContext.drawImage(sourceImage, 0, 0, destCanvas.width, destCanvas.height); + + let imageData = destContext.getImageData(0, 0, destCanvas.width, destCanvas.height); + let data = imageData.data; + + for (var i = 0; i < data.length; i += 4) { + var alpha = data[i + 3]; + data[i] = 255; + data[i + 1] = 255; + data[i + 2] = 255; + data[i + 3] = alpha; + } + + destContext.putImageData(imageData, 0, 0); } window.addEventListener("keydown", function(event) { - if (event.key === "0") { - let source = "#txt2img_controlnet_ControlNet-0_input_image img"; - let dest = "#txt2img_controlnet_ControlNet-0_input_image canvas[key=mask]"; - if(source && dest){ - copyCanvas(source, dest); - console.log("copy to canvas inpaint"); - } - } + if (event.key === "0") { + let source = "#txt2img_controlnet_ControlNet-0_input_image img"; + let dest = "#txt2img_controlnet_ControlNet-0_input_image canvas[key=mask]"; + if(source && dest){ + copyCanvas(source, dest); + console.log("copy to canvas inpaint"); + } + } + + if (event.key === "9") { + let source = "#img2img_controlnet_ControlNet-0_input_image img"; + let dest = "#img2img_controlnet_ControlNet-0_input_image canvas[key=mask]"; + if(source && dest){ + copyCanvas(source, dest); + console.log("copy to canvas inpaint"); + } + } + - if (event.key === "9") { - let source = "#img2img_controlnet_ControlNet-0_input_image img"; - let dest = "#img2img_controlnet_ControlNet-0_input_image canvas[key=mask]"; - if(source && dest){ - copyCanvas(source, dest); - console.log("copy to canvas inpaint"); - } - } - - }); */ - - } - }, 500); + + } + }, 500); } -function setupGenerateObservers(){ +function setupGenerateObservers() { - const keys = ["#txt2img", "#img2img"]; - keys.forEach((key) => { - - const tib = document.querySelector(key+'_interrupt'); - const tgb = document.querySelector(key+'_generate'); - const ti = tib.closest('.portal'); - const tg = tgb.closest('.ae-button'); - const ts = document.querySelector(key+'_skip').closest('.portal'); - const loop = document.querySelector(key+'_loop'); + const keys = ["#txt2img", "#img2img"]; + keys.forEach((key) => { - tib.addEventListener("click", function () { - loop.classList.add('stop'); - }); + const tib = document.querySelector(key + '_interrupt'); + const tgb = document.querySelector(key + '_generate'); + const ti = tib.closest('.portal'); + const tg = tgb.closest('.ae-button'); + const ts = document.querySelector(key + '_skip').closest('.portal'); + const loop = document.querySelector(key + '_loop'); - const gen_observer = new MutationObserver(function (mutations) { - mutations.forEach(function (m) { + tib.addEventListener("click", function() { + loop.classList.add('stop'); + }); - if (tib.style.display === 'none') { + const gen_observer = new MutationObserver(function(mutations) { + mutations.forEach(function(m) { - if (loop.className.indexOf('stop') !== -1 || loop.className.indexOf('active') === -1) { - loop.classList.remove('stop'); - ti.classList.add('disable'); - ts.classList.add('disable'); - tg.classList.remove('active'); - } else if (loop.className.indexOf('active') !== -1) { - tgb.click(); - } - } else { - ti.classList.remove('disable'); - ts.classList.remove('disable'); - tg.classList.add('active'); - } - }); - }); - - gen_observer.observe(tib, { attributes: true, attributeFilter: ['style'] }); - }); + if (tib.style.display === 'none') { + + if (loop.className.indexOf('stop') !== -1 || loop.className.indexOf('active') === -1) { + loop.classList.remove('stop'); + ti.classList.add('disable'); + ts.classList.add('disable'); + tg.classList.remove('active'); + } else if (loop.className.indexOf('active') !== -1) { + tgb.click(); + } + } else { + ti.classList.remove('disable'); + ts.classList.remove('disable'); + tg.classList.add('active'); + } + }); + }); + + gen_observer.observe(tib, {attributes: true, attributeFilter: ['style']}); + }); } function initDefaultComponents(content_div) { - const anapnoe_app = document.querySelector(anapnoe_app_id); - content_div.querySelectorAll(`div.split`).forEach((el) => { + const anapnoe_app = document.querySelector(anapnoe_app_id); + content_div.querySelectorAll(`div.split`).forEach((el) => { - let id = el.id; - let nid = content_div.querySelector(`#${id}`); + let id = el.id; + let nid = content_div.querySelector(`#${id}`); - const dir = nid?.getAttribute('direction') === 'vertical' ? 'vertical' : 'horizontal'; - const gutter = nid?.getAttribute('gutterSize') || '8'; + const dir = nid?.getAttribute('direction') === 'vertical' ? 'vertical' : 'horizontal'; + const gutter = nid?.getAttribute('gutterSize') || '8'; - const containers = content_div.querySelectorAll(`#${id} > div.split-container`); - const len = containers.length; - const r = 100 / len; - const ids = [], isize = [], msize = []; + const containers = content_div.querySelectorAll(`#${id} > div.split-container`); + const len = containers.length; + const r = 100 / len; + const ids = [], isize = [], msize = []; - for (let j = 0; j < len; j++) { - const c = containers[j]; - ids.push(`#${c.id}`); - const ji = c.getAttribute('data-initSize'); - const jm = c.getAttribute('data-minSize'); - isize.push(ji ? parseInt(ji) : r); - msize.push(jm ? parseInt(jm) : Infinity); + for (let j = 0; j < len; j++) { + const c = containers[j]; + ids.push(`#${c.id}`); + const ji = c.getAttribute('data-initSize'); + const jm = c.getAttribute('data-minSize'); + isize.push(ji ? parseInt(ji) : r); + msize.push(jm ? parseInt(jm) : Infinity); + } + + console.log("Split component", ids, isize, msize, dir, gutter); + + /* eslint-disable no-undef */ + split_instances[id] = Split(ids, { + sizes: isize, + minSize: msize, + direction: dir, + gutterSize: parseInt(gutter), + snapOffset: 0, + dragInterval: 1, + //expandToMin: true, + elementStyle: function(dimension, size, gutterSize) { + //console.log(dimension, size, gutterSize); + return { + 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)', + }; + }, + gutterStyle: function(dimension, gutterSize) { + return { + 'flex-basis': gutterSize + 'px', + 'min-width': gutterSize + 'px', + 'min-height': gutterSize + 'px', + }; + }, + }); + + }); + + //console.log(split_instances) + + content_div.querySelectorAll(`.portal`).forEach((el, index, array) => { + setAttrSelector(el, content_div, 0, index, array.length); + }); + + content_div.querySelectorAll(`.accordion-bar`).forEach((c) => { + const acc = c.parentElement; + const acc_split = acc.closest('.split-container'); + + let ctrg = c; + const atg = acc.getAttribute('iconTrigger'); + if (atg) { + const icn = content_div.querySelector(atg); + if (icn) { + ctrg = icn; + c.classList.add('pointer-events-none'); + } + } + + if (acc.className.indexOf('accordion-vertical') !== -1 && acc_split.className.indexOf('split') !== -1) { + + acc.classList.add('expand'); + //const acc_gutter = acc_split.previousElementSibling; + const acc_split_id = acc_split.parentElement.id; + const split_instance = split_instances[acc_split_id]; + acc_split.setAttribute('data-sizes', JSON.stringify(split_instance.getSizes())); + + ctrg?.addEventListener("click", () => { + acc.classList.toggle('expand'); + //acc_gutter.classList.toggle('pointer-events-none'); + if (acc_split.className.indexOf('v-expand') !== -1) { + acc_split.classList.remove('v-expand'); + split_instance.setSizes(JSON.parse(acc_split.getAttribute('data-sizes'))); + } else { + acc_split.classList.add('v-expand'); + let sizes = split_instance.getSizes(); + acc_split.setAttribute('data-sizes', JSON.stringify(sizes)); + + //console.log(sizes) + sizes[sizes.length - 1] = 0; + sizes[sizes.length - 2] = 100; + + const padding = parseFloat(window.getComputedStyle(c, null).getPropertyValue('padding-left')) * 2; + acc_split.style.minWidth = c.offsetWidth + padding + "px"; + + split_instance.setSizes(sizes); + } + }); + + } else { + ctrg?.addEventListener("click", () => { + acc.classList.toggle('expand'); + }); + } + }); + + + function callToAction(el, tids, pid) { + + const acc_bar = el.closest(".accordion-bar"); + if (acc_bar) { + const acc = acc_bar.parentElement; + if (acc.className.indexOf('expand') === -1) { + let ctrg = acc_bar; + const atg = acc.getAttribute('iconTrigger'); + if (atg) { + const icn = content_div.querySelector(atg); + if (icn) { + ctrg = icn; + } + } + ctrg.click(); + } + } + + const txt = el.querySelector('span')?.innerHTML.toLowerCase(); + //console.log(txt, pid) + //window.alert(txt, pid); + /* + if (txt && pid) { + document.querySelectorAll(`${pid} .tab-nav button, [data-parent-selector="${pid}"] .tab-nav button`).forEach(function (elm) { + //console.log(elm.innerHTML, txt); + if (elm.innerHTML.toLowerCase().indexOf(txt) !== -1) { + elm.click(); + } + }); } - - console.log("Split component", ids, isize, msize, dir, gutter); - - split_instances[id] = Split(ids, { - sizes: isize, - minSize: msize, - direction: dir, - gutterSize: parseInt(gutter), - snapOffset: 0, - dragInterval: 1, - //expandToMin: true, - elementStyle: function (dimension, size, gutterSize) { - //console.log(dimension, size, gutterSize); - return { - 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)', - } - }, - gutterStyle: function (dimension, gutterSize) { - return { - 'flex-basis': gutterSize + 'px', - 'min-width': gutterSize + 'px', - 'min-height': gutterSize + 'px', - } - }, - }); - - }); - - //console.log(split_instances) - - content_div.querySelectorAll(`.portal`).forEach((el, index, array) => { - setAttrSelector(el, content_div, 0, index, array.length); - }); - - content_div.querySelectorAll(`.accordion-bar`).forEach((c) => { - const acc = c.parentElement; - const acc_split = acc.closest('.split-container'); - - let ctrg = c; - const atg = acc.getAttribute('iconTrigger'); - if (atg) { - const icn = content_div.querySelector(atg); - if (icn) { - ctrg = icn; - c.classList.add('pointer-events-none'); - } - } - - if (acc.className.indexOf('accordion-vertical') !== -1 && acc_split.className.indexOf('split') !== -1) { - - acc.classList.add('expand'); - //const acc_gutter = acc_split.previousElementSibling; - const acc_split_id = acc_split.parentElement.id; - const split_instance = split_instances[acc_split_id]; - acc_split.setAttribute('data-sizes', JSON.stringify(split_instance.getSizes())); - - ctrg?.addEventListener("click", () => { - acc.classList.toggle('expand'); - //acc_gutter.classList.toggle('pointer-events-none'); - if (acc_split.className.indexOf('v-expand') !== -1) { - acc_split.classList.remove('v-expand'); - split_instance.setSizes(JSON.parse(acc_split.getAttribute('data-sizes'))) - } else { - acc_split.classList.add('v-expand'); - let sizes = split_instance.getSizes(); - acc_split.setAttribute('data-sizes', JSON.stringify(sizes)); - - //console.log(sizes) - sizes[sizes.length-1] = 0; - sizes[sizes.length-2] = 100; - - const padding = parseFloat(window.getComputedStyle(c, null).getPropertyValue('padding-left')) * 2; - acc_split.style.minWidth = c.offsetWidth+padding+"px"; - - split_instance.setSizes(sizes) - } - }); - - } else { - ctrg?.addEventListener("click", () => { acc.classList.toggle('expand') }); - } - }); + */ - function callToAction(el, tids, pid) { + } - const acc_bar = el.closest(".accordion-bar"); - if (acc_bar) { - const acc = acc_bar.parentElement; - if (acc.className.indexOf('expand') === -1) { - let ctrg = acc_bar; - const atg = acc.getAttribute('iconTrigger'); - if (atg) { - const icn = content_div.querySelector(atg); - if (icn) { - ctrg = icn; - } - } - ctrg.click(); - } - } + content_div.querySelectorAll(`.xtabs-tab`).forEach((el) => { - const txt = el.querySelector('span')?.innerHTML.toLowerCase(); - //console.log(txt, pid) - if (txt && pid) { - document.querySelectorAll(`${pid} .tab-nav button, [data-parent-selector="${pid}"] .tab-nav button`).forEach(function (elm) { - /* console.log(elm.innerHTML, txt) */ - if (elm.innerHTML.toLowerCase().indexOf(txt) !== -1) { - elm.click(); - } - }); - } + el.addEventListener('click', () => { + const tabParent = el.parentElement; + const tgroup = el.getAttribute("tabGroup"); + const pid = el.getAttribute("data-click"); - - } + function hideActive(tab) { + tab.classList.remove('active'); + const tids = tab.getAttribute("tabItemId"); + anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { + //tabItem.classList.add('hidden'); + tabItem.classList.remove('fade-in'); + tabItem.classList.add('fade-out'); + }); + } - content_div.querySelectorAll(`.xtabs-tab`).forEach((el) => { + if (tgroup) { + anapnoe_app.querySelectorAll(`[tabGroup="${tgroup}"]`) + .forEach((tab) => { + if (tab.className.indexOf('active') !== -1) { + hideActive(tab); + } + }); - el.addEventListener('click', () => { - const tabParent = el.parentElement; - const tgroup = el.getAttribute("tabGroup"); - const pid = el.getAttribute("data-click"); + } else if (tabParent) { + const tabs = [].slice.call(tabParent.children); + tabs.forEach((tab) => { + if (tab.className.indexOf('active') !== -1) { + hideActive(tab); + } + }); + } - function hideActive(tab) { - tab.classList.remove('active'); - const tids = tab.getAttribute("tabItemId"); - anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { - //tabItem.classList.add('hidden'); - tabItem.classList.remove('fade-in'); - tabItem.classList.add('fade-out'); - }); - } + const tids = el.getAttribute("tabItemId"); + anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { + //tabItem.classList.remove('hidden'); + tabItem.classList.remove('fade-out'); + tabItem.classList.add('fade-in'); + //console.log('tab', tids, tabItem); + }); - if (tgroup) { - anapnoe_app.querySelectorAll(`[tabGroup="${tgroup}"]`) - .forEach((tab) => { - if (tab.className.indexOf('active') !== -1) { - hideActive(tab); - } - }); + el.classList.add('active'); + callToAction(el, tids, pid); - } else if (tabParent) { - const tabs = [].slice.call(tabParent.children); - tabs.forEach((tab) => { - if (tab.className.indexOf('active') !== -1) { - hideActive(tab); - } - }); - } + }); - const tids = el.getAttribute("tabItemId"); - anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { - //tabItem.classList.remove('hidden'); - tabItem.classList.remove('fade-out'); - tabItem.classList.add('fade-in'); - //console.log('tab', tids, tabItem); - }); + const active = el.getAttribute("active"); + if (!active) { + const tids = el.getAttribute("tabItemId"); + anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { + //tabItem.classList.add('hidden'); + tabItem.classList.remove('fade-in'); + tabItem.classList.add('fade-out'); + }); + } - el.classList.add('active'); - callToAction(el, tids, pid); + }); - }); + content_div.querySelectorAll(`.xtabs-tab[active]`).forEach((el) => { + el.classList.add('active'); + const tids = el.getAttribute("tabItemId"); + const pid = el.getAttribute("data-click"); + anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { + //tabItem.classList.remove('hidden'); + tabItem.classList.remove('fade-out'); + tabItem.classList.add('fade-in'); + }); + callToAction(el, tids, pid); + //console.log('tab', tids, el); + }); - const active = el.getAttribute("active"); - if (!active) { - const tids = el.getAttribute("tabItemId"); - anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { - //tabItem.classList.add('hidden'); - tabItem.classList.remove('fade-in'); - tabItem.classList.add('fade-out'); - }); - } + content_div.querySelectorAll(`.ae-button`).forEach((el) => { + const toggle = el.getAttribute("toggle"); + const active = el.getAttribute("active"); + const input = el.querySelector('input'); - }); + if (input) { + if (input.checked === true && !active) { + input.click(); + } else if (input.checked === false && active) { + input.click(); + } + } - content_div.querySelectorAll(`.xtabs-tab[active]`).forEach((el) => { - el.classList.add('active'); - const tids = el.getAttribute("tabItemId"); - const pid = el.getAttribute("data-click"); - anapnoe_app.querySelectorAll(tids).forEach((tabItem) => { - //tabItem.classList.remove('hidden'); - tabItem.classList.remove('fade-out'); - tabItem.classList.add('fade-in'); - }); - callToAction(el, tids, pid); - //console.log('tab', tids, el); - }); - - content_div.querySelectorAll(`.ae-button`).forEach((el) => { - const toggle = el.getAttribute("toggle"); - const active = el.getAttribute("active"); - const input = el.querySelector('input'); - - if (input) { - if (input.checked === true && !active) { - input.click(); - } else if (input.checked === false && active) { - input.click(); - } - } - - if (active) { - el.classList.add('active'); - } else { - el.classList.remove('active'); - } + if (active) { + el.classList.add('active'); + } else { + el.classList.remove('active'); + } - if (toggle) { - el.addEventListener('click', (e) => { - const input = el.querySelector('input'); - if (input) { - input.click(); - if (input.checked === true) { - el.classList.add('active'); - } else if (input.checked === false) { - el.classList.remove('active'); - } - } else { - el.classList.toggle('active'); - } - }); - } + if (toggle) { + el.addEventListener('click', (e) => { + const input = el.querySelector('input'); + if (input) { + input.click(); + if (input.checked === true) { + el.classList.add('active'); + } else if (input.checked === false) { + el.classList.remove('active'); + } + } else { + el.classList.toggle('active'); + } + }); + } - const adc = el.getAttribute("data-click"); - if(adc){ - el.addEventListener('click', (e) => { + const adc = el.getAttribute("data-click"); + if (adc) { + el.addEventListener('click', (e) => { - if(el.className.indexOf("refresh-extra-networks") !== -1){ - const ctemp = el.closest(".template"); - const ckey = ctemp?.getAttribute("key") || "txt2img"; - //console.log(ckey); - setTimeout(() => { - const tempnet = document.querySelector(`#txt2img_temp_tabitem #${ckey}_cards`); - if(tempnet){ - ctemp.querySelector(".extra-network-cards")?.remove(); - ctemp.querySelector(".extra-network-subdirs")?.remove(); - ctemp.querySelectorAll(`.portal`).forEach((el, index, array) => { - setAttrSelector(el, ctemp, 0, index, array.length); - }); - updateExtraNetworksCards(ctemp); - } - }, 1000); - - } + if (el.className.indexOf("refresh-extra-networks") !== -1) { + const ctemp = el.closest(".template"); + const ckey = ctemp?.getAttribute("key") || "txt2img"; - //}else{ - document.querySelectorAll(adc).forEach((el) => { - el.click(); - }) - //} - }) - } + const cpane = document.querySelector(`#${ckey}_pane > div`); + const elementsToAppend = [ + document.querySelector(`#${ckey}_dirs`), + document.querySelector(`#${ckey}_tree`), + document.querySelector(`#${ckey}_cards`) + ]; - }); + elementsToAppend.forEach(element => { + if (element) { + element.querySelectorAll("[data-apply]").forEach((ca) => { + const data_apply = ca.getAttribute("data-apply"); + ca.setAttribute("onClick", data_apply); + ca.removeAttribute("data-apply"); + }); + cpane.append(element); + } + }); + + const btn_refresh_internal = document.querySelector(`#${ckey}_extra_refresh_internal`); + btn_refresh_internal.dispatchEvent(new Event("click")); + + setTimeout(() => { + + const asd = document.querySelector(`#${ckey}_aside_split`); + asd.querySelectorAll(`.portal`).forEach((elm, index, array) => { + setAttrSelector(elm, asd, 0, index, array.length); + }); + + //setupExtraNetworksForTab('txt2img'); + + //updateExtraNetworksCards(asd); + + }, 1000); + + } + + //}else{ + document.querySelectorAll(adc).forEach((el) => { + el.click(); + }); + //} + }); + } + + }); - // try to attach Logger Screen to main before full UIUXReady - const asideconsole = document.querySelector("#layout-console-log"); - asideconsole.append(loggerUiUx); - document.querySelector("#logger_screen")?.remove(); + // try to attach Logger Screen to main before full UIUXReady + const asideconsole = document.querySelector("#layout-console-log"); + asideconsole.append(loggerUiUx); + document.querySelector("#logger_screen")?.remove(); } -function uiuxOptionSettings(){ +function uiuxOptionSettings() { + const anapnoe_app = document.querySelector(anapnoe_app_id); // sd max resolution output function sdMaxOutputResolution(value) { gradioApp().querySelectorAll('[id$="2img_width"] input,[id$="2img_height"] input').forEach((elem) => { elem.max = value; - }) + }); } - gradioApp().querySelector("#setting_uiux_max_resolution_output").addEventListener('input', function (e) { + gradioApp().querySelector("#setting_uiux_max_resolution_output").addEventListener('input', function(e) { let intvalue = parseInt(e.target.value); intvalue = Math.min(Math.max(intvalue, 512), 16384); - sdMaxOutputResolution(intvalue); - }) + sdMaxOutputResolution(intvalue); + }); sdMaxOutputResolution(window.opts.uiux_max_resolution_output); - // step ticks for performant input range - function uiux_show_input_range_ticks(value, interactive) { - if (value) { - const range_selectors = "input[type='range']"; - //const range_selectors = "[id$='_clone']:is(input[type='range'])"; - gradioApp() - .querySelectorAll(range_selectors) - .forEach(function (elem) { - let spacing = (elem.step / (elem.max - elem.min)) * 100.0; - let tsp = "max(3px, calc(" + spacing + "% - 1px))"; - let fsp = "max(4px, calc(" + spacing + "% + 0px))"; - var style = elem.style; - style.setProperty( - "--ae-slider-bg-overlay", - "repeating-linear-gradient( 90deg, transparent, transparent " + + // step ticks for performant input range + function uiux_show_input_range_ticks(value, interactive) { + if (value) { + const range_selectors = "input[type='range']"; + //const range_selectors = "[id$='_clone']:is(input[type='range'])"; + gradioApp() + .querySelectorAll(range_selectors) + .forEach(function(elem) { + let spacing = (elem.step / (elem.max - elem.min)) * 100.0; + let tsp = "max(3px, calc(" + spacing + "% - 1px))"; + let fsp = "max(4px, calc(" + spacing + "% + 0px))"; + var style = elem.style; + style.setProperty( + "--ae-slider-bg-overlay", + "repeating-linear-gradient( 90deg, transparent, transparent " + tsp + ", var(--ae-input-border-color) " + tsp + ", var(--ae-input-border-color) " + fsp + " )" - ); - }); - } else if (interactive) { - gradioApp() - .querySelectorAll("input[type='range']") - .forEach(function (elem) { - var style = elem.style; - style.setProperty("--ae-slider-bg-overlay", "transparent"); - }); - } - } - - gradioApp().querySelector("#setting_uiux_show_input_range_ticks input").addEventListener("click", function (e) { - uiux_show_input_range_ticks(e.target.checked, true); - }); - uiux_show_input_range_ticks(window.opts.uiux_show_input_range_ticks); + ); + }); + } else if (interactive) { + gradioApp() + .querySelectorAll("input[type='range']") + .forEach(function(elem) { + var style = elem.style; + style.setProperty("--ae-slider-bg-overlay", "transparent"); + }); + } + } + + gradioApp().querySelector("#setting_uiux_show_input_range_ticks input").addEventListener("click", function(e) { + uiux_show_input_range_ticks(e.target.checked, true); + }); + uiux_show_input_range_ticks(window.opts.uiux_show_input_range_ticks); - function remove_overrides(){ - let checked_overrides = []; - gradioApp().querySelectorAll("#setting_uiux_ignore_overrides input").forEach(function (elem, i){ - if(elem.checked){ - checked_overrides[i] = elem.nextElementSibling.innerHTML; - } - }) - //console.log(checked_overrides); - gradioApp().querySelectorAll("[id$='2img_override_settings'] .token").forEach(function (token){ - let token_arr = token.querySelector("span").innerHTML.split(":"); - let token_name = token_arr[0]; - let token_value = token_arr[1]; - token_value = token_value.replaceAll(" ", ""); - - /* if(token_name.indexOf("Model hash") != -1){ - const info_label = gradioApp().querySelector("[id$='2img_override_settings'] label span"); - info_label.innerHTML = "Override settings MDL: unknown"; - for (let m=0; m"; - break; - } - } + function remove_overrides() { + let checked_overrides = []; + gradioApp().querySelectorAll("#setting_uiux_ignore_overrides input").forEach(function(elem, i) { + if (elem.checked) { + checked_overrides[i] = elem.nextElementSibling.innerHTML; + } + }); + + gradioApp().querySelectorAll("[id$='2img_override_settings'] .token").forEach(function(token) { + let token_arr = token.querySelector("span").innerHTML.split(":"); + let token_name = token_arr[0]; + let token_value = token_arr[1]; + token_value = token_value.replaceAll(" ", ""); + + /* if(token_name.indexOf("Model hash") != -1){ + const info_label = gradioApp().querySelector("[id$='2img_override_settings'] label span"); + info_label.innerHTML = "Override settings MDL: unknown"; + for (let m=0; m"; + break; + } + } } */ - if(checked_overrides.indexOf(token_name) != -1){ - token.querySelector(".token-remove").click(); - gradioApp().querySelector("[id$='2img_override_settings']").classList.add("show"); - }else{ - // maybe we add them again, for now we can select and add the removed tokens manually from the drop down - } - }) - } - gradioApp().querySelector("#setting_uiux_ignore_overrides").addEventListener('click', function (e) { - setTimeout(function() { remove_overrides(); }, 100); - }) - /* - gradioApp().querySelectorAll("#pnginfo_send_buttons button, #paste").forEach(function (elem) { - elem.addEventListener("click", function (e) { - setTimeout(function () { + + if (checked_overrides.indexOf(token_name) != -1) { + gradioApp().querySelector("[id$='2img_override_settings']").classList.add("show"); + token.querySelector(".token-remove").click(); + + } else { + // maybe we add them again, for now we can select and add the removed tokens manually from the drop down + } + }); + } + gradioApp().querySelector("#setting_uiux_ignore_overrides").addEventListener('click', function(e) { + setTimeout(function() { remove_overrides(); - }, 500); - }); - }); - */ - const overrides_observer = new MutationObserver(function(mutations) { + }, 100); + }); + /* + gradioApp().querySelectorAll("#pnginfo_send_buttons button, #paste").forEach(function (elem) { + elem.addEventListener("click", function (e) { + setTimeout(function () { + remove_overrides(); + }, 500); + }); + }); + */ + + const overrides_observer_class = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { - if(mutation.addedNodes){ - //console.log(mutation.addedNodes.length + ' nodes added'); - setTimeout(function() { remove_overrides(); }, 100); + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + //console.log(`Class changed to: ${mutation.target.className}`); + setTimeout(function() { + remove_overrides(); + }, 1000); } }); }); - - gradioApp().querySelectorAll("#txt2img_override_settings .wrap-inner, #img2img_override_settings .wrap_inner").forEach(function (elem) { - overrides_observer.observe(elem, { childList: true }); - }); - + + const overrides_observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.addedNodes.length) { + //console.log(mutation.addedNodes.length + ' nodes added'); + setTimeout(function() { + remove_overrides(); + }, 1000); + } + }); + }); + + document.querySelectorAll("#txt2img_override_settings .wrap-inner, #img2img_override_settings .wrap_inner").forEach(function(elem) { + overrides_observer.observe(elem, { + childList: true + }); + }); + + document.querySelectorAll("#txt2img_nav, #img2img_nav").forEach(function(elem) { + overrides_observer_class.observe(elem, { + attributes: true + }); + }); + function uiux_no_slider_layout(value) { if (value) { @@ -1135,7 +1202,7 @@ function uiuxOptionSettings(){ } } - gradioApp().querySelector("#setting_uiux_no_slider_layout input").addEventListener("click", function (e) { + gradioApp().querySelector("#setting_uiux_no_slider_layout input").addEventListener("click", function(e) { uiux_no_slider_layout(e.target.checked, true); }); @@ -1148,7 +1215,7 @@ function uiuxOptionSettings(){ anapnoe_app.classList.remove("aside-labels"); } } - gradioApp().querySelector("#setting_uiux_show_labels_aside input").addEventListener("click", function (e) { + gradioApp().querySelector("#setting_uiux_show_labels_aside input").addEventListener("click", function(e) { uiux_show_labels_aside(e.target.checked, true); }); uiux_show_labels_aside(window.opts.uiux_show_labels_aside); @@ -1161,7 +1228,7 @@ function uiuxOptionSettings(){ anapnoe_app.classList.remove("main-labels"); } } - gradioApp().querySelector("#setting_uiux_show_labels_main input").addEventListener("click", function (e) { + gradioApp().querySelector("#setting_uiux_show_labels_main input").addEventListener("click", function(e) { uiux_show_labels_main(e.target.checked, true); }); uiux_show_labels_main(window.opts.uiux_show_labels_main); @@ -1174,241 +1241,241 @@ function uiuxOptionSettings(){ anapnoe_app.classList.remove("tab-labels"); } } - gradioApp().querySelector("#setting_uiux_show_labels_tabs input").addEventListener("click", function (e) { + gradioApp().querySelector("#setting_uiux_show_labels_tabs input").addEventListener("click", function(e) { uiux_show_labels_tabs(e.target.checked, true); }); uiux_show_labels_tabs(window.opts.uiux_show_labels_tabs); - + const comp_mobile_scale_range = gradioApp().querySelector("#setting_uiux_mobile_scale input[type=range]"); comp_mobile_scale_range.classList.add("hidden"); const comp_mobile_scale = gradioApp().querySelector("#setting_uiux_mobile_scale input[type=number]"); function uiux_mobile_scale(value) { const viewport = document.head.querySelector('meta[name="viewport"]'); - viewport.setAttribute("content", `width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=${value}`); + viewport.setAttribute("content", `width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=${value}`); } - comp_mobile_scale.addEventListener("change", function (e) { + comp_mobile_scale.addEventListener("change", function(e) { //e.preventDefault(); //e.stopImmediatePropagation() comp_mobile_scale.value = e.target.value; window.updateInput(comp_mobile_scale); console.log('change', e.target.value); - uiux_mobile_scale(e.target.value); + uiux_mobile_scale(e.target.value); }); - + uiux_mobile_scale(window.opts.uiux_mobile_scale); } -function setupAdditionalStylesForExtensions() { - // styles for Infinite_image_browsing - const iibframe = document.querySelector("#infinite_image_browsing_container_wrapper > iframe"); - if(iibframe){ - var css = document.querySelector('[rel="stylesheet"][href*="user"]'); - var rootRules = Array.from(css.sheet.cssRules).filter(function(cssRule) { - return (cssRule instanceof CSSStyleRule && cssRule.selectorText === ":root"); - }); - var rootCssText = rootRules[0].cssText; +function setupAdditionalStylesForExtensions() { + // styles for Infinite_image_browsing + const iibframe = document.querySelector("#infinite_image_browsing_container_wrapper > iframe"); + if (iibframe) { + var css = document.querySelector('[rel="stylesheet"][href*="user"]'); + var rootRules = Array.from(css.sheet.cssRules).filter(function(cssRule) { + return (cssRule instanceof CSSStyleRule && cssRule.selectorText === ":root"); + }); + var rootCssText = rootRules[0].cssText; - iibframe.addEventListener("load", ev => { - const new_style_element = document.createElement("style"); - //getComputedStyle(document.documentElement).getPropertyValue('--my-variable-name'); - new_style_element.textContent = - ` - ${rootCssText} - :root body { - --zp-primary: var(--ae-primary-color)!important; - --zp-secondary: var(--ae-secondary-color)!important; - --zp-tertiary: var(--ae-secondary-color)!important; - --zp-primary-background: var(--ae-panel-bg-color)!important; - --zp-secondary-background: var(--ae-panel-bg-color)!important; - --zp-secondary-variant-background: var(--ae-input-bg-color)!important; - --zp-tertiary-background: var(--ae-input-bg-color)!important; - --zp-border: var(--ae-input-border-color)!important; - --zp-icon-bg: var(--ae-primary-color)!important; - } - #zanllp_dev_gradio_fe .ant-tabs-tabpane > .container .header + .ant-alert, - #zanllp_dev_gradio_fe .ant-tabs-tabpane > .container .header{ - /*display:none!important;*/ - } - #zanllp_dev_gradio_fe .ant-tabs-tabpane > .container .ant-radio-group{ - display:none!important; - } + iibframe.addEventListener("load", ev => { + const new_style_element = document.createElement("style"); + //getComputedStyle(document.documentElement).getPropertyValue('--my-variable-name'); + new_style_element.textContent = + ` + ${rootCssText} + :root body { + --zp-primary: var(--ae-primary-color)!important; + --zp-secondary: var(--ae-secondary-color)!important; + --zp-tertiary: var(--ae-secondary-color)!important; + --zp-primary-background: var(--ae-panel-bg-color)!important; + --zp-secondary-background: var(--ae-panel-bg-color)!important; + --zp-secondary-variant-background: var(--ae-input-bg-color)!important; + --zp-tertiary-background: var(--ae-input-bg-color)!important; + --zp-border: var(--ae-input-border-color)!important; + --zp-icon-bg: var(--ae-primary-color)!important; + } + #zanllp_dev_gradio_fe .ant-tabs-tabpane > .container .header + .ant-alert, + #zanllp_dev_gradio_fe .ant-tabs-tabpane > .container .header{ + /*display:none!important;*/ + } + #zanllp_dev_gradio_fe .ant-tabs-tabpane > .container .ant-radio-group{ + display:none!important; + } - #zanllp_dev_gradio_fe .file.grid,{ - border-radius:var(--ae-input-border-radius)!important; - margin:0 !important; - } - #zanllp_dev_gradio_fe .file.grid .ant-image, .file.grid .preview-icon-wrap { - border-radius: var(--ae-border-radius); - border: var(--ae-input-border-size) solid var(--ae-input-border-color); - background-color: var(--ae-input-bg-color); - border:0; - } + #zanllp_dev_gradio_fe .file.grid,{ + border-radius:var(--ae-input-border-radius)!important; + margin:0 !important; + } + #zanllp_dev_gradio_fe .file.grid .ant-image, .file.grid .preview-icon-wrap { + border-radius: var(--ae-border-radius); + border: var(--ae-input-border-size) solid var(--ae-input-border-color); + background-color: var(--ae-input-bg-color); + border:0; + } - .ant-image-preview-wrap { - background-color: var(--ae-main-bg-color) !important; - } + .ant-image-preview-wrap { + background-color: var(--ae-main-bg-color) !important; + } - #zanllp_dev_gradio_fe .view .file-list, - #zanllp_dev_gradio_fe .view { - padding: 0; - } + #zanllp_dev_gradio_fe .view .file-list, + #zanllp_dev_gradio_fe .view { + padding: 0; + } - #zanllp_dev_gradio_fe .file.grid { - margin: 2px; - border-radius: var(--ae-border-radius); - background-color: var(--ae-input-bg-color) !important; - padding: 4px; - border: var(--ae-input-border-size) solid var(--ae-input-border-color); - } + #zanllp_dev_gradio_fe .file.grid { + margin: 2px; + border-radius: var(--ae-border-radius); + background-color: var(--ae-input-bg-color) !important; + padding: 4px; + border: var(--ae-input-border-size) solid var(--ae-input-border-color); + } - #zanllp_dev_gradio_fe .location-bar { - padding: 5px; - border-bottom: 0; - } + #zanllp_dev_gradio_fe .location-bar { + padding: 5px; + border-bottom: 0; + } - #zanllp_dev_gradio_fe a { - color: var(--ae-primary-color); - } + #zanllp_dev_gradio_fe a { + color: var(--ae-primary-color); + } - #zanllp_dev_gradio_fe .file.selected{ - outline: 2px solid var(--ae-primary-color); - } + #zanllp_dev_gradio_fe .file.selected{ + outline: 2px solid var(--ae-primary-color); + } - li.file.grid .ant-image, li.file.grid .preview-icon-wrap { - border: 0 !important; - border-radius:0 !important; - } - .file.grid .preview-icon-wrap { - margin-bottom:6px !important; - } - .ant-dropdown-menu { - background-color: var(--ae-input-bg-color) !important; - - } - .ant-dropdown-menu-item, .ant-dropdown-menu-submenu-title { - color: var(--ae-input-text-color) !important; - } - .ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover { - background-color: var(--ae-primary-color) !important; - color: var(--ae-input-hover-text-color) !important; - } - .ant-btn:hover, .ant-btn:focus { - color: var(--ae-input-hover-text-color) !important; - border-color: transparent !important; - background: var(--ae-primary-color) !important; - } + li.file.grid .ant-image, li.file.grid .preview-icon-wrap { + border: 0 !important; + border-radius:0 !important; + } + .file.grid .preview-icon-wrap { + margin-bottom:6px !important; + } + .ant-dropdown-menu { + background-color: var(--ae-input-bg-color) !important; + + } + .ant-dropdown-menu-item, .ant-dropdown-menu-submenu-title { + color: var(--ae-input-text-color) !important; + } + .ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover { + background-color: var(--ae-primary-color) !important; + color: var(--ae-input-hover-text-color) !important; + } + .ant-btn:hover, .ant-btn:focus { + color: var(--ae-input-hover-text-color) !important; + border-color: transparent !important; + background: var(--ae-primary-color) !important; + } - .ant-menu-item-divider { - border-color: var(--ae-input-border-color) !important; - } - .ant-tabs { - color: var(--ae-label-color) !important; - } - body ::-webkit-scrollbar-thumb:hover { - background-color: var(--ae-primary-color) !important; - } - .ant-btn, - .ant-tabs-card>.ant-tabs-nav .ant-tabs-tab, .ant-tabs-card>div>.ant-tabs-nav .ant-tabs-tab { - border-radius: var(--ae-border-radius) !important; - background-color: var(--ae-input-bg-color) !important; - border: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; - color: var(--ae-primary-color) !important; - } + .ant-menu-item-divider { + border-color: var(--ae-input-border-color) !important; + } + .ant-tabs { + color: var(--ae-label-color) !important; + } + body ::-webkit-scrollbar-thumb:hover { + background-color: var(--ae-primary-color) !important; + } + .ant-btn, + .ant-tabs-card>.ant-tabs-nav .ant-tabs-tab, .ant-tabs-card>div>.ant-tabs-nav .ant-tabs-tab { + border-radius: var(--ae-border-radius) !important; + background-color: var(--ae-input-bg-color) !important; + border: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; + color: var(--ae-primary-color) !important; + } - .ant-tabs-card>.ant-tabs-nav .ant-tabs-tab-active, - .ant-tabs-card>div>.ant-tabs-nav .ant-tabs-tab-active - { - background-color: var(--ae-primary-color) !important; - color: var(--ae-input-hover-text-color) !important; - } + .ant-tabs-card>.ant-tabs-nav .ant-tabs-tab-active, + .ant-tabs-card>div>.ant-tabs-nav .ant-tabs-tab-active + { + background-color: var(--ae-primary-color) !important; + color: var(--ae-input-hover-text-color) !important; + } - .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { - color: var(--ae-primary-color) !important; - text-shadow: 0 0 .25px var(--ae-primary-color) !important; - } - .ant-tabs-ink-bar { - background: var(--ae-primary-color) !important; - } - .ant-tabs-tab:hover, - .ant-tabs-tab-btn:focus, .ant-tabs-tab-remove:focus, - .ant-tabs-tab-btn:active, .ant-tabs-tab-remove:active { - color: var(--ae-primary-color) !important; - } + .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { + color: var(--ae-primary-color) !important; + text-shadow: 0 0 .25px var(--ae-primary-color) !important; + } + .ant-tabs-ink-bar { + background: var(--ae-primary-color) !important; + } + .ant-tabs-tab:hover, + .ant-tabs-tab-btn:focus, .ant-tabs-tab-remove:focus, + .ant-tabs-tab-btn:active, .ant-tabs-tab-remove:active { + color: var(--ae-primary-color) !important; + } - .ant-tabs-tab.ant-tabs-tab-with-remove.ant-tabs-tab-active .ant-tabs-tab-btn { - color: var(--ae-input-hover-text-color) !important; - } + .ant-tabs-tab.ant-tabs-tab-with-remove.ant-tabs-tab-active .ant-tabs-tab-btn { + color: var(--ae-input-hover-text-color) !important; + } - .ant-tabs>.ant-tabs-nav .ant-tabs-nav-add, .ant-tabs>div>.ant-tabs-nav .ant-tabs-nav-add { - border-radius: var(--ae-border-radius) !important; - background-color: var(--ae-input-bg-color) !important; - border: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; - color: var(--ae-primary-color) !important; - } + .ant-tabs>.ant-tabs-nav .ant-tabs-nav-add, .ant-tabs>div>.ant-tabs-nav .ant-tabs-nav-add { + border-radius: var(--ae-border-radius) !important; + background-color: var(--ae-input-bg-color) !important; + border: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; + color: var(--ae-primary-color) !important; + } - .ant-tabs-top>.ant-tabs-nav:before, - .ant-tabs-bottom>.ant-tabs-nav:before, - .ant-tabs-top>div>.ant-tabs-nav:before, - .ant-tabs-bottom>div>.ant-tabs-nav:before { - border-bottom: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; - } - .feature-item .item:hover { - color: var(--ae-primary-color) !important; - } - .preview-switch>* { - color: var(--ae-primary-color) !important; - font-size: 2em !important; - } - .feature-item { - border-radius: var(--ae-border-radius) !important; - background-color: var(--ae-input-bg-color) !important; - border: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; - } - .content{ - grid-gap: var(--ae-gap-size) !important; - margin: var(--ae-gap-size) !important; - margin-left: 0 !important; - } - .container{ - padding:0!important; - } - h1, h2, h3, h4, h5, h6 { - color: var(--ae-label-color) !important; - } + .ant-tabs-top>.ant-tabs-nav:before, + .ant-tabs-bottom>.ant-tabs-nav:before, + .ant-tabs-top>div>.ant-tabs-nav:before, + .ant-tabs-bottom>div>.ant-tabs-nav:before { + border-bottom: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; + } + .feature-item .item:hover { + color: var(--ae-primary-color) !important; + } + .preview-switch>* { + color: var(--ae-primary-color) !important; + font-size: 2em !important; + } + .feature-item { + border-radius: var(--ae-border-radius) !important; + background-color: var(--ae-input-bg-color) !important; + border: var(--ae-input-border-size) solid var(--ae-input-border-color) !important; + } + .content{ + grid-gap: var(--ae-gap-size) !important; + margin: var(--ae-gap-size) !important; + margin-left: 0 !important; + } + .container{ + padding:0!important; + } + h1, h2, h3, h4, h5, h6 { + color: var(--ae-label-color) !important; + } - - `; - ev.target.contentDocument.head.appendChild(new_style_element); - }); - } + + `; + ev.target.contentDocument.head.appendChild(new_style_element); + }); + } } function setupOnLoadResources() { - const content_div = document.querySelector("#anapnoe_app"); - gradioApp().insertAdjacentElement('afterbegin', content_div); + const content_div = document.querySelector("#anapnoe_app"); + gradioApp().insertAdjacentElement('afterbegin', content_div); - active_main_tab = document.querySelector("#tab_txt2img"); - createButtons4Extensions(); - setupAnimationEventListeners(); + active_main_tab = document.querySelector("#tab_txt2img"); + createButtons4Extensions(); + setupAnimationEventListeners(); - //const tempDiv = document.createElement('div'); - const initSplitLib = function () { - initDefaultComponents(content_div); - onUiUxReady(content_div); - } + //const tempDiv = document.createElement('div'); + const initSplitLib = function() { + initDefaultComponents(content_div); + onUiUxReady(content_div); + }; - const script = document.createElement('script'); - script.id = 'splitjs-main'; - script.setAttribute("data-scope", "#anapnoe_app"); - script.onload = initSplitLib; - script.src = './file=extensions-builtin/anapnoe-sd-uiux/libs/split.js'; + const script = document.createElement('script'); + script.id = 'splitjs-main'; + script.setAttribute("data-scope", "#anapnoe_app"); + script.onload = initSplitLib; + script.src = './file=extensions-builtin/anapnoe-sd-uiux/libs/split.js'; - content_div.appendChild(script); - setupAdditionalStylesForExtensions(); + content_div.appendChild(script); + setupAdditionalStylesForExtensions(); const iibframe = document.querySelector("#infinite_image_browsing_container_wrapper > iframe"); if(iibframe){ @@ -1594,224 +1661,223 @@ function setupOnLoadResources() { } -function removeStyleAssets(){ - - //console.log("Starting optimizations for Extra Networks"); - console.log("Starting optimizations"); - document.querySelector("#img2img_textual_inversion_cards_html")?.remove(); - document.querySelector("#img2img_checkpoints_cards_html")?.remove(); - document.querySelector("#img2img_hypernetworks_cards_html")?.remove(); - document.querySelector("#img2img_lora_cards_html")?.remove(); +function removeStyleAssets() { - console.log("Remove element #img2img_textual_inversion_cards_html"); - console.log("Remove element #img2img_checkpoints_cards_html"); - console.log("Remove element #img2img_hypernetworks_cards_html"); - console.log("Remove element #img2img_lora_cards_html"); + //console.log("Starting optimizations for Extra Networks"); + console.log("Starting optimizations"); + document.querySelector("#img2img_textual_inversion_cards_html")?.remove(); + document.querySelector("#img2img_checkpoints_cards_html")?.remove(); + document.querySelector("#img2img_hypernetworks_cards_html")?.remove(); + document.querySelector("#img2img_lora_cards_html")?.remove(); - document.querySelectorAll(` - [rel="stylesheet"][href*="/assets/"], - [rel="stylesheet"][href*="theme.css"], - [rel="stylesheet"][href*="file=style.css"]` - ).forEach((c) => { - c.remove(); - //loggerUiUx.innerHTML = `Remove stylesheets ${c.getAttribute("href")}`; - console.log("Remove stylesheet", c.getAttribute("href")); - - }); + console.log("Remove element #img2img_textual_inversion_cards_html"); + console.log("Remove element #img2img_checkpoints_cards_html"); + console.log("Remove element #img2img_hypernetworks_cards_html"); + console.log("Remove element #img2img_lora_cards_html"); - const styler = document.querySelectorAll('.styler, [class*="svelte"]:not(input)'); - const count = styler.length; - let s = 0; - styler.forEach((c) => { - if(c.style.display !== "none" && c.style.display !== "block"){ - //if(c.className.indexOf('hidden') === -1 && c.className.indexOf('hide') === -1){ - c.removeAttribute("style"); - s++; - //} - } + document.querySelectorAll(` + [rel="stylesheet"][href*="/assets/"], + [rel="stylesheet"][href*="theme.css"], + [rel="stylesheet"][href*="file=style.css"]`).forEach((c) => { + c.remove(); + //loggerUiUx.innerHTML = `Remove stylesheets ${c.getAttribute("href")}`; + console.log("Remove stylesheet", c.getAttribute("href")); - [...c.classList].filter(c => { - return c.match(/^svelte.*/) - }).forEach(e => { - c.classList.remove(e) - }); - - }); - //loggerUiUx.innerHTML = `Remove inline styles from DOM selectors:${count} removed:${s}`; - console.log("Remove inline styles from DOM", "Total Selectors:", count, "Removed Selectors:", s); - console.log("Finishing optimizations"); - //console.log("Loading template files"); - templateData(); + }); + + const styler = document.querySelectorAll('.styler, [class*="svelte"]:not(input)'); + const count = styler.length; + let s = 0; + styler.forEach((c) => { + if (c.style.display !== "none" && c.style.display !== "block") { + //if(c.className.indexOf('hidden') === -1 && c.className.indexOf('hide') === -1){ + c.removeAttribute("style"); + s++; + //} + } + + [...c.classList].filter(c => { + return c.match(/^svelte.*/); + }).forEach(e => { + c.classList.remove(e); + }); + + }); + //loggerUiUx.innerHTML = `Remove inline styles from DOM selectors:${count} removed:${s}`; + console.log("Remove inline styles from DOM", "Total Selectors:", count, "Removed Selectors:", s); + console.log("Finishing optimizations"); + //console.log("Loading template files"); + templateData(); } function getNestedTemplates(container) { - const nestedData = []; - container.querySelectorAll(`.template:not([status])`).forEach((el, j) => { - //console.log(el, j) - const obj = {}; - const url = el.getAttribute('url'); - if(url){ - obj.url = url; - }else{ - obj.url = default_ext_path; - } - const key = el.getAttribute('key'); - if(key){ - obj.key = key; - } - const template = el.getAttribute('template'); - if(template){ - obj.template = `${template}.html`; - }else{ - obj.template = `${el.id}.html`; - } - obj.id = el.id; - nestedData.push(obj); - }); + const nestedData = []; + container.querySelectorAll(`.template:not([status])`).forEach((el, j) => { + //console.log(el, j) + const obj = {}; + const url = el.getAttribute('url'); + if (url) { + obj.url = url; + } else { + obj.url = default_ext_path; + } + const key = el.getAttribute('key'); + if (key) { + obj.key = key; + } + const template = el.getAttribute('template'); + if (template) { + obj.template = `${template}.html`; + } else { + obj.template = `${el.id}.html`; + } + obj.id = el.id; + nestedData.push(obj); + }); - return nestedData; + return nestedData; } function loadCurrentTemplate(data, i, callback) { - const curr_data = data[i]; - const xmlHttp = new XMLHttpRequest(); - const next = i < data.length; - let target; + const curr_data = data[i]; + const xmlHttp = new XMLHttpRequest(); + const next = i < data.length; + let target; - if(next){ + if (next) { - if(curr_data?.parent){ - target = curr_data.parent; - }else if(curr_data?.id){ - target = document.querySelector(`#${curr_data.id}`); - } + if (curr_data?.parent) { + target = curr_data.parent; + } else if (curr_data?.id) { + target = document.querySelector(`#${curr_data.id}`); + } - if(target){ - - xmlHttp.onreadystatechange = function () { - - if (xmlHttp.readyState == 4 && xmlHttp.status == 200) { - let tempDiv = document.createElement('div'); - if(curr_data.key){ - const filtered = xmlHttp.responseText.replace(/\s*\{\{.*?\}\}\s*/g, curr_data.key); - tempDiv.innerHTML = filtered; - }else{ - tempDiv.innerHTML = xmlHttp.responseText; - } + if (target) { - const nestedData = getNestedTemplates(tempDiv); - if(nestedData.length > 0){ - data = data.concat(nestedData); - } - - //console.log(data) - target.setAttribute("status", "true"); - target.append(tempDiv.firstElementChild); - - loadCurrentTemplate(data, i+1, callback); - - }else if (xmlHttp.readyState == 4 && xmlHttp.status == 404) { - target.setAttribute("status", "error"); - loadCurrentTemplate(data, i+1, callback) - } - }; + xmlHttp.onreadystatechange = function() { - const url = `${curr_data.url}${curr_data.template}`; - console.log("Loading template", url); - xmlHttp.open("GET", url, true); // true for asynchronous - xmlHttp.send(null); + if (xmlHttp.readyState == 4 && xmlHttp.status == 200) { + let tempDiv = document.createElement('div'); + if (curr_data.key) { + const filtered = xmlHttp.responseText.replace(/\s*\{\{.*?\}\}\s*/g, curr_data.key); + tempDiv.innerHTML = filtered; + } else { + tempDiv.innerHTML = xmlHttp.responseText; + } - } + const nestedData = getNestedTemplates(tempDiv); + if (nestedData.length > 0) { + data = data.concat(nestedData); + } + + //console.log(data) + target.setAttribute("status", "true"); + target.append(tempDiv.firstElementChild); + + loadCurrentTemplate(data, i + 1, callback); + + } else if (xmlHttp.readyState == 4 && xmlHttp.status == 404) { + target.setAttribute("status", "error"); + loadCurrentTemplate(data, i + 1, callback); + } + }; + + const url = `${curr_data.url}${curr_data.template}`; + console.log("Loading template", url); + xmlHttp.open("GET", url, true); // true for asynchronous + xmlHttp.send(null); + + } + + } else { + //console.log("InitScripts") + //loggerUiUx.innerHTML = `Finished`; + console.log("Template files merged successfully"); + console.log("Init runtime components"); + callback(); + //setupOnLoadResources(); + + } - }else{ - //console.log("InitScripts") - //loggerUiUx.innerHTML = `Finished`; - console.log("Template files merged successfully"); - console.log("Init runtime components"); - callback(); - //setupOnLoadResources(); - - } - } function templateData() { - - const path = default_ext_path; - const data = [ - { - url: path, - template:'template-app-root.html', - parent: getRootContainer() - } - ]; - loadCurrentTemplate(data, 0, setupOnLoadResources); - + const path = default_ext_path; + const data = [ + { + url: path, + template: 'template-app-root.html', + parent: getRootContainer() + } + ]; + + loadCurrentTemplate(data, 0, setupOnLoadResources); + } function setupLogger() { - const tempDiv = document.createElement('div'); - tempDiv.id = "logger_screen"; - tempDiv.style = ` - position: fixed; - inset: 0; - background-color: black; - z-index: 99999; - display: flex; + const tempDiv = document.createElement('div'); + tempDiv.id = "logger_screen"; + tempDiv.style = ` + position: fixed; + inset: 0; + background-color: black; + z-index: 99999; + display: flex; flex-direction: column; - overflow:auto; - `; - - loggerUiUx = document.createElement('div'); - loggerUiUx.id = "logger"; - tempDiv.append(loggerUiUx); - document.body.append(tempDiv); + overflow:auto; + `; - (function (logger) { + loggerUiUx = document.createElement('div'); + loggerUiUx.id = "logger"; + tempDiv.append(loggerUiUx); + document.body.append(tempDiv); + + (function(logger) { console.old = console.log; - console.log = function () { + console.log = function() { var output = "", arg, i; - + output += ` -
${new Date().toLocaleString().replace(',','')}`; +
${new Date().toLocaleString().replace(',', '')}`; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; const argstr = arg.toString().toLowerCase(); let acolor = ""; - if(argstr.indexOf("remove") !== -1 || argstr.indexOf("error") !== -1){ - acolor += " log-remove" - }else if(argstr.indexOf("loading") !== -1 - || argstr.indexOf("| ref") !== -1 - || argstr.indexOf("initial") !== -1 - || argstr.indexOf("optimiz") !== -1 - || argstr.indexOf("python") !== -1 - || argstr.indexOf("success") !== -1){ + if (argstr.indexOf("remove") !== -1 || argstr.indexOf("error") !== -1) { + acolor += " log-remove"; + } else if (argstr.indexOf("loading") !== -1 || + argstr.indexOf("| ref") !== -1 || + argstr.indexOf("initial") !== -1 || + argstr.indexOf("optimiz") !== -1 || + argstr.indexOf("python") !== -1 || + argstr.indexOf("success") !== -1) { acolor += " log-load"; - }else if(argstr.indexOf("[") !== -1){ + } else if (argstr.indexOf("[") !== -1) { acolor += " log-object"; } - - if(arg.toString().indexOf(".css") !== -1 || arg.toString().indexOf(".html") !== -1 ){ + + if (arg.toString().indexOf(".css") !== -1 || arg.toString().indexOf(".html") !== -1) { acolor += " log-url"; - }else if(arg.toString().indexOf("\n") !== -1 ){ - output += "
" + } else if (arg.toString().indexOf("\n") !== -1) { + output += "
"; } output += ` - `; + `; if ( typeof arg === "object" && - typeof JSON === "object" && - typeof JSON.stringify === "function" + typeof JSON === "object" && + typeof JSON.stringify === "function" ) { - output += JSON.stringify(arg); + output += JSON.stringify(arg); } else { - output += arg; + output += arg; } output += " "; @@ -1821,19 +1887,16 @@ function setupLogger() { console.old.apply(undefined, arguments); }; })(document.getElementById("logger")); - - console.log( - '\n',"╔═╗╔═╦╦═╗╔═╦═╦╦═╦═╗", - '\n',"║╬╚╣║║║╬╚╣╬║║║║╬║╩╣", - '\n',"╚══╩╩═╩══╣╔╩╩═╩═╩═╝", - '\n',"─────────╚╝" - ); - console.log("Initialize Anapnoe UI/UX runtime engine version 0.0.1"); - console.log(navigator.userAgent); + console.log( + '\n', "╔═╗╔═╦╦═╗╔═╦═╦╦═╦═╗", '\n', "║╬╚╣║║║╬╚╣╬║║║║╬║╩╣", '\n', "╚══╩╩═╩══╣╔╩╩═╩═╩═╝", '\n', "─────────╚╝" + ); + + console.log("Initialize Anapnoe UI/UX runtime engine version 0.0.1"); + console.log(navigator.userAgent); const versions = gradioApp().querySelector(".versions"); - console.log(versions.innerHTML); + console.log(versions.innerHTML); console.log("Console log enabled: ", window.opts.uiux_enable_console_log); console.log("Maximum resolution output: ", window.opts.uiux_max_resolution_output); @@ -1844,59 +1907,50 @@ function setupLogger() { console.log("Aside labels: ", window.opts.uiux_show_labels_aside); console.log("Main labels: ", window.opts.uiux_show_labels_main); console.log("Tabs labels: ", window.opts.uiux_show_labels_tabs); - - const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); - if(isFirefox){ - console.log("Go to the Firefox about:config page, then search and toggle layout. css.has-selector. enabled") - } - if(!window.opts.uiux_enable_console_log){ - console.log = function() {} + const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); + if (isFirefox) { + console.log("Go to the Firefox about:config page, then search and toggle layout. css.has-selector. enabled"); } - - let link = document.querySelector("link[rel~='icon']"); - if (!link) { - link = document.createElement('link'); - link.rel = 'icon'; - document.head.appendChild(link); - } - link.href = './file=extensions-builtin/anapnoe-sd-uiux/html/favicon.svg'; + if (!window.opts.uiux_enable_console_log) { + console.log = function() { }; + } - removeStyleAssets(); + + let link = document.querySelector("link[rel~='icon']"); + if (!link) { + link = document.createElement('link'); + link.rel = 'icon'; + document.head.appendChild(link); + } + link.href = './file=extensions-builtin/anapnoe-sd-uiux/html/favicon.svg'; + + removeStyleAssets(); } function observeGradioInit() { - const observer = new MutationObserver(() => { - const block = gradioApp().querySelector("#tab_anapnoe_sd_uiux_core"); - //const t = gradioApp().querySelector("#txt2img_textual_inversion_cards_html .card:last-child"); - //const c = gradioApp().querySelector("#txt2img_checkpoints_cards_html .card:last-child"); - //const h = gradioApp().querySelector("#txt2img_hypernetworks_cards_html > div:first-child"); - //const l = gradioApp().querySelector("#txt2img_lora_cards_html .card:last-child"); - if (block) { - //if (block && t && c && h && l) { + const observer = new MutationObserver(() => { + const block = gradioApp().querySelector("#tab_anapnoe_sd_uiux_core"); + if (block) { + //if (block && t && c && h && l) { if (window.opts && Object.keys(window.opts).length) { observer.disconnect(); setTimeout(() => { - setupLogger(); - }, 1000); + setupLogger(); + }, 1000); } - - - } - }); - observer.observe(gradioApp(), { childList: true, subtree: true }); -} -/* onUiLoaded(function() { - setupLogger(); -}); */ + } + }); + observer.observe(gradioApp(), {childList: true, subtree: true}); +} document.addEventListener("DOMContentLoaded", () => { - observeGradioInit(); -}); \ No newline at end of file + observeGradioInit(); +}); diff --git a/extensions-builtin/anapnoe-sd-uiux/libs/split.js b/extensions-builtin/anapnoe-sd-uiux/libs/split.js index 1a17ed2e..c3cdea10 100644 --- a/extensions-builtin/anapnoe-sd-uiux/libs/split.js +++ b/extensions-builtin/anapnoe-sd-uiux/libs/split.js @@ -1,10 +1,13 @@ /*! Split.js - v1.6.5 */ -(function (global, factory) { +(function(global, factory) { + /* eslint-disable no-undef */ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Split = factory()); -}(this, (function () { 'use strict'; + /* eslint-disable no-undef */ + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Split = factory()); +}(this, (function() { + 'use strict'; // The programming goals of Split.js are to deliver readable, understandable and // maintainable code, while at the same time manually optimizing for tiny minified file size, @@ -23,79 +26,83 @@ var aGutterSize = '_b'; var bGutterSize = '_c'; var HORIZONTAL = 'horizontal'; - var NOOP = function () { return false; }; + var NOOP = function() { + return false; + }; // Helper function determines which prefixes of CSS calc we need. // We only need to do this once on startup, when this anonymous function is called. // // Tests -webkit, -moz and -o prefixes. Modified from StackOverflow: // http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167 - var calc = ssr - ? 'calc' - : ((['', '-webkit-', '-moz-', '-o-'] - .filter(function (prefix) { - var el = document.createElement('div'); - el.style.cssText = "width:" + prefix + "calc(9px)"; + var calc = ssr ? + 'calc' : + ((['', '-webkit-', '-moz-', '-o-'] + .filter(function(prefix) { + var el = document.createElement('div'); + el.style.cssText = "width:" + prefix + "calc(9px)"; - return !!el.style.length - }) - .shift()) + "calc"); + return !!el.style.length; + }) + .shift()) + "calc"); // Helper function checks if its argument is a string-like type - var isString = function (v) { return typeof v === 'string' || v instanceof String; }; + var isString = function(v) { + return typeof v === 'string' || v instanceof String; + }; // Helper function allows elements and string selectors to be used // interchangeably. In either case an element is returned. This allows us to // do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`. - var elementOrSelector = function (el) { + var elementOrSelector = function(el) { if (isString(el)) { var ele = document.querySelector(el); if (!ele) { - throw new Error(("Selector " + el + " did not match a DOM element")) + throw new Error(("Selector " + el + " did not match a DOM element")); } - return ele + return ele; } - return el + return el; }; // Helper function gets a property from the properties object, with a default fallback - var getOption = function (options, propName, def) { + var getOption = function(options, propName, def) { var value = options[propName]; if (value !== undefined) { - return value + return value; } - return def + return def; }; - var getGutterSize = function (gutterSize, isFirst, isLast, gutterAlign) { + var getGutterSize = function(gutterSize, isFirst, isLast, gutterAlign) { if (isFirst) { if (gutterAlign === 'end') { - return 0 + return 0; } if (gutterAlign === 'center') { - return gutterSize / 2 + return gutterSize / 2; } } else if (isLast) { if (gutterAlign === 'start') { - return 0 + return 0; } if (gutterAlign === 'center') { - return gutterSize / 2 + return gutterSize / 2; } } - return gutterSize + return gutterSize; }; // Default options - var defaultGutterFn = function (i, gutterDirection) { + var defaultGutterFn = function(i, gutterDirection) { var gut = document.createElement('div'); gut.className = "gutter gutter-" + gutterDirection; - return gut + return gut; }; - var defaultElementStyleFn = function (dim, size, gutSize) { + var defaultElementStyleFn = function(dim, size, gutSize) { var style = {}; if (!isString(size)) { @@ -104,13 +111,13 @@ style[dim] = size; } - return style + return style; }; - var defaultGutterStyleFn = function (dim, gutSize) { + var defaultGutterStyleFn = function(dim, gutSize) { var obj; - return (( obj = {}, obj[dim] = (gutSize + "px"), obj )); + return ((obj = {}, obj[dim] = (gutSize + "px"), obj)); }; // The main function to initialize a split. Split.js thinks about each pair @@ -140,10 +147,12 @@ // 4. Loop through the elements while pairing them off. Every pair gets an // `pair` object and a gutter. // 5. Actually size the pair elements, insert gutters and attach event listeners. - var Split = function (idsOption, options) { - if ( options === void 0 ) options = {}; + var Split = function(idsOption, options) { + if (options === void 0) options = {}; - if (ssr) { return {} } + if (ssr) { + return {}; + } var ids = idsOption; var dimension; @@ -167,21 +176,29 @@ var parentFlexDirection = parentStyle ? parentStyle.flexDirection : null; // Set default options.sizes to equal percentages of the parent element. - var sizes = getOption(options, 'sizes') || ids.map(function () { return 100 / ids.length; }); + var sizes = getOption(options, 'sizes') || ids.map(function() { + return 100 / ids.length; + }); // Standardize minSize and maxSize to an array if it isn't already. // This allows minSize and maxSize to be passed as a number. var minSize = getOption(options, 'minSize', 100); - var minSizes = Array.isArray(minSize) ? minSize : ids.map(function () { return minSize; }); + var minSizes = Array.isArray(minSize) ? minSize : ids.map(function() { + return minSize; + }); var maxSize = getOption(options, 'maxSize', Infinity); - var maxSizes = Array.isArray(maxSize) ? maxSize : ids.map(function () { return maxSize; }); + var maxSizes = Array.isArray(maxSize) ? maxSize : ids.map(function() { + return maxSize; + }); // Get other options var expandToMin = getOption(options, 'expandToMin', false); var gutterSize = getOption(options, 'gutterSize', 10); var gutterAlign = getOption(options, 'gutterAlign', 'center'); var snapOffset = getOption(options, 'snapOffset', 30); - var snapOffsets = Array.isArray(snapOffset) ? snapOffset : ids.map(function () { return snapOffset; }); + var snapOffsets = Array.isArray(snapOffset) ? snapOffset : ids.map(function() { + return snapOffset; + }); var dragInterval = getOption(options, 'dragInterval', 1); var direction = getOption(options, 'direction', HORIZONTAL); var cursor = getOption( @@ -232,7 +249,7 @@ // make sure you calculate the gutter size by hand. var style = elementStyle(dimension, size, gutSize, i); - Object.keys(style).forEach(function (prop) { + Object.keys(style).forEach(function(prop) { // eslint-disable-next-line no-param-reassign el.style[prop] = style[prop]; }); @@ -241,21 +258,25 @@ function setGutterSize(gutterElement, gutSize, i) { var style = gutterStyle(dimension, gutSize, i); - Object.keys(style).forEach(function (prop) { + Object.keys(style).forEach(function(prop) { // eslint-disable-next-line no-param-reassign gutterElement.style[prop] = style[prop]; }); } function getSizes() { - return elements.map(function (element) { return element.size; }) + return elements.map(function(element) { + return element.size; + }); } // Supports touch events, but not multitouch, so only the first // finger `touches[0]` is counted. function getMousePosition(e) { - if ('touches' in e) { return e.touches[0][clientAxis] } - return e[clientAxis] + if ('touches' in e) { + return e.touches[0][clientAxis]; + } + return e[clientAxis]; } // Actually adjust the size of elements `a` and `b` to `offset` while dragging. @@ -295,7 +316,9 @@ var a = elements[this.a]; var b = elements[this.b]; - if (!this.dragging) { return } + if (!this.dragging) { + return; + } // Get the offset of the event from the first side of the // pair `this.start`. Then offset by the initial position of the @@ -371,15 +394,21 @@ function innerSize(element) { // Return nothing if getComputedStyle is not supported (< IE9) // Or if parent element has no layout yet - if (!getComputedStyle) { return null } + if (!getComputedStyle) { + return null; + } var computedStyle = getComputedStyle(element); - if (!computedStyle) { return null } + if (!computedStyle) { + return null; + } var size = element[clientSize]; - if (size === 0) { return null } + if (size === 0) { + return null; + } if (direction === HORIZONTAL) { size -= @@ -391,7 +420,7 @@ parseFloat(computedStyle.paddingBottom); } - return size + return size; } // When specifying percentage sizes that are less than the computed @@ -403,11 +432,13 @@ // If it's no supported, return original sizes. var parentSize = innerSize(parent); if (parentSize === null) { - return sizesToTrim + return sizesToTrim; } - if (minSizes.reduce(function (a, b) { return a + b; }, 0) > parentSize) { - return sizesToTrim + if (minSizes.reduce(function(a, b) { + return a + b; + }, 0) > parentSize) { + return sizesToTrim; } // Keep track of the excess pixels, the amount of pixels over the desired percentage @@ -415,7 +446,7 @@ var excessPixels = 0; var toSpare = []; - var pixelSizes = sizesToTrim.map(function (size, i) { + var pixelSizes = sizesToTrim.map(function(size, i) { // Convert requested percentages to pixel sizes var pixelSize = (parentSize * size) / 100; var elementGutterSize = getGutterSize( @@ -431,20 +462,20 @@ if (pixelSize < elementMinSize) { excessPixels += elementMinSize - pixelSize; toSpare.push(0); - return elementMinSize + return elementMinSize; } // Otherwise, mark the pixels it has to spare and return it's original size toSpare.push(pixelSize - elementMinSize); - return pixelSize + return pixelSize; }); // If nothing was adjusted, return the original sizes if (excessPixels === 0) { - return sizesToTrim + return sizesToTrim; } - return pixelSizes.map(function (pixelSize, i) { + return pixelSizes.map(function(pixelSize, i) { var newPixelSize = pixelSize; // While there's still pixels to take, and there's enough pixels to spare, @@ -461,8 +492,8 @@ } // Return the pixel size adjusted as a percentage - return (newPixelSize / parentSize) * 100 - }) + return (newPixelSize / parentSize) * 100; + }); } // stopDragging is very similar to startDragging in reverse. @@ -514,7 +545,7 @@ function startDragging(e) { // Right-clicking can't start dragging. if ('button' in e && e.button !== 0) { - return + return; } // Alias frequently used variables to save space. 200 bytes. @@ -597,7 +628,7 @@ // | | | | | // ----------------------------------------------------------------------- var pairs = []; - elements = ids.map(function (id, i) { + elements = ids.map(function(id, i) { // Create the element object. var element = { element: elementOrSelector(id), @@ -689,7 +720,7 @@ pairs.push(pair); } - return element + return element; }); function adjustToMin(element) { @@ -698,14 +729,14 @@ calculateSizes.call(pair); - var size = isLast - ? pair.size - element.minSize - pair[bGutterSize] - : element.minSize + pair[aGutterSize]; + var size = isLast ? + pair.size - element.minSize - pair[bGutterSize] : + element.minSize + pair[aGutterSize]; adjust.call(pair, size); } - elements.forEach(function (element) { + elements.forEach(function(element) { var computedSize = element.element[getBoundingClientRect]()[dimension]; if (computedSize < element.minSize) { @@ -720,7 +751,7 @@ function setSizes(newSizes) { var trimmed = trimToMin(newSizes); - trimmed.forEach(function (newSize, i) { + trimmed.forEach(function(newSize, i) { if (i > 0) { var pair = pairs[i - 1]; @@ -737,7 +768,7 @@ } function destroy(preserveStyles, preserveGutter) { - pairs.forEach(function (pair) { + pairs.forEach(function(pair) { if (preserveGutter !== true) { pair.parent.removeChild(pair.gutter); } else { @@ -758,7 +789,7 @@ pair[aGutterSize] ); - Object.keys(style).forEach(function (prop) { + Object.keys(style).forEach(function(prop) { elements[pair.a].element.style[prop] = ''; elements[pair.b].element.style[prop] = ''; }); @@ -775,7 +806,7 @@ destroy: destroy, parent: parent, pairs: pairs, - } + }; }; return Split; diff --git a/extensions-builtin/anapnoe-sd-uiux/scripts/anapnoe_sd_uiux.py b/extensions-builtin/anapnoe-sd-uiux/scripts/anapnoe_sd_uiux.py index ef869ccd..5a701f36 100644 --- a/extensions-builtin/anapnoe-sd-uiux/scripts/anapnoe_sd_uiux.py +++ b/extensions-builtin/anapnoe-sd-uiux/scripts/anapnoe_sd_uiux.py @@ -1,133 +1,57 @@ -import os -from pathlib import Path import gradio as gr -import modules.scripts as scripts from modules import script_callbacks, shared -from modules.options import options_section, OptionInfo, OptionHTML, categories - -categories.register_category("anapnoe", "Anapnoe") - - -mapping = [(info.infotext, k) for k, info in shared.opts.data_labels.items() if info.infotext] -#shared.options_templates.update(shared.options_section(('uiux_core', "Anapnoe UI-UX"), { -shared.options_templates.update(options_section(('uiux_core', "Anapnoe UI-UX", "anapnoe"), { - "uiux_enable_console_log": shared.OptionInfo(False, "Enable console log"), - "uiux_max_resolution_output": shared.OptionInfo(2048, "Max resolution output for txt2img and img2img"), - "uiux_show_input_range_ticks": shared.OptionInfo(True, "Show ticks for input range slider"), - "uiux_no_slider_layout": shared.OptionInfo(False, "No input range sliders"), - "uiux_disable_transitions": shared.OptionInfo(False, "Disable transitions"), - "uiux_default_layout": shared.OptionInfo("Auto", "Layout", gr.Radio, {"choices": ["Auto","Desktop", "Mobile"]}), - "uiux_mobile_scale": shared.OptionInfo(0.7, "Mobile scale", gr.Slider, {"minimum": 0.5, "maximum": 1, "step": 0.05}), - "uiux_show_labels_aside": shared.OptionInfo(False, "Show labels for aside tabs"), - "uiux_show_labels_main": shared.OptionInfo(False, "Show labels for main tabs"), - "uiux_show_labels_tabs": shared.OptionInfo(False, "Show labels for page tabs"), - "uiux_ignore_overrides": shared.OptionInfo([], "Ignore Overrides", gr.CheckboxGroup, lambda: {"choices": list(mapping)}) -})) - - -basedir = scripts.basedir() -html_folder = os.path.join(basedir, "html") - -layouts_folder = os.path.join(basedir, "layouts") -javascript_folder = os.path.join(basedir, "javascript") - -def get_files(folder, file_filter=[], file_list=[], split=False): - file_list = [file_name if not split else os.path.splitext(file_name)[0] for file_name in os.listdir(folder) if os.path.isfile(os.path.join(folder, file_name)) and file_name not in file_filter] - return file_list - -html = """ - - -

My First JavaScript

- -
- - -""" - -scripts = """ -async () => { - // set testFn() function on globalThis, so you html onlclick can access it - globalThis.testFn = () => { - document.getElementById('demo').innerHTML = "Hello" - } -} -""" - -# with gr.Blocks() as demo: - # input_mic = gr.HTML(html) - # out_text = gr.Textbox() - # # run script function on load, - # demo.load(None,None,None,_js=scripts) -# static_dir = Path('./static') -# static_dir.mkdir(parents=True, exist_ok=True) - -# def predict(text_input): - # file_name = f"{datetime.utcnow().strftime('%s')}.html" - # file_path = html_folder / file_name - # print(file_path) - # with open(file_path, "w") as f: - # f.write(f""" - # - # - #
- #

Hello {text_input} From Gradio Iframe

- #

Filename: {file_name}

- # - # """) - # iframe = f"""""" - # link = f'{file_name}' - # return link, iframe +from modules.options import options_section +mapping = [ + (info.infotext, k) for k, info in shared.opts.data_labels.items() if info.infotext +] +shared.options_templates.update( + options_section( + ("uiux_core", "Anapnoe UI-UX", "anapnoe"), + { + "uiux_enable_console_log": shared.OptionInfo(False, "Enable console log"), + "uiux_max_resolution_output": shared.OptionInfo( + 2048, "Max resolution output for txt2img and img2img" + ), + "uiux_show_input_range_ticks": shared.OptionInfo( + True, "Show ticks for input range slider" + ), + "uiux_no_slider_layout": shared.OptionInfo(False, "No input range sliders"), + "uiux_disable_transitions": shared.OptionInfo(False, "Disable transitions"), + "uiux_default_layout": shared.OptionInfo( + "Auto", "Layout", gr.Radio, {"choices": ["Auto", "Desktop", "Mobile"]} + ), + "uiux_mobile_scale": shared.OptionInfo( + 0.7, + "Mobile scale", + gr.Slider, + {"minimum": 0.5, "maximum": 1, "step": 0.05}, + ), + "uiux_show_labels_aside": shared.OptionInfo( + False, "Show labels for aside tabs" + ), + "uiux_show_labels_main": shared.OptionInfo( + False, "Show labels for main tabs" + ), + "uiux_show_labels_tabs": shared.OptionInfo( + False, "Show labels for page tabs" + ), + "uiux_ignore_overrides": shared.OptionInfo( + [], + "Ignore Overrides", + gr.CheckboxGroup, + lambda: {"choices": list(mapping)}, + ), + }, + ) +) def on_ui_tabs(): + with gr.Blocks(analytics_enabled=False) as anapnoe_sd_uiux_core: + pass - #print(mapping) + return ((anapnoe_sd_uiux_core, "UI-UX Core", "anapnoe_sd_uiux_core"),) - with gr.Blocks(analytics_enabled=False) as anapnoe_sd_uiux_core: - #override_settings = gr.CheckboxGroup(label="Ignore override settings", elem_id="ignore_overrides", choices=list(mapping)) - """ with gr.Row(): - with gr.Column(): - with gr.Row(): - layouts_dropdown = gr.Dropdown(label="Layout", elem_id="layout_drop_down", interactive=True, choices=get_files(layouts_folder,[".json"]), type="value") - layout_save_as_filename = gr.Text(label="Save / Save as", elem_id="layout_save_as_name",) - with gr.Row(): - layout_reset_button = gr.Button(elem_id="layout_reset_btn", value="Reset", variant="primary") - layout_save_button = gr.Button(value="Save", elem_id="layout_save_btn", variant="primary") - with gr.Row(elem_id="layout_hidden"): - layout_json = gr.Textbox(label="Json", elem_id="layout_json", show_label=True, lines=7, interactive=False, visible=True) +script_callbacks.on_ui_tabs(on_ui_tabs) - def save_layout( layout_json, filename): - with open(os.path.join(layouts_folder, f"{filename}.json"), 'w', encoding="utf-8") as file: - file.write(layout_json) - file.close() - layouts_dropdown.choices=get_files(layouts_folder,[".json"]) - return gr.update(choices=layouts_dropdown.choices, value=f"{filename}.json") - - def open_layout(filename): - with open(os.path.join(layouts_folder, f"{filename}"), 'r') as file: - layout_json=file.read() - no_ext=filename.rsplit('.', 1)[0] - return [layout_json, no_ext] - - - layout_save_button.click( - fn=save_layout, - inputs=[layout_json, layout_save_as_filename], - outputs=layouts_dropdown - ) - - layouts_dropdown.change( - fn=open_layout, - inputs=[layouts_dropdown], - outputs=[layout_json, layout_save_as_filename] - ) """ - - - - return (anapnoe_sd_uiux_core, 'UI-UX Core', 'anapnoe_sd_uiux_core'), - - - -script_callbacks.on_ui_tabs(on_ui_tabs) \ No newline at end of file diff --git a/extensions-builtin/anapnoe-sd-uiux/style.css b/extensions-builtin/anapnoe-sd-uiux/style.css index b4e58e9d..7259ed7e 100644 --- a/extensions-builtin/anapnoe-sd-uiux/style.css +++ b/extensions-builtin/anapnoe-sd-uiux/style.css @@ -3286,6 +3286,11 @@ input[type=checkbox]:checked:after { display: flex; align-items: center; } + +.gradio-accordion .label-wrap:not(.open) > span.icon{ + transform: rotate(90deg); +} + .gradio-checkbox > div.mask-icon{ margin: 0; } @@ -3411,6 +3416,17 @@ span.settings-category { -webkit-mask-image: url(./html/svg/clipboard-fill.svg); } +.token_counter * { + display: block !important; +} + +#accordions-extras > div > div { + padding: 0; + outline: 0; +} +#accordions-extras > div > div:first-child { + display:none; +} @-webkit-keyframes fade-out { 0% { diff --git a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js index 71429c0d..205531fb 100644 --- a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js +++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js @@ -29,6 +29,7 @@ onUiLoaded(async() => { }); function getActiveTab(elements, all = false) { + if (!elements.img2imgTabs) return null; const tabs = elements.img2imgTabs.querySelectorAll("button"); if (all) return tabs; @@ -43,6 +44,7 @@ onUiLoaded(async() => { // Get tab ID function getTabId(elements) { const activeTab = getActiveTab(elements); + if (!activeTab) return null; return tabNameToElementId[activeTab.innerText]; } @@ -252,6 +254,7 @@ onUiLoaded(async() => { let isMoving = false; let mouseX, mouseY; let activeElement; + let interactedWithAltKey = false; const elements = Object.fromEntries( Object.keys(elementIDs).map(id => [ @@ -277,7 +280,7 @@ onUiLoaded(async() => { const targetElement = gradioApp().querySelector(elemId); if (!targetElement) { - console.log("Element not found"); + console.log("Element not found", elemId); return; } @@ -292,7 +295,7 @@ onUiLoaded(async() => { // Create tooltip function createTooltip() { - const toolTipElemnt = + const toolTipElement = targetElement.querySelector(".image-container"); const tooltip = document.createElement("div"); tooltip.className = "canvas-tooltip"; @@ -355,7 +358,7 @@ onUiLoaded(async() => { tooltip.appendChild(tooltipContent); // Add a hint element to the target element - toolTipElemnt.appendChild(tooltip); + toolTipElement.appendChild(tooltip); } //Show tool tip if setting enable @@ -365,13 +368,13 @@ onUiLoaded(async() => { // In the course of research, it was found that the tag img is very harmful when zooming and creates white canvases. This hack allows you to almost never think about this problem, it has no effect on webui. function fixCanvas() { - const activeTab = getActiveTab(elements).textContent.trim(); + const activeTab = getActiveTab(elements)?.textContent.trim(); - if (activeTab !== "img2img") { + if (activeTab && activeTab !== "img2img") { const img = targetElement.querySelector(`${elemId} img`); - if (img && img.style.display !== "none") { - //img.style.display = "none"; + if (img && img.style.display !== "hidden") { + /*img.style.display = "none";*/ img.style.visibility = "hidden"; } } @@ -508,6 +511,10 @@ onUiLoaded(async() => { if (isModifierKey(e, hotkeysConfig.canvas_hotkey_zoom)) { e.preventDefault(); + if (hotkeysConfig.canvas_hotkey_zoom === "Alt") { + interactedWithAltKey = true; + } + let zoomPosX, zoomPosY; let delta = 0.2; if (elemData[elemId].zoomLevel > 7) { @@ -783,23 +790,29 @@ onUiLoaded(async() => { targetElement.addEventListener("mouseleave", handleMouseLeave); // Reset zoom when click on another tab - elements.img2imgTabs.addEventListener("click", resetZoom); - elements.img2imgTabs.addEventListener("click", () => { - // targetElement.style.width = ""; - if (parseInt(targetElement.style.width) > 865) { - setTimeout(fitToElement, 0); - } - }); + if (elements.img2imgTabs) { + elements.img2imgTabs.addEventListener("click", resetZoom); + elements.img2imgTabs.addEventListener("click", () => { + // targetElement.style.width = ""; + if (parseInt(targetElement.style.width) > 865) { + setTimeout(fitToElement, 0); + } + }); + } targetElement.addEventListener("wheel", e => { // change zoom level - const operation = e.deltaY > 0 ? "-" : "+"; + const operation = (e.deltaY || -e.wheelDelta) > 0 ? "-" : "+"; changeZoomLevel(operation, e); // Handle brush size adjustment with ctrl key pressed if (isModifierKey(e, hotkeysConfig.canvas_hotkey_adjust)) { e.preventDefault(); + if (hotkeysConfig.canvas_hotkey_adjust === "Alt") { + interactedWithAltKey = true; + } + // Increase or decrease brush size based on scroll direction adjustBrushSize(elemId, e.deltaY); } @@ -839,6 +852,20 @@ onUiLoaded(async() => { document.addEventListener("keydown", handleMoveKeyDown); document.addEventListener("keyup", handleMoveKeyUp); + + // Prevent firefox from opening main menu when alt is used as a hotkey for zoom or brush size + function handleAltKeyUp(e) { + if (e.key !== "Alt" || !interactedWithAltKey) { + return; + } + + e.preventDefault(); + interactedWithAltKey = false; + } + + document.addEventListener("keyup", handleAltKeyUp); + + // Detect zoom level and update the pan speed. function updatePanPosition(movementX, movementY) { let panSpeed = 2; diff --git a/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py index 89b7c31f..17b27b27 100644 --- a/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py +++ b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py @@ -8,8 +8,8 @@ shared.options_templates.update(shared.options_section(('canvas_hotkey', "Canvas "canvas_hotkey_grow_brush": shared.OptionInfo("W", "Enlarge the brush size"), "canvas_hotkey_move": shared.OptionInfo("F", "Moving the canvas").info("To work correctly in firefox, turn off 'Automatically search the page text when typing' in the browser settings"), "canvas_hotkey_fullscreen": shared.OptionInfo("S", "Fullscreen Mode, maximizes the picture so that it fits into the screen and stretches it to its full width "), - "canvas_hotkey_reset": shared.OptionInfo("R", "Reset zoom and canvas positon"), - "canvas_hotkey_overlap": shared.OptionInfo("O", "Toggle overlap").info("Technical button, neededs for testing"), + "canvas_hotkey_reset": shared.OptionInfo("R", "Reset zoom and canvas position"), + "canvas_hotkey_overlap": shared.OptionInfo("O", "Toggle overlap").info("Technical button, needed for testing"), "canvas_show_tooltip": shared.OptionInfo(True, "Enable tooltip on the canvas"), "canvas_auto_expand": shared.OptionInfo(True, "Automatically expands an image that does not fit completely in the canvas area, similar to manually pressing the S and R buttons"), "canvas_blur_prompt": shared.OptionInfo(False, "Take the focus off the prompt when working with a canvas"), diff --git a/extensions-builtin/extra-options-section/scripts/extra_options_section.py b/extensions-builtin/extra-options-section/scripts/extra_options_section.py index 4c10d9c7..a91bea4f 100644 --- a/extensions-builtin/extra-options-section/scripts/extra_options_section.py +++ b/extensions-builtin/extra-options-section/scripts/extra_options_section.py @@ -1,7 +1,7 @@ import math import gradio as gr -from modules import scripts, shared, ui_components, ui_settings, infotext_utils +from modules import scripts, shared, ui_components, ui_settings, infotext_utils, errors from modules.ui_components import FormColumn @@ -42,7 +42,11 @@ class ExtraOptionsSection(scripts.Script): setting_name = extra_options[index] with FormColumn(): - comp = ui_settings.create_setting_component(setting_name) + try: + comp = ui_settings.create_setting_component(setting_name) + except KeyError: + errors.report(f"Can't add extra options for {setting_name} in ui") + continue self.comps.append(comp) self.setting_names.append(setting_name) diff --git a/extensions-builtin/hypertile/scripts/hypertile_script.py b/extensions-builtin/hypertile/scripts/hypertile_script.py index 395d584b..59e7f990 100644 --- a/extensions-builtin/hypertile/scripts/hypertile_script.py +++ b/extensions-builtin/hypertile/scripts/hypertile_script.py @@ -1,6 +1,5 @@ import hypertile from modules import scripts, script_callbacks, shared -from scripts.hypertile_xyz import add_axis_options class ScriptHypertile(scripts.Script): @@ -93,7 +92,6 @@ def on_ui_settings(): "hypertile_max_depth_unet": shared.OptionInfo(3, "Hypertile U-Net max depth", gr.Slider, {"minimum": 0, "maximum": 3, "step": 1}, infotext="Hypertile U-Net max depth").info("larger = more neural network layers affected; minor effect on performance"), "hypertile_max_tile_unet": shared.OptionInfo(256, "Hypertile U-Net max tile size", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}, infotext="Hypertile U-Net max tile size").info("larger = worse performance"), "hypertile_swap_size_unet": shared.OptionInfo(3, "Hypertile U-Net swap size", gr.Slider, {"minimum": 0, "maximum": 64, "step": 1}, infotext="Hypertile U-Net swap size"), - "hypertile_enable_vae": shared.OptionInfo(False, "Enable Hypertile VAE", infotext="Hypertile VAE").info("minimal change in the generated picture"), "hypertile_max_depth_vae": shared.OptionInfo(3, "Hypertile VAE max depth", gr.Slider, {"minimum": 0, "maximum": 3, "step": 1}, infotext="Hypertile VAE max depth"), "hypertile_max_tile_vae": shared.OptionInfo(128, "Hypertile VAE max tile size", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}, infotext="Hypertile VAE max tile size"), @@ -105,5 +103,20 @@ def on_ui_settings(): shared.opts.add_option(name, opt) +def add_axis_options(): + xyz_grid = [x for x in scripts.scripts_data if x.script_class.__module__ == "xyz_grid.py"][0].module + xyz_grid.axis_options.extend([ + xyz_grid.AxisOption("[Hypertile] Unet First pass Enabled", str, xyz_grid.apply_override('hypertile_enable_unet', boolean=True), choices=xyz_grid.boolean_choice(reverse=True)), + xyz_grid.AxisOption("[Hypertile] Unet Second pass Enabled", str, xyz_grid.apply_override('hypertile_enable_unet_secondpass', boolean=True), choices=xyz_grid.boolean_choice(reverse=True)), + xyz_grid.AxisOption("[Hypertile] Unet Max Depth", int, xyz_grid.apply_override("hypertile_max_depth_unet"), confirm=xyz_grid.confirm_range(0, 3, '[Hypertile] Unet Max Depth'), choices=lambda: [str(x) for x in range(4)]), + xyz_grid.AxisOption("[Hypertile] Unet Max Tile Size", int, xyz_grid.apply_override("hypertile_max_tile_unet"), confirm=xyz_grid.confirm_range(0, 512, '[Hypertile] Unet Max Tile Size')), + xyz_grid.AxisOption("[Hypertile] Unet Swap Size", int, xyz_grid.apply_override("hypertile_swap_size_unet"), confirm=xyz_grid.confirm_range(0, 64, '[Hypertile] Unet Swap Size')), + xyz_grid.AxisOption("[Hypertile] VAE Enabled", str, xyz_grid.apply_override('hypertile_enable_vae', boolean=True), choices=xyz_grid.boolean_choice(reverse=True)), + xyz_grid.AxisOption("[Hypertile] VAE Max Depth", int, xyz_grid.apply_override("hypertile_max_depth_vae"), confirm=xyz_grid.confirm_range(0, 3, '[Hypertile] VAE Max Depth'), choices=lambda: [str(x) for x in range(4)]), + xyz_grid.AxisOption("[Hypertile] VAE Max Tile Size", int, xyz_grid.apply_override("hypertile_max_tile_vae"), confirm=xyz_grid.confirm_range(0, 512, '[Hypertile] VAE Max Tile Size')), + xyz_grid.AxisOption("[Hypertile] VAE Swap Size", int, xyz_grid.apply_override("hypertile_swap_size_vae"), confirm=xyz_grid.confirm_range(0, 64, '[Hypertile] VAE Swap Size')), + ]) + + script_callbacks.on_ui_settings(on_ui_settings) script_callbacks.on_before_ui(add_axis_options) diff --git a/extensions-builtin/hypertile/scripts/hypertile_xyz.py b/extensions-builtin/hypertile/scripts/hypertile_xyz.py deleted file mode 100644 index 9e96ae3c..00000000 --- a/extensions-builtin/hypertile/scripts/hypertile_xyz.py +++ /dev/null @@ -1,51 +0,0 @@ -from modules import scripts -from modules.shared import opts - -xyz_grid = [x for x in scripts.scripts_data if x.script_class.__module__ == "xyz_grid.py"][0].module - -def int_applier(value_name:str, min_range:int = -1, max_range:int = -1): - """ - Returns a function that applies the given value to the given value_name in opts.data. - """ - def validate(value_name:str, value:str): - value = int(value) - # validate value - if not min_range == -1: - assert value >= min_range, f"Value {value} for {value_name} must be greater than or equal to {min_range}" - if not max_range == -1: - assert value <= max_range, f"Value {value} for {value_name} must be less than or equal to {max_range}" - def apply_int(p, x, xs): - validate(value_name, x) - opts.data[value_name] = int(x) - return apply_int - -def bool_applier(value_name:str): - """ - Returns a function that applies the given value to the given value_name in opts.data. - """ - def validate(value_name:str, value:str): - assert value.lower() in ["true", "false"], f"Value {value} for {value_name} must be either true or false" - def apply_bool(p, x, xs): - validate(value_name, x) - value_boolean = x.lower() == "true" - opts.data[value_name] = value_boolean - return apply_bool - -def add_axis_options(): - extra_axis_options = [ - xyz_grid.AxisOption("[Hypertile] Unet First pass Enabled", str, bool_applier("hypertile_enable_unet"), choices=xyz_grid.boolean_choice(reverse=True)), - xyz_grid.AxisOption("[Hypertile] Unet Second pass Enabled", str, bool_applier("hypertile_enable_unet_secondpass"), choices=xyz_grid.boolean_choice(reverse=True)), - xyz_grid.AxisOption("[Hypertile] Unet Max Depth", int, int_applier("hypertile_max_depth_unet", 0, 3), choices=lambda: [str(x) for x in range(4)]), - xyz_grid.AxisOption("[Hypertile] Unet Max Tile Size", int, int_applier("hypertile_max_tile_unet", 0, 512)), - xyz_grid.AxisOption("[Hypertile] Unet Swap Size", int, int_applier("hypertile_swap_size_unet", 0, 64)), - xyz_grid.AxisOption("[Hypertile] VAE Enabled", str, bool_applier("hypertile_enable_vae"), choices=xyz_grid.boolean_choice(reverse=True)), - xyz_grid.AxisOption("[Hypertile] VAE Max Depth", int, int_applier("hypertile_max_depth_vae", 0, 3), choices=lambda: [str(x) for x in range(4)]), - xyz_grid.AxisOption("[Hypertile] VAE Max Tile Size", int, int_applier("hypertile_max_tile_vae", 0, 512)), - xyz_grid.AxisOption("[Hypertile] VAE Swap Size", int, int_applier("hypertile_swap_size_vae", 0, 64)), - ] - set_a = {opt.label for opt in xyz_grid.axis_options} - set_b = {opt.label for opt in extra_axis_options} - if set_a.intersection(set_b): - return - - xyz_grid.axis_options.extend(extra_axis_options) diff --git a/scripts/processing_autosized_crop.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_autosized_crop.py similarity index 100% rename from scripts/processing_autosized_crop.py rename to extensions-builtin/postprocessing-for-training/scripts/postprocessing_autosized_crop.py diff --git a/scripts/postprocessing_caption.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_caption.py similarity index 100% rename from scripts/postprocessing_caption.py rename to extensions-builtin/postprocessing-for-training/scripts/postprocessing_caption.py diff --git a/scripts/postprocessing_create_flipped_copies.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_create_flipped_copies.py similarity index 100% rename from scripts/postprocessing_create_flipped_copies.py rename to extensions-builtin/postprocessing-for-training/scripts/postprocessing_create_flipped_copies.py diff --git a/scripts/postprocessing_focal_crop.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_focal_crop.py similarity index 100% rename from scripts/postprocessing_focal_crop.py rename to extensions-builtin/postprocessing-for-training/scripts/postprocessing_focal_crop.py diff --git a/scripts/postprocessing_split_oversized.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_split_oversized.py similarity index 100% rename from scripts/postprocessing_split_oversized.py rename to extensions-builtin/postprocessing-for-training/scripts/postprocessing_split_oversized.py diff --git a/extensions-builtin/soft-inpainting/scripts/soft_inpainting.py b/extensions-builtin/soft-inpainting/scripts/soft_inpainting.py index d9024344..0e629963 100644 --- a/extensions-builtin/soft-inpainting/scripts/soft_inpainting.py +++ b/extensions-builtin/soft-inpainting/scripts/soft_inpainting.py @@ -3,6 +3,7 @@ import gradio as gr import math from modules.ui_components import InputAccordion import modules.scripts as scripts +from modules.torch_utils import float64 class SoftInpaintingSettings: @@ -57,10 +58,14 @@ def latent_blend(settings, a, b, t): # NOTE: We use inplace operations wherever possible. - # [4][w][h] to [1][4][w][h] - t2 = t.unsqueeze(0) - # [4][w][h] to [1][1][w][h] - the [4] seem redundant. - t3 = t[0].unsqueeze(0).unsqueeze(0) + if len(t.shape) == 3: + # [4][w][h] to [1][4][w][h] + t2 = t.unsqueeze(0) + # [4][w][h] to [1][1][w][h] - the [4] seem redundant. + t3 = t[0].unsqueeze(0).unsqueeze(0) + else: + t2 = t + t3 = t[:, 0][:, None] one_minus_t2 = 1 - t2 one_minus_t3 = 1 - t3 @@ -75,13 +80,11 @@ def latent_blend(settings, a, b, t): # Calculate the magnitude of the interpolated vectors. (We will remove this magnitude.) # 64-bit operations are used here to allow large exponents. - current_magnitude = torch.norm(image_interp, p=2, dim=1, keepdim=True).to(torch.float64).add_(0.00001) + current_magnitude = torch.norm(image_interp, p=2, dim=1, keepdim=True).to(float64(image_interp)).add_(0.00001) # Interpolate the powered magnitudes, then un-power them (bring them back to a power of 1). - a_magnitude = torch.norm(a, p=2, dim=1, keepdim=True).to(torch.float64).pow_( - settings.inpaint_detail_preservation) * one_minus_t3 - b_magnitude = torch.norm(b, p=2, dim=1, keepdim=True).to(torch.float64).pow_( - settings.inpaint_detail_preservation) * t3 + a_magnitude = torch.norm(a, p=2, dim=1, keepdim=True).to(float64(a)).pow_(settings.inpaint_detail_preservation) * one_minus_t3 + b_magnitude = torch.norm(b, p=2, dim=1, keepdim=True).to(float64(b)).pow_(settings.inpaint_detail_preservation) * t3 desired_magnitude = a_magnitude desired_magnitude.add_(b_magnitude).pow_(1 / settings.inpaint_detail_preservation) del a_magnitude, b_magnitude, t3, one_minus_t3 @@ -104,7 +107,7 @@ def latent_blend(settings, a, b, t): def get_modified_nmask(settings, nmask, sigma): """ - Converts a negative mask representing the transparency of the original latent vectors being overlayed + Converts a negative mask representing the transparency of the original latent vectors being overlaid to a mask that is scaled according to the denoising strength for this step. Where: @@ -135,7 +138,10 @@ def apply_adaptive_masks( from PIL import Image, ImageOps, ImageFilter # TODO: Bias the blending according to the latent mask, add adjustable parameter for bias control. - latent_mask = nmask[0].float() + if len(nmask.shape) == 3: + latent_mask = nmask[0].float() + else: + latent_mask = nmask[:, 0].float() # convert the original mask into a form we use to scale distances for thresholding mask_scalar = 1 - (torch.clamp(latent_mask, min=0, max=1) ** (settings.mask_blend_scale / 2)) mask_scalar = (0.5 * (1 - settings.composite_mask_influence) @@ -157,7 +163,14 @@ def apply_adaptive_masks( percentile_min=0.25, percentile_max=0.75, min_width=1) # The distance at which opacity of original decreases to 50% - half_weighted_distance = settings.composite_difference_threshold * mask_scalar + if len(mask_scalar.shape) == 3: + if mask_scalar.shape[0] > i: + half_weighted_distance = settings.composite_difference_threshold * mask_scalar[i] + else: + half_weighted_distance = settings.composite_difference_threshold * mask_scalar[0] + else: + half_weighted_distance = settings.composite_difference_threshold * mask_scalar + converted_mask = converted_mask / half_weighted_distance converted_mask = 1 / (1 + converted_mask ** settings.composite_difference_contrast) diff --git a/html/extra-networks-copy-path-button.html b/html/extra-networks-copy-path-button.html index 8083bb03..50304b42 100644 --- a/html/extra-networks-copy-path-button.html +++ b/html/extra-networks-copy-path-button.html @@ -1,5 +1,5 @@
\ No newline at end of file diff --git a/html/extra-networks-edit-item-button.html b/html/extra-networks-edit-item-button.html index 0fe43082..fd728600 100644 --- a/html/extra-networks-edit-item-button.html +++ b/html/extra-networks-edit-item-button.html @@ -1,4 +1,4 @@
+ onclick="extraNetworksEditUserMetadata(event, '{tabname}', '{extra_networks_tabname}')">
\ No newline at end of file diff --git a/html/extra-networks-metadata-button.html b/html/extra-networks-metadata-button.html index 285b5b3b..4ef013bc 100644 --- a/html/extra-networks-metadata-button.html +++ b/html/extra-networks-metadata-button.html @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/html/extra-networks-pane-dirs.html b/html/extra-networks-pane-dirs.html new file mode 100644 index 00000000..5ce04289 --- /dev/null +++ b/html/extra-networks-pane-dirs.html @@ -0,0 +1,8 @@ +
+
+ {dirs_html} +
+
+ {items_html} +
+
diff --git a/html/extra-networks-pane-tree.html b/html/extra-networks-pane-tree.html new file mode 100644 index 00000000..88561fcd --- /dev/null +++ b/html/extra-networks-pane-tree.html @@ -0,0 +1,8 @@ +
+
+ {tree_html} +
+
+ {items_html} +
+
\ No newline at end of file diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index 0c763f71..9a67baea 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -1,23 +1,53 @@ -
+
-
-
- {tree_html} -
-
- {items_html} -
-
-
\ No newline at end of file + {pane_content} +
diff --git a/javascript/aspectRatioOverlay.js b/javascript/aspectRatioOverlay.js index 2cf2d571..c8751fe4 100644 --- a/javascript/aspectRatioOverlay.js +++ b/javascript/aspectRatioOverlay.js @@ -50,17 +50,17 @@ function dimensionChange(e, is_width, is_height) { var scaledx = targetElement.naturalWidth * viewportscale; var scaledy = targetElement.naturalHeight * viewportscale; - var cleintRectTop = (viewportOffset.top + window.scrollY); - var cleintRectLeft = (viewportOffset.left + window.scrollX); - var cleintRectCentreY = cleintRectTop + (targetElement.clientHeight / 2); - var cleintRectCentreX = cleintRectLeft + (targetElement.clientWidth / 2); + var clientRectTop = (viewportOffset.top + window.scrollY); + var clientRectLeft = (viewportOffset.left + window.scrollX); + var clientRectCentreY = clientRectTop + (targetElement.clientHeight / 2); + var clientRectCentreX = clientRectLeft + (targetElement.clientWidth / 2); var arscale = Math.min(scaledx / currentWidth, scaledy / currentHeight); var arscaledx = currentWidth * arscale; var arscaledy = currentHeight * arscale; - var arRectTop = cleintRectCentreY - (arscaledy / 2); - var arRectLeft = cleintRectCentreX - (arscaledx / 2); + var arRectTop = clientRectCentreY - (arscaledy / 2); + var arRectLeft = clientRectCentreX - (arscaledx / 2); var arRectWidth = arscaledx; var arRectHeight = arscaledy; diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js index ccae242f..e01fd67e 100644 --- a/javascript/contextMenus.js +++ b/javascript/contextMenus.js @@ -8,9 +8,6 @@ var contextMenuInit = function() { }; function showContextMenu(event, element, menuEntries) { - let posx = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; - let posy = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; - let oldMenu = gradioApp().querySelector('#context-menu'); if (oldMenu) { oldMenu.remove(); @@ -23,10 +20,8 @@ var contextMenuInit = function() { contextMenu.style.background = baseStyle.background; contextMenu.style.color = baseStyle.color; contextMenu.style.fontFamily = baseStyle.fontFamily; - contextMenu.style.top = posy + 'px'; - contextMenu.style.left = posx + 'px'; - - + contextMenu.style.top = event.pageY + 'px'; + contextMenu.style.left = event.pageX + 'px'; const contextMenuList = document.createElement('ul'); contextMenuList.className = 'context-menu-items'; @@ -43,21 +38,6 @@ var contextMenuInit = function() { }); gradioApp().appendChild(contextMenu); - - let menuWidth = contextMenu.offsetWidth + 4; - let menuHeight = contextMenu.offsetHeight + 4; - - let windowWidth = window.innerWidth; - let windowHeight = window.innerHeight; - - if ((windowWidth - posx) < menuWidth) { - contextMenu.style.left = windowWidth - menuWidth + "px"; - } - - if ((windowHeight - posy) < menuHeight) { - contextMenu.style.top = windowHeight - menuHeight + "px"; - } - } function appendContextMenuOption(targetElementSelector, entryName, entryFunction) { @@ -107,16 +87,23 @@ var contextMenuInit = function() { oldMenu.remove(); } }); - gradioApp().addEventListener("contextmenu", function(e) { - let oldMenu = gradioApp().querySelector('#context-menu'); - if (oldMenu) { - oldMenu.remove(); - } - menuSpecs.forEach(function(v, k) { - if (e.composedPath()[0].matches(k)) { - showContextMenu(e, e.composedPath()[0], v); - e.preventDefault(); + ['contextmenu', 'touchstart'].forEach((eventType) => { + gradioApp().addEventListener(eventType, function(e) { + let ev = e; + if (eventType.startsWith('touch')) { + if (e.touches.length !== 2) return; + ev = e.touches[0]; } + let oldMenu = gradioApp().querySelector('#context-menu'); + if (oldMenu) { + oldMenu.remove(); + } + menuSpecs.forEach(function(v, k) { + if (e.composedPath()[0].matches(k)) { + showContextMenu(ev, e.composedPath()[0], v); + e.preventDefault(); + } + }); }); }); eventListenerApplied = true; diff --git a/javascript/dragdrop.js b/javascript/dragdrop.js index d680daf5..882562d7 100644 --- a/javascript/dragdrop.js +++ b/javascript/dragdrop.js @@ -56,6 +56,15 @@ function eventHasFiles(e) { return false; } +function isURL(url) { + try { + const _ = new URL(url); + return true; + } catch { + return false; + } +} + function dragDropTargetIsPrompt(target) { if (target?.placeholder && target?.placeholder.indexOf("Prompt") >= 0) return true; if (target?.parentNode?.parentNode?.className?.indexOf("prompt") > 0) return true; @@ -74,22 +83,39 @@ window.document.addEventListener('dragover', e => { e.dataTransfer.dropEffect = 'copy'; }); -window.document.addEventListener('drop', e => { +window.document.addEventListener('drop', async e => { const target = e.composedPath()[0]; - if (!eventHasFiles(e)) return; + const url = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain'); + if (!eventHasFiles(e) && !isURL(url)) return; if (dragDropTargetIsPrompt(target)) { e.stopPropagation(); e.preventDefault(); - let prompt_target = get_tab_index('tabs') == 1 ? "img2img_prompt_image" : "txt2img_prompt_image"; + const isImg2img = get_tab_index('tabs') == 1; + let prompt_image_target = isImg2img ? "img2img_prompt_image" : "txt2img_prompt_image"; - const imgParent = gradioApp().getElementById(prompt_target); + const imgParent = gradioApp().getElementById(prompt_image_target); const files = e.dataTransfer.files; const fileInput = imgParent.querySelector('input[type="file"]'); - if (fileInput) { + if (eventHasFiles(e) && fileInput) { fileInput.files = files; fileInput.dispatchEvent(new Event('change')); + } else if (url) { + try { + const request = await fetch(url); + if (!request.ok) { + console.error('Error fetching URL:', url, request.status); + return; + } + const data = new DataTransfer(); + data.items.add(new File([await request.blob()], 'image.png')); + fileInput.files = data.files; + fileInput.dispatchEvent(new Event('change')); + } catch (error) { + console.error('Error fetching URL:', url, error); + return; + } } } diff --git a/javascript/edit-attention.js b/javascript/edit-attention.js index 688c2f11..b07ba97c 100644 --- a/javascript/edit-attention.js +++ b/javascript/edit-attention.js @@ -64,6 +64,14 @@ function keyupEditAttention(event) { selectionEnd++; } + // deselect surrounding whitespace + while (text[selectionStart] == " " && selectionStart < selectionEnd) { + selectionStart++; + } + while (text[selectionEnd - 1] == " " && selectionEnd > selectionStart) { + selectionEnd--; + } + target.setSelectionRange(selectionStart, selectionEnd); return true; } diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index d5855fe9..c5cced97 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -39,12 +39,12 @@ function setupExtraNetworksForTab(tabname) { // tabname_full = {tabname}_{extra_networks_tabname} var tabname_full = elem.id; var search = gradioApp().querySelector("#" + tabname_full + "_extra_search"); - var sort_mode = gradioApp().querySelector("#" + tabname_full + "_extra_sort"); var sort_dir = gradioApp().querySelector("#" + tabname_full + "_extra_sort_dir"); var refresh = gradioApp().querySelector("#" + tabname_full + "_extra_refresh"); + var currentSort = ''; // If any of the buttons above don't exist, we want to skip this iteration of the loop. - if (!search || !sort_mode || !sort_dir || !refresh) { + if (!search || !sort_dir || !refresh) { return; // `return` is equivalent of `continue` but for forEach loops. } @@ -52,7 +52,7 @@ function setupExtraNetworksForTab(tabname) { var searchTerm = search.value.toLowerCase(); gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) { var searchOnly = elem.querySelector('.search_only'); - var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms'), function(t) { + var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms, .description'), function(t) { return t.textContent.toLowerCase(); }).join(" "); @@ -71,42 +71,46 @@ function setupExtraNetworksForTab(tabname) { }; var applySort = function(force) { - var cards = gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card'); + var cards = gradioApp().querySelectorAll('#' + tabname_full + ' div.card'); + var parent = gradioApp().querySelector('#' + tabname_full + "_cards"); var reverse = sort_dir.dataset.sortdir == "Descending"; - var sortKey = sort_mode.dataset.sortmode.toLowerCase().replace("sort", "").replaceAll(" ", "_").replace(/_+$/, "").trim() || "name"; - sortKey = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1); - var sortKeyStore = sortKey + "-" + (reverse ? "Descending" : "Ascending") + "-" + cards.length; + var activeSearchElem = gradioApp().querySelector('#' + tabname_full + "_controls .extra-network-control--sort.extra-network-control--enabled"); + var sortKey = activeSearchElem ? activeSearchElem.dataset.sortkey : "default"; + var sortKeyDataField = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1); + var sortKeyStore = sortKey + "-" + sort_dir.dataset.sortdir + "-" + cards.length; - if (sortKeyStore == sort_mode.dataset.sortkey && !force) { + if (sortKeyStore == currentSort && !force) { return; } - sort_mode.dataset.sortkey = sortKeyStore; + currentSort = sortKeyStore; - cards.forEach(function(card) { - card.originalParentElement = card.parentElement; - }); var sortedCards = Array.from(cards); sortedCards.sort(function(cardA, cardB) { - var a = cardA.dataset[sortKey]; - var b = cardB.dataset[sortKey]; + var a = cardA.dataset[sortKeyDataField]; + var b = cardB.dataset[sortKeyDataField]; if (!isNaN(a) && !isNaN(b)) { return parseInt(a) - parseInt(b); } return (a < b ? -1 : (a > b ? 1 : 0)); }); + if (reverse) { sortedCards.reverse(); } - cards.forEach(function(card) { - card.remove(); - }); + + parent.innerHTML = ''; + + var frag = document.createDocumentFragment(); sortedCards.forEach(function(card) { - card.originalParentElement.appendChild(card); + frag.appendChild(card); }); + parent.appendChild(frag); }; - search.addEventListener("input", applyFilter); + search.addEventListener("input", function() { + applyFilter(); + }); applySort(); applyFilter(); extraNetworksApplySort[tabname_full] = applySort; @@ -272,6 +276,15 @@ function saveCardPreview(event, tabname, filename) { event.preventDefault(); } +function extraNetworksSearchButton(tabname, extra_networks_tabname, event) { + var searchTextarea = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_search"); + var button = event.target; + var text = button.classList.contains("search-all") ? "" : button.textContent.trim(); + + searchTextarea.value = text; + updateInput(searchTextarea); +} + function extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname) { /** * Processes `onclick` events when user clicks on files in tree. @@ -290,7 +303,7 @@ function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_netwo * Processes `onclick` events when user clicks on directories in tree. * * Here is how the tree reacts to clicks for various states: - * unselected unopened directory: Diretory is selected and expanded. + * unselected unopened directory: Directory is selected and expanded. * unselected opened directory: Directory is selected. * selected opened directory: Directory is collapsed and deselected. * chevron is clicked: Directory is expanded or collapsed. Selected state unchanged. @@ -383,36 +396,17 @@ function extraNetworksTreeOnClick(event, tabname, extra_networks_tabname) { } function extraNetworksControlSortOnClick(event, tabname, extra_networks_tabname) { - /** - * Handles `onclick` events for the Sort Mode button. - * - * Modifies the data attributes of the Sort Mode button to cycle between - * various sorting modes. - * - * @param event The generated event. - * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. - * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. - */ - var curr_mode = event.currentTarget.dataset.sortmode; - var el_sort_dir = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_sort_dir"); - var sort_dir = el_sort_dir.dataset.sortdir; - if (curr_mode == "path") { - event.currentTarget.dataset.sortmode = "name"; - event.currentTarget.dataset.sortkey = "sortName-" + sort_dir + "-640"; - event.currentTarget.setAttribute("title", "Sort by filename"); - } else if (curr_mode == "name") { - event.currentTarget.dataset.sortmode = "date_created"; - event.currentTarget.dataset.sortkey = "sortDate_created-" + sort_dir + "-640"; - event.currentTarget.setAttribute("title", "Sort by date created"); - } else if (curr_mode == "date_created") { - event.currentTarget.dataset.sortmode = "date_modified"; - event.currentTarget.dataset.sortkey = "sortDate_modified-" + sort_dir + "-640"; - event.currentTarget.setAttribute("title", "Sort by date modified"); - } else { - event.currentTarget.dataset.sortmode = "path"; - event.currentTarget.dataset.sortkey = "sortPath-" + sort_dir + "-640"; - event.currentTarget.setAttribute("title", "Sort by path"); - } + /** Handles `onclick` events for Sort Mode buttons. */ + + var self = event.currentTarget; + var parent = event.currentTarget.parentElement; + + parent.querySelectorAll('.extra-network-control--sort').forEach(function(x) { + x.classList.remove('extra-network-control--enabled'); + }); + + self.classList.add('extra-network-control--enabled'); + applyExtraNetworkSort(tabname + "_" + extra_networks_tabname); } @@ -447,8 +441,12 @@ function extraNetworksControlTreeViewOnClick(event, tabname, extra_networks_tabn * @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc. * @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc. */ - gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_tree").classList.toggle("hidden"); - event.currentTarget.classList.toggle("extra-network-control--enabled"); + var button = event.currentTarget; + button.classList.toggle("extra-network-control--enabled"); + var show = !button.classList.contains("extra-network-control--enabled"); + + var pane = gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_pane"); + pane.classList.toggle("extra-network-dirs-hidden", show); } function extraNetworksControlRefreshOnClick(event, tabname, extra_networks_tabname) { @@ -509,12 +507,76 @@ function popupId(id) { popup(storedPopupIds[id]); } +function extraNetworksFlattenMetadata(obj) { + const result = {}; + + // Convert any stringified JSON objects to actual objects + for (const key of Object.keys(obj)) { + if (typeof obj[key] === 'string') { + try { + const parsed = JSON.parse(obj[key]); + if (parsed && typeof parsed === 'object') { + obj[key] = parsed; + } + } catch (error) { + continue; + } + } + } + + // Flatten the object + for (const key of Object.keys(obj)) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + const nested = extraNetworksFlattenMetadata(obj[key]); + for (const nestedKey of Object.keys(nested)) { + result[`${key}/${nestedKey}`] = nested[nestedKey]; + } + } else { + result[key] = obj[key]; + } + } + + // Special case for handling modelspec keys + for (const key of Object.keys(result)) { + if (key.startsWith("modelspec.")) { + result[key.replaceAll(".", "/")] = result[key]; + delete result[key]; + } + } + + // Add empty keys to designate hierarchy + for (const key of Object.keys(result)) { + const parts = key.split("/"); + for (let i = 1; i < parts.length; i++) { + const parent = parts.slice(0, i).join("/"); + if (!result[parent]) { + result[parent] = ""; + } + } + } + + return result; +} + function extraNetworksShowMetadata(text) { + try { + let parsed = JSON.parse(text); + if (parsed && typeof parsed === 'object') { + parsed = extraNetworksFlattenMetadata(parsed); + const table = createVisualizationTable(parsed, 0); + popup(table); + return; + } + } catch (error) { + console.error(error); + } + var elem = document.createElement('pre'); elem.classList.add('popup-metadata'); elem.textContent = text; popup(elem); + return; } function requestGet(url, data, handler, errorHandler) { @@ -543,16 +605,18 @@ function requestGet(url, data, handler, errorHandler) { xhr.send(js); } -function extraNetworksCopyCardPath(event, path) { - navigator.clipboard.writeText(path); +function extraNetworksCopyCardPath(event) { + navigator.clipboard.writeText(event.target.getAttribute("data-clipboard-text")); event.stopPropagation(); } -function extraNetworksRequestMetadata(event, extraPage, cardName) { +function extraNetworksRequestMetadata(event, extraPage) { var showError = function() { extraNetworksShowMetadata("there was an error getting metadata"); }; + var cardName = event.target.parentElement.parentElement.getAttribute("data-name"); + requestGet("./sd_extra_networks/metadata", {page: extraPage, item: cardName}, function(data) { if (data && data.metadata) { extraNetworksShowMetadata(data.metadata); @@ -566,7 +630,7 @@ function extraNetworksRequestMetadata(event, extraPage, cardName) { var extraPageUserMetadataEditors = {}; -function extraNetworksEditUserMetadata(event, tabname, extraPage, cardName) { +function extraNetworksEditUserMetadata(event, tabname, extraPage) { var id = tabname + '_' + extraPage + '_edit_user_metadata'; var editor = extraPageUserMetadataEditors[id]; @@ -578,6 +642,7 @@ function extraNetworksEditUserMetadata(event, tabname, extraPage, cardName) { extraPageUserMetadataEditors[id] = editor; } + var cardName = event.target.parentElement.parentElement.getAttribute("data-name"); editor.nameTextarea.value = cardName; updateInput(editor.nameTextarea); diff --git a/javascript/imageviewer.js b/javascript/imageviewer.js index 625c5d14..9b23f470 100644 --- a/javascript/imageviewer.js +++ b/javascript/imageviewer.js @@ -6,6 +6,8 @@ function closeModal() { function showModal(event) { const source = event.target || event.srcElement; const modalImage = gradioApp().getElementById("modalImage"); + const modalToggleLivePreviewBtn = gradioApp().getElementById("modal_toggle_live_preview"); + modalToggleLivePreviewBtn.innerHTML = opts.js_live_preview_in_modal_lightbox ? "🗇" : "🗆"; const lb = gradioApp().getElementById("lightboxModal"); modalImage.src = source.src; if (modalImage.style.display === 'none') { @@ -51,14 +53,7 @@ function modalImageSwitch(offset) { var galleryButtons = all_gallery_buttons(); if (galleryButtons.length > 1) { - var currentButton = selected_gallery_button(); - - var result = -1; - galleryButtons.forEach(function(v, i) { - if (v == currentButton) { - result = i; - } - }); + var result = selected_gallery_index(); if (result != -1) { var nextButton = galleryButtons[negmod((result + offset), galleryButtons.length)]; @@ -131,19 +126,15 @@ function setupImageForLightbox(e) { e.style.cursor = 'pointer'; e.style.userSelect = 'none'; - var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; - - // For Firefox, listening on click first switched to next image then shows the lightbox. - // If you know how to fix this without switching to mousedown event, please. - // For other browsers the event is click to make it possiblr to drag picture. - var event = isFirefox ? 'mousedown' : 'click'; - - e.addEventListener(event, function(evt) { + e.addEventListener('mousedown', function(evt) { if (evt.button == 1) { open(evt.target.src); evt.preventDefault(); return; } + }, true); + + e.addEventListener('click', function(evt) { if (!opts.js_modal_lightbox || evt.button != 0) return; modalZoomSet(gradioApp().getElementById('modalImage'), opts.js_modal_lightbox_initially_zoomed); @@ -163,6 +154,13 @@ function modalZoomToggle(event) { event.stopPropagation(); } +function modalLivePreviewToggle(event) { + const modalToggleLivePreview = gradioApp().getElementById("modal_toggle_live_preview"); + opts.js_live_preview_in_modal_lightbox = !opts.js_live_preview_in_modal_lightbox; + modalToggleLivePreview.innerHTML = opts.js_live_preview_in_modal_lightbox ? "🗇" : "🗆"; + event.stopPropagation(); +} + function modalTileImageToggle(event) { const modalImage = gradioApp().getElementById("modalImage"); const modal = gradioApp().getElementById("lightboxModal"); @@ -220,6 +218,14 @@ document.addEventListener("DOMContentLoaded", function() { modalSave.title = "Save Image(s)"; modalControls.appendChild(modalSave); + const modalToggleLivePreview = document.createElement('span'); + modalToggleLivePreview.className = 'modalToggleLivePreview cursor'; + modalToggleLivePreview.id = "modal_toggle_live_preview"; + modalToggleLivePreview.innerHTML = "🗆"; + modalToggleLivePreview.onclick = modalLivePreviewToggle; + modalToggleLivePreview.title = "Toggle live preview"; + modalControls.appendChild(modalToggleLivePreview); + const modalClose = document.createElement('span'); modalClose.className = 'modalClose cursor'; modalClose.innerHTML = '×'; diff --git a/javascript/profilerVisualization.js b/javascript/profilerVisualization.js index 9d8e5f42..9822f4b2 100644 --- a/javascript/profilerVisualization.js +++ b/javascript/profilerVisualization.js @@ -33,120 +33,141 @@ function createRow(table, cellName, items) { return res; } -function showProfile(path, cutoff = 0.05) { - requestGet(path, {}, function(data) { - var table = document.createElement('table'); - table.className = 'popup-table'; +function createVisualizationTable(data, cutoff = 0, sort = "") { + var table = document.createElement('table'); + table.className = 'popup-table'; - data.records['total'] = data.total; - var keys = Object.keys(data.records).sort(function(a, b) { - return data.records[b] - data.records[a]; + var keys = Object.keys(data); + if (sort === "number") { + keys = keys.sort(function(a, b) { + return data[b] - data[a]; }); - var items = keys.map(function(x) { - return {key: x, parts: x.split('/'), time: data.records[x]}; + } else { + keys = keys.sort(); + } + var items = keys.map(function(x) { + return {key: x, parts: x.split('/'), value: data[x]}; + }); + var maxLength = items.reduce(function(a, b) { + return Math.max(a, b.parts.length); + }, 0); + + var cols = createRow( + table, + 'th', + [ + cutoff === 0 ? 'key' : 'record', + cutoff === 0 ? 'value' : 'seconds' + ] + ); + cols[0].colSpan = maxLength; + + function arraysEqual(a, b) { + return !(a < b || b < a); + } + + var addLevel = function(level, parent, hide) { + var matching = items.filter(function(x) { + return x.parts[level] && !x.parts[level + 1] && arraysEqual(x.parts.slice(0, level), parent); }); - var maxLength = items.reduce(function(a, b) { - return Math.max(a, b.parts.length); - }, 0); - - var cols = createRow(table, 'th', ['record', 'seconds']); - cols[0].colSpan = maxLength; - - function arraysEqual(a, b) { - return !(a < b || b < a); + if (sort === "number") { + matching = matching.sort(function(a, b) { + return b.value - a.value; + }); + } else { + matching = matching.sort(); } + var othersTime = 0; + var othersList = []; + var othersRows = []; + var childrenRows = []; + matching.forEach(function(x) { + var visible = (cutoff === 0 && !hide) || (x.value >= cutoff && !hide); - var addLevel = function(level, parent, hide) { - var matching = items.filter(function(x) { - return x.parts[level] && !x.parts[level + 1] && arraysEqual(x.parts.slice(0, level), parent); - }); - var sorted = matching.sort(function(a, b) { - return b.time - a.time; - }); - var othersTime = 0; - var othersList = []; - var othersRows = []; - var childrenRows = []; - sorted.forEach(function(x) { - var visible = x.time >= cutoff && !hide; + var cells = []; + for (var i = 0; i < maxLength; i++) { + cells.push(x.parts[i]); + } + cells.push(cutoff === 0 ? x.value : x.value.toFixed(3)); + var cols = createRow(table, 'td', cells); + for (i = 0; i < level; i++) { + cols[i].className = 'muted'; + } - var cells = []; - for (var i = 0; i < maxLength; i++) { - cells.push(x.parts[i]); - } - cells.push(x.time.toFixed(3)); - var cols = createRow(table, 'td', cells); - for (i = 0; i < level; i++) { - cols[i].className = 'muted'; - } + var tr = cols[0].parentNode; + if (!visible) { + tr.classList.add("hidden"); + } - var tr = cols[0].parentNode; - if (!visible) { - tr.classList.add("hidden"); - } - - if (x.time >= cutoff) { - childrenRows.push(tr); - } else { - othersTime += x.time; - othersList.push(x.parts[level]); - othersRows.push(tr); - } - - var children = addLevel(level + 1, parent.concat([x.parts[level]]), true); - if (children.length > 0) { - var cell = cols[level]; - var onclick = function() { - cell.classList.remove("link"); - cell.removeEventListener("click", onclick); - children.forEach(function(x) { - x.classList.remove("hidden"); - }); - }; - cell.classList.add("link"); - cell.addEventListener("click", onclick); - } - }); - - if (othersTime > 0) { - var cells = []; - for (var i = 0; i < maxLength; i++) { - cells.push(parent[i]); - } - cells.push(othersTime.toFixed(3)); - cells[level] = 'others'; - var cols = createRow(table, 'td', cells); - for (i = 0; i < level; i++) { - cols[i].className = 'muted'; - } + if (cutoff === 0 || x.value >= cutoff) { + childrenRows.push(tr); + } else { + othersTime += x.value; + othersList.push(x.parts[level]); + othersRows.push(tr); + } + var children = addLevel(level + 1, parent.concat([x.parts[level]]), true); + if (children.length > 0) { var cell = cols[level]; - var tr = cell.parentNode; var onclick = function() { - tr.classList.add("hidden"); cell.classList.remove("link"); cell.removeEventListener("click", onclick); - othersRows.forEach(function(x) { + children.forEach(function(x) { x.classList.remove("hidden"); }); }; - - cell.title = othersList.join(", "); cell.classList.add("link"); cell.addEventListener("click", onclick); + } + }); - if (hide) { - tr.classList.add("hidden"); - } - - childrenRows.push(tr); + if (othersTime > 0) { + var cells = []; + for (var i = 0; i < maxLength; i++) { + cells.push(parent[i]); + } + cells.push(othersTime.toFixed(3)); + cells[level] = 'others'; + var cols = createRow(table, 'td', cells); + for (i = 0; i < level; i++) { + cols[i].className = 'muted'; } - return childrenRows; - }; + var cell = cols[level]; + var tr = cell.parentNode; + var onclick = function() { + tr.classList.add("hidden"); + cell.classList.remove("link"); + cell.removeEventListener("click", onclick); + othersRows.forEach(function(x) { + x.classList.remove("hidden"); + }); + }; - addLevel(0, []); + cell.title = othersList.join(", "); + cell.classList.add("link"); + cell.addEventListener("click", onclick); + if (hide) { + tr.classList.add("hidden"); + } + + childrenRows.push(tr); + } + + return childrenRows; + }; + + addLevel(0, []); + + return table; +} + +function showProfile(path, cutoff = 0.05) { + requestGet(path, {}, function(data) { + data.records['total'] = data.total; + const table = createVisualizationTable(data.records, cutoff, "number"); popup(table); }); } diff --git a/javascript/progressbar.js b/javascript/progressbar.js index f068bac6..23dea64c 100644 --- a/javascript/progressbar.js +++ b/javascript/progressbar.js @@ -76,6 +76,26 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgre var dateStart = new Date(); var wasEverActive = false; var parentProgressbar = progressbarContainer.parentNode; + var wakeLock = null; + + var requestWakeLock = async function() { + if (!opts.prevent_screen_sleep_during_generation || wakeLock) return; + try { + wakeLock = await navigator.wakeLock.request('screen'); + } catch (err) { + console.error('Wake Lock is not supported.'); + } + }; + + var releaseWakeLock = async function() { + if (!opts.prevent_screen_sleep_during_generation || !wakeLock) return; + try { + await wakeLock.release(); + wakeLock = null; + } catch (err) { + console.error('Wake Lock release failed', err); + } + }; var divProgress = document.createElement('div'); divProgress.className = 'progressDiv'; @@ -89,6 +109,7 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgre var livePreview = null; var removeProgressBar = function() { + releaseWakeLock(); if (!divProgress) return; setTitle(""); @@ -100,6 +121,7 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgre }; var funProgress = function(id_task) { + requestWakeLock(); request("./internal/progress", {id_task: id_task, live_preview: false}, function(res) { if (res.completed) { removeProgressBar(); diff --git a/javascript/resizeHandle.js b/javascript/resizeHandle.js index c4e9de58..4aeb14b4 100644 --- a/javascript/resizeHandle.js +++ b/javascript/resizeHandle.js @@ -22,6 +22,9 @@ } function displayResizeHandle(parent) { + if (!parent.needHideOnMoblie) { + return true; + } if (window.innerWidth < GRADIO_MIN_WIDTH * 2 + PAD * 4) { parent.style.display = 'flex'; parent.resizeHandle.style.display = "none"; @@ -41,7 +44,7 @@ const ratio = newParentWidth / oldParentWidth; - const newWidthL = Math.max(Math.floor(ratio * widthL), GRADIO_MIN_WIDTH); + const newWidthL = Math.max(Math.floor(ratio * widthL), parent.minLeftColWidth); setLeftColGridTemplate(parent, newWidthL); R.parentWidth = newParentWidth; @@ -64,7 +67,24 @@ parent.style.display = 'grid'; parent.style.gap = '0'; - const gridTemplateColumns = `${parent.children[0].style.flexGrow}fr ${PAD}px ${parent.children[1].style.flexGrow}fr`; + let leftColTemplate = ""; + if (parent.children[0].style.flexGrow) { + leftColTemplate = `${parent.children[0].style.flexGrow}fr`; + parent.minLeftColWidth = GRADIO_MIN_WIDTH; + parent.minRightColWidth = GRADIO_MIN_WIDTH; + parent.needHideOnMoblie = true; + } else { + leftColTemplate = parent.children[0].style.flexBasis; + parent.minLeftColWidth = parent.children[0].style.flexBasis.slice(0, -2) / 2; + parent.minRightColWidth = 0; + parent.needHideOnMoblie = false; + } + + if (!leftColTemplate) { + leftColTemplate = '1fr'; + } + + const gridTemplateColumns = `${leftColTemplate} ${PAD}px ${parent.children[1].style.flexGrow}fr`; parent.style.gridTemplateColumns = gridTemplateColumns; parent.style.originalGridTemplateColumns = gridTemplateColumns; @@ -132,7 +152,7 @@ } else { delta = R.screenX - evt.changedTouches[0].screenX; } - const leftColWidth = Math.max(Math.min(R.leftColStartWidth - delta, R.parent.offsetWidth - GRADIO_MIN_WIDTH - PAD), GRADIO_MIN_WIDTH); + const leftColWidth = Math.max(Math.min(R.leftColStartWidth - delta, R.parent.offsetWidth - R.parent.minRightColWidth - PAD), R.parent.minLeftColWidth); setLeftColGridTemplate(R.parent, leftColWidth); } }); @@ -171,10 +191,15 @@ setupResizeHandle = setup; })(); -onUiLoaded(function() { + +function setupAllResizeHandles() { for (var elem of gradioApp().querySelectorAll('.resize-handle-row')) { - if (!elem.querySelector('.resize-handle')) { + if (!elem.querySelector('.resize-handle') && !elem.children[0].classList.contains("hidden")) { setupResizeHandle(elem); } } -}); +} + + +onUiLoaded(setupAllResizeHandles); + diff --git a/javascript/ui.js b/javascript/ui.js index 3d079b3d..20309634 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -26,6 +26,14 @@ function selected_gallery_index() { return all_gallery_buttons().findIndex(elem => elem.classList.contains('selected')); } +function gallery_container_buttons(gallery_container) { + return gradioApp().querySelectorAll(`#${gallery_container} .thumbnail-item.thumbnail-small`); +} + +function selected_gallery_index_id(gallery_container) { + return Array.from(gallery_container_buttons(gallery_container)).findIndex(elem => elem.classList.contains('selected')); +} + function extract_image_from_gallery(gallery) { if (gallery.length == 0) { return [null]; @@ -136,8 +144,7 @@ function showSubmitInterruptingPlaceholder(tabname) { function showRestoreProgressButton(tabname, show) { var button = gradioApp().getElementById(tabname + "_restore_progress"); if (!button) return; - - button.style.display = show ? "flex" : "none"; + button.style.setProperty('display', show ? 'flex' : 'none', 'important'); } function submit() { @@ -209,6 +216,7 @@ function restoreProgressTxt2img() { var id = localGet("txt2img_task_id"); if (id) { + showSubmitInterruptingPlaceholder('txt2img'); requestProgress(id, gradioApp().getElementById('txt2img_gallery_container'), gradioApp().getElementById('txt2img_gallery'), function() { showSubmitButtons('txt2img', true); }, null, 0); @@ -223,6 +231,7 @@ function restoreProgressImg2img() { var id = localGet("img2img_task_id"); if (id) { + showSubmitInterruptingPlaceholder('img2img'); requestProgress(id, gradioApp().getElementById('img2img_gallery_container'), gradioApp().getElementById('img2img_gallery'), function() { showSubmitButtons('img2img', true); }, null, 0); @@ -298,6 +307,7 @@ onAfterUiUpdate(function() { var jsdata = textarea.value; opts = JSON.parse(jsdata); + executeCallbacks(optionsAvailableCallbacks); /*global optionsAvailableCallbacks*/ executeCallbacks(optionsChangedCallbacks); /*global optionsChangedCallbacks*/ Object.defineProperty(textarea, 'value', { @@ -336,8 +346,8 @@ onOptionsChanged(function() { let txt2img_textarea, img2img_textarea = undefined; function restart_reload() { + document.body.style.backgroundColor = "var(--background-fill-primary)"; document.body.innerHTML = '

Reloading...

'; - var requestPing = function() { requestGet("./internal/ping", {}, function(data) { location.reload(); @@ -411,7 +421,7 @@ function switchWidthHeight(tabname) { var onEditTimers = {}; -// calls func after afterMs milliseconds has passed since the input elem has beed enited by user +// calls func after afterMs milliseconds has passed since the input elem has been edited by user function onEdit(editId, elem, afterMs, func) { var edited = function() { var existingTimer = onEditTimers[editId]; diff --git a/modules/api/api.py b/modules/api/api.py index 4e656082..97ec7514 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -17,13 +17,13 @@ from fastapi.encoders import jsonable_encoder from secrets import compare_digest import modules.shared as shared -from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing, errors, restart, shared_items, script_callbacks, infotext_utils, sd_models +from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing, errors, restart, shared_items, script_callbacks, infotext_utils, sd_models, sd_schedulers from modules.api import models from modules.shared import opts from modules.processing import StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img, process_images from modules.textual_inversion.textual_inversion import create_embedding, train_embedding from modules.hypernetworks.hypernetwork import create_hypernetwork, train_hypernetwork -from PIL import PngImagePlugin, Image +from PIL import PngImagePlugin from modules.sd_models_config import find_checkpoint_config_near_filename from modules.realesrgan_model import get_realesrgan_models from modules import devices @@ -43,7 +43,7 @@ def script_name_to_index(name, scripts): def validate_sampler_name(name): config = sd_samplers.all_samplers_map.get(name, None) if config is None: - raise HTTPException(status_code=404, detail="Sampler not found") + raise HTTPException(status_code=400, detail="Sampler not found") return name @@ -85,7 +85,7 @@ def decode_base64_to_image(encoding): headers = {'user-agent': opts.api_useragent} if opts.api_useragent else {} response = requests.get(encoding, timeout=30, headers=headers) try: - image = Image.open(BytesIO(response.content)) + image = images.read(BytesIO(response.content)) return image except Exception as e: raise HTTPException(status_code=500, detail="Invalid image url") from e @@ -93,7 +93,7 @@ def decode_base64_to_image(encoding): if encoding.startswith("data:image/"): encoding = encoding.split(";")[1].split(",")[1] try: - image = Image.open(BytesIO(base64.b64decode(encoding))) + image = images.read(BytesIO(base64.b64decode(encoding))) return image except Exception as e: raise HTTPException(status_code=500, detail="Invalid encoded image") from e @@ -113,7 +113,7 @@ def encode_pil_to_base64(image): image.save(output_bytes, format="PNG", pnginfo=(metadata if use_metadata else None), quality=opts.jpeg_quality) elif opts.samples_format.lower() in ("jpg", "jpeg", "webp"): - if image.mode == "RGBA": + if image.mode in ("RGBA", "P"): image = image.convert("RGB") parameters = image.info.get('parameters', None) exif_bytes = piexif.dump({ @@ -221,6 +221,7 @@ class Api: self.add_api_route("/sdapi/v1/options", self.set_config, methods=["POST"]) self.add_api_route("/sdapi/v1/cmd-flags", self.get_cmd_flags, methods=["GET"], response_model=models.FlagsModel) self.add_api_route("/sdapi/v1/samplers", self.get_samplers, methods=["GET"], response_model=list[models.SamplerItem]) + self.add_api_route("/sdapi/v1/schedulers", self.get_schedulers, methods=["GET"], response_model=list[models.SchedulerItem]) self.add_api_route("/sdapi/v1/upscalers", self.get_upscalers, methods=["GET"], response_model=list[models.UpscalerItem]) self.add_api_route("/sdapi/v1/latent-upscale-modes", self.get_latent_upscale_modes, methods=["GET"], response_model=list[models.LatentUpscalerModeItem]) self.add_api_route("/sdapi/v1/sd-models", self.get_sd_models, methods=["GET"], response_model=list[models.SDModelItem]) @@ -360,7 +361,7 @@ class Api: return script_args def apply_infotext(self, request, tabname, *, script_runner=None, mentioned_script_args=None): - """Processes `infotext` field from the `request`, and sets other fields of the `request` accoring to what's in infotext. + """Processes `infotext` field from the `request`, and sets other fields of the `request` according to what's in infotext. If request already has a field set, and that field is encountered in infotext too, the value from infotext is ignored. @@ -371,7 +372,7 @@ class Api: return {} possible_fields = infotext_utils.paste_fields[tabname]["fields"] - set_fields = request.model_dump(exclude_unset=True) if hasattr(request, "request") else request.dict(exclude_unset=True) # pydantic v1/v2 have differenrt names for this + set_fields = request.model_dump(exclude_unset=True) if hasattr(request, "request") else request.dict(exclude_unset=True) # pydantic v1/v2 have different names for this params = infotext_utils.parse_generation_parameters(request.infotext) def get_field_value(field, params): @@ -409,8 +410,8 @@ class Api: if request.override_settings is None: request.override_settings = {} - overriden_settings = infotext_utils.get_override_settings(params) - for _, setting_name, value in overriden_settings: + overridden_settings = infotext_utils.get_override_settings(params) + for _, setting_name, value in overridden_settings: if setting_name not in request.override_settings: request.override_settings[setting_name] = value @@ -437,15 +438,19 @@ class Api: self.apply_infotext(txt2imgreq, "txt2img", script_runner=script_runner, mentioned_script_args=infotext_script_args) selectable_scripts, selectable_script_idx = self.get_selectable_script(txt2imgreq.script_name, script_runner) + sampler, scheduler = sd_samplers.get_sampler_and_scheduler(txt2imgreq.sampler_name or txt2imgreq.sampler_index, txt2imgreq.scheduler) populate = txt2imgreq.copy(update={ # Override __init__ params - "sampler_name": validate_sampler_name(txt2imgreq.sampler_name or txt2imgreq.sampler_index), + "sampler_name": validate_sampler_name(sampler), "do_not_save_samples": not txt2imgreq.save_images, "do_not_save_grid": not txt2imgreq.save_images, }) if populate.sampler_name: populate.sampler_index = None # prevent a warning later on + if not populate.scheduler and scheduler != "Automatic": + populate.scheduler = scheduler + args = vars(populate) args.pop('script_name', None) args.pop('script_args', None) # will refeed them to the pipeline directly after initializing them @@ -501,9 +506,10 @@ class Api: self.apply_infotext(img2imgreq, "img2img", script_runner=script_runner, mentioned_script_args=infotext_script_args) selectable_scripts, selectable_script_idx = self.get_selectable_script(img2imgreq.script_name, script_runner) + sampler, scheduler = sd_samplers.get_sampler_and_scheduler(img2imgreq.sampler_name or img2imgreq.sampler_index, img2imgreq.scheduler) populate = img2imgreq.copy(update={ # Override __init__ params - "sampler_name": validate_sampler_name(img2imgreq.sampler_name or img2imgreq.sampler_index), + "sampler_name": validate_sampler_name(sampler), "do_not_save_samples": not img2imgreq.save_images, "do_not_save_grid": not img2imgreq.save_images, "mask": mask, @@ -511,6 +517,9 @@ class Api: if populate.sampler_name: populate.sampler_index = None # prevent a warning later on + if not populate.scheduler and scheduler != "Automatic": + populate.scheduler = scheduler + args = vars(populate) args.pop('include_init_images', None) # this is meant to be done by "exclude": True in model, but it's for a reason that I cannot determine. args.pop('script_name', None) @@ -683,6 +692,17 @@ class Api: def get_samplers(self): return [{"name": sampler[0], "aliases":sampler[2], "options":sampler[3]} for sampler in sd_samplers.all_samplers] + def get_schedulers(self): + return [ + { + "name": scheduler.name, + "label": scheduler.label, + "aliases": scheduler.aliases, + "default_rho": scheduler.default_rho, + "need_inner_model": scheduler.need_inner_model, + } + for scheduler in sd_schedulers.schedulers] + def get_upscalers(self): return [ { diff --git a/modules/api/models.py b/modules/api/models.py index 16edf11c..79c4cbbb 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -147,7 +147,7 @@ class ExtrasBaseRequest(BaseModel): gfpgan_visibility: float = Field(default=0, title="GFPGAN Visibility", ge=0, le=1, allow_inf_nan=False, description="Sets the visibility of GFPGAN, values should be between 0 and 1.") codeformer_visibility: float = Field(default=0, title="CodeFormer Visibility", ge=0, le=1, allow_inf_nan=False, description="Sets the visibility of CodeFormer, values should be between 0 and 1.") codeformer_weight: float = Field(default=0, title="CodeFormer Weight", ge=0, le=1, allow_inf_nan=False, description="Sets the weight of CodeFormer, values should be between 0 and 1.") - upscaling_resize: float = Field(default=2, title="Upscaling Factor", ge=1, le=8, description="By how much to upscale the image, only used when resize_mode=0.") + upscaling_resize: float = Field(default=2, title="Upscaling Factor", gt=0, description="By how much to upscale the image, only used when resize_mode=0.") upscaling_resize_w: int = Field(default=512, title="Target Width", ge=1, description="Target width for the upscaler to hit. Only used when resize_mode=1.") upscaling_resize_h: int = Field(default=512, title="Target Height", ge=1, description="Target height for the upscaler to hit. Only used when resize_mode=1.") upscaling_crop: bool = Field(default=True, title="Crop to fit", description="Should the upscaler crop the image to fit in the chosen size?") @@ -235,6 +235,13 @@ class SamplerItem(BaseModel): aliases: list[str] = Field(title="Aliases") options: dict[str, str] = Field(title="Options") +class SchedulerItem(BaseModel): + name: str = Field(title="Name") + label: str = Field(title="Label") + aliases: Optional[list[str]] = Field(title="Aliases") + default_rho: Optional[float] = Field(title="Default Rho") + need_inner_model: Optional[bool] = Field(title="Needs Inner Model") + class UpscalerItem(BaseModel): name: str = Field(title="Name") model_name: Optional[str] = Field(title="Model Name") diff --git a/modules/cache.py b/modules/cache.py index a9822a0e..f4e5f702 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -2,48 +2,55 @@ import json import os import os.path import threading -import time + +import diskcache +import tqdm from modules.paths import data_path, script_path cache_filename = os.environ.get('SD_WEBUI_CACHE_FILE', os.path.join(data_path, "cache.json")) -cache_data = None +cache_dir = os.environ.get('SD_WEBUI_CACHE_DIR', os.path.join(data_path, "cache")) +caches = {} cache_lock = threading.Lock() -dump_cache_after = None -dump_cache_thread = None - def dump_cache(): - """ - Marks cache for writing to disk. 5 seconds after no one else flags the cache for writing, it is written. - """ + """old function for dumping cache to disk; does nothing since diskcache.""" - global dump_cache_after - global dump_cache_thread + pass - def thread_func(): - global dump_cache_after - global dump_cache_thread - while dump_cache_after is not None and time.time() < dump_cache_after: - time.sleep(1) +def make_cache(subsection: str) -> diskcache.Cache: + return diskcache.Cache( + os.path.join(cache_dir, subsection), + size_limit=2**32, # 4 GB, culling oldest first + disk_min_file_size=2**18, # keep up to 256KB in Sqlite + ) - with cache_lock: - cache_filename_tmp = cache_filename + "-" - with open(cache_filename_tmp, "w", encoding="utf8") as file: - json.dump(cache_data, file, indent=4, ensure_ascii=False) - os.replace(cache_filename_tmp, cache_filename) +def convert_old_cached_data(): + try: + with open(cache_filename, "r", encoding="utf8") as file: + data = json.load(file) + except FileNotFoundError: + return + except Exception: + os.replace(cache_filename, os.path.join(script_path, "tmp", "cache.json")) + print('[ERROR] issue occurred while trying to read cache.json; old cache has been moved to tmp/cache.json') + return - dump_cache_after = None - dump_cache_thread = None + total_count = sum(len(keyvalues) for keyvalues in data.values()) - with cache_lock: - dump_cache_after = time.time() + 5 - if dump_cache_thread is None: - dump_cache_thread = threading.Thread(name='cache-writer', target=thread_func) - dump_cache_thread.start() + with tqdm.tqdm(total=total_count, desc="converting cache") as progress: + for subsection, keyvalues in data.items(): + cache_obj = caches.get(subsection) + if cache_obj is None: + cache_obj = make_cache(subsection) + caches[subsection] = cache_obj + + for key, value in keyvalues.items(): + cache_obj[key] = value + progress.update(1) def cache(subsection): @@ -54,28 +61,21 @@ def cache(subsection): subsection (str): The subsection identifier for the cache. Returns: - dict: The cache data for the specified subsection. + diskcache.Cache: The cache data for the specified subsection. """ - global cache_data - - if cache_data is None: + cache_obj = caches.get(subsection) + if not cache_obj: with cache_lock: - if cache_data is None: - try: - with open(cache_filename, "r", encoding="utf8") as file: - cache_data = json.load(file) - except FileNotFoundError: - cache_data = {} - except Exception: - os.replace(cache_filename, os.path.join(script_path, "tmp", "cache.json")) - print('[ERROR] issue occurred while trying to read cache.json, move current cache to tmp/cache.json and create new cache') - cache_data = {} + if not os.path.exists(cache_dir) and os.path.isfile(cache_filename): + convert_old_cached_data() - s = cache_data.get(subsection, {}) - cache_data[subsection] = s + cache_obj = caches.get(subsection) + if not cache_obj: + cache_obj = make_cache(subsection) + caches[subsection] = cache_obj - return s + return cache_obj def cached_data_for_file(subsection, title, filename, func): diff --git a/modules/call_queue.py b/modules/call_queue.py index bcd7c546..555c3531 100644 --- a/modules/call_queue.py +++ b/modules/call_queue.py @@ -1,8 +1,9 @@ +import os.path from functools import wraps import html import time -from modules import shared, progress, errors, devices, fifo_lock +from modules import shared, progress, errors, devices, fifo_lock, profiling queue_lock = fifo_lock.FIFOLock() @@ -46,6 +47,22 @@ def wrap_gradio_gpu_call(func, extra_outputs=None): def wrap_gradio_call(func, extra_outputs=None, add_stats=False): + @wraps(func) + def f(*args, **kwargs): + try: + res = func(*args, **kwargs) + finally: + shared.state.skipped = False + shared.state.interrupted = False + shared.state.stopping_generation = False + shared.state.job_count = 0 + shared.state.job = "" + return res + + return wrap_gradio_call_no_job(f, extra_outputs, add_stats) + + +def wrap_gradio_call_no_job(func, extra_outputs=None, add_stats=False): @wraps(func) def f(*args, extra_outputs_array=extra_outputs, **kwargs): run_memmon = shared.opts.memmon_poll_rate > 0 and not shared.mem_mon.disabled and add_stats @@ -65,9 +82,6 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False): arg_str += f" (Argument list truncated at {max_debug_str_len}/{len(arg_str)} characters)" errors.report(f"{message}\n{arg_str}", exc_info=True) - shared.state.job = "" - shared.state.job_count = 0 - if extra_outputs_array is None: extra_outputs_array = [None, ''] @@ -76,11 +90,6 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False): devices.torch_gc() - shared.state.skipped = False - shared.state.interrupted = False - shared.state.stopping_generation = False - shared.state.job_count = 0 - if not add_stats: return tuple(res) @@ -100,8 +109,8 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False): sys_pct = sys_peak/max(sys_total, 1) * 100 toltip_a = "Active: peak amount of video memory used during generation (excluding cached data)" - toltip_r = "Reserved: total amout of video memory allocated by the Torch library " - toltip_sys = "System: peak amout of video memory allocated by all running programs, out of total capacity" + toltip_r = "Reserved: total amount of video memory allocated by the Torch library " + toltip_sys = "System: peak amount of video memory allocated by all running programs, out of total capacity" text_a = f"A: {active_peak/1024:.2f} GB" text_r = f"R: {reserved_peak/1024:.2f} GB" @@ -111,9 +120,15 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False): else: vram_html = '' + if shared.opts.profiling_enable and os.path.exists(shared.opts.profiling_filename): + profiling_html = f"

[ Profile ]

" + else: + profiling_html = '' + # last item is always HTML - res[-1] += f"

Time taken: {elapsed_text}

{vram_html}
" + res[-1] += f"

Time taken: {elapsed_text}

{vram_html}{profiling_html}
" return tuple(res) return f + diff --git a/modules/cmd_args.py b/modules/cmd_args.py index 213cba98..d71982b2 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -20,6 +20,7 @@ parser.add_argument("--dump-sysinfo", action='store_true', help="launch.py argum parser.add_argument("--loglevel", type=str, help="log level; one of: CRITICAL, ERROR, WARNING, INFO, DEBUG", default=None) parser.add_argument("--do-not-download-clip", action='store_true', help="do not download CLIP model even if it's not included in the checkpoint") parser.add_argument("--data-dir", type=normalized_filepath, default=os.path.dirname(os.path.dirname(os.path.realpath(__file__))), help="base path where all user data is stored") +parser.add_argument("--models-dir", type=normalized_filepath, default=None, help="base path where models are stored; overrides --data-dir") parser.add_argument("--config", type=normalized_filepath, default=sd_default_config, help="path to config which constructs model",) parser.add_argument("--ckpt", type=normalized_filepath, default=sd_model_file, help="path to checkpoint of stable diffusion model; if specified, this checkpoint will be added to the list of checkpoints and loaded",) parser.add_argument("--ckpt-dir", type=normalized_filepath, default=None, help="Path to directory with stable diffusion checkpoints") @@ -29,7 +30,7 @@ parser.add_argument("--gfpgan-model", type=normalized_filepath, help="GFPGAN mod parser.add_argument("--no-half", action='store_true', help="do not switch the model to 16-bit floats") parser.add_argument("--no-half-vae", action='store_true', help="do not switch the VAE model to 16-bit floats") parser.add_argument("--no-progressbar-hiding", action='store_true', help="do not hide progressbar in gradio UI (we hide it because it slows down ML if you have hardware acceleration in browser)") -parser.add_argument("--max-batch-count", type=int, default=16, help="maximum batch count value for the UI") +parser.add_argument("--max-batch-count", type=int, default=16, help="does not do anything") parser.add_argument("--embeddings-dir", type=normalized_filepath, default=os.path.join(data_path, 'embeddings'), help="embeddings directory for textual inversion (default: embeddings)") parser.add_argument("--textual-inversion-templates-dir", type=normalized_filepath, default=os.path.join(script_path, 'textual_inversion_templates'), help="directory with textual inversion templates") parser.add_argument("--hypernetwork-dir", type=normalized_filepath, default=os.path.join(models_path, 'hypernetworks'), help="hypernetwork directory") @@ -41,7 +42,7 @@ parser.add_argument("--lowvram", action='store_true', help="enable stable diffus parser.add_argument("--lowram", action='store_true', help="load stable diffusion checkpoint weights to VRAM instead of RAM") parser.add_argument("--always-batch-cond-uncond", action='store_true', help="does not do anything") parser.add_argument("--unload-gfpgan", action='store_true', help="does not do anything.") -parser.add_argument("--precision", type=str, help="evaluate at this precision", choices=["full", "autocast"], default="autocast") +parser.add_argument("--precision", type=str, help="evaluate at this precision", choices=["full", "half", "autocast"], default="autocast") parser.add_argument("--upcast-sampling", action='store_true', help="upcast sampling. No effect with --no-half. Usually produces similar results to --no-half with better performance while using less memory.") parser.add_argument("--share", action='store_true', help="use share=True for gradio and make the UI accessible through their site") parser.add_argument("--ngrok", type=str, help="ngrok authtoken, alternative to gradio --share", default=None) @@ -121,4 +122,7 @@ parser.add_argument('--api-server-stop', action='store_true', help='enable serve parser.add_argument('--timeout-keep-alive', type=int, default=30, help='set timeout_keep_alive for uvicorn') parser.add_argument("--disable-all-extensions", action='store_true', help="prevent all extensions from running regardless of any other settings", default=False) parser.add_argument("--disable-extra-extensions", action='store_true', help="prevent all extensions except built-in from running regardless of any other settings", default=False) -parser.add_argument("--skip-load-model-at-start", action='store_true', help="if load a model at web start, only take effect when --nowebui", ) +parser.add_argument("--skip-load-model-at-start", action='store_true', help="if load a model at web start, only take effect when --nowebui") +parser.add_argument("--unix-filenames-sanitization", action='store_true', help="allow any symbols except '/' in filenames. May conflict with your browser and file system") +parser.add_argument("--filenames-max-length", type=int, default=128, help='maximal length of filenames of saved images. If you override it, it can conflict with your file system') +parser.add_argument("--no-prompt-history", action='store_true', help="disable read prompt from last generation feature; settings this argument will not create '--data_path/params.txt' file") diff --git a/modules/codeformer_model.py b/modules/codeformer_model.py index 44b84618..0b353353 100644 --- a/modules/codeformer_model.py +++ b/modules/codeformer_model.py @@ -50,7 +50,7 @@ class FaceRestorerCodeFormer(face_restoration_utils.CommonFaceRestoration): def restore_face(cropped_face_t): assert self.net is not None - return self.net(cropped_face_t, w=w, adain=True)[0] + return self.net(cropped_face_t, weight=w, adain=True)[0] return self.restore_with_helper(np_image, restore_face) diff --git a/modules/deepbooru.py b/modules/deepbooru.py index 547e1b4c..fb043feb 100644 --- a/modules/deepbooru.py +++ b/modules/deepbooru.py @@ -57,7 +57,7 @@ class DeepDanbooru: a = np.expand_dims(np.array(pic, dtype=np.float32), 0) / 255 with torch.no_grad(), devices.autocast(): - x = torch.from_numpy(a).to(devices.device) + x = torch.from_numpy(a).to(devices.device, devices.dtype) y = self.model(x)[0].detach().cpu().numpy() probability_dict = {} diff --git a/modules/devices.py b/modules/devices.py index 28c0c54d..ee679141 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -114,6 +114,9 @@ errors.run(enable_tf32, "Enabling TF32") cpu: torch.device = torch.device("cpu") fp8: bool = False +# Force fp16 for all models in inference. No casting during inference. +# This flag is controlled by "--precision half" command line arg. +force_fp16: bool = False device: torch.device = None device_interrogate: torch.device = None device_gfpgan: torch.device = None @@ -127,6 +130,8 @@ unet_needs_upcast = False def cond_cast_unet(input): + if force_fp16: + return input.to(torch.float16) return input.to(dtype_unet) if unet_needs_upcast else input @@ -206,6 +211,11 @@ def autocast(disable=False): if disable: return contextlib.nullcontext() + if force_fp16: + # No casting during inference if force_fp16 is enabled. + # All tensor dtype conversion happens before inference. + return contextlib.nullcontext() + if fp8 and device==cpu: return torch.autocast("cpu", dtype=torch.bfloat16, enabled=True) @@ -233,22 +243,22 @@ def test_for_nans(x, where): if shared.cmd_opts.disable_nan_check: return - if not torch.all(torch.isnan(x)).item(): + if not torch.isnan(x[(0, ) * len(x.shape)]): return if where == "unet": - message = "A tensor with all NaNs was produced in Unet." + message = "A tensor with NaNs was produced in Unet." if not shared.cmd_opts.no_half: message += " This could be either because there's not enough precision to represent the picture, or because your video card does not support half type. Try setting the \"Upcast cross attention layer to float32\" option in Settings > Stable Diffusion or using the --no-half commandline argument to fix this." elif where == "vae": - message = "A tensor with all NaNs was produced in VAE." + message = "A tensor with NaNs was produced in VAE." if not shared.cmd_opts.no_half and not shared.cmd_opts.no_half_vae: message += " This could be because there's not enough precision to represent the picture. Try adding --no-half-vae commandline argument to fix this." else: - message = "A tensor with all NaNs was produced." + message = "A tensor with NaNs was produced." message += " Use --disable-nan-check commandline argument to disable this check." @@ -258,8 +268,8 @@ def test_for_nans(x, where): @lru_cache def first_time_calculation(): """ - just do any calculation with pytorch layers - the first time this is done it allocaltes about 700MB of memory and - spends about 2.7 seconds doing that, at least wih NVidia. + just do any calculation with pytorch layers - the first time this is done it allocates about 700MB of memory and + spends about 2.7 seconds doing that, at least with NVidia. """ x = torch.zeros((1, 1)).to(device, dtype) @@ -269,3 +279,17 @@ def first_time_calculation(): x = torch.zeros((1, 1, 3, 3)).to(device, dtype) conv2d = torch.nn.Conv2d(1, 1, (3, 3)).to(device, dtype) conv2d(x) + + +def force_model_fp16(): + """ + ldm and sgm has modules.diffusionmodules.util.GroupNorm32.forward, which + force conversion of input to float32. If force_fp16 is enabled, we need to + prevent this casting. + """ + assert force_fp16 + import sgm.modules.diffusionmodules.util as sgm_util + import ldm.modules.diffusionmodules.util as ldm_util + sgm_util.GroupNorm32 = torch.nn.GroupNorm + ldm_util.GroupNorm32 = torch.nn.GroupNorm + print("ldm/sgm GroupNorm32 replaced with normal torch.nn.GroupNorm due to `--precision half`.") diff --git a/modules/extensions.py b/modules/extensions.py index 04bda297..24de766e 100644 --- a/modules/extensions.py +++ b/modules/extensions.py @@ -1,6 +1,7 @@ from __future__ import annotations import configparser +import dataclasses import os import threading import re @@ -9,6 +10,10 @@ from modules import shared, errors, cache, scripts from modules.gitpython_hack import Repo from modules.paths_internal import extensions_dir, extensions_builtin_dir, script_path # noqa: F401 +extensions: list[Extension] = [] +extension_paths: dict[str, Extension] = {} +loaded_extensions: dict[str, Exception] = {} + os.makedirs(extensions_dir, exist_ok=True) @@ -22,6 +27,13 @@ def active(): return [x for x in extensions if x.enabled] +@dataclasses.dataclass +class CallbackOrderInfo: + name: str + before: list + after: list + + class ExtensionMetadata: filename = "metadata.ini" config: configparser.ConfigParser @@ -42,7 +54,7 @@ class ExtensionMetadata: self.canonical_name = self.config.get("Extension", "Name", fallback=canonical_name) self.canonical_name = canonical_name.lower().strip() - self.requires = self.get_script_requirements("Requires", "Extension") + self.requires = None def get_script_requirements(self, field, section, extra_section=None): """reads a list of requirements from the config; field is the name of the field in the ini file, @@ -54,7 +66,15 @@ class ExtensionMetadata: if extra_section: x = x + ', ' + self.config.get(extra_section, field, fallback='') - return self.parse_list(x.lower()) + listed_requirements = self.parse_list(x.lower()) + res = [] + + for requirement in listed_requirements: + loaded_requirements = (x for x in requirement.split("|") if x in loaded_extensions) + relevant_requirement = next(loaded_requirements, requirement) + res.append(relevant_requirement) + + return res def parse_list(self, text): """converts a line from config ("ext1 ext2, ext3 ") into a python list (["ext1", "ext2", "ext3"])""" @@ -65,6 +85,22 @@ class ExtensionMetadata: # both "," and " " are accepted as separator return [x for x in re.split(r"[,\s]+", text.strip()) if x] + def list_callback_order_instructions(self): + for section in self.config.sections(): + if not section.startswith("callbacks/"): + continue + + callback_name = section[10:] + + if not callback_name.startswith(self.canonical_name): + errors.report(f"Callback order section for extension {self.canonical_name} is referencing the wrong extension: {section}") + continue + + before = self.parse_list(self.config.get(section, 'Before', fallback='')) + after = self.parse_list(self.config.get(section, 'After', fallback='')) + + yield CallbackOrderInfo(callback_name, before, after) + class Extension: lock = threading.Lock() @@ -155,14 +191,17 @@ class Extension: def check_updates(self): repo = Repo(self.path) + branch_name = f'{repo.remote().name}/{self.branch}' for fetch in repo.remote().fetch(dry_run=True): + if self.branch and fetch.name != branch_name: + continue if fetch.flags != fetch.HEAD_UPTODATE: self.can_update = True self.status = "new commits" return try: - origin = repo.rev_parse('origin') + origin = repo.rev_parse(branch_name) if repo.head.commit != origin: self.can_update = True self.status = "behind HEAD" @@ -175,8 +214,10 @@ class Extension: self.can_update = False self.status = "latest" - def fetch_and_reset_hard(self, commit='origin'): + def fetch_and_reset_hard(self, commit=None): repo = Repo(self.path) + if commit is None: + commit = f'{repo.remote().name}/{self.branch}' # Fix: `error: Your local changes to the following files would be overwritten by merge`, # because WSL2 Docker set 755 file permissions instead of 644, this results to the error. repo.git.fetch(all=True) @@ -186,6 +227,8 @@ class Extension: def list_extensions(): extensions.clear() + extension_paths.clear() + loaded_extensions.clear() if shared.cmd_opts.disable_all_extensions: print("*** \"--disable-all-extensions\" arg was used, will not load any extensions ***") @@ -196,7 +239,6 @@ def list_extensions(): elif shared.opts.disable_all_extensions == "extra": print("*** \"Disable all extensions\" option was set, will only load built-in extensions ***") - loaded_extensions = {} # scan through extensions directory and load metadata for dirname in [extensions_builtin_dir, extensions_dir]: @@ -220,8 +262,12 @@ def list_extensions(): is_builtin = dirname == extensions_builtin_dir extension = Extension(name=extension_dirname, path=path, enabled=extension_dirname not in shared.opts.disabled_extensions, is_builtin=is_builtin, metadata=metadata) extensions.append(extension) + extension_paths[extension.path] = extension loaded_extensions[canonical_name] = extension + for extension in extensions: + extension.metadata.requires = extension.metadata.get_script_requirements("Requires", "Extension") + # check for requirements for extension in extensions: if not extension.enabled: @@ -238,4 +284,16 @@ def list_extensions(): continue -extensions: list[Extension] = [] +def find_extension(filename): + parentdir = os.path.dirname(os.path.realpath(filename)) + + while parentdir != filename: + extension = extension_paths.get(parentdir) + if extension is not None: + return extension + + filename = parentdir + parentdir = os.path.dirname(filename) + + return None + diff --git a/modules/extra_networks.py b/modules/extra_networks.py index 04249dff..ae8d42d9 100644 --- a/modules/extra_networks.py +++ b/modules/extra_networks.py @@ -60,7 +60,7 @@ class ExtraNetwork: Where name matches the name of this ExtraNetwork object, and arg1:arg2:arg3 are any natural number of text arguments separated by colon. - Even if the user does not mention this ExtraNetwork in his prompt, the call will stil be made, with empty params_list - + Even if the user does not mention this ExtraNetwork in his prompt, the call will still be made, with empty params_list - in this case, all effects of this extra networks should be disabled. Can be called multiple times before deactivate() - each new call should override the previous call completely. diff --git a/modules/gfpgan_model.py b/modules/gfpgan_model.py index 445b0409..01ef899e 100644 --- a/modules/gfpgan_model.py +++ b/modules/gfpgan_model.py @@ -36,13 +36,11 @@ class FaceRestorerGFPGAN(face_restoration_utils.CommonFaceRestoration): ext_filter=['.pth'], ): if 'GFPGAN' in os.path.basename(model_path): - model = modelloader.load_spandrel_model( + return modelloader.load_spandrel_model( model_path, device=self.get_device(), expected_architecture='GFPGAN', ).model - model.different_w = True # see https://github.com/chaiNNer-org/spandrel/pull/81 - return model raise ValueError("No GFPGAN model found") def restore(self, np_image): diff --git a/modules/hypernetworks/hypernetwork.py b/modules/hypernetworks/hypernetwork.py index be3e4648..17454665 100644 --- a/modules/hypernetworks/hypernetwork.py +++ b/modules/hypernetworks/hypernetwork.py @@ -11,7 +11,7 @@ import tqdm from einops import rearrange, repeat from ldm.util import default from modules import devices, sd_models, shared, sd_samplers, hashes, sd_hijack_checkpoint, errors -from modules.textual_inversion import textual_inversion, logging +from modules.textual_inversion import textual_inversion, saving_settings from modules.textual_inversion.learn_schedule import LearnRateScheduler from torch import einsum from torch.nn.init import normal_, xavier_normal_, xavier_uniform_, kaiming_normal_, kaiming_uniform_, zeros_ @@ -95,6 +95,7 @@ class HypernetworkModule(torch.nn.Module): zeros_(b) else: raise KeyError(f"Key {weight_init} is not defined as initialization!") + devices.torch_npu_set_device() self.to(devices.device) def fix_old_state_dict(self, state_dict): @@ -532,7 +533,7 @@ def train_hypernetwork(id_task, hypernetwork_name: str, learn_rate: float, batch model_name=checkpoint.model_name, model_hash=checkpoint.shorthash, num_of_dataset_images=len(ds), **{field: getattr(hypernetwork, field) for field in ['layer_structure', 'activation_func', 'weight_init', 'add_layer_norm', 'use_dropout', ]} ) - logging.save_settings_to_file(log_directory, {**saved_params, **locals()}) + saving_settings.save_settings_to_file(log_directory, {**saved_params, **locals()}) latent_sampling_method = ds.latent_sampling_method diff --git a/modules/images.py b/modules/images.py index b6f2358c..cfdfb338 100644 --- a/modules/images.py +++ b/modules/images.py @@ -1,7 +1,7 @@ from __future__ import annotations import datetime - +import functools import pytz import io import math @@ -12,7 +12,9 @@ import re import numpy as np import piexif import piexif.helper -from PIL import Image, ImageFont, ImageDraw, ImageColor, PngImagePlugin +from PIL import Image, ImageFont, ImageDraw, ImageColor, PngImagePlugin, ImageOps +# pillow_avif needs to be imported somewhere in code for it to work +import pillow_avif # noqa: F401 import string import json import hashlib @@ -52,11 +54,14 @@ def image_grid(imgs, batch_size=1, rows=None): params = script_callbacks.ImageGridLoopParams(imgs, cols, rows) script_callbacks.image_grid_callback(params) - w, h = imgs[0].size - grid = Image.new('RGB', size=(params.cols * w, params.rows * h), color='black') + w, h = map(max, zip(*(img.size for img in imgs))) + grid_background_color = ImageColor.getcolor(opts.grid_background_color, 'RGB') + grid = Image.new('RGB', size=(params.cols * w, params.rows * h), color=grid_background_color) for i, img in enumerate(params.imgs): - grid.paste(img, box=(i % params.cols * w, i // params.cols * h)) + img_w, img_h = img.size + w_offset, h_offset = 0 if img_w == w else (w - img_w) // 2, 0 if img_h == h else (h - img_h) // 2 + grid.paste(img, box=(i % params.cols * w + w_offset, i // params.cols * h + h_offset)) return grid @@ -321,13 +326,16 @@ def resize_image(resize_mode, im, width, height, upscaler_name=None): return res -invalid_filename_chars = '#<>:"/\\|?*\n\r\t' +if not shared.cmd_opts.unix_filenames_sanitization: + invalid_filename_chars = '#<>:"/\\|?*\n\r\t' +else: + invalid_filename_chars = '/' invalid_filename_prefix = ' ' invalid_filename_postfix = ' .' re_nonletters = re.compile(r'[\s' + string.punctuation + ']+') re_pattern = re.compile(r"(.*?)(?:\[([^\[\]]+)\]|$)") re_pattern_arg = re.compile(r"(.*)<([^>]*)>$") -max_filename_part_length = 128 +max_filename_part_length = shared.cmd_opts.filenames_max_length NOTHING_AND_SKIP_PREVIOUS_TEXT = object() @@ -344,8 +352,35 @@ def sanitize_filename_part(text, replace_spaces=True): return text +@functools.cache +def get_scheduler_str(sampler_name, scheduler_name): + """Returns {Scheduler} if the scheduler is applicable to the sampler""" + if scheduler_name == 'Automatic': + config = sd_samplers.find_sampler_config(sampler_name) + scheduler_name = config.options.get('scheduler', 'Automatic') + return scheduler_name.capitalize() + + +@functools.cache +def get_sampler_scheduler_str(sampler_name, scheduler_name): + """Returns the '{Sampler} {Scheduler}' if the scheduler is applicable to the sampler""" + return f'{sampler_name} {get_scheduler_str(sampler_name, scheduler_name)}' + + +def get_sampler_scheduler(p, sampler): + """Returns '{Sampler} {Scheduler}' / '{Scheduler}' / 'NOTHING_AND_SKIP_PREVIOUS_TEXT'""" + if hasattr(p, 'scheduler') and hasattr(p, 'sampler_name'): + if sampler: + sampler_scheduler = get_sampler_scheduler_str(p.sampler_name, p.scheduler) + else: + sampler_scheduler = get_scheduler_str(p.sampler_name, p.scheduler) + return sanitize_filename_part(sampler_scheduler, replace_spaces=False) + return NOTHING_AND_SKIP_PREVIOUS_TEXT + + class FilenameGenerator: replacements = { + 'basename': lambda self: self.basename or 'img', 'seed': lambda self: self.seed if self.seed is not None else '', 'seed_first': lambda self: self.seed if self.p.batch_size == 1 else self.p.all_seeds[0], 'seed_last': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p.batch_size == 1 else self.p.all_seeds[-1], @@ -355,6 +390,8 @@ class FilenameGenerator: 'height': lambda self: self.image.height, 'styles': lambda self: self.p and sanitize_filename_part(", ".join([style for style in self.p.styles if not style == "None"]) or "None", replace_spaces=False), 'sampler': lambda self: self.p and sanitize_filename_part(self.p.sampler_name, replace_spaces=False), + 'sampler_scheduler': lambda self: self.p and get_sampler_scheduler(self.p, True), + 'scheduler': lambda self: self.p and get_sampler_scheduler(self.p, False), 'model_hash': lambda self: getattr(self.p, "sd_model_hash", shared.sd_model.sd_model_hash), 'model_name': lambda self: sanitize_filename_part(shared.sd_model.sd_checkpoint_info.name_for_extra, replace_spaces=False), 'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'), @@ -380,12 +417,13 @@ class FilenameGenerator: } default_time_format = '%Y%m%d%H%M%S' - def __init__(self, p, seed, prompt, image, zip=False): + def __init__(self, p, seed, prompt, image, zip=False, basename=""): self.p = p self.seed = seed self.prompt = prompt self.image = image self.zip = zip + self.basename = basename def get_vae_filename(self): """Get the name of the VAE file.""" @@ -566,6 +604,17 @@ def save_image_with_geninfo(image, geninfo, filename, extension=None, existing_p }) piexif.insert(exif_bytes, filename) + elif extension.lower() == '.avif': + if opts.enable_pnginfo and geninfo is not None: + exif_bytes = piexif.dump({ + "Exif": { + piexif.ExifIFD.UserComment: piexif.helper.UserComment.dump(geninfo or "", encoding="unicode") + }, + }) + else: + exif_bytes = None + + image.save(filename,format=image_format, quality=opts.jpeg_quality, exif=exif_bytes) elif extension.lower() == ".gif": image.save(filename, format=image_format, comment=geninfo) else: @@ -605,12 +654,12 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='png', i txt_fullfn (`str` or None): If a text file is saved for this image, this will be its full path. Otherwise None. """ - namegen = FilenameGenerator(p, seed, prompt, image) + namegen = FilenameGenerator(p, seed, prompt, image, basename=basename) # WebP and JPG formats have maximum dimension limits of 16383 and 65535 respectively. switch to PNG which has a much higher limit if (image.height > 65535 or image.width > 65535) and extension.lower() in ("jpg", "jpeg") or (image.height > 16383 or image.width > 16383) and extension.lower() == "webp": print('Image dimensions too large; saving as PNG') - extension = ".png" + extension = "png" if save_to_dirs is None: save_to_dirs = (grid and opts.grid_save_to_dirs) or (not grid and opts.save_to_dirs and not no_prompt) @@ -744,10 +793,12 @@ def read_info_from_image(image: Image.Image) -> tuple[str | None, dict]: exif_comment = exif_comment.decode('utf8', errors="ignore") if exif_comment: - items['exif comment'] = exif_comment geninfo = exif_comment elif "comment" in items: # for gif - geninfo = items["comment"].decode('utf8', errors="ignore") + if isinstance(items["comment"], bytes): + geninfo = items["comment"].decode('utf8', errors="ignore") + else: + geninfo = items["comment"] for field in IGNORED_INFO_KEYS: items.pop(field, None) @@ -770,7 +821,7 @@ def image_data(data): import gradio as gr try: - image = Image.open(io.BytesIO(data)) + image = read(io.BytesIO(data)) textinfo, _ = read_info_from_image(image) return textinfo, None except Exception: @@ -797,3 +848,30 @@ def flatten(img, bgcolor): return img.convert('RGB') + +def read(fp, **kwargs): + image = Image.open(fp, **kwargs) + image = fix_image(image) + + return image + + +def fix_image(image: Image.Image): + if image is None: + return None + + try: + image = ImageOps.exif_transpose(image) + image = fix_png_transparency(image) + except Exception: + pass + + return image + + +def fix_png_transparency(image: Image.Image): + if image.mode not in ("RGB", "P") or not isinstance(image.info.get("transparency"), bytes): + return image + + image = image.convert("RGBA") + return image diff --git a/modules/img2img.py b/modules/img2img.py index f81405df..24f869f5 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -6,7 +6,7 @@ import numpy as np from PIL import Image, ImageOps, ImageFilter, ImageEnhance, UnidentifiedImageError import gradio as gr -from modules import images as imgutil +from modules import images from modules.infotext_utils import create_override_settings_dict, parse_generation_parameters from modules.processing import Processed, StableDiffusionProcessingImg2Img, process_images from modules.shared import opts, state @@ -17,11 +17,14 @@ from modules.ui import plaintext_to_html import modules.scripts -def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=False, scale_by=1.0, use_png_info=False, png_info_props=None, png_info_dir=None): +def process_batch(p, input, output_dir, inpaint_mask_dir, args, to_scale=False, scale_by=1.0, use_png_info=False, png_info_props=None, png_info_dir=None): output_dir = output_dir.strip() processing.fix_seed(p) - images = list(shared.walk_files(input_dir, allowed_extensions=(".png", ".jpg", ".jpeg", ".webp", ".tif", ".tiff"))) + if isinstance(input, str): + batch_images = list(shared.walk_files(input, allowed_extensions=(".png", ".jpg", ".jpeg", ".webp", ".tif", ".tiff"))) + else: + batch_images = [os.path.abspath(x.name) for x in input] is_inpaint_batch = False if inpaint_mask_dir: @@ -31,9 +34,9 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=Fal if is_inpaint_batch: print(f"\nInpaint batch is enabled. {len(inpaint_masks)} masks found.") - print(f"Will process {len(images)} images, creating {p.n_iter * p.batch_size} new images for each.") + print(f"Will process {len(batch_images)} images, creating {p.n_iter * p.batch_size} new images for each.") - state.job_count = len(images) * p.n_iter + state.job_count = len(batch_images) * p.n_iter # extract "default" params to use in case getting png info fails prompt = p.prompt @@ -46,8 +49,8 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=Fal sd_model_checkpoint_override = get_closet_checkpoint_match(override_settings.get("sd_model_checkpoint", None)) batch_results = None discard_further_results = False - for i, image in enumerate(images): - state.job = f"{i+1} out of {len(images)}" + for i, image in enumerate(batch_images): + state.job = f"{i+1} out of {len(batch_images)}" if state.skipped: state.skipped = False @@ -55,7 +58,7 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=Fal break try: - img = Image.open(image) + img = images.read(image) except UnidentifiedImageError as e: print(e) continue @@ -86,7 +89,7 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=Fal # otherwise user has many masks with the same name but different extensions mask_image_path = masks_found[0] - mask_image = Image.open(mask_image_path) + mask_image = images.read(mask_image_path) p.image_mask = mask_image if use_png_info: @@ -94,8 +97,8 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=Fal info_img = img if png_info_dir: info_img_path = os.path.join(png_info_dir, os.path.basename(image)) - info_img = Image.open(info_img_path) - geninfo, _ = imgutil.read_info_from_image(info_img) + info_img = images.read(info_img_path) + geninfo, _ = images.read_info_from_image(info_img) parsed_parameters = parse_generation_parameters(geninfo) parsed_parameters = {k: v for k, v in parsed_parameters.items() if k in (png_info_props or {})} except Exception: @@ -146,7 +149,7 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=Fal return batch_results -def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_styles, init_img, sketch, init_img_with_mask, inpaint_color_sketch, inpaint_color_sketch_orig, init_img_inpaint, init_mask_inpaint, steps: int, sampler_name: str, mask_blur: int, mask_alpha: float, inpainting_fill: int, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, denoising_strength: float, selected_scale_tab: int, height: int, width: int, scale_by: float, resize_mode: int, inpaint_full_res: bool, inpaint_full_res_padding: int, inpainting_mask_invert: int, img2img_batch_input_dir: str, img2img_batch_output_dir: str, img2img_batch_inpaint_mask_dir: str, override_settings_texts, img2img_batch_use_png_info: bool, img2img_batch_png_info_props: list, img2img_batch_png_info_dir: str, request: gr.Request, *args): +def img2img(id_task: str, request: gr.Request, mode: int, prompt: str, negative_prompt: str, prompt_styles, init_img, sketch, init_img_with_mask, inpaint_color_sketch, inpaint_color_sketch_orig, init_img_inpaint, init_mask_inpaint, mask_blur: int, mask_alpha: float, inpainting_fill: int, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, denoising_strength: float, selected_scale_tab: int, height: int, width: int, scale_by: float, resize_mode: int, inpaint_full_res: bool, inpaint_full_res_padding: int, inpainting_mask_invert: int, img2img_batch_input_dir: str, img2img_batch_output_dir: str, img2img_batch_inpaint_mask_dir: str, override_settings_texts, img2img_batch_use_png_info: bool, img2img_batch_png_info_props: list, img2img_batch_png_info_dir: str, img2img_batch_source_type: str, img2img_batch_upload: list, *args): override_settings = create_override_settings_dict(override_settings_texts) is_batch = mode == 5 @@ -175,9 +178,8 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s image = None mask = None - # Use the EXIF orientation of photos taken by smartphones. - if image is not None: - image = ImageOps.exif_transpose(image) + image = images.fix_image(image) + mask = images.fix_image(mask) if selected_scale_tab == 1 and not is_batch: assert image, "Can't scale by because no image is selected" @@ -194,10 +196,8 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s prompt=prompt, negative_prompt=negative_prompt, styles=prompt_styles, - sampler_name=sampler_name, batch_size=batch_size, n_iter=n_iter, - steps=steps, cfg_scale=cfg_scale, width=width, height=height, @@ -224,8 +224,15 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s with closing(p): if is_batch: - assert not shared.cmd_opts.hide_ui_dir_config, "Launched with --hide-ui-dir-config, batch img2img disabled" - processed = process_batch(p, img2img_batch_input_dir, img2img_batch_output_dir, img2img_batch_inpaint_mask_dir, args, to_scale=selected_scale_tab == 1, scale_by=scale_by, use_png_info=img2img_batch_use_png_info, png_info_props=img2img_batch_png_info_props, png_info_dir=img2img_batch_png_info_dir) + if img2img_batch_source_type == "upload": + assert isinstance(img2img_batch_upload, list) and img2img_batch_upload + output_dir = "" + inpaint_mask_dir = "" + png_info_dir = img2img_batch_png_info_dir if not shared.cmd_opts.hide_ui_dir_config else "" + processed = process_batch(p, img2img_batch_upload, output_dir, inpaint_mask_dir, args, to_scale=selected_scale_tab == 1, scale_by=scale_by, use_png_info=img2img_batch_use_png_info, png_info_props=img2img_batch_png_info_props, png_info_dir=png_info_dir) + else: # "from dir" + assert not shared.cmd_opts.hide_ui_dir_config, "Launched with --hide-ui-dir-config, batch img2img disabled" + processed = process_batch(p, img2img_batch_input_dir, img2img_batch_output_dir, img2img_batch_inpaint_mask_dir, args, to_scale=selected_scale_tab == 1, scale_by=scale_by, use_png_info=img2img_batch_use_png_info, png_info_props=img2img_batch_png_info_props, png_info_dir=img2img_batch_png_info_dir) if processed is None: processed = Processed(p, [], p.seed, "") diff --git a/modules/infotext_utils.py b/modules/infotext_utils.py index a938aa2a..32dbafa6 100644 --- a/modules/infotext_utils.py +++ b/modules/infotext_utils.py @@ -8,7 +8,7 @@ import sys import gradio as gr from modules.paths import data_path -from modules import shared, ui_tempdir, script_callbacks, processing, infotext_versions +from modules import shared, ui_tempdir, script_callbacks, processing, infotext_versions, images, prompt_parser, errors from PIL import Image sys.modules['modules.generation_parameters_copypaste'] = sys.modules[__name__] # alias for old name @@ -83,7 +83,7 @@ def image_from_url_text(filedata): assert is_in_right_dir, 'trying to open image file outside of allowed directories' filename = filename.rsplit('?', 1)[0] - return Image.open(filename) + return images.read(filename) if type(filedata) == list: if len(filedata) == 0: @@ -95,7 +95,7 @@ def image_from_url_text(filedata): filedata = filedata[len("data:image/png;base64,"):] filedata = base64.decodebytes(filedata.encode('utf-8')) - image = Image.open(io.BytesIO(filedata)) + image = images.read(io.BytesIO(filedata)) return image @@ -146,18 +146,19 @@ def connect_paste_params_buttons(): destination_height_component = next(iter([field for field, name in fields if name == "Size-2"] if fields else []), None) if binding.source_image_component and destination_image_component: + need_send_dementions = destination_width_component and binding.tabname != 'inpaint' if isinstance(binding.source_image_component, gr.Gallery): - func = send_image_and_dimensions if destination_width_component else image_from_url_text + func = send_image_and_dimensions if need_send_dementions else image_from_url_text jsfunc = "extract_image_from_gallery" else: - func = send_image_and_dimensions if destination_width_component else lambda x: x + func = send_image_and_dimensions if need_send_dementions else lambda x: x jsfunc = None binding.paste_button.click( fn=func, _js=jsfunc, inputs=[binding.source_image_component], - outputs=[destination_image_component, destination_width_component, destination_height_component] if destination_width_component else [destination_image_component], + outputs=[destination_image_component, destination_width_component, destination_height_component] if need_send_dementions else [destination_image_component], show_progress=False, ) @@ -265,17 +266,6 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model else: prompt += ("" if prompt == "" else "\n") + line - if shared.opts.infotext_styles != "Ignore": - found_styles, prompt, negative_prompt = shared.prompt_styles.extract_styles_from_prompt(prompt, negative_prompt) - - if shared.opts.infotext_styles == "Apply": - res["Styles array"] = found_styles - elif shared.opts.infotext_styles == "Apply if any" and found_styles: - res["Styles array"] = found_styles - - res["Prompt"] = prompt - res["Negative prompt"] = negative_prompt - for k, v in re_param.findall(lastline): try: if v[0] == '"' and v[-1] == '"': @@ -290,6 +280,26 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model except Exception: print(f"Error parsing \"{k}: {v}\"") + # Extract styles from prompt + if shared.opts.infotext_styles != "Ignore": + found_styles, prompt_no_styles, negative_prompt_no_styles = shared.prompt_styles.extract_styles_from_prompt(prompt, negative_prompt) + + same_hr_styles = True + if ("Hires prompt" in res or "Hires negative prompt" in res) and (infotext_ver > infotext_versions.v180_hr_styles if (infotext_ver := infotext_versions.parse_version(res.get("Version"))) else True): + hr_prompt, hr_negative_prompt = res.get("Hires prompt", prompt), res.get("Hires negative prompt", negative_prompt) + hr_found_styles, hr_prompt_no_styles, hr_negative_prompt_no_styles = shared.prompt_styles.extract_styles_from_prompt(hr_prompt, hr_negative_prompt) + if same_hr_styles := found_styles == hr_found_styles: + res["Hires prompt"] = '' if hr_prompt_no_styles == prompt_no_styles else hr_prompt_no_styles + res['Hires negative prompt'] = '' if hr_negative_prompt_no_styles == negative_prompt_no_styles else hr_negative_prompt_no_styles + + if same_hr_styles: + prompt, negative_prompt = prompt_no_styles, negative_prompt_no_styles + if (shared.opts.infotext_styles == "Apply if any" and found_styles) or shared.opts.infotext_styles == "Apply": + res['Styles array'] = found_styles + + res["Prompt"] = prompt + res["Negative prompt"] = negative_prompt + # Missing CLIP skip means it was set to 1 (the default) if "Clip skip" not in res: res["Clip skip"] = "1" @@ -305,6 +315,9 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model if "Hires sampler" not in res: res["Hires sampler"] = "Use same sampler" + if "Hires schedule type" not in res: + res["Hires schedule type"] = "Use same scheduler" + if "Hires checkpoint" not in res: res["Hires checkpoint"] = "Use same checkpoint" @@ -356,9 +369,15 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model if "Cache FP16 weight for LoRA" not in res and res["FP8 weight"] != "Disable": res["Cache FP16 weight for LoRA"] = False - if "Emphasis" not in res: + prompt_attention = prompt_parser.parse_prompt_attention(prompt) + prompt_attention += prompt_parser.parse_prompt_attention(negative_prompt) + prompt_uses_emphasis = len(prompt_attention) != len([p for p in prompt_attention if p[1] == 1.0 or p[0] == 'BREAK']) + if "Emphasis" not in res and prompt_uses_emphasis: res["Emphasis"] = "Original" + if "Refiner switch by sampling steps" not in res: + res["Refiner switch by sampling steps"] = False + infotext_versions.backcompat(res) for key in skip_fields: @@ -456,7 +475,7 @@ def get_override_settings(params, *, skip_fields=None): def connect_paste(button, paste_fields, input_comp, override_settings_component, tabname): def paste_func(prompt): - if not prompt and not shared.cmd_opts.hide_ui_dir_config: + if not prompt and not shared.cmd_opts.hide_ui_dir_config and not shared.cmd_opts.no_prompt_history: filename = os.path.join(data_path, "params.txt") try: with open(filename, "r", encoding="utf8") as file: @@ -470,7 +489,11 @@ def connect_paste(button, paste_fields, input_comp, override_settings_component, for output, key in paste_fields: if callable(key): - v = key(params) + try: + v = key(params) + except Exception: + errors.report(f"Error executing {key}", exc_info=True) + v = None else: v = params.get(key, None) diff --git a/modules/infotext_versions.py b/modules/infotext_versions.py index 23b45c3f..cea676cd 100644 --- a/modules/infotext_versions.py +++ b/modules/infotext_versions.py @@ -5,6 +5,8 @@ import re v160 = version.parse("1.6.0") v170_tsnr = version.parse("v1.7.0-225") +v180 = version.parse("1.8.0") +v180_hr_styles = version.parse("1.8.0-139") def parse_version(text): @@ -40,3 +42,5 @@ def backcompat(d): if ver < v170_tsnr: d["Downcast alphas_cumprod"] = True + if ver < v180 and d.get('Refiner'): + d["Refiner switch by sampling steps"] = True diff --git a/modules/initialize.py b/modules/initialize.py index f7313ff4..0365bbb3 100644 --- a/modules/initialize.py +++ b/modules/initialize.py @@ -51,6 +51,7 @@ def check_versions(): def initialize(): from modules import initialize_util initialize_util.fix_torch_version() + initialize_util.fix_pytorch_lightning() initialize_util.fix_asyncio_event_loop_policy() initialize_util.validate_tls_options() initialize_util.configure_sigint_handler() @@ -109,7 +110,7 @@ def initialize_rest(*, reload_script_modules=False): with startup_timer.subcategory("load scripts"): scripts.load_scripts() - if reload_script_modules: + if reload_script_modules and shared.opts.enable_reloading_ui_scripts: for module in [module for name, module in sys.modules.items() if name.startswith("modules.ui")]: importlib.reload(module) startup_timer.record("reload script modules") @@ -139,7 +140,7 @@ def initialize_rest(*, reload_script_modules=False): """ Accesses shared.sd_model property to load model. After it's available, if it has been loaded before this access by some extension, - its optimization may be None because the list of optimizaers has neet been filled + its optimization may be None because the list of optimizers has not been filled by that time, so we apply optimization again. """ from modules import devices diff --git a/modules/initialize_util.py b/modules/initialize_util.py index b6767138..79a72cb3 100644 --- a/modules/initialize_util.py +++ b/modules/initialize_util.py @@ -24,6 +24,13 @@ def fix_torch_version(): torch.__long_version__ = torch.__version__ torch.__version__ = re.search(r'[\d.]+[\d]', torch.__version__).group(0) +def fix_pytorch_lightning(): + # Checks if pytorch_lightning.utilities.distributed already exists in the sys.modules cache + if 'pytorch_lightning.utilities.distributed' not in sys.modules: + import pytorch_lightning + # Lets the user know that the library was not found and then will set it to pytorch_lightning.utilities.rank_zero + print("Pytorch_lightning.distributed not found, attempting pytorch_lightning.rank_zero") + sys.modules["pytorch_lightning.utilities.distributed"] = pytorch_lightning.utilities.rank_zero def fix_asyncio_event_loop_policy(): """ diff --git a/modules/launch_utils.py b/modules/launch_utils.py index ad04eb36..20c7dc12 100644 --- a/modules/launch_utils.py +++ b/modules/launch_utils.py @@ -9,6 +9,7 @@ import importlib.util import importlib.metadata import platform import json +import shlex from functools import lru_cache from modules import cmd_args, errors @@ -55,7 +56,7 @@ and delete current Python and "venv" folder in WebUI's directory. You can download 3.10 Python from here: https://www.python.org/downloads/release/python-3106/ -{"Alternatively, use a binary release of WebUI: https://github.com/AUTOMATIC1111/stable-diffusion-webui/releases" if is_windows else ""} +{"Alternatively, use a binary release of WebUI: https://github.com/AUTOMATIC1111/stable-diffusion-webui/releases/tag/v1.0.0-pre" if is_windows else ""} Use --skip-python-version-check to suppress this warning. """) @@ -76,7 +77,7 @@ def git_tag(): except Exception: try: - changelog_md = os.path.join(os.path.dirname(os.path.dirname(__file__)), "CHANGELOG.md") + changelog_md = os.path.join(script_path, "CHANGELOG.md") with open(changelog_md, "r", encoding="utf-8") as file: line = next((line.strip() for line in file if line.strip()), "") line = line.replace("## ", "") @@ -231,7 +232,7 @@ def run_extension_installer(extension_dir): try: env = os.environ.copy() - env['PYTHONPATH'] = f"{os.path.abspath('.')}{os.pathsep}{env.get('PYTHONPATH', '')}" + env['PYTHONPATH'] = f"{script_path}{os.pathsep}{env.get('PYTHONPATH', '')}" stdout = run(f'"{python}" "{path_installer}"', errdesc=f"Error running install.py for extension {extension_dir}", custom_env=env).strip() if stdout: @@ -445,7 +446,6 @@ def prepare_environment(): exit(0) - def configure_for_tests(): if "--api" not in sys.argv: sys.argv.append("--api") @@ -461,7 +461,7 @@ def configure_for_tests(): def start(): - print(f"Launching {'API server' if '--nowebui' in sys.argv else 'Web UI'} with arguments: {' '.join(sys.argv[1:])}") + print(f"Launching {'API server' if '--nowebui' in sys.argv else 'Web UI'} with arguments: {shlex.join(sys.argv[1:])}") import webui if '--nowebui' in sys.argv: webui.api_only() diff --git a/modules/lowvram.py b/modules/lowvram.py index 45701046..6728c337 100644 --- a/modules/lowvram.py +++ b/modules/lowvram.py @@ -1,9 +1,12 @@ +from collections import namedtuple + import torch from modules import devices, shared module_in_gpu = None cpu = torch.device("cpu") +ModuleWithParent = namedtuple('ModuleWithParent', ['module', 'parent'], defaults=['None']) def send_everything_to_cpu(): global module_in_gpu @@ -75,13 +78,14 @@ def setup_for_low_vram(sd_model, use_medvram): (sd_model, 'depth_model'), (sd_model, 'embedder'), (sd_model, 'model'), - (sd_model, 'embedder'), ] is_sdxl = hasattr(sd_model, 'conditioner') is_sd2 = not is_sdxl and hasattr(sd_model.cond_stage_model, 'model') - if is_sdxl: + if hasattr(sd_model, 'medvram_fields'): + to_remain_in_cpu = sd_model.medvram_fields() + elif is_sdxl: to_remain_in_cpu.append((sd_model, 'conditioner')) elif is_sd2: to_remain_in_cpu.append((sd_model.cond_stage_model, 'model')) @@ -103,7 +107,21 @@ def setup_for_low_vram(sd_model, use_medvram): setattr(obj, field, module) # register hooks for those the first three models - if is_sdxl: + if hasattr(sd_model, "cond_stage_model") and hasattr(sd_model.cond_stage_model, "medvram_modules"): + for module in sd_model.cond_stage_model.medvram_modules(): + if isinstance(module, ModuleWithParent): + parent = module.parent + module = module.module + else: + parent = None + + if module: + module.register_forward_pre_hook(send_me_to_gpu) + + if parent: + parents[module] = parent + + elif is_sdxl: sd_model.conditioner.register_forward_pre_hook(send_me_to_gpu) elif is_sd2: sd_model.cond_stage_model.model.register_forward_pre_hook(send_me_to_gpu) @@ -117,9 +135,9 @@ def setup_for_low_vram(sd_model, use_medvram): sd_model.first_stage_model.register_forward_pre_hook(send_me_to_gpu) sd_model.first_stage_model.encode = first_stage_model_encode_wrap sd_model.first_stage_model.decode = first_stage_model_decode_wrap - if sd_model.depth_model: + if getattr(sd_model, 'depth_model', None) is not None: sd_model.depth_model.register_forward_pre_hook(send_me_to_gpu) - if sd_model.embedder: + if getattr(sd_model, 'embedder', None) is not None: sd_model.embedder.register_forward_pre_hook(send_me_to_gpu) if use_medvram: diff --git a/modules/mac_specific.py b/modules/mac_specific.py index d96d86d7..039689f3 100644 --- a/modules/mac_specific.py +++ b/modules/mac_specific.py @@ -12,7 +12,7 @@ log = logging.getLogger(__name__) # before torch version 1.13, has_mps is only available in nightly pytorch and macOS 12.3+, # use check `getattr` and try it for compatibility. -# in torch version 1.13, backends.mps.is_available() and backends.mps.is_built() are introduced in to check mps availabilty, +# in torch version 1.13, backends.mps.is_available() and backends.mps.is_built() are introduced in to check mps availability, # since torch 2.0.1+ nightly build, getattr(torch, 'has_mps', False) was deprecated, see https://github.com/pytorch/pytorch/pull/103279 def check_for_mps() -> bool: if version.parse(torch.__version__) <= version.parse("2.0.1"): diff --git a/modules/masking.py b/modules/masking.py index 29a39452..2fc83031 100644 --- a/modules/masking.py +++ b/modules/masking.py @@ -1,17 +1,39 @@ from PIL import Image, ImageFilter, ImageOps -def get_crop_region(mask, pad=0): - """finds a rectangular region that contains all masked ares in an image. Returns (x1, y1, x2, y2) coordinates of the rectangle. - For example, if a user has painted the top-right part of a 512x512 image, the result may be (256, 0, 512, 256)""" - mask_img = mask if isinstance(mask, Image.Image) else Image.fromarray(mask) - box = mask_img.getbbox() - if box: +def get_crop_region_v2(mask, pad=0): + """ + Finds a rectangular region that contains all masked ares in a mask. + Returns None if mask is completely black mask (all 0) + + Parameters: + mask: PIL.Image.Image L mode or numpy 1d array + pad: int number of pixels that the region will be extended on all sides + Returns: (x1, y1, x2, y2) | None + + Introduced post 1.9.0 + """ + mask = mask if isinstance(mask, Image.Image) else Image.fromarray(mask) + if box := mask.getbbox(): x1, y1, x2, y2 = box - else: # when no box is found - x1, y1 = mask_img.size - x2 = y2 = 0 - return max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask_img.size[0]), min(y2 + pad, mask_img.size[1]) + return (max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask.size[0]), min(y2 + pad, mask.size[1])) if pad else box + + +def get_crop_region(mask, pad=0): + """ + Same function as get_crop_region_v2 but handles completely black mask (all 0) differently + when mask all black still return coordinates but the coordinates may be invalid ie x2>x1 or y2>y1 + Notes: it is possible for the coordinates to be "valid" again if pad size is sufficiently large + (mask_size.x-pad, mask_size.y-pad, pad, pad) + + Extension developer should use get_crop_region_v2 instead unless for compatibility considerations. + """ + mask = mask if isinstance(mask, Image.Image) else Image.fromarray(mask) + if box := get_crop_region_v2(mask, pad): + return box + x1, y1 = mask.size + x2 = y2 = 0 + return max(x1 - pad, 0), max(y1 - pad, 0), min(x2 + pad, mask.size[0]), min(y2 + pad, mask.size[1]) def expand_crop_region(crop_region, processing_width, processing_height, image_width, image_height): diff --git a/modules/modelloader.py b/modules/modelloader.py index e100bb24..36e7415a 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -23,6 +23,7 @@ def load_file_from_url( model_dir: str, progress: bool = True, file_name: str | None = None, + hash_prefix: str | None = None, ) -> str: """Download a file from `url` into `model_dir`, using the file present if possible. @@ -36,11 +37,11 @@ def load_file_from_url( if not os.path.exists(cached_file): print(f'Downloading: "{url}" to {cached_file}\n') from torch.hub import download_url_to_file - download_url_to_file(url, cached_file, progress=progress) + download_url_to_file(url, cached_file, progress=progress, hash_prefix=hash_prefix) return cached_file -def load_models(model_path: str, model_url: str = None, command_path: str = None, ext_filter=None, download_name=None, ext_blacklist=None) -> list: +def load_models(model_path: str, model_url: str = None, command_path: str = None, ext_filter=None, download_name=None, ext_blacklist=None, hash_prefix=None) -> list: """ A one-and done loader to try finding the desired models in specified directories. @@ -49,6 +50,7 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None @param model_path: The location to store/find models in. @param command_path: A command-line argument to search for models in first. @param ext_filter: An optional list of filename extensions to filter by + @param hash_prefix: the expected sha256 of the model_url @return: A list of paths containing the desired model(s) """ output = [] @@ -78,7 +80,7 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None if model_url is not None and len(output) == 0: if download_name is not None: - output.append(load_file_from_url(model_url, model_dir=places[0], file_name=download_name)) + output.append(load_file_from_url(model_url, model_dir=places[0], file_name=download_name, hash_prefix=hash_prefix)) else: output.append(model_url) @@ -110,7 +112,7 @@ def load_upscalers(): except Exception: pass - datas = [] + data = [] commandline_options = vars(shared.cmd_opts) # some of upscaler classes will not go away after reloading their modules, and we'll end @@ -129,14 +131,35 @@ def load_upscalers(): scaler = cls(commandline_model_path) scaler.user_path = commandline_model_path scaler.model_download_path = commandline_model_path or scaler.model_path - datas += scaler.scalers + data += scaler.scalers shared.sd_upscalers = sorted( - datas, + data, # Special case for UpscalerNone keeps it at the beginning of the list. key=lambda x: x.name.lower() if not isinstance(x.scaler, (UpscalerNone, UpscalerLanczos, UpscalerNearest)) else "" ) +# None: not loaded, False: failed to load, True: loaded +_spandrel_extra_init_state = None + + +def _init_spandrel_extra_archs() -> None: + """ + Try to initialize `spandrel_extra_archs` (exactly once). + """ + global _spandrel_extra_init_state + if _spandrel_extra_init_state is not None: + return + + try: + import spandrel + import spandrel_extra_arches + spandrel.MAIN_REGISTRY.add(*spandrel_extra_arches.EXTRA_REGISTRY) + _spandrel_extra_init_state = True + except Exception: + logger.warning("Failed to load spandrel_extra_arches", exc_info=True) + _spandrel_extra_init_state = False + def load_spandrel_model( path: str | os.PathLike, @@ -146,11 +169,16 @@ def load_spandrel_model( dtype: str | torch.dtype | None = None, expected_architecture: str | None = None, ) -> spandrel.ModelDescriptor: + global _spandrel_extra_init_state + import spandrel + _init_spandrel_extra_archs() + model_descriptor = spandrel.ModelLoader(device=device).load_from_file(str(path)) - if expected_architecture and model_descriptor.architecture != expected_architecture: + arch = model_descriptor.architecture + if expected_architecture and arch.name != expected_architecture: logger.warning( - f"Model {path!r} is not a {expected_architecture!r} model (got {model_descriptor.architecture!r})", + f"Model {path!r} is not a {expected_architecture!r} model (got {arch.name!r})", ) half = False if prefer_half: @@ -164,6 +192,6 @@ def load_spandrel_model( model_descriptor.model.eval() logger.debug( "Loaded %s from %s (device=%s, half=%s, dtype=%s)", - model_descriptor, path, device, half, dtype, + arch, path, device, half, dtype, ) return model_descriptor diff --git a/modules/models/diffusion/ddpm_edit.py b/modules/models/diffusion/ddpm_edit.py index 6db340da..7b51c83c 100644 --- a/modules/models/diffusion/ddpm_edit.py +++ b/modules/models/diffusion/ddpm_edit.py @@ -341,7 +341,7 @@ class DDPM(pl.LightningModule): elif self.parameterization == "x0": target = x_start else: - raise NotImplementedError(f"Paramterization {self.parameterization} not yet supported") + raise NotImplementedError(f"Parameterization {self.parameterization} not yet supported") loss = self.get_loss(model_out, target, mean=False).mean(dim=[1, 2, 3]) @@ -901,7 +901,7 @@ class LatentDiffusion(DDPM): def apply_model(self, x_noisy, t, cond, return_ids=False): if isinstance(cond, dict): - # hybrid case, cond is exptected to be a dict + # hybrid case, cond is expected to be a dict pass else: if not isinstance(cond, list): @@ -937,7 +937,7 @@ class LatentDiffusion(DDPM): cond_list = [{c_key: [c[:, :, :, :, i]]} for i in range(c.shape[-1])] elif self.cond_stage_key == 'coordinates_bbox': - assert 'original_image_size' in self.split_input_params, 'BoudingBoxRescaling is missing original_image_size' + assert 'original_image_size' in self.split_input_params, 'BoundingBoxRescaling is missing original_image_size' # assuming padding of unfold is always 0 and its dilation is always 1 n_patches_per_row = int((w - ks[0]) / stride[0] + 1) @@ -947,7 +947,7 @@ class LatentDiffusion(DDPM): num_downs = self.first_stage_model.encoder.num_resolutions - 1 rescale_latent = 2 ** (num_downs) - # get top left postions of patches as conforming for the bbbox tokenizer, therefore we + # get top left positions of patches as conforming for the bbbox tokenizer, therefore we # need to rescale the tl patch coordinates to be in between (0,1) tl_patch_coordinates = [(rescale_latent * stride[0] * (patch_nr % n_patches_per_row) / full_img_w, rescale_latent * stride[1] * (patch_nr // n_patches_per_row) / full_img_h) diff --git a/modules/models/diffusion/uni_pc/uni_pc.py b/modules/models/diffusion/uni_pc/uni_pc.py index d257a728..3333bc80 100644 --- a/modules/models/diffusion/uni_pc/uni_pc.py +++ b/modules/models/diffusion/uni_pc/uni_pc.py @@ -323,7 +323,7 @@ def model_wrapper( def model_fn(x, t_continuous, condition, unconditional_condition): """ - The noise predicition model function that is used for DPM-Solver. + The noise prediction model function that is used for DPM-Solver. """ if t_continuous.reshape((-1,)).shape[0] == 1: t_continuous = t_continuous.expand((x.shape[0])) diff --git a/modules/models/sd3/mmdit.py b/modules/models/sd3/mmdit.py new file mode 100644 index 00000000..8ddf49a4 --- /dev/null +++ b/modules/models/sd3/mmdit.py @@ -0,0 +1,622 @@ +### This file contains impls for MM-DiT, the core model component of SD3 + +import math +from typing import Dict, Optional +import numpy as np +import torch +import torch.nn as nn +from einops import rearrange, repeat +from modules.models.sd3.other_impls import attention, Mlp + + +class PatchEmbed(nn.Module): + """ 2D Image to Patch Embedding""" + def __init__( + self, + img_size: Optional[int] = 224, + patch_size: int = 16, + in_chans: int = 3, + embed_dim: int = 768, + flatten: bool = True, + bias: bool = True, + strict_img_size: bool = True, + dynamic_img_pad: bool = False, + dtype=None, + device=None, + ): + super().__init__() + self.patch_size = (patch_size, patch_size) + if img_size is not None: + self.img_size = (img_size, img_size) + self.grid_size = tuple([s // p for s, p in zip(self.img_size, self.patch_size)]) + self.num_patches = self.grid_size[0] * self.grid_size[1] + else: + self.img_size = None + self.grid_size = None + self.num_patches = None + + # flatten spatial dim and transpose to channels last, kept for bwd compat + self.flatten = flatten + self.strict_img_size = strict_img_size + self.dynamic_img_pad = dynamic_img_pad + + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias=bias, dtype=dtype, device=device) + + def forward(self, x): + B, C, H, W = x.shape + x = self.proj(x) + if self.flatten: + x = x.flatten(2).transpose(1, 2) # NCHW -> NLC + return x + + +def modulate(x, shift, scale): + if shift is None: + shift = torch.zeros_like(scale) + return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + + +################################################################################# +# Sine/Cosine Positional Embedding Functions # +################################################################################# + + +def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0, scaling_factor=None, offset=None): + """ + grid_size: int of the grid height and width + return: + pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token) + """ + grid_h = np.arange(grid_size, dtype=np.float32) + grid_w = np.arange(grid_size, dtype=np.float32) + grid = np.meshgrid(grid_w, grid_h) # here w goes first + grid = np.stack(grid, axis=0) + if scaling_factor is not None: + grid = grid / scaling_factor + if offset is not None: + grid = grid - offset + grid = grid.reshape([2, 1, grid_size, grid_size]) + pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid) + if cls_token and extra_tokens > 0: + pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0) + return pos_embed + + +def get_2d_sincos_pos_embed_from_grid(embed_dim, grid): + assert embed_dim % 2 == 0 + # use half of dimensions to encode grid_h + emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2) + emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2) + emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D) + return emb + + +def get_1d_sincos_pos_embed_from_grid(embed_dim, pos): + """ + embed_dim: output dimension for each position + pos: a list of positions to be encoded: size (M,) + out: (M, D) + """ + assert embed_dim % 2 == 0 + omega = np.arange(embed_dim // 2, dtype=np.float64) + omega /= embed_dim / 2.0 + omega = 1.0 / 10000**omega # (D/2,) + pos = pos.reshape(-1) # (M,) + out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product + emb_sin = np.sin(out) # (M, D/2) + emb_cos = np.cos(out) # (M, D/2) + return np.concatenate([emb_sin, emb_cos], axis=1) # (M, D) + + +################################################################################# +# Embedding Layers for Timesteps and Class Labels # +################################################################################# + + +class TimestepEmbedder(nn.Module): + """Embeds scalar timesteps into vector representations.""" + + def __init__(self, hidden_size, frequency_embedding_size=256, dtype=None, device=None): + super().__init__() + self.mlp = nn.Sequential( + nn.Linear(frequency_embedding_size, hidden_size, bias=True, dtype=dtype, device=device), + nn.SiLU(), + nn.Linear(hidden_size, hidden_size, bias=True, dtype=dtype, device=device), + ) + self.frequency_embedding_size = frequency_embedding_size + + @staticmethod + def timestep_embedding(t, dim, max_period=10000): + """ + Create sinusoidal timestep embeddings. + :param t: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param dim: the dimension of the output. + :param max_period: controls the minimum frequency of the embeddings. + :return: an (N, D) Tensor of positional embeddings. + """ + half = dim // 2 + freqs = torch.exp( + -math.log(max_period) + * torch.arange(start=0, end=half, dtype=torch.float32) + / half + ).to(device=t.device) + args = t[:, None].float() * freqs[None] + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) + if dim % 2: + embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) + if torch.is_floating_point(t): + embedding = embedding.to(dtype=t.dtype) + return embedding + + def forward(self, t, dtype, **kwargs): + t_freq = self.timestep_embedding(t, self.frequency_embedding_size).to(dtype) + t_emb = self.mlp(t_freq) + return t_emb + + +class VectorEmbedder(nn.Module): + """Embeds a flat vector of dimension input_dim""" + + def __init__(self, input_dim: int, hidden_size: int, dtype=None, device=None): + super().__init__() + self.mlp = nn.Sequential( + nn.Linear(input_dim, hidden_size, bias=True, dtype=dtype, device=device), + nn.SiLU(), + nn.Linear(hidden_size, hidden_size, bias=True, dtype=dtype, device=device), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.mlp(x) + + +################################################################################# +# Core DiT Model # +################################################################################# + + +class QkvLinear(torch.nn.Linear): + pass + +def split_qkv(qkv, head_dim): + qkv = qkv.reshape(qkv.shape[0], qkv.shape[1], 3, -1, head_dim).movedim(2, 0) + return qkv[0], qkv[1], qkv[2] + +def optimized_attention(qkv, num_heads): + return attention(qkv[0], qkv[1], qkv[2], num_heads) + +class SelfAttention(nn.Module): + ATTENTION_MODES = ("xformers", "torch", "torch-hb", "math", "debug") + + def __init__( + self, + dim: int, + num_heads: int = 8, + qkv_bias: bool = False, + qk_scale: Optional[float] = None, + attn_mode: str = "xformers", + pre_only: bool = False, + qk_norm: Optional[str] = None, + rmsnorm: bool = False, + dtype=None, + device=None, + ): + super().__init__() + self.num_heads = num_heads + self.head_dim = dim // num_heads + + self.qkv = QkvLinear(dim, dim * 3, bias=qkv_bias, dtype=dtype, device=device) + if not pre_only: + self.proj = nn.Linear(dim, dim, dtype=dtype, device=device) + assert attn_mode in self.ATTENTION_MODES + self.attn_mode = attn_mode + self.pre_only = pre_only + + if qk_norm == "rms": + self.ln_q = RMSNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device) + self.ln_k = RMSNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device) + elif qk_norm == "ln": + self.ln_q = nn.LayerNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device) + self.ln_k = nn.LayerNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device) + elif qk_norm is None: + self.ln_q = nn.Identity() + self.ln_k = nn.Identity() + else: + raise ValueError(qk_norm) + + def pre_attention(self, x: torch.Tensor): + B, L, C = x.shape + qkv = self.qkv(x) + q, k, v = split_qkv(qkv, self.head_dim) + q = self.ln_q(q).reshape(q.shape[0], q.shape[1], -1) + k = self.ln_k(k).reshape(q.shape[0], q.shape[1], -1) + return (q, k, v) + + def post_attention(self, x: torch.Tensor) -> torch.Tensor: + assert not self.pre_only + x = self.proj(x) + return x + + def forward(self, x: torch.Tensor) -> torch.Tensor: + (q, k, v) = self.pre_attention(x) + x = attention(q, k, v, self.num_heads) + x = self.post_attention(x) + return x + + +class RMSNorm(torch.nn.Module): + def __init__( + self, dim: int, elementwise_affine: bool = False, eps: float = 1e-6, device=None, dtype=None + ): + """ + Initialize the RMSNorm normalization layer. + Args: + dim (int): The dimension of the input tensor. + eps (float, optional): A small value added to the denominator for numerical stability. Default is 1e-6. + Attributes: + eps (float): A small value added to the denominator for numerical stability. + weight (nn.Parameter): Learnable scaling parameter. + """ + super().__init__() + self.eps = eps + self.learnable_scale = elementwise_affine + if self.learnable_scale: + self.weight = nn.Parameter(torch.empty(dim, device=device, dtype=dtype)) + else: + self.register_parameter("weight", None) + + def _norm(self, x): + """ + Apply the RMSNorm normalization to the input tensor. + Args: + x (torch.Tensor): The input tensor. + Returns: + torch.Tensor: The normalized tensor. + """ + return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) + + def forward(self, x): + """ + Forward pass through the RMSNorm layer. + Args: + x (torch.Tensor): The input tensor. + Returns: + torch.Tensor: The output tensor after applying RMSNorm. + """ + x = self._norm(x) + if self.learnable_scale: + return x * self.weight.to(device=x.device, dtype=x.dtype) + else: + return x + + +class SwiGLUFeedForward(nn.Module): + def __init__( + self, + dim: int, + hidden_dim: int, + multiple_of: int, + ffn_dim_multiplier: Optional[float] = None, + ): + """ + Initialize the FeedForward module. + + Args: + dim (int): Input dimension. + hidden_dim (int): Hidden dimension of the feedforward layer. + multiple_of (int): Value to ensure hidden dimension is a multiple of this value. + ffn_dim_multiplier (float, optional): Custom multiplier for hidden dimension. Defaults to None. + + Attributes: + w1 (ColumnParallelLinear): Linear transformation for the first layer. + w2 (RowParallelLinear): Linear transformation for the second layer. + w3 (ColumnParallelLinear): Linear transformation for the third layer. + + """ + super().__init__() + hidden_dim = int(2 * hidden_dim / 3) + # custom dim factor multiplier + if ffn_dim_multiplier is not None: + hidden_dim = int(ffn_dim_multiplier * hidden_dim) + hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) + + self.w1 = nn.Linear(dim, hidden_dim, bias=False) + self.w2 = nn.Linear(hidden_dim, dim, bias=False) + self.w3 = nn.Linear(dim, hidden_dim, bias=False) + + def forward(self, x): + return self.w2(nn.functional.silu(self.w1(x)) * self.w3(x)) + + +class DismantledBlock(nn.Module): + """A DiT block with gated adaptive layer norm (adaLN) conditioning.""" + + ATTENTION_MODES = ("xformers", "torch", "torch-hb", "math", "debug") + + def __init__( + self, + hidden_size: int, + num_heads: int, + mlp_ratio: float = 4.0, + attn_mode: str = "xformers", + qkv_bias: bool = False, + pre_only: bool = False, + rmsnorm: bool = False, + scale_mod_only: bool = False, + swiglu: bool = False, + qk_norm: Optional[str] = None, + dtype=None, + device=None, + **block_kwargs, + ): + super().__init__() + assert attn_mode in self.ATTENTION_MODES + if not rmsnorm: + self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6, dtype=dtype, device=device) + else: + self.norm1 = RMSNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.attn = SelfAttention(dim=hidden_size, num_heads=num_heads, qkv_bias=qkv_bias, attn_mode=attn_mode, pre_only=pre_only, qk_norm=qk_norm, rmsnorm=rmsnorm, dtype=dtype, device=device) + if not pre_only: + if not rmsnorm: + self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6, dtype=dtype, device=device) + else: + self.norm2 = RMSNorm(hidden_size, elementwise_affine=False, eps=1e-6) + mlp_hidden_dim = int(hidden_size * mlp_ratio) + if not pre_only: + if not swiglu: + self.mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=nn.GELU(approximate="tanh"), dtype=dtype, device=device) + else: + self.mlp = SwiGLUFeedForward(dim=hidden_size, hidden_dim=mlp_hidden_dim, multiple_of=256) + self.scale_mod_only = scale_mod_only + if not scale_mod_only: + n_mods = 6 if not pre_only else 2 + else: + n_mods = 4 if not pre_only else 1 + self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, n_mods * hidden_size, bias=True, dtype=dtype, device=device)) + self.pre_only = pre_only + + def pre_attention(self, x: torch.Tensor, c: torch.Tensor): + assert x is not None, "pre_attention called with None input" + if not self.pre_only: + if not self.scale_mod_only: + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(6, dim=1) + else: + shift_msa = None + shift_mlp = None + scale_msa, gate_msa, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(4, dim=1) + qkv = self.attn.pre_attention(modulate(self.norm1(x), shift_msa, scale_msa)) + return qkv, (x, gate_msa, shift_mlp, scale_mlp, gate_mlp) + else: + if not self.scale_mod_only: + shift_msa, scale_msa = self.adaLN_modulation(c).chunk(2, dim=1) + else: + shift_msa = None + scale_msa = self.adaLN_modulation(c) + qkv = self.attn.pre_attention(modulate(self.norm1(x), shift_msa, scale_msa)) + return qkv, None + + def post_attention(self, attn, x, gate_msa, shift_mlp, scale_mlp, gate_mlp): + assert not self.pre_only + x = x + gate_msa.unsqueeze(1) * self.attn.post_attention(attn) + x = x + gate_mlp.unsqueeze(1) * self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp)) + return x + + def forward(self, x: torch.Tensor, c: torch.Tensor) -> torch.Tensor: + assert not self.pre_only + (q, k, v), intermediates = self.pre_attention(x, c) + attn = attention(q, k, v, self.attn.num_heads) + return self.post_attention(attn, *intermediates) + + +def block_mixing(context, x, context_block, x_block, c): + assert context is not None, "block_mixing called with None context" + context_qkv, context_intermediates = context_block.pre_attention(context, c) + + x_qkv, x_intermediates = x_block.pre_attention(x, c) + + o = [] + for t in range(3): + o.append(torch.cat((context_qkv[t], x_qkv[t]), dim=1)) + q, k, v = tuple(o) + + attn = attention(q, k, v, x_block.attn.num_heads) + context_attn, x_attn = (attn[:, : context_qkv[0].shape[1]], attn[:, context_qkv[0].shape[1] :]) + + if not context_block.pre_only: + context = context_block.post_attention(context_attn, *context_intermediates) + else: + context = None + x = x_block.post_attention(x_attn, *x_intermediates) + return context, x + + +class JointBlock(nn.Module): + """just a small wrapper to serve as a fsdp unit""" + + def __init__(self, *args, **kwargs): + super().__init__() + pre_only = kwargs.pop("pre_only") + qk_norm = kwargs.pop("qk_norm", None) + self.context_block = DismantledBlock(*args, pre_only=pre_only, qk_norm=qk_norm, **kwargs) + self.x_block = DismantledBlock(*args, pre_only=False, qk_norm=qk_norm, **kwargs) + + def forward(self, *args, **kwargs): + return block_mixing(*args, context_block=self.context_block, x_block=self.x_block, **kwargs) + + +class FinalLayer(nn.Module): + """ + The final layer of DiT. + """ + + def __init__(self, hidden_size: int, patch_size: int, out_channels: int, total_out_channels: Optional[int] = None, dtype=None, device=None): + super().__init__() + self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6, dtype=dtype, device=device) + self.linear = ( + nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True, dtype=dtype, device=device) + if (total_out_channels is None) + else nn.Linear(hidden_size, total_out_channels, bias=True, dtype=dtype, device=device) + ) + self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True, dtype=dtype, device=device)) + + def forward(self, x: torch.Tensor, c: torch.Tensor) -> torch.Tensor: + shift, scale = self.adaLN_modulation(c).chunk(2, dim=1) + x = modulate(self.norm_final(x), shift, scale) + x = self.linear(x) + return x + + +class MMDiT(nn.Module): + """Diffusion model with a Transformer backbone.""" + + def __init__( + self, + input_size: int = 32, + patch_size: int = 2, + in_channels: int = 4, + depth: int = 28, + mlp_ratio: float = 4.0, + learn_sigma: bool = False, + adm_in_channels: Optional[int] = None, + context_embedder_config: Optional[Dict] = None, + register_length: int = 0, + attn_mode: str = "torch", + rmsnorm: bool = False, + scale_mod_only: bool = False, + swiglu: bool = False, + out_channels: Optional[int] = None, + pos_embed_scaling_factor: Optional[float] = None, + pos_embed_offset: Optional[float] = None, + pos_embed_max_size: Optional[int] = None, + num_patches = None, + qk_norm: Optional[str] = None, + qkv_bias: bool = True, + dtype = None, + device = None, + ): + super().__init__() + self.dtype = dtype + self.learn_sigma = learn_sigma + self.in_channels = in_channels + default_out_channels = in_channels * 2 if learn_sigma else in_channels + self.out_channels = out_channels if out_channels is not None else default_out_channels + self.patch_size = patch_size + self.pos_embed_scaling_factor = pos_embed_scaling_factor + self.pos_embed_offset = pos_embed_offset + self.pos_embed_max_size = pos_embed_max_size + + # apply magic --> this defines a head_size of 64 + hidden_size = 64 * depth + num_heads = depth + + self.num_heads = num_heads + + self.x_embedder = PatchEmbed(input_size, patch_size, in_channels, hidden_size, bias=True, strict_img_size=self.pos_embed_max_size is None, dtype=dtype, device=device) + self.t_embedder = TimestepEmbedder(hidden_size, dtype=dtype, device=device) + + if adm_in_channels is not None: + assert isinstance(adm_in_channels, int) + self.y_embedder = VectorEmbedder(adm_in_channels, hidden_size, dtype=dtype, device=device) + + self.context_embedder = nn.Identity() + if context_embedder_config is not None: + if context_embedder_config["target"] == "torch.nn.Linear": + self.context_embedder = nn.Linear(**context_embedder_config["params"], dtype=dtype, device=device) + + self.register_length = register_length + if self.register_length > 0: + self.register = nn.Parameter(torch.randn(1, register_length, hidden_size, dtype=dtype, device=device)) + + # num_patches = self.x_embedder.num_patches + # Will use fixed sin-cos embedding: + # just use a buffer already + if num_patches is not None: + self.register_buffer( + "pos_embed", + torch.zeros(1, num_patches, hidden_size, dtype=dtype, device=device), + ) + else: + self.pos_embed = None + + self.joint_blocks = nn.ModuleList( + [ + JointBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, attn_mode=attn_mode, pre_only=i == depth - 1, rmsnorm=rmsnorm, scale_mod_only=scale_mod_only, swiglu=swiglu, qk_norm=qk_norm, dtype=dtype, device=device) + for i in range(depth) + ] + ) + + self.final_layer = FinalLayer(hidden_size, patch_size, self.out_channels, dtype=dtype, device=device) + + def cropped_pos_embed(self, hw): + assert self.pos_embed_max_size is not None + p = self.x_embedder.patch_size[0] + h, w = hw + # patched size + h = h // p + w = w // p + assert h <= self.pos_embed_max_size, (h, self.pos_embed_max_size) + assert w <= self.pos_embed_max_size, (w, self.pos_embed_max_size) + top = (self.pos_embed_max_size - h) // 2 + left = (self.pos_embed_max_size - w) // 2 + spatial_pos_embed = rearrange( + self.pos_embed, + "1 (h w) c -> 1 h w c", + h=self.pos_embed_max_size, + w=self.pos_embed_max_size, + ) + spatial_pos_embed = spatial_pos_embed[:, top : top + h, left : left + w, :] + spatial_pos_embed = rearrange(spatial_pos_embed, "1 h w c -> 1 (h w) c") + return spatial_pos_embed + + def unpatchify(self, x, hw=None): + """ + x: (N, T, patch_size**2 * C) + imgs: (N, H, W, C) + """ + c = self.out_channels + p = self.x_embedder.patch_size[0] + if hw is None: + h = w = int(x.shape[1] ** 0.5) + else: + h, w = hw + h = h // p + w = w // p + assert h * w == x.shape[1] + + x = x.reshape(shape=(x.shape[0], h, w, p, p, c)) + x = torch.einsum("nhwpqc->nchpwq", x) + imgs = x.reshape(shape=(x.shape[0], c, h * p, w * p)) + return imgs + + def forward_core_with_concat(self, x: torch.Tensor, c_mod: torch.Tensor, context: Optional[torch.Tensor] = None) -> torch.Tensor: + if self.register_length > 0: + context = torch.cat((repeat(self.register, "1 ... -> b ...", b=x.shape[0]), context if context is not None else torch.Tensor([]).type_as(x)), 1) + + # context is B, L', D + # x is B, L, D + for block in self.joint_blocks: + context, x = block(context, x, c=c_mod) + + x = self.final_layer(x, c_mod) # (N, T, patch_size ** 2 * out_channels) + return x + + def forward(self, x: torch.Tensor, t: torch.Tensor, y: Optional[torch.Tensor] = None, context: Optional[torch.Tensor] = None) -> torch.Tensor: + """ + Forward pass of DiT. + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + t: (N,) tensor of diffusion timesteps + y: (N,) tensor of class labels + """ + hw = x.shape[-2:] + x = self.x_embedder(x) + self.cropped_pos_embed(hw) + c = self.t_embedder(t, dtype=x.dtype) # (N, D) + if y is not None: + y = self.y_embedder(y) # (N, D) + c = c + y # (N, D) + + context = self.context_embedder(context) + + x = self.forward_core_with_concat(x, c, context) + + x = self.unpatchify(x, hw=hw) # (N, out_channels, H, W) + return x diff --git a/modules/models/sd3/other_impls.py b/modules/models/sd3/other_impls.py new file mode 100644 index 00000000..78c1dc68 --- /dev/null +++ b/modules/models/sd3/other_impls.py @@ -0,0 +1,510 @@ +### This file contains impls for underlying related models (CLIP, T5, etc) + +import torch +import math +from torch import nn +from transformers import CLIPTokenizer, T5TokenizerFast + +from modules import sd_hijack + + +################################################################################################# +### Core/Utility +################################################################################################# + + +class AutocastLinear(nn.Linear): + """Same as usual linear layer, but casts its weights to whatever the parameter type is. + + This is different from torch.autocast in a way that float16 layer processing float32 input + will return float16 with autocast on, and float32 with this. T5 seems to be fucked + if you do it in full float16 (returning almost all zeros in the final output). + """ + + def forward(self, x): + return torch.nn.functional.linear(x, self.weight.to(x.dtype), self.bias.to(x.dtype) if self.bias is not None else None) + + +def attention(q, k, v, heads, mask=None): + """Convenience wrapper around a basic attention operation""" + b, _, dim_head = q.shape + dim_head //= heads + q, k, v = [t.view(b, -1, heads, dim_head).transpose(1, 2) for t in (q, k, v)] + out = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=0.0, is_causal=False) + return out.transpose(1, 2).reshape(b, -1, heads * dim_head) + + +class Mlp(nn.Module): + """ MLP as used in Vision Transformer, MLP-Mixer and related networks""" + def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, bias=True, dtype=None, device=None): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + + self.fc1 = nn.Linear(in_features, hidden_features, bias=bias, dtype=dtype, device=device) + self.act = act_layer + self.fc2 = nn.Linear(hidden_features, out_features, bias=bias, dtype=dtype, device=device) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.fc2(x) + return x + + +################################################################################################# +### CLIP +################################################################################################# + + +class CLIPAttention(torch.nn.Module): + def __init__(self, embed_dim, heads, dtype, device): + super().__init__() + self.heads = heads + self.q_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device) + self.k_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device) + self.v_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device) + self.out_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device) + + def forward(self, x, mask=None): + q = self.q_proj(x) + k = self.k_proj(x) + v = self.v_proj(x) + out = attention(q, k, v, self.heads, mask) + return self.out_proj(out) + + +ACTIVATIONS = { + "quick_gelu": lambda a: a * torch.sigmoid(1.702 * a), + "gelu": torch.nn.functional.gelu, +} + +class CLIPLayer(torch.nn.Module): + def __init__(self, embed_dim, heads, intermediate_size, intermediate_activation, dtype, device): + super().__init__() + self.layer_norm1 = nn.LayerNorm(embed_dim, dtype=dtype, device=device) + self.self_attn = CLIPAttention(embed_dim, heads, dtype, device) + self.layer_norm2 = nn.LayerNorm(embed_dim, dtype=dtype, device=device) + #self.mlp = CLIPMLP(embed_dim, intermediate_size, intermediate_activation, dtype, device) + self.mlp = Mlp(embed_dim, intermediate_size, embed_dim, act_layer=ACTIVATIONS[intermediate_activation], dtype=dtype, device=device) + + def forward(self, x, mask=None): + x += self.self_attn(self.layer_norm1(x), mask) + x += self.mlp(self.layer_norm2(x)) + return x + + +class CLIPEncoder(torch.nn.Module): + def __init__(self, num_layers, embed_dim, heads, intermediate_size, intermediate_activation, dtype, device): + super().__init__() + self.layers = torch.nn.ModuleList([CLIPLayer(embed_dim, heads, intermediate_size, intermediate_activation, dtype, device) for i in range(num_layers)]) + + def forward(self, x, mask=None, intermediate_output=None): + if intermediate_output is not None: + if intermediate_output < 0: + intermediate_output = len(self.layers) + intermediate_output + intermediate = None + for i, layer in enumerate(self.layers): + x = layer(x, mask) + if i == intermediate_output: + intermediate = x.clone() + return x, intermediate + + +class CLIPEmbeddings(torch.nn.Module): + def __init__(self, embed_dim, vocab_size=49408, num_positions=77, dtype=None, device=None, textual_inversion_key="clip_l"): + super().__init__() + self.token_embedding = sd_hijack.TextualInversionEmbeddings(vocab_size, embed_dim, dtype=dtype, device=device, textual_inversion_key=textual_inversion_key) + self.position_embedding = torch.nn.Embedding(num_positions, embed_dim, dtype=dtype, device=device) + + def forward(self, input_tokens): + return self.token_embedding(input_tokens) + self.position_embedding.weight + + +class CLIPTextModel_(torch.nn.Module): + def __init__(self, config_dict, dtype, device): + num_layers = config_dict["num_hidden_layers"] + embed_dim = config_dict["hidden_size"] + heads = config_dict["num_attention_heads"] + intermediate_size = config_dict["intermediate_size"] + intermediate_activation = config_dict["hidden_act"] + super().__init__() + self.embeddings = CLIPEmbeddings(embed_dim, dtype=torch.float32, device=device, textual_inversion_key=config_dict.get('textual_inversion_key', 'clip_l')) + self.encoder = CLIPEncoder(num_layers, embed_dim, heads, intermediate_size, intermediate_activation, dtype, device) + self.final_layer_norm = nn.LayerNorm(embed_dim, dtype=dtype, device=device) + + def forward(self, input_tokens, intermediate_output=None, final_layer_norm_intermediate=True): + x = self.embeddings(input_tokens) + causal_mask = torch.empty(x.shape[1], x.shape[1], dtype=x.dtype, device=x.device).fill_(float("-inf")).triu_(1) + x, i = self.encoder(x, mask=causal_mask, intermediate_output=intermediate_output) + x = self.final_layer_norm(x) + if i is not None and final_layer_norm_intermediate: + i = self.final_layer_norm(i) + pooled_output = x[torch.arange(x.shape[0], device=x.device), input_tokens.to(dtype=torch.int, device=x.device).argmax(dim=-1),] + return x, i, pooled_output + + +class CLIPTextModel(torch.nn.Module): + def __init__(self, config_dict, dtype, device): + super().__init__() + self.num_layers = config_dict["num_hidden_layers"] + self.text_model = CLIPTextModel_(config_dict, dtype, device) + embed_dim = config_dict["hidden_size"] + self.text_projection = nn.Linear(embed_dim, embed_dim, bias=False, dtype=dtype, device=device) + self.text_projection.weight.copy_(torch.eye(embed_dim)) + self.dtype = dtype + + def get_input_embeddings(self): + return self.text_model.embeddings.token_embedding + + def set_input_embeddings(self, embeddings): + self.text_model.embeddings.token_embedding = embeddings + + def forward(self, *args, **kwargs): + x = self.text_model(*args, **kwargs) + out = self.text_projection(x[2]) + return (x[0], x[1], out, x[2]) + + +class SDTokenizer: + def __init__(self, max_length=77, pad_with_end=True, tokenizer=None, has_start_token=True, pad_to_max_length=True, min_length=None): + self.tokenizer = tokenizer + self.max_length = max_length + self.min_length = min_length + empty = self.tokenizer('')["input_ids"] + if has_start_token: + self.tokens_start = 1 + self.start_token = empty[0] + self.end_token = empty[1] + else: + self.tokens_start = 0 + self.start_token = None + self.end_token = empty[0] + self.pad_with_end = pad_with_end + self.pad_to_max_length = pad_to_max_length + vocab = self.tokenizer.get_vocab() + self.inv_vocab = {v: k for k, v in vocab.items()} + self.max_word_length = 8 + + + def tokenize_with_weights(self, text:str): + """Tokenize the text, with weight values - presume 1.0 for all and ignore other features here. The details aren't relevant for a reference impl, and weights themselves has weak effect on SD3.""" + if self.pad_with_end: + pad_token = self.end_token + else: + pad_token = 0 + batch = [] + if self.start_token is not None: + batch.append((self.start_token, 1.0)) + to_tokenize = text.replace("\n", " ").split(' ') + to_tokenize = [x for x in to_tokenize if x != ""] + for word in to_tokenize: + batch.extend([(t, 1) for t in self.tokenizer(word)["input_ids"][self.tokens_start:-1]]) + batch.append((self.end_token, 1.0)) + if self.pad_to_max_length: + batch.extend([(pad_token, 1.0)] * (self.max_length - len(batch))) + if self.min_length is not None and len(batch) < self.min_length: + batch.extend([(pad_token, 1.0)] * (self.min_length - len(batch))) + return [batch] + + +class SDXLClipGTokenizer(SDTokenizer): + def __init__(self, tokenizer): + super().__init__(pad_with_end=False, tokenizer=tokenizer) + + +class SD3Tokenizer: + def __init__(self): + clip_tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") + self.clip_l = SDTokenizer(tokenizer=clip_tokenizer) + self.clip_g = SDXLClipGTokenizer(clip_tokenizer) + self.t5xxl = T5XXLTokenizer() + + def tokenize_with_weights(self, text:str): + out = {} + out["g"] = self.clip_g.tokenize_with_weights(text) + out["l"] = self.clip_l.tokenize_with_weights(text) + out["t5xxl"] = self.t5xxl.tokenize_with_weights(text) + return out + + +class ClipTokenWeightEncoder: + def encode_token_weights(self, token_weight_pairs): + tokens = [a[0] for a in token_weight_pairs[0]] + out, pooled = self([tokens]) + if pooled is not None: + first_pooled = pooled[0:1].cpu() + else: + first_pooled = pooled + output = [out[0:1]] + return torch.cat(output, dim=-2).cpu(), first_pooled + + +class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder): + """Uses the CLIP transformer encoder for text (from huggingface)""" + LAYERS = ["last", "pooled", "hidden"] + def __init__(self, device="cpu", max_length=77, layer="last", layer_idx=None, textmodel_json_config=None, dtype=None, model_class=CLIPTextModel, + special_tokens=None, layer_norm_hidden_state=True, return_projected_pooled=True): + super().__init__() + assert layer in self.LAYERS + self.transformer = model_class(textmodel_json_config, dtype, device) + self.num_layers = self.transformer.num_layers + self.max_length = max_length + self.transformer = self.transformer.eval() + for param in self.parameters(): + param.requires_grad = False + self.layer = layer + self.layer_idx = None + self.special_tokens = special_tokens if special_tokens is not None else {"start": 49406, "end": 49407, "pad": 49407} + self.logit_scale = torch.nn.Parameter(torch.tensor(4.6055)) + self.layer_norm_hidden_state = layer_norm_hidden_state + self.return_projected_pooled = return_projected_pooled + if layer == "hidden": + assert layer_idx is not None + assert abs(layer_idx) < self.num_layers + self.set_clip_options({"layer": layer_idx}) + self.options_default = (self.layer, self.layer_idx, self.return_projected_pooled) + + def set_clip_options(self, options): + layer_idx = options.get("layer", self.layer_idx) + self.return_projected_pooled = options.get("projected_pooled", self.return_projected_pooled) + if layer_idx is None or abs(layer_idx) > self.num_layers: + self.layer = "last" + else: + self.layer = "hidden" + self.layer_idx = layer_idx + + def forward(self, tokens): + backup_embeds = self.transformer.get_input_embeddings() + tokens = torch.asarray(tokens, dtype=torch.int64, device=backup_embeds.weight.device) + outputs = self.transformer(tokens, intermediate_output=self.layer_idx, final_layer_norm_intermediate=self.layer_norm_hidden_state) + self.transformer.set_input_embeddings(backup_embeds) + if self.layer == "last": + z = outputs[0] + else: + z = outputs[1] + pooled_output = None + if len(outputs) >= 3: + if not self.return_projected_pooled and len(outputs) >= 4 and outputs[3] is not None: + pooled_output = outputs[3].float() + elif outputs[2] is not None: + pooled_output = outputs[2].float() + return z.float(), pooled_output + + +class SDXLClipG(SDClipModel): + """Wraps the CLIP-G model into the SD-CLIP-Model interface""" + def __init__(self, config, device="cpu", layer="penultimate", layer_idx=None, dtype=None): + if layer == "penultimate": + layer="hidden" + layer_idx=-2 + super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=config, dtype=dtype, special_tokens={"start": 49406, "end": 49407, "pad": 0}, layer_norm_hidden_state=False) + + +class T5XXLModel(SDClipModel): + """Wraps the T5-XXL model into the SD-CLIP-Model interface for convenience""" + def __init__(self, config, device="cpu", layer="last", layer_idx=None, dtype=None): + super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=T5) + + +################################################################################################# +### T5 implementation, for the T5-XXL text encoder portion, largely pulled from upstream impl +################################################################################################# + +class T5XXLTokenizer(SDTokenizer): + """Wraps the T5 Tokenizer from HF into the SDTokenizer interface""" + def __init__(self): + super().__init__(pad_with_end=False, tokenizer=T5TokenizerFast.from_pretrained("google/t5-v1_1-xxl"), has_start_token=False, pad_to_max_length=False, max_length=99999999, min_length=77) + + +class T5LayerNorm(torch.nn.Module): + def __init__(self, hidden_size, eps=1e-6, dtype=None, device=None): + super().__init__() + self.weight = torch.nn.Parameter(torch.ones(hidden_size, dtype=dtype, device=device)) + self.variance_epsilon = eps + + def forward(self, x): + variance = x.pow(2).mean(-1, keepdim=True) + x = x * torch.rsqrt(variance + self.variance_epsilon) + return self.weight.to(device=x.device, dtype=x.dtype) * x + + +class T5DenseGatedActDense(torch.nn.Module): + def __init__(self, model_dim, ff_dim, dtype, device): + super().__init__() + self.wi_0 = AutocastLinear(model_dim, ff_dim, bias=False, dtype=dtype, device=device) + self.wi_1 = AutocastLinear(model_dim, ff_dim, bias=False, dtype=dtype, device=device) + self.wo = AutocastLinear(ff_dim, model_dim, bias=False, dtype=dtype, device=device) + + def forward(self, x): + hidden_gelu = torch.nn.functional.gelu(self.wi_0(x), approximate="tanh") + hidden_linear = self.wi_1(x) + x = hidden_gelu * hidden_linear + x = self.wo(x) + return x + + +class T5LayerFF(torch.nn.Module): + def __init__(self, model_dim, ff_dim, dtype, device): + super().__init__() + self.DenseReluDense = T5DenseGatedActDense(model_dim, ff_dim, dtype, device) + self.layer_norm = T5LayerNorm(model_dim, dtype=dtype, device=device) + + def forward(self, x): + forwarded_states = self.layer_norm(x) + forwarded_states = self.DenseReluDense(forwarded_states) + x += forwarded_states + return x + + +class T5Attention(torch.nn.Module): + def __init__(self, model_dim, inner_dim, num_heads, relative_attention_bias, dtype, device): + super().__init__() + # Mesh TensorFlow initialization to avoid scaling before softmax + self.q = AutocastLinear(model_dim, inner_dim, bias=False, dtype=dtype, device=device) + self.k = AutocastLinear(model_dim, inner_dim, bias=False, dtype=dtype, device=device) + self.v = AutocastLinear(model_dim, inner_dim, bias=False, dtype=dtype, device=device) + self.o = AutocastLinear(inner_dim, model_dim, bias=False, dtype=dtype, device=device) + self.num_heads = num_heads + self.relative_attention_bias = None + if relative_attention_bias: + self.relative_attention_num_buckets = 32 + self.relative_attention_max_distance = 128 + self.relative_attention_bias = torch.nn.Embedding(self.relative_attention_num_buckets, self.num_heads, device=device) + + @staticmethod + def _relative_position_bucket(relative_position, bidirectional=True, num_buckets=32, max_distance=128): + """ + Adapted from Mesh Tensorflow: + https://github.com/tensorflow/mesh/blob/0cb87fe07da627bf0b7e60475d59f95ed6b5be3d/mesh_tensorflow/transformer/transformer_layers.py#L593 + + Translate relative position to a bucket number for relative attention. The relative position is defined as + memory_position - query_position, i.e. the distance in tokens from the attending position to the attended-to + position. If bidirectional=False, then positive relative positions are invalid. We use smaller buckets for + small absolute relative_position and larger buckets for larger absolute relative_positions. All relative + positions >=max_distance map to the same bucket. All relative positions <=-max_distance map to the same bucket. + This should allow for more graceful generalization to longer sequences than the model has been trained on + + Args: + relative_position: an int32 Tensor + bidirectional: a boolean - whether the attention is bidirectional + num_buckets: an integer + max_distance: an integer + + Returns: + a Tensor with the same shape as relative_position, containing int32 values in the range [0, num_buckets) + """ + relative_buckets = 0 + if bidirectional: + num_buckets //= 2 + relative_buckets += (relative_position > 0).to(torch.long) * num_buckets + relative_position = torch.abs(relative_position) + else: + relative_position = -torch.min(relative_position, torch.zeros_like(relative_position)) + # now relative_position is in the range [0, inf) + # half of the buckets are for exact increments in positions + max_exact = num_buckets // 2 + is_small = relative_position < max_exact + # The other half of the buckets are for logarithmically bigger bins in positions up to max_distance + relative_position_if_large = max_exact + ( + torch.log(relative_position.float() / max_exact) + / math.log(max_distance / max_exact) + * (num_buckets - max_exact) + ).to(torch.long) + relative_position_if_large = torch.min(relative_position_if_large, torch.full_like(relative_position_if_large, num_buckets - 1)) + relative_buckets += torch.where(is_small, relative_position, relative_position_if_large) + return relative_buckets + + def compute_bias(self, query_length, key_length, device): + """Compute binned relative position bias""" + context_position = torch.arange(query_length, dtype=torch.long, device=device)[:, None] + memory_position = torch.arange(key_length, dtype=torch.long, device=device)[None, :] + relative_position = memory_position - context_position # shape (query_length, key_length) + relative_position_bucket = self._relative_position_bucket( + relative_position, # shape (query_length, key_length) + bidirectional=True, + num_buckets=self.relative_attention_num_buckets, + max_distance=self.relative_attention_max_distance, + ) + values = self.relative_attention_bias(relative_position_bucket) # shape (query_length, key_length, num_heads) + values = values.permute([2, 0, 1]).unsqueeze(0) # shape (1, num_heads, query_length, key_length) + return values + + def forward(self, x, past_bias=None): + q = self.q(x) + k = self.k(x) + v = self.v(x) + + if self.relative_attention_bias is not None: + past_bias = self.compute_bias(x.shape[1], x.shape[1], x.device) + if past_bias is not None: + mask = past_bias + else: + mask = None + + out = attention(q, k * ((k.shape[-1] / self.num_heads) ** 0.5), v, self.num_heads, mask.to(x.dtype) if mask is not None else None) + + return self.o(out), past_bias + + +class T5LayerSelfAttention(torch.nn.Module): + def __init__(self, model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias, dtype, device): + super().__init__() + self.SelfAttention = T5Attention(model_dim, inner_dim, num_heads, relative_attention_bias, dtype, device) + self.layer_norm = T5LayerNorm(model_dim, dtype=dtype, device=device) + + def forward(self, x, past_bias=None): + output, past_bias = self.SelfAttention(self.layer_norm(x), past_bias=past_bias) + x += output + return x, past_bias + + +class T5Block(torch.nn.Module): + def __init__(self, model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias, dtype, device): + super().__init__() + self.layer = torch.nn.ModuleList() + self.layer.append(T5LayerSelfAttention(model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias, dtype, device)) + self.layer.append(T5LayerFF(model_dim, ff_dim, dtype, device)) + + def forward(self, x, past_bias=None): + x, past_bias = self.layer[0](x, past_bias) + x = self.layer[-1](x) + return x, past_bias + + +class T5Stack(torch.nn.Module): + def __init__(self, num_layers, model_dim, inner_dim, ff_dim, num_heads, vocab_size, dtype, device): + super().__init__() + self.embed_tokens = torch.nn.Embedding(vocab_size, model_dim, device=device) + self.block = torch.nn.ModuleList([T5Block(model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias=(i == 0), dtype=dtype, device=device) for i in range(num_layers)]) + self.final_layer_norm = T5LayerNorm(model_dim, dtype=dtype, device=device) + + def forward(self, input_ids, intermediate_output=None, final_layer_norm_intermediate=True): + intermediate = None + x = self.embed_tokens(input_ids).to(torch.float32) # needs float32 or else T5 returns all zeroes + past_bias = None + for i, layer in enumerate(self.block): + x, past_bias = layer(x, past_bias) + if i == intermediate_output: + intermediate = x.clone() + x = self.final_layer_norm(x) + if intermediate is not None and final_layer_norm_intermediate: + intermediate = self.final_layer_norm(intermediate) + return x, intermediate + + +class T5(torch.nn.Module): + def __init__(self, config_dict, dtype, device): + super().__init__() + self.num_layers = config_dict["num_layers"] + self.encoder = T5Stack(self.num_layers, config_dict["d_model"], config_dict["d_model"], config_dict["d_ff"], config_dict["num_heads"], config_dict["vocab_size"], dtype, device) + self.dtype = dtype + + def get_input_embeddings(self): + return self.encoder.embed_tokens + + def set_input_embeddings(self, embeddings): + self.encoder.embed_tokens = embeddings + + def forward(self, *args, **kwargs): + return self.encoder(*args, **kwargs) diff --git a/modules/models/sd3/sd3_cond.py b/modules/models/sd3/sd3_cond.py new file mode 100644 index 00000000..325c512d --- /dev/null +++ b/modules/models/sd3/sd3_cond.py @@ -0,0 +1,222 @@ +import os +import safetensors +import torch +import typing + +from transformers import CLIPTokenizer, T5TokenizerFast + +from modules import shared, devices, modelloader, sd_hijack_clip, prompt_parser +from modules.models.sd3.other_impls import SDClipModel, SDXLClipG, T5XXLModel, SD3Tokenizer + + +class SafetensorsMapping(typing.Mapping): + def __init__(self, file): + self.file = file + + def __len__(self): + return len(self.file.keys()) + + def __iter__(self): + for key in self.file.keys(): + yield key + + def __getitem__(self, key): + return self.file.get_tensor(key) + + +CLIPL_URL = "https://huggingface.co/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/clip_l.safetensors" +CLIPL_CONFIG = { + "hidden_act": "quick_gelu", + "hidden_size": 768, + "intermediate_size": 3072, + "num_attention_heads": 12, + "num_hidden_layers": 12, +} + +CLIPG_URL = "https://huggingface.co/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/clip_g.safetensors" +CLIPG_CONFIG = { + "hidden_act": "gelu", + "hidden_size": 1280, + "intermediate_size": 5120, + "num_attention_heads": 20, + "num_hidden_layers": 32, + "textual_inversion_key": "clip_g", +} + +T5_URL = "https://huggingface.co/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/t5xxl_fp16.safetensors" +T5_CONFIG = { + "d_ff": 10240, + "d_model": 4096, + "num_heads": 64, + "num_layers": 24, + "vocab_size": 32128, +} + + +class Sd3ClipLG(sd_hijack_clip.TextConditionalModel): + def __init__(self, clip_l, clip_g): + super().__init__() + + self.clip_l = clip_l + self.clip_g = clip_g + + self.tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") + + empty = self.tokenizer('')["input_ids"] + self.id_start = empty[0] + self.id_end = empty[1] + self.id_pad = empty[1] + + self.return_pooled = True + + def tokenize(self, texts): + return self.tokenizer(texts, truncation=False, add_special_tokens=False)["input_ids"] + + def encode_with_transformers(self, tokens): + tokens_g = tokens.clone() + + for batch_pos in range(tokens_g.shape[0]): + index = tokens_g[batch_pos].cpu().tolist().index(self.id_end) + tokens_g[batch_pos, index+1:tokens_g.shape[1]] = 0 + + l_out, l_pooled = self.clip_l(tokens) + g_out, g_pooled = self.clip_g(tokens_g) + + lg_out = torch.cat([l_out, g_out], dim=-1) + lg_out = torch.nn.functional.pad(lg_out, (0, 4096 - lg_out.shape[-1])) + + vector_out = torch.cat((l_pooled, g_pooled), dim=-1) + + lg_out.pooled = vector_out + return lg_out + + def encode_embedding_init_text(self, init_text, nvpt): + return torch.zeros((nvpt, 768+1280), device=devices.device) # XXX + + +class Sd3T5(torch.nn.Module): + def __init__(self, t5xxl): + super().__init__() + + self.t5xxl = t5xxl + self.tokenizer = T5TokenizerFast.from_pretrained("google/t5-v1_1-xxl") + + empty = self.tokenizer('', padding='max_length', max_length=2)["input_ids"] + self.id_end = empty[0] + self.id_pad = empty[1] + + def tokenize(self, texts): + return self.tokenizer(texts, truncation=False, add_special_tokens=False)["input_ids"] + + def tokenize_line(self, line, *, target_token_count=None): + if shared.opts.emphasis != "None": + parsed = prompt_parser.parse_prompt_attention(line) + else: + parsed = [[line, 1.0]] + + tokenized = self.tokenize([text for text, _ in parsed]) + + tokens = [] + multipliers = [] + + for text_tokens, (text, weight) in zip(tokenized, parsed): + if text == 'BREAK' and weight == -1: + continue + + tokens += text_tokens + multipliers += [weight] * len(text_tokens) + + tokens += [self.id_end] + multipliers += [1.0] + + if target_token_count is not None: + if len(tokens) < target_token_count: + tokens += [self.id_pad] * (target_token_count - len(tokens)) + multipliers += [1.0] * (target_token_count - len(tokens)) + else: + tokens = tokens[0:target_token_count] + multipliers = multipliers[0:target_token_count] + + return tokens, multipliers + + def forward(self, texts, *, token_count): + if not self.t5xxl or not shared.opts.sd3_enable_t5: + return torch.zeros((len(texts), token_count, 4096), device=devices.device, dtype=devices.dtype) + + tokens_batch = [] + + for text in texts: + tokens, multipliers = self.tokenize_line(text, target_token_count=token_count) + tokens_batch.append(tokens) + + t5_out, t5_pooled = self.t5xxl(tokens_batch) + + return t5_out + + def encode_embedding_init_text(self, init_text, nvpt): + return torch.zeros((nvpt, 4096), device=devices.device) # XXX + + +class SD3Cond(torch.nn.Module): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.tokenizer = SD3Tokenizer() + + with torch.no_grad(): + self.clip_g = SDXLClipG(CLIPG_CONFIG, device="cpu", dtype=devices.dtype) + self.clip_l = SDClipModel(layer="hidden", layer_idx=-2, device="cpu", dtype=devices.dtype, layer_norm_hidden_state=False, return_projected_pooled=False, textmodel_json_config=CLIPL_CONFIG) + + if shared.opts.sd3_enable_t5: + self.t5xxl = T5XXLModel(T5_CONFIG, device="cpu", dtype=devices.dtype) + else: + self.t5xxl = None + + self.model_lg = Sd3ClipLG(self.clip_l, self.clip_g) + self.model_t5 = Sd3T5(self.t5xxl) + + def forward(self, prompts: list[str]): + with devices.without_autocast(): + lg_out, vector_out = self.model_lg(prompts) + t5_out = self.model_t5(prompts, token_count=lg_out.shape[1]) + lgt_out = torch.cat([lg_out, t5_out], dim=-2) + + return { + 'crossattn': lgt_out, + 'vector': vector_out, + } + + def before_load_weights(self, state_dict): + clip_path = os.path.join(shared.models_path, "CLIP") + + if 'text_encoders.clip_g.transformer.text_model.embeddings.position_embedding.weight' not in state_dict: + clip_g_file = modelloader.load_file_from_url(CLIPG_URL, model_dir=clip_path, file_name="clip_g.safetensors") + with safetensors.safe_open(clip_g_file, framework="pt") as file: + self.clip_g.transformer.load_state_dict(SafetensorsMapping(file)) + + if 'text_encoders.clip_l.transformer.text_model.embeddings.position_embedding.weight' not in state_dict: + clip_l_file = modelloader.load_file_from_url(CLIPL_URL, model_dir=clip_path, file_name="clip_l.safetensors") + with safetensors.safe_open(clip_l_file, framework="pt") as file: + self.clip_l.transformer.load_state_dict(SafetensorsMapping(file), strict=False) + + if self.t5xxl and 'text_encoders.t5xxl.transformer.encoder.embed_tokens.weight' not in state_dict: + t5_file = modelloader.load_file_from_url(T5_URL, model_dir=clip_path, file_name="t5xxl_fp16.safetensors") + with safetensors.safe_open(t5_file, framework="pt") as file: + self.t5xxl.transformer.load_state_dict(SafetensorsMapping(file), strict=False) + + def encode_embedding_init_text(self, init_text, nvpt): + return self.model_lg.encode_embedding_init_text(init_text, nvpt) + + def tokenize(self, texts): + return self.model_lg.tokenize(texts) + + def medvram_modules(self): + return [self.clip_g, self.clip_l, self.t5xxl] + + def get_token_count(self, text): + _, token_count = self.model_lg.process_texts([text]) + + return token_count + + def get_target_prompt_token_count(self, token_count): + return self.model_lg.get_target_prompt_token_count(token_count) diff --git a/modules/models/sd3/sd3_impls.py b/modules/models/sd3/sd3_impls.py new file mode 100644 index 00000000..59f11b2c --- /dev/null +++ b/modules/models/sd3/sd3_impls.py @@ -0,0 +1,374 @@ +### Impls of the SD3 core diffusion model and VAE + +import torch +import math +import einops +from modules.models.sd3.mmdit import MMDiT +from PIL import Image + + +################################################################################################# +### MMDiT Model Wrapping +################################################################################################# + + +class ModelSamplingDiscreteFlow(torch.nn.Module): + """Helper for sampler scheduling (ie timestep/sigma calculations) for Discrete Flow models""" + def __init__(self, shift=1.0): + super().__init__() + self.shift = shift + timesteps = 1000 + ts = self.sigma(torch.arange(1, timesteps + 1, 1)) + self.register_buffer('sigmas', ts) + + @property + def sigma_min(self): + return self.sigmas[0] + + @property + def sigma_max(self): + return self.sigmas[-1] + + def timestep(self, sigma): + return sigma * 1000 + + def sigma(self, timestep: torch.Tensor): + timestep = timestep / 1000.0 + if self.shift == 1.0: + return timestep + return self.shift * timestep / (1 + (self.shift - 1) * timestep) + + def calculate_denoised(self, sigma, model_output, model_input): + sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1)) + return model_input - model_output * sigma + + def noise_scaling(self, sigma, noise, latent_image, max_denoise=False): + return sigma * noise + (1.0 - sigma) * latent_image + + +class BaseModel(torch.nn.Module): + """Wrapper around the core MM-DiT model""" + def __init__(self, shift=1.0, device=None, dtype=torch.float32, state_dict=None, prefix=""): + super().__init__() + # Important configuration values can be quickly determined by checking shapes in the source file + # Some of these will vary between models (eg 2B vs 8B primarily differ in their depth, but also other details change) + patch_size = state_dict[f"{prefix}x_embedder.proj.weight"].shape[2] + depth = state_dict[f"{prefix}x_embedder.proj.weight"].shape[0] // 64 + num_patches = state_dict[f"{prefix}pos_embed"].shape[1] + pos_embed_max_size = round(math.sqrt(num_patches)) + adm_in_channels = state_dict[f"{prefix}y_embedder.mlp.0.weight"].shape[1] + context_shape = state_dict[f"{prefix}context_embedder.weight"].shape + context_embedder_config = { + "target": "torch.nn.Linear", + "params": { + "in_features": context_shape[1], + "out_features": context_shape[0] + } + } + self.diffusion_model = MMDiT(input_size=None, pos_embed_scaling_factor=None, pos_embed_offset=None, pos_embed_max_size=pos_embed_max_size, patch_size=patch_size, in_channels=16, depth=depth, num_patches=num_patches, adm_in_channels=adm_in_channels, context_embedder_config=context_embedder_config, device=device, dtype=dtype) + self.model_sampling = ModelSamplingDiscreteFlow(shift=shift) + self.depth = depth + + def apply_model(self, x, sigma, c_crossattn=None, y=None): + dtype = self.get_dtype() + timestep = self.model_sampling.timestep(sigma).float() + model_output = self.diffusion_model(x.to(dtype), timestep, context=c_crossattn.to(dtype), y=y.to(dtype)).float() + return self.model_sampling.calculate_denoised(sigma, model_output, x) + + def forward(self, *args, **kwargs): + return self.apply_model(*args, **kwargs) + + def get_dtype(self): + return self.diffusion_model.dtype + + +class CFGDenoiser(torch.nn.Module): + """Helper for applying CFG Scaling to diffusion outputs""" + def __init__(self, model): + super().__init__() + self.model = model + + def forward(self, x, timestep, cond, uncond, cond_scale): + # Run cond and uncond in a batch together + batched = self.model.apply_model(torch.cat([x, x]), torch.cat([timestep, timestep]), c_crossattn=torch.cat([cond["c_crossattn"], uncond["c_crossattn"]]), y=torch.cat([cond["y"], uncond["y"]])) + # Then split and apply CFG Scaling + pos_out, neg_out = batched.chunk(2) + scaled = neg_out + (pos_out - neg_out) * cond_scale + return scaled + + +class SD3LatentFormat: + """Latents are slightly shifted from center - this class must be called after VAE Decode to correct for the shift""" + def __init__(self): + self.scale_factor = 1.5305 + self.shift_factor = 0.0609 + + def process_in(self, latent): + return (latent - self.shift_factor) * self.scale_factor + + def process_out(self, latent): + return (latent / self.scale_factor) + self.shift_factor + + def decode_latent_to_preview(self, x0): + """Quick RGB approximate preview of sd3 latents""" + factors = torch.tensor([ + [-0.0645, 0.0177, 0.1052], [ 0.0028, 0.0312, 0.0650], + [ 0.1848, 0.0762, 0.0360], [ 0.0944, 0.0360, 0.0889], + [ 0.0897, 0.0506, -0.0364], [-0.0020, 0.1203, 0.0284], + [ 0.0855, 0.0118, 0.0283], [-0.0539, 0.0658, 0.1047], + [-0.0057, 0.0116, 0.0700], [-0.0412, 0.0281, -0.0039], + [ 0.1106, 0.1171, 0.1220], [-0.0248, 0.0682, -0.0481], + [ 0.0815, 0.0846, 0.1207], [-0.0120, -0.0055, -0.0867], + [-0.0749, -0.0634, -0.0456], [-0.1418, -0.1457, -0.1259] + ], device="cpu") + latent_image = x0[0].permute(1, 2, 0).cpu() @ factors + + latents_ubyte = (((latent_image + 1) / 2) + .clamp(0, 1) # change scale from -1..1 to 0..1 + .mul(0xFF) # to 0..255 + .byte()).cpu() + + return Image.fromarray(latents_ubyte.numpy()) + + +################################################################################################# +### K-Diffusion Sampling +################################################################################################# + + +def append_dims(x, target_dims): + """Appends dimensions to the end of a tensor until it has target_dims dimensions.""" + dims_to_append = target_dims - x.ndim + return x[(...,) + (None,) * dims_to_append] + + +def to_d(x, sigma, denoised): + """Converts a denoiser output to a Karras ODE derivative.""" + return (x - denoised) / append_dims(sigma, x.ndim) + + +@torch.no_grad() +@torch.autocast("cuda", dtype=torch.float16) +def sample_euler(model, x, sigmas, extra_args=None): + """Implements Algorithm 2 (Euler steps) from Karras et al. (2022).""" + extra_args = {} if extra_args is None else extra_args + s_in = x.new_ones([x.shape[0]]) + for i in range(len(sigmas) - 1): + sigma_hat = sigmas[i] + denoised = model(x, sigma_hat * s_in, **extra_args) + d = to_d(x, sigma_hat, denoised) + dt = sigmas[i + 1] - sigma_hat + # Euler method + x = x + d * dt + return x + + +################################################################################################# +### VAE +################################################################################################# + + +def Normalize(in_channels, num_groups=32, dtype=torch.float32, device=None): + return torch.nn.GroupNorm(num_groups=num_groups, num_channels=in_channels, eps=1e-6, affine=True, dtype=dtype, device=device) + + +class ResnetBlock(torch.nn.Module): + def __init__(self, *, in_channels, out_channels=None, dtype=torch.float32, device=None): + super().__init__() + self.in_channels = in_channels + out_channels = in_channels if out_channels is None else out_channels + self.out_channels = out_channels + + self.norm1 = Normalize(in_channels, dtype=dtype, device=device) + self.conv1 = torch.nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device) + self.norm2 = Normalize(out_channels, dtype=dtype, device=device) + self.conv2 = torch.nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device) + if self.in_channels != self.out_channels: + self.nin_shortcut = torch.nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device) + else: + self.nin_shortcut = None + self.swish = torch.nn.SiLU(inplace=True) + + def forward(self, x): + hidden = x + hidden = self.norm1(hidden) + hidden = self.swish(hidden) + hidden = self.conv1(hidden) + hidden = self.norm2(hidden) + hidden = self.swish(hidden) + hidden = self.conv2(hidden) + if self.in_channels != self.out_channels: + x = self.nin_shortcut(x) + return x + hidden + + +class AttnBlock(torch.nn.Module): + def __init__(self, in_channels, dtype=torch.float32, device=None): + super().__init__() + self.norm = Normalize(in_channels, dtype=dtype, device=device) + self.q = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device) + self.k = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device) + self.v = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device) + self.proj_out = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device) + + def forward(self, x): + hidden = self.norm(x) + q = self.q(hidden) + k = self.k(hidden) + v = self.v(hidden) + b, c, h, w = q.shape + q, k, v = [einops.rearrange(x, "b c h w -> b 1 (h w) c").contiguous() for x in (q, k, v)] + hidden = torch.nn.functional.scaled_dot_product_attention(q, k, v) # scale is dim ** -0.5 per default + hidden = einops.rearrange(hidden, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b) + hidden = self.proj_out(hidden) + return x + hidden + + +class Downsample(torch.nn.Module): + def __init__(self, in_channels, dtype=torch.float32, device=None): + super().__init__() + self.conv = torch.nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0, dtype=dtype, device=device) + + def forward(self, x): + pad = (0,1,0,1) + x = torch.nn.functional.pad(x, pad, mode="constant", value=0) + x = self.conv(x) + return x + + +class Upsample(torch.nn.Module): + def __init__(self, in_channels, dtype=torch.float32, device=None): + super().__init__() + self.conv = torch.nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device) + + def forward(self, x): + x = torch.nn.functional.interpolate(x, scale_factor=2.0, mode="nearest") + x = self.conv(x) + return x + + +class VAEEncoder(torch.nn.Module): + def __init__(self, ch=128, ch_mult=(1,2,4,4), num_res_blocks=2, in_channels=3, z_channels=16, dtype=torch.float32, device=None): + super().__init__() + self.num_resolutions = len(ch_mult) + self.num_res_blocks = num_res_blocks + # downsampling + self.conv_in = torch.nn.Conv2d(in_channels, ch, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device) + in_ch_mult = (1,) + tuple(ch_mult) + self.in_ch_mult = in_ch_mult + self.down = torch.nn.ModuleList() + for i_level in range(self.num_resolutions): + block = torch.nn.ModuleList() + attn = torch.nn.ModuleList() + block_in = ch*in_ch_mult[i_level] + block_out = ch*ch_mult[i_level] + for _ in range(num_res_blocks): + block.append(ResnetBlock(in_channels=block_in, out_channels=block_out, dtype=dtype, device=device)) + block_in = block_out + down = torch.nn.Module() + down.block = block + down.attn = attn + if i_level != self.num_resolutions - 1: + down.downsample = Downsample(block_in, dtype=dtype, device=device) + self.down.append(down) + # middle + self.mid = torch.nn.Module() + self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device) + self.mid.attn_1 = AttnBlock(block_in, dtype=dtype, device=device) + self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device) + # end + self.norm_out = Normalize(block_in, dtype=dtype, device=device) + self.conv_out = torch.nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device) + self.swish = torch.nn.SiLU(inplace=True) + + def forward(self, x): + # downsampling + hs = [self.conv_in(x)] + for i_level in range(self.num_resolutions): + for i_block in range(self.num_res_blocks): + h = self.down[i_level].block[i_block](hs[-1]) + hs.append(h) + if i_level != self.num_resolutions-1: + hs.append(self.down[i_level].downsample(hs[-1])) + # middle + h = hs[-1] + h = self.mid.block_1(h) + h = self.mid.attn_1(h) + h = self.mid.block_2(h) + # end + h = self.norm_out(h) + h = self.swish(h) + h = self.conv_out(h) + return h + + +class VAEDecoder(torch.nn.Module): + def __init__(self, ch=128, out_ch=3, ch_mult=(1, 2, 4, 4), num_res_blocks=2, resolution=256, z_channels=16, dtype=torch.float32, device=None): + super().__init__() + self.num_resolutions = len(ch_mult) + self.num_res_blocks = num_res_blocks + block_in = ch * ch_mult[self.num_resolutions - 1] + curr_res = resolution // 2 ** (self.num_resolutions - 1) + # z to block_in + self.conv_in = torch.nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device) + # middle + self.mid = torch.nn.Module() + self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device) + self.mid.attn_1 = AttnBlock(block_in, dtype=dtype, device=device) + self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device) + # upsampling + self.up = torch.nn.ModuleList() + for i_level in reversed(range(self.num_resolutions)): + block = torch.nn.ModuleList() + block_out = ch * ch_mult[i_level] + for _ in range(self.num_res_blocks + 1): + block.append(ResnetBlock(in_channels=block_in, out_channels=block_out, dtype=dtype, device=device)) + block_in = block_out + up = torch.nn.Module() + up.block = block + if i_level != 0: + up.upsample = Upsample(block_in, dtype=dtype, device=device) + curr_res = curr_res * 2 + self.up.insert(0, up) # prepend to get consistent order + # end + self.norm_out = Normalize(block_in, dtype=dtype, device=device) + self.conv_out = torch.nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device) + self.swish = torch.nn.SiLU(inplace=True) + + def forward(self, z): + # z to block_in + hidden = self.conv_in(z) + # middle + hidden = self.mid.block_1(hidden) + hidden = self.mid.attn_1(hidden) + hidden = self.mid.block_2(hidden) + # upsampling + for i_level in reversed(range(self.num_resolutions)): + for i_block in range(self.num_res_blocks + 1): + hidden = self.up[i_level].block[i_block](hidden) + if i_level != 0: + hidden = self.up[i_level].upsample(hidden) + # end + hidden = self.norm_out(hidden) + hidden = self.swish(hidden) + hidden = self.conv_out(hidden) + return hidden + + +class SDVAE(torch.nn.Module): + def __init__(self, dtype=torch.float32, device=None): + super().__init__() + self.encoder = VAEEncoder(dtype=dtype, device=device) + self.decoder = VAEDecoder(dtype=dtype, device=device) + + @torch.autocast("cuda", dtype=torch.float16) + def decode(self, latent): + return self.decoder(latent) + + @torch.autocast("cuda", dtype=torch.float16) + def encode(self, image): + hidden = self.encoder(image) + mean, logvar = torch.chunk(hidden, 2, dim=1) + logvar = torch.clamp(logvar, -30.0, 20.0) + std = torch.exp(0.5 * logvar) + return mean + std * torch.randn_like(mean) diff --git a/modules/models/sd3/sd3_model.py b/modules/models/sd3/sd3_model.py new file mode 100644 index 00000000..37cf85eb --- /dev/null +++ b/modules/models/sd3/sd3_model.py @@ -0,0 +1,96 @@ +import contextlib + +import torch + +import k_diffusion +from modules.models.sd3.sd3_impls import BaseModel, SDVAE, SD3LatentFormat +from modules.models.sd3.sd3_cond import SD3Cond + +from modules import shared, devices + + +class SD3Denoiser(k_diffusion.external.DiscreteSchedule): + def __init__(self, inner_model, sigmas): + super().__init__(sigmas, quantize=shared.opts.enable_quantization) + self.inner_model = inner_model + + def forward(self, input, sigma, **kwargs): + return self.inner_model.apply_model(input, sigma, **kwargs) + + +class SD3Inferencer(torch.nn.Module): + def __init__(self, state_dict, shift=3, use_ema=False): + super().__init__() + + self.shift = shift + + with torch.no_grad(): + self.model = BaseModel(shift=shift, state_dict=state_dict, prefix="model.diffusion_model.", device="cpu", dtype=devices.dtype) + self.first_stage_model = SDVAE(device="cpu", dtype=devices.dtype_vae) + self.first_stage_model.dtype = self.model.diffusion_model.dtype + + self.alphas_cumprod = 1 / (self.model.model_sampling.sigmas ** 2 + 1) + + self.text_encoders = SD3Cond() + self.cond_stage_key = 'txt' + + self.parameterization = "eps" + self.model.conditioning_key = "crossattn" + + self.latent_format = SD3LatentFormat() + self.latent_channels = 16 + + @property + def cond_stage_model(self): + return self.text_encoders + + def before_load_weights(self, state_dict): + self.cond_stage_model.before_load_weights(state_dict) + + def ema_scope(self): + return contextlib.nullcontext() + + def get_learned_conditioning(self, batch: list[str]): + return self.cond_stage_model(batch) + + def apply_model(self, x, t, cond): + return self.model(x, t, c_crossattn=cond['crossattn'], y=cond['vector']) + + def decode_first_stage(self, latent): + latent = self.latent_format.process_out(latent) + return self.first_stage_model.decode(latent) + + def encode_first_stage(self, image): + latent = self.first_stage_model.encode(image) + return self.latent_format.process_in(latent) + + def get_first_stage_encoding(self, x): + return x + + def create_denoiser(self): + return SD3Denoiser(self, self.model.model_sampling.sigmas) + + def medvram_fields(self): + return [ + (self, 'first_stage_model'), + (self, 'text_encoders'), + (self, 'model'), + ] + + def add_noise_to_latent(self, x, noise, amount): + return x * (1 - amount) + noise * amount + + def fix_dimensions(self, width, height): + return width // 16 * 16, height // 16 * 16 + + def diffusers_weight_mapping(self): + for i in range(self.model.depth): + yield f"transformer.transformer_blocks.{i}.attn.to_q", f"diffusion_model_joint_blocks_{i}_x_block_attn_qkv_q_proj" + yield f"transformer.transformer_blocks.{i}.attn.to_k", f"diffusion_model_joint_blocks_{i}_x_block_attn_qkv_k_proj" + yield f"transformer.transformer_blocks.{i}.attn.to_v", f"diffusion_model_joint_blocks_{i}_x_block_attn_qkv_v_proj" + yield f"transformer.transformer_blocks.{i}.attn.to_out.0", f"diffusion_model_joint_blocks_{i}_x_block_attn_proj" + + yield f"transformer.transformer_blocks.{i}.attn.add_q_proj", f"diffusion_model_joint_blocks_{i}_context_block.attn_qkv_q_proj" + yield f"transformer.transformer_blocks.{i}.attn.add_k_proj", f"diffusion_model_joint_blocks_{i}_context_block.attn_qkv_k_proj" + yield f"transformer.transformer_blocks.{i}.attn.add_v_proj", f"diffusion_model_joint_blocks_{i}_context_block.attn_qkv_v_proj" + yield f"transformer.transformer_blocks.{i}.attn.add_out_proj.0", f"diffusion_model_joint_blocks_{i}_context_block_attn_proj" diff --git a/modules/options.py b/modules/options.py index 35ccade2..2a78a825 100644 --- a/modules/options.py +++ b/modules/options.py @@ -240,6 +240,9 @@ class Options: item_categories = {} for item in self.data_labels.values(): + if item.section[0] is None: + continue + category = categories.mapping.get(item.category_id) category = "Uncategorized" if category is None else category.label if category not in item_categories: diff --git a/modules/paths_internal.py b/modules/paths_internal.py index 6058b0cd..67521f5c 100644 --- a/modules/paths_internal.py +++ b/modules/paths_internal.py @@ -24,14 +24,15 @@ default_sd_model_file = sd_model_file # Parse the --data-dir flag first so we can use it as a base for our other argument default values parser_pre = argparse.ArgumentParser(add_help=False) parser_pre.add_argument("--data-dir", type=str, default=os.path.dirname(modules_path), help="base path where all user data is stored", ) +parser_pre.add_argument("--models-dir", type=str, default=None, help="base path where models are stored; overrides --data-dir", ) cmd_opts_pre = parser_pre.parse_known_args()[0] data_path = cmd_opts_pre.data_dir -models_path = os.path.join(data_path, "models") +models_path = cmd_opts_pre.models_dir if cmd_opts_pre.models_dir else os.path.join(data_path, "models") extensions_dir = os.path.join(data_path, "extensions") extensions_builtin_dir = os.path.join(script_path, "extensions-builtin") config_states_dir = os.path.join(script_path, "config_states") -default_output_dir = os.path.join(data_path, "output") +default_output_dir = os.path.join(data_path, "outputs") roboto_ttf_file = os.path.join(modules_path, 'Roboto-Regular.ttf') diff --git a/modules/postprocessing.py b/modules/postprocessing.py index f1488232..a413d102 100644 --- a/modules/postprocessing.py +++ b/modules/postprocessing.py @@ -17,10 +17,10 @@ def run_postprocessing(extras_mode, image, image_folder, input_dir, output_dir, if extras_mode == 1: for img in image_folder: if isinstance(img, Image.Image): - image = img + image = images.fix_image(img) fn = '' else: - image = Image.open(os.path.abspath(img.name)) + image = images.read(os.path.abspath(img.name)) fn = os.path.splitext(img.orig_name)[0] yield image, fn elif extras_mode == 2: @@ -51,22 +51,24 @@ def run_postprocessing(extras_mode, image, image_folder, input_dir, output_dir, shared.state.textinfo = name shared.state.skipped = False - if shared.state.interrupted: + if shared.state.interrupted or shared.state.stopping_generation: break if isinstance(image_placeholder, str): try: - image_data = Image.open(image_placeholder) + image_data = images.read(image_placeholder) except Exception: continue else: image_data = image_placeholder + image_data = image_data if image_data.mode in ("RGBA", "RGB") else image_data.convert("RGB") + parameters, existing_pnginfo = images.read_info_from_image(image_data) if parameters: existing_pnginfo["parameters"] = parameters - initial_pp = scripts_postprocessing.PostprocessedImage(image_data.convert("RGB")) + initial_pp = scripts_postprocessing.PostprocessedImage(image_data) scripts.scripts_postproc.run(initial_pp, args) @@ -122,8 +124,6 @@ def run_postprocessing(extras_mode, image, image_folder, input_dir, output_dir, if extras_mode != 2 or show_extras_results: outputs.append(pp.image) - image_data.close() - devices.torch_gc() shared.state.end() return outputs, ui_common.plaintext_to_html(infotext), '' @@ -133,13 +133,15 @@ def run_postprocessing_webui(id_task, *args, **kwargs): return run_postprocessing(*args, **kwargs) -def run_extras(extras_mode, resize_mode, image, image_folder, input_dir, output_dir, show_extras_results, gfpgan_visibility, codeformer_visibility, codeformer_weight, upscaling_resize, upscaling_resize_w, upscaling_resize_h, upscaling_crop, extras_upscaler_1, extras_upscaler_2, extras_upscaler_2_visibility, upscale_first: bool, save_output: bool = True): +def run_extras(extras_mode, resize_mode, image, image_folder, input_dir, output_dir, show_extras_results, gfpgan_visibility, codeformer_visibility, codeformer_weight, upscaling_resize, upscaling_resize_w, upscaling_resize_h, upscaling_crop, extras_upscaler_1, extras_upscaler_2, extras_upscaler_2_visibility, upscale_first: bool, save_output: bool = True, max_side_length: int = 0): """old handler for API""" args = scripts.scripts_postproc.create_args_for_run({ "Upscale": { + "upscale_enabled": True, "upscale_mode": resize_mode, "upscale_by": upscaling_resize, + "max_side_length": max_side_length, "upscale_to_width": upscaling_resize_w, "upscale_to_height": upscaling_resize_h, "upscale_crop": upscaling_crop, diff --git a/modules/processing.py b/modules/processing.py index 411c7c3f..7535b56e 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -16,7 +16,7 @@ from skimage import exposure from typing import Any import modules.sd_hijack -from modules import devices, prompt_parser, masking, sd_samplers, lowvram, infotext_utils, extra_networks, sd_vae_approx, scripts, sd_samplers_common, sd_unet, errors, rng +from modules import devices, prompt_parser, masking, sd_samplers, lowvram, infotext_utils, extra_networks, sd_vae_approx, scripts, sd_samplers_common, sd_unet, errors, rng, profiling from modules.rng import slerp # noqa: F401 from modules.sd_hijack import model_hijack from modules.sd_samplers_common import images_tensor_to_samples, decode_first_stage, approximation_indexes @@ -115,20 +115,17 @@ def txt2img_image_conditioning(sd_model, x, width, height): return x.new_zeros(x.shape[0], 2*sd_model.noise_augmentor.time_embed.dim, dtype=x.dtype, device=x.device) else: - sd = sd_model.model.state_dict() - diffusion_model_input = sd.get('diffusion_model.input_blocks.0.0.weight', None) - if diffusion_model_input is not None: - if diffusion_model_input.shape[1] == 9: - # The "masked-image" in this case will just be all 0.5 since the entire image is masked. - image_conditioning = torch.ones(x.shape[0], 3, height, width, device=x.device) * 0.5 - image_conditioning = images_tensor_to_samples(image_conditioning, - approximation_indexes.get(opts.sd_vae_encode_method)) + if sd_model.is_sdxl_inpaint: + # The "masked-image" in this case will just be all 0.5 since the entire image is masked. + image_conditioning = torch.ones(x.shape[0], 3, height, width, device=x.device) * 0.5 + image_conditioning = images_tensor_to_samples(image_conditioning, + approximation_indexes.get(opts.sd_vae_encode_method)) - # Add the fake full 1s mask to the first dimension. - image_conditioning = torch.nn.functional.pad(image_conditioning, (0, 0, 0, 0, 1, 0), value=1.0) - image_conditioning = image_conditioning.to(x.dtype) + # Add the fake full 1s mask to the first dimension. + image_conditioning = torch.nn.functional.pad(image_conditioning, (0, 0, 0, 0, 1, 0), value=1.0) + image_conditioning = image_conditioning.to(x.dtype) - return image_conditioning + return image_conditioning # Dummy zero conditioning if we're not using inpainting or unclip models. # Still takes up a bit of memory, but no encoder call. @@ -152,6 +149,7 @@ class StableDiffusionProcessing: seed_resize_from_w: int = -1 seed_enable_extras: bool = True sampler_name: str = None + scheduler: str = None batch_size: int = 1 n_iter: int = 1 steps: int = 50 @@ -237,11 +235,6 @@ class StableDiffusionProcessing: self.styles = [] self.sampler_noise_scheduler_override = None - self.s_min_uncond = self.s_min_uncond if self.s_min_uncond is not None else opts.s_min_uncond - self.s_churn = self.s_churn if self.s_churn is not None else opts.s_churn - self.s_tmin = self.s_tmin if self.s_tmin is not None else opts.s_tmin - self.s_tmax = (self.s_tmax if self.s_tmax is not None else opts.s_tmax) or float('inf') - self.s_noise = self.s_noise if self.s_noise is not None else opts.s_noise self.extra_generation_params = self.extra_generation_params or {} self.override_settings = self.override_settings or {} @@ -258,6 +251,13 @@ class StableDiffusionProcessing: self.cached_uc = StableDiffusionProcessing.cached_uc self.cached_c = StableDiffusionProcessing.cached_c + def fill_fields_from_opts(self): + self.s_min_uncond = self.s_min_uncond if self.s_min_uncond is not None else opts.s_min_uncond + self.s_churn = self.s_churn if self.s_churn is not None else opts.s_churn + self.s_tmin = self.s_tmin if self.s_tmin is not None else opts.s_tmin + self.s_tmax = (self.s_tmax if self.s_tmax is not None else opts.s_tmax) or float('inf') + self.s_noise = self.s_noise if self.s_noise is not None else opts.s_noise + @property def sd_model(self): return shared.sd_model @@ -389,11 +389,8 @@ class StableDiffusionProcessing: if self.sampler.conditioning_key == "crossattn-adm": return self.unclip_image_conditioning(source_image) - sd = self.sampler.model_wrap.inner_model.model.state_dict() - diffusion_model_input = sd.get('diffusion_model.input_blocks.0.0.weight', None) - if diffusion_model_input is not None: - if diffusion_model_input.shape[1] == 9: - return self.inpainting_image_conditioning(source_image, latent_image, image_mask=image_mask) + if self.sampler.model_wrap.inner_model.is_sdxl_inpaint: + return self.inpainting_image_conditioning(source_image, latent_image, image_mask=image_mask) # Dummy zero conditioning if we're not using inpainting or depth model. return latent_image.new_zeros(latent_image.shape[0], 5, 1, 1) @@ -568,7 +565,7 @@ class Processed: self.all_negative_prompts = all_negative_prompts or p.all_negative_prompts or [self.negative_prompt] self.all_seeds = all_seeds or p.all_seeds or [self.seed] self.all_subseeds = all_subseeds or p.all_subseeds or [self.subseed] - self.infotexts = infotexts or [info] + self.infotexts = infotexts or [info] * len(images_list) self.version = program_version() def js(self): @@ -607,7 +604,7 @@ class Processed: "version": self.version, } - return json.dumps(obj) + return json.dumps(obj, default=lambda o: None) def infotext(self, p: StableDiffusionProcessing, index): return create_infotext(p, self.all_prompts, self.all_seeds, self.all_subseeds, comments=[], position_in_batch=index % self.batch_size, iteration=index // self.batch_size) @@ -628,6 +625,9 @@ class DecodedSamples(list): def decode_latent_batch(model, batch, target_device=None, check_for_nans=False): samples = DecodedSamples() + if check_for_nans: + devices.test_for_nans(batch, "unet") + for i in range(batch.shape[0]): sample = decode_first_stage(model, batch[i:i + 1])[0] @@ -703,7 +703,53 @@ def program_version(): def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iteration=0, position_in_batch=0, use_main_prompt=False, index=None, all_negative_prompts=None): - if index is None: + """ + this function is used to generate the infotext that is stored in the generated images, it's contains the parameters that are required to generate the imagee + Args: + p: StableDiffusionProcessing + all_prompts: list[str] + all_seeds: list[int] + all_subseeds: list[int] + comments: list[str] + iteration: int + position_in_batch: int + use_main_prompt: bool + index: int + all_negative_prompts: list[str] + + Returns: str + + Extra generation params + p.extra_generation_params dictionary allows for additional parameters to be added to the infotext + this can be use by the base webui or extensions. + To add a new entry, add a new key value pair, the dictionary key will be used as the key of the parameter in the infotext + the value generation_params can be defined as: + - str | None + - List[str|None] + - callable func(**kwargs) -> str | None + + When defined as a string, it will be used as without extra processing; this is this most common use case. + + Defining as a list allows for parameter that changes across images in the job, for example, the 'Seed' parameter. + The list should have the same length as the total number of images in the entire job. + + Defining as a callable function allows parameter cannot be generated earlier or when extra logic is required. + For example 'Hires prompt', due to reasons the hr_prompt might be changed by process in the pipeline or extensions + and may vary across different images, defining as a static string or list would not work. + + The function takes locals() as **kwargs, as such will have access to variables like 'p' and 'index'. + the base signature of the function should be: + func(**kwargs) -> str | None + optionally it can have additional arguments that will be used in the function: + func(p, index, **kwargs) -> str | None + note: for better future compatibility even though this function will have access to all variables in the locals(), + it is recommended to only use the arguments present in the function signature of create_infotext. + For actual implementation examples, see StableDiffusionProcessingTxt2Img.init > get_hr_prompt. + """ + + if use_main_prompt: + index = 0 + elif index is None: index = position_in_batch + iteration * p.batch_size if all_negative_prompts is None: @@ -714,6 +760,9 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter token_merging_ratio = p.get_token_merging_ratio() token_merging_ratio_hr = p.get_token_merging_ratio(for_hr=True) + prompt_text = p.main_prompt if use_main_prompt else all_prompts[index] + negative_prompt = p.main_negative_prompt if use_main_prompt else all_negative_prompts[index] + uses_ensd = opts.eta_noise_seed_delta != 0 if uses_ensd: uses_ensd = sd_samplers_common.is_sampler_using_eta_noise_seed_delta(p) @@ -721,6 +770,7 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter generation_params = { "Steps": p.steps, "Sampler": p.sampler_name, + "Schedule type": p.scheduler, "CFG scale": p.cfg_scale, "Image CFG scale": getattr(p, 'image_cfg_scale', None), "Seed": p.all_seeds[0] if use_main_prompt else all_seeds[index], @@ -743,17 +793,25 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter "Token merging ratio hr": None if not enable_hr or token_merging_ratio_hr == 0 else token_merging_ratio_hr, "Init image hash": getattr(p, 'init_img_hash', None), "RNG": opts.randn_source if opts.randn_source != "GPU" else None, - "NGMS": None if p.s_min_uncond == 0 else p.s_min_uncond, "Tiling": "True" if p.tiling else None, **p.extra_generation_params, "Version": program_version() if opts.add_version_to_infotext else None, "User": p.user if opts.add_user_name_to_info else None, } + for key, value in generation_params.items(): + try: + if isinstance(value, list): + generation_params[key] = value[index] + elif callable(value): + generation_params[key] = value(**locals()) + except Exception: + errors.report(f'Error creating infotext for key "{key}"', exc_info=True) + generation_params[key] = None + generation_params_text = ", ".join([k if k == v else f'{k}: {infotext_utils.quote(v)}' for k, v in generation_params.items() if v is not None]) - prompt_text = p.main_prompt if use_main_prompt else all_prompts[index] - negative_prompt_text = f"\nNegative prompt: {p.main_negative_prompt if use_main_prompt else all_negative_prompts[index]}" if all_negative_prompts[index] else "" + negative_prompt_text = f"\nNegative prompt: {negative_prompt}" if negative_prompt else "" return f"{prompt_text}{negative_prompt_text}\n{generation_params_text}".strip() @@ -782,7 +840,11 @@ def process_images(p: StableDiffusionProcessing) -> Processed: sd_models.apply_token_merging(p.sd_model, p.get_token_merging_ratio()) - res = process_images_inner(p) + # backwards compatibility, fix sampler and scheduler if invalid + sd_samplers.fix_p_invalid_sampler_and_scheduler(p) + + with profiling.Profiler(): + res = process_images_inner(p) finally: sd_models.apply_token_merging(p.sd_model, 0) @@ -822,6 +884,9 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if p.refiner_checkpoint_info is None: raise Exception(f'Could not find checkpoint with name {p.refiner_checkpoint}') + if hasattr(shared.sd_model, 'fix_dimensions'): + p.width, p.height = shared.sd_model.fix_dimensions(p.width, p.height) + p.sd_model_name = shared.sd_model.sd_checkpoint_info.name_for_extra p.sd_model_hash = shared.sd_model.sd_model_hash p.sd_vae_name = sd_vae.get_loaded_vae_name() @@ -830,6 +895,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: modules.sd_hijack.model_hijack.apply_circular(p.tiling) modules.sd_hijack.model_hijack.clear_comments() + p.fill_fields_from_opts() p.setup_prompts() if isinstance(seed, list): @@ -879,7 +945,8 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.seeds = p.all_seeds[n * p.batch_size:(n + 1) * p.batch_size] p.subseeds = p.all_subseeds[n * p.batch_size:(n + 1) * p.batch_size] - p.rng = rng.ImageRNG((opt_C, p.height // opt_f, p.width // opt_f), p.seeds, subseeds=p.subseeds, subseed_strength=p.subseed_strength, seed_resize_from_h=p.seed_resize_from_h, seed_resize_from_w=p.seed_resize_from_w) + latent_channels = getattr(shared.sd_model, 'latent_channels', opt_C) + p.rng = rng.ImageRNG((latent_channels, p.height // opt_f, p.width // opt_f), p.seeds, subseeds=p.subseeds, subseed_strength=p.subseed_strength, seed_resize_from_h=p.seed_resize_from_h, seed_resize_from_w=p.seed_resize_from_w) if p.scripts is not None: p.scripts.before_process_batch(p, batch_number=n, prompts=p.prompts, seeds=p.seeds, subseeds=p.subseeds) @@ -896,22 +963,22 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if p.scripts is not None: p.scripts.process_batch(p, batch_number=n, prompts=p.prompts, seeds=p.seeds, subseeds=p.subseeds) + p.setup_conds() + + p.extra_generation_params.update(model_hijack.extra_generation_params) + # params.txt should be saved after scripts.process_batch, since the # infotext could be modified by that callback # Example: a wildcard processed by process_batch sets an extra model # strength, which is saved as "Model Strength: 1.0" in the infotext - if n == 0: + if n == 0 and not cmd_opts.no_prompt_history: with open(os.path.join(paths.data_path, "params.txt"), "w", encoding="utf8") as file: processed = Processed(p, []) file.write(processed.infotext(p, 0)) - p.setup_conds() - for comment in model_hijack.comments: p.comment(comment) - p.extra_generation_params.update(model_hijack.extra_generation_params) - if p.n_iter > 1: shared.state.job = f"Batch {n+1} out of {p.n_iter}" @@ -928,6 +995,8 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if getattr(samples_ddim, 'already_decoded', False): x_samples_ddim = samples_ddim else: + devices.test_for_nans(samples_ddim, "unet") + if opts.sd_vae_decode_method != 'Full': p.extra_generation_params['VAE Decoder'] = opts.sd_vae_decode_method x_samples_ddim = decode_latent_batch(p.sd_model, samples_ddim, target_device=devices.cpu, check_for_nans=True) @@ -1106,6 +1175,7 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): hr_resize_y: int = 0 hr_checkpoint_name: str = None hr_sampler_name: str = None + hr_scheduler: str = None hr_prompt: str = '' hr_negative_prompt: str = '' force_task_id: str = None @@ -1194,11 +1264,21 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): if self.hr_sampler_name is not None and self.hr_sampler_name != self.sampler_name: self.extra_generation_params["Hires sampler"] = self.hr_sampler_name - if tuple(self.hr_prompt) != tuple(self.prompt): - self.extra_generation_params["Hires prompt"] = self.hr_prompt + def get_hr_prompt(p, index, prompt_text, **kwargs): + hr_prompt = p.all_hr_prompts[index] + return hr_prompt if hr_prompt != prompt_text else None - if tuple(self.hr_negative_prompt) != tuple(self.negative_prompt): - self.extra_generation_params["Hires negative prompt"] = self.hr_negative_prompt + def get_hr_negative_prompt(p, index, negative_prompt, **kwargs): + hr_negative_prompt = p.all_hr_negative_prompts[index] + return hr_negative_prompt if hr_negative_prompt != negative_prompt else None + + self.extra_generation_params["Hires prompt"] = get_hr_prompt + self.extra_generation_params["Hires negative prompt"] = get_hr_negative_prompt + + self.extra_generation_params["Hires schedule type"] = None # to be set in sd_samplers_kdiffusion.py + + if self.hr_scheduler is None: + self.hr_scheduler = self.scheduler self.latent_scale_mode = shared.latent_upscale_modes.get(self.hr_upscaler, None) if self.hr_upscaler is not None else shared.latent_upscale_modes.get(shared.latent_upscale_default_mode, "nearest") if self.enable_hr and self.latent_scale_mode is None: @@ -1254,6 +1334,15 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): # here we generate an image normally x = self.rng.next() + if self.scripts is not None: + self.scripts.process_before_every_sampling( + p=self, + x=x, + noise=x, + c=conditioning, + uc=unconditional_conditioning + ) + samples = self.sampler.sample(self, x, conditioning, unconditional_conditioning, image_conditioning=self.txt2img_image_conditioning(x)) del x @@ -1354,6 +1443,13 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): if self.scripts is not None: self.scripts.before_hr(self) + self.scripts.process_before_every_sampling( + p=self, + x=samples, + noise=noise, + c=self.hr_c, + uc=self.hr_uc, + ) samples = self.sampler.sample_img2img(self, samples, noise, self.hr_c, self.hr_uc, steps=self.hr_second_pass_steps or self.steps, image_conditioning=image_conditioning) @@ -1540,16 +1636,23 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): if self.inpaint_full_res: self.mask_for_overlay = image_mask mask = image_mask.convert('L') - crop_region = masking.get_crop_region(mask, self.inpaint_full_res_padding) - crop_region = masking.expand_crop_region(crop_region, self.width, self.height, mask.width, mask.height) - x1, y1, x2, y2 = crop_region - - mask = mask.crop(crop_region) - image_mask = images.resize_image(2, mask, self.width, self.height) - self.paste_to = (x1, y1, x2-x1, y2-y1) - - self.extra_generation_params["Inpaint area"] = "Only masked" - self.extra_generation_params["Masked area padding"] = self.inpaint_full_res_padding + crop_region = masking.get_crop_region_v2(mask, self.inpaint_full_res_padding) + if crop_region: + crop_region = masking.expand_crop_region(crop_region, self.width, self.height, mask.width, mask.height) + x1, y1, x2, y2 = crop_region + mask = mask.crop(crop_region) + image_mask = images.resize_image(2, mask, self.width, self.height) + self.paste_to = (x1, y1, x2-x1, y2-y1) + self.extra_generation_params["Inpaint area"] = "Only masked" + self.extra_generation_params["Masked area padding"] = self.inpaint_full_res_padding + else: + crop_region = None + image_mask = None + self.mask_for_overlay = None + self.inpaint_full_res = False + massage = 'Unable to perform "Inpaint Only mask" because mask is blank, switch to img2img mode.' + model_hijack.comments.append(massage) + logging.info(massage) else: image_mask = images.resize_image(self.resize_mode, image_mask, self.width, self.height) np_mask = np.array(image_mask) @@ -1577,6 +1680,8 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): image = images.resize_image(self.resize_mode, image, self.width, self.height) if image_mask is not None: + if self.mask_for_overlay.size != (image.width, image.height): + self.mask_for_overlay = images.resize_image(self.resize_mode, self.mask_for_overlay, image.width, image.height) image_masked = Image.new('RGBa', (image.width, image.height)) image_masked.paste(image.convert("RGBA").convert("RGBa"), mask=ImageOps.invert(self.mask_for_overlay.convert('L'))) @@ -1635,10 +1740,10 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): latmask = latmask[0] if self.mask_round: latmask = np.around(latmask) - latmask = np.tile(latmask[None], (4, 1, 1)) + latmask = np.tile(latmask[None], (self.init_latent.shape[1], 1, 1)) - self.mask = torch.asarray(1.0 - latmask).to(shared.device).type(self.sd_model.dtype) - self.nmask = torch.asarray(latmask).to(shared.device).type(self.sd_model.dtype) + self.mask = torch.asarray(1.0 - latmask).to(shared.device).type(devices.dtype) + self.nmask = torch.asarray(latmask).to(shared.device).type(devices.dtype) # this needs to be fixed to be done in sample() using actual seeds for batches if self.inpainting_fill == 2: @@ -1658,6 +1763,14 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): self.extra_generation_params["Noise multiplier"] = self.initial_noise_multiplier x *= self.initial_noise_multiplier + if self.scripts is not None: + self.scripts.process_before_every_sampling( + p=self, + x=self.init_latent, + noise=x, + c=conditioning, + uc=unconditional_conditioning + ) samples = self.sampler.sample_img2img(self, self.init_latent, x, conditioning, unconditional_conditioning, image_conditioning=self.image_conditioning) if self.mask is not None: diff --git a/modules/processing_scripts/comments.py b/modules/processing_scripts/comments.py index 638e39f2..cf81dfd8 100644 --- a/modules/processing_scripts/comments.py +++ b/modules/processing_scripts/comments.py @@ -26,6 +26,13 @@ class ScriptStripComments(scripts.Script): p.main_prompt = strip_comments(p.main_prompt) p.main_negative_prompt = strip_comments(p.main_negative_prompt) + if getattr(p, 'enable_hr', False): + p.all_hr_prompts = [strip_comments(x) for x in p.all_hr_prompts] + p.all_hr_negative_prompts = [strip_comments(x) for x in p.all_hr_negative_prompts] + + p.hr_prompt = strip_comments(p.hr_prompt) + p.hr_negative_prompt = strip_comments(p.hr_negative_prompt) + def before_token_counter(params: script_callbacks.BeforeTokenCounterParams): if not shared.opts.enable_prompt_comments: diff --git a/modules/processing_scripts/sampler.py b/modules/processing_scripts/sampler.py new file mode 100644 index 00000000..5d50a162 --- /dev/null +++ b/modules/processing_scripts/sampler.py @@ -0,0 +1,45 @@ +import gradio as gr + +from modules import scripts, sd_samplers, sd_schedulers, shared +from modules.infotext_utils import PasteField +from modules.ui_components import FormRow, FormGroup + + +class ScriptSampler(scripts.ScriptBuiltinUI): + section = "sampler" + + def __init__(self): + self.steps = None + self.sampler_name = None + self.scheduler = None + + def title(self): + return "Sampler" + + def ui(self, is_img2img): + sampler_names = [x.name for x in sd_samplers.visible_samplers()] + scheduler_names = [x.label for x in sd_schedulers.schedulers] + + if shared.opts.samplers_in_dropdown: + with FormRow(elem_id=f"sampler_selection_{self.tabname}"): + self.sampler_name = gr.Dropdown(label='Sampling method', elem_id=f"{self.tabname}_sampling", choices=sampler_names, value=sampler_names[0]) + self.scheduler = gr.Dropdown(label='Schedule type', elem_id=f"{self.tabname}_scheduler", choices=scheduler_names, value=scheduler_names[0]) + self.steps = gr.Slider(minimum=1, maximum=150, step=1, elem_id=f"{self.tabname}_steps", label="Sampling steps", value=20) + else: + with FormGroup(elem_id=f"sampler_selection_{self.tabname}"): + self.steps = gr.Slider(minimum=1, maximum=150, step=1, elem_id=f"{self.tabname}_steps", label="Sampling steps", value=20) + self.sampler_name = gr.Radio(label='Sampling method', elem_id=f"{self.tabname}_sampling", choices=sampler_names, value=sampler_names[0]) + self.scheduler = gr.Dropdown(label='Schedule type', elem_id=f"{self.tabname}_scheduler", choices=scheduler_names, value=scheduler_names[0]) + + self.infotext_fields = [ + PasteField(self.steps, "Steps", api="steps"), + PasteField(self.sampler_name, sd_samplers.get_sampler_from_infotext, api="sampler_name"), + PasteField(self.scheduler, sd_samplers.get_scheduler_from_infotext, api="scheduler"), + ] + + return self.steps, self.sampler_name, self.scheduler + + def setup(self, p, steps, sampler_name, scheduler): + p.steps = steps + p.sampler_name = sampler_name + p.scheduler = scheduler diff --git a/modules/profiling.py b/modules/profiling.py new file mode 100644 index 00000000..95b59f71 --- /dev/null +++ b/modules/profiling.py @@ -0,0 +1,46 @@ +import torch + +from modules import shared, ui_gradio_extensions + + +class Profiler: + def __init__(self): + if not shared.opts.profiling_enable: + self.profiler = None + return + + activities = [] + if "CPU" in shared.opts.profiling_activities: + activities.append(torch.profiler.ProfilerActivity.CPU) + if "CUDA" in shared.opts.profiling_activities: + activities.append(torch.profiler.ProfilerActivity.CUDA) + + if not activities: + self.profiler = None + return + + self.profiler = torch.profiler.profile( + activities=activities, + record_shapes=shared.opts.profiling_record_shapes, + profile_memory=shared.opts.profiling_profile_memory, + with_stack=shared.opts.profiling_with_stack + ) + + def __enter__(self): + if self.profiler: + self.profiler.__enter__() + + return self + + def __exit__(self, exc_type, exc, exc_tb): + if self.profiler: + shared.state.textinfo = "Finishing profile..." + + self.profiler.__exit__(exc_type, exc, exc_tb) + + self.profiler.export_chrome_trace(shared.opts.profiling_filename) + + +def webpath(): + return ui_gradio_extensions.webpath(shared.opts.profiling_filename) + diff --git a/modules/prompt_parser.py b/modules/prompt_parser.py index cba13455..4e393d28 100644 --- a/modules/prompt_parser.py +++ b/modules/prompt_parser.py @@ -268,7 +268,7 @@ def get_multicond_learned_conditioning(model, prompts, steps, hires_steps=None, class DictWithShape(dict): - def __init__(self, x, shape): + def __init__(self, x, shape=None): super().__init__() self.update(x) diff --git a/modules/rng.py b/modules/rng.py index 8934d39b..5390d1bb 100644 --- a/modules/rng.py +++ b/modules/rng.py @@ -34,7 +34,7 @@ def randn_local(seed, shape): def randn_like(x): - """Generate a tensor with random numbers from a normal distribution using the previously initialized genrator. + """Generate a tensor with random numbers from a normal distribution using the previously initialized generator. Use either randn() or manual_seed() to initialize the generator.""" @@ -48,7 +48,7 @@ def randn_like(x): def randn_without_seed(shape, generator=None): - """Generate a tensor with random numbers from a normal distribution using the previously initialized genrator. + """Generate a tensor with random numbers from a normal distribution using the previously initialized generator. Use either randn() or manual_seed() to initialize the generator.""" diff --git a/modules/safe.py b/modules/safe.py index b1d08a79..af019ffd 100644 --- a/modules/safe.py +++ b/modules/safe.py @@ -64,8 +64,8 @@ class RestrictedUnpickler(pickle.Unpickler): raise Exception(f"global '{module}/{name}' is forbidden") -# Regular expression that accepts 'dirname/version', 'dirname/data.pkl', and 'dirname/data/' -allowed_zip_names_re = re.compile(r"^([^/]+)/((data/\d+)|version|(data\.pkl))$") +# Regular expression that accepts 'dirname/version', 'dirname/byteorder', 'dirname/data.pkl', '.data/serialization_id', and 'dirname/data/' +allowed_zip_names_re = re.compile(r"^([^/]+)/((data/\d+)|version|byteorder|.data/serialization_id|(data\.pkl))$") data_pkl_re = re.compile(r"^([^/]+)/data\.pkl$") def check_zip_filenames(filename, names): diff --git a/modules/script_callbacks.py b/modules/script_callbacks.py index 08bc5256..9059d4d9 100644 --- a/modules/script_callbacks.py +++ b/modules/script_callbacks.py @@ -1,13 +1,14 @@ +from __future__ import annotations + import dataclasses import inspect import os -from collections import namedtuple from typing import Optional, Any from fastapi import FastAPI from gradio import Blocks -from modules import errors, timer +from modules import errors, timer, extensions, shared, util def report_exception(c, job): @@ -116,7 +117,105 @@ class BeforeTokenCounterParams: is_positive: bool = True -ScriptCallback = namedtuple("ScriptCallback", ["script", "callback"]) +@dataclasses.dataclass +class ScriptCallback: + script: str + callback: any + name: str = "unnamed" + + +def add_callback(callbacks, fun, *, name=None, category='unknown', filename=None): + if filename is None: + stack = [x for x in inspect.stack() if x.filename != __file__] + filename = stack[0].filename if stack else 'unknown file' + + extension = extensions.find_extension(filename) + extension_name = extension.canonical_name if extension else 'base' + + callback_name = f"{extension_name}/{os.path.basename(filename)}/{category}" + if name is not None: + callback_name += f'/{name}' + + unique_callback_name = callback_name + for index in range(1000): + existing = any(x.name == unique_callback_name for x in callbacks) + if not existing: + break + + unique_callback_name = f'{callback_name}-{index+1}' + + callbacks.append(ScriptCallback(filename, fun, unique_callback_name)) + + +def sort_callbacks(category, unordered_callbacks, *, enable_user_sort=True): + callbacks = unordered_callbacks.copy() + callback_lookup = {x.name: x for x in callbacks} + dependencies = {} + + order_instructions = {} + for extension in extensions.extensions: + for order_instruction in extension.metadata.list_callback_order_instructions(): + if order_instruction.name in callback_lookup: + if order_instruction.name not in order_instructions: + order_instructions[order_instruction.name] = [] + + order_instructions[order_instruction.name].append(order_instruction) + + if order_instructions: + for callback in callbacks: + dependencies[callback.name] = [] + + for callback in callbacks: + for order_instruction in order_instructions.get(callback.name, []): + for after in order_instruction.after: + if after not in callback_lookup: + continue + + dependencies[callback.name].append(after) + + for before in order_instruction.before: + if before not in callback_lookup: + continue + + dependencies[before].append(callback.name) + + sorted_names = util.topological_sort(dependencies) + callbacks = [callback_lookup[x] for x in sorted_names] + + if enable_user_sort: + for name in reversed(getattr(shared.opts, 'prioritized_callbacks_' + category, [])): + index = next((i for i, callback in enumerate(callbacks) if callback.name == name), None) + if index is not None: + callbacks.insert(0, callbacks.pop(index)) + + return callbacks + + +def ordered_callbacks(category, unordered_callbacks=None, *, enable_user_sort=True): + if unordered_callbacks is None: + unordered_callbacks = callback_map.get('callbacks_' + category, []) + + if not enable_user_sort: + return sort_callbacks(category, unordered_callbacks, enable_user_sort=False) + + callbacks = ordered_callbacks_map.get(category) + if callbacks is not None and len(callbacks) == len(unordered_callbacks): + return callbacks + + callbacks = sort_callbacks(category, unordered_callbacks) + + ordered_callbacks_map[category] = callbacks + return callbacks + + +def enumerate_callbacks(): + for category, callbacks in callback_map.items(): + if category.startswith('callbacks_'): + category = category[10:] + + yield category, callbacks + + callback_map = dict( callbacks_app_started=[], callbacks_model_loaded=[], @@ -141,14 +240,18 @@ callback_map = dict( callbacks_before_token_counter=[], ) +ordered_callbacks_map = {} + def clear_callbacks(): for callback_list in callback_map.values(): callback_list.clear() + ordered_callbacks_map.clear() + def app_started_callback(demo: Optional[Blocks], app: FastAPI): - for c in callback_map['callbacks_app_started']: + for c in ordered_callbacks('app_started'): try: c.callback(demo, app) timer.startup_timer.record(os.path.basename(c.script)) @@ -157,7 +260,7 @@ def app_started_callback(demo: Optional[Blocks], app: FastAPI): def app_reload_callback(): - for c in callback_map['callbacks_on_reload']: + for c in ordered_callbacks('on_reload'): try: c.callback() except Exception: @@ -165,7 +268,7 @@ def app_reload_callback(): def model_loaded_callback(sd_model): - for c in callback_map['callbacks_model_loaded']: + for c in ordered_callbacks('model_loaded'): try: c.callback(sd_model) except Exception: @@ -175,7 +278,7 @@ def model_loaded_callback(sd_model): def ui_tabs_callback(): res = [] - for c in callback_map['callbacks_ui_tabs']: + for c in ordered_callbacks('ui_tabs'): try: res += c.callback() or [] except Exception: @@ -185,7 +288,7 @@ def ui_tabs_callback(): def ui_train_tabs_callback(params: UiTrainTabParams): - for c in callback_map['callbacks_ui_train_tabs']: + for c in ordered_callbacks('ui_train_tabs'): try: c.callback(params) except Exception: @@ -193,7 +296,7 @@ def ui_train_tabs_callback(params: UiTrainTabParams): def ui_settings_callback(): - for c in callback_map['callbacks_ui_settings']: + for c in ordered_callbacks('ui_settings'): try: c.callback() except Exception: @@ -201,7 +304,7 @@ def ui_settings_callback(): def before_image_saved_callback(params: ImageSaveParams): - for c in callback_map['callbacks_before_image_saved']: + for c in ordered_callbacks('before_image_saved'): try: c.callback(params) except Exception: @@ -209,7 +312,7 @@ def before_image_saved_callback(params: ImageSaveParams): def image_saved_callback(params: ImageSaveParams): - for c in callback_map['callbacks_image_saved']: + for c in ordered_callbacks('image_saved'): try: c.callback(params) except Exception: @@ -217,7 +320,7 @@ def image_saved_callback(params: ImageSaveParams): def extra_noise_callback(params: ExtraNoiseParams): - for c in callback_map['callbacks_extra_noise']: + for c in ordered_callbacks('extra_noise'): try: c.callback(params) except Exception: @@ -225,7 +328,7 @@ def extra_noise_callback(params: ExtraNoiseParams): def cfg_denoiser_callback(params: CFGDenoiserParams): - for c in callback_map['callbacks_cfg_denoiser']: + for c in ordered_callbacks('cfg_denoiser'): try: c.callback(params) except Exception: @@ -233,7 +336,7 @@ def cfg_denoiser_callback(params: CFGDenoiserParams): def cfg_denoised_callback(params: CFGDenoisedParams): - for c in callback_map['callbacks_cfg_denoised']: + for c in ordered_callbacks('cfg_denoised'): try: c.callback(params) except Exception: @@ -241,7 +344,7 @@ def cfg_denoised_callback(params: CFGDenoisedParams): def cfg_after_cfg_callback(params: AfterCFGCallbackParams): - for c in callback_map['callbacks_cfg_after_cfg']: + for c in ordered_callbacks('cfg_after_cfg'): try: c.callback(params) except Exception: @@ -249,7 +352,7 @@ def cfg_after_cfg_callback(params: AfterCFGCallbackParams): def before_component_callback(component, **kwargs): - for c in callback_map['callbacks_before_component']: + for c in ordered_callbacks('before_component'): try: c.callback(component, **kwargs) except Exception: @@ -257,7 +360,7 @@ def before_component_callback(component, **kwargs): def after_component_callback(component, **kwargs): - for c in callback_map['callbacks_after_component']: + for c in ordered_callbacks('after_component'): try: c.callback(component, **kwargs) except Exception: @@ -265,7 +368,7 @@ def after_component_callback(component, **kwargs): def image_grid_callback(params: ImageGridLoopParams): - for c in callback_map['callbacks_image_grid']: + for c in ordered_callbacks('image_grid'): try: c.callback(params) except Exception: @@ -273,7 +376,7 @@ def image_grid_callback(params: ImageGridLoopParams): def infotext_pasted_callback(infotext: str, params: dict[str, Any]): - for c in callback_map['callbacks_infotext_pasted']: + for c in ordered_callbacks('infotext_pasted'): try: c.callback(infotext, params) except Exception: @@ -281,7 +384,7 @@ def infotext_pasted_callback(infotext: str, params: dict[str, Any]): def script_unloaded_callback(): - for c in reversed(callback_map['callbacks_script_unloaded']): + for c in reversed(ordered_callbacks('script_unloaded')): try: c.callback() except Exception: @@ -289,7 +392,7 @@ def script_unloaded_callback(): def before_ui_callback(): - for c in reversed(callback_map['callbacks_before_ui']): + for c in reversed(ordered_callbacks('before_ui')): try: c.callback() except Exception: @@ -299,7 +402,7 @@ def before_ui_callback(): def list_optimizers_callback(): res = [] - for c in callback_map['callbacks_list_optimizers']: + for c in ordered_callbacks('list_optimizers'): try: c.callback(res) except Exception: @@ -311,7 +414,7 @@ def list_optimizers_callback(): def list_unets_callback(): res = [] - for c in callback_map['callbacks_list_unets']: + for c in ordered_callbacks('list_unets'): try: c.callback(res) except Exception: @@ -321,20 +424,13 @@ def list_unets_callback(): def before_token_counter_callback(params: BeforeTokenCounterParams): - for c in callback_map['callbacks_before_token_counter']: + for c in ordered_callbacks('before_token_counter'): try: c.callback(params) except Exception: report_exception(c, 'before_token_counter') -def add_callback(callbacks, fun): - stack = [x for x in inspect.stack() if x.filename != __file__] - filename = stack[0].filename if stack else 'unknown file' - - callbacks.append(ScriptCallback(filename, fun)) - - def remove_current_script_callbacks(): stack = [x for x in inspect.stack() if x.filename != __file__] filename = stack[0].filename if stack else 'unknown file' @@ -343,32 +439,38 @@ def remove_current_script_callbacks(): for callback_list in callback_map.values(): for callback_to_remove in [cb for cb in callback_list if cb.script == filename]: callback_list.remove(callback_to_remove) + for ordered_callbacks_list in ordered_callbacks_map.values(): + for callback_to_remove in [cb for cb in ordered_callbacks_list if cb.script == filename]: + ordered_callbacks_list.remove(callback_to_remove) def remove_callbacks_for_function(callback_func): for callback_list in callback_map.values(): for callback_to_remove in [cb for cb in callback_list if cb.callback == callback_func]: callback_list.remove(callback_to_remove) + for ordered_callback_list in ordered_callbacks_map.values(): + for callback_to_remove in [cb for cb in ordered_callback_list if cb.callback == callback_func]: + ordered_callback_list.remove(callback_to_remove) -def on_app_started(callback): +def on_app_started(callback, *, name=None): """register a function to be called when the webui started, the gradio `Block` component and fastapi `FastAPI` object are passed as the arguments""" - add_callback(callback_map['callbacks_app_started'], callback) + add_callback(callback_map['callbacks_app_started'], callback, name=name, category='app_started') -def on_before_reload(callback): +def on_before_reload(callback, *, name=None): """register a function to be called just before the server reloads.""" - add_callback(callback_map['callbacks_on_reload'], callback) + add_callback(callback_map['callbacks_on_reload'], callback, name=name, category='on_reload') -def on_model_loaded(callback): +def on_model_loaded(callback, *, name=None): """register a function to be called when the stable diffusion model is created; the model is passed as an argument; this function is also called when the script is reloaded. """ - add_callback(callback_map['callbacks_model_loaded'], callback) + add_callback(callback_map['callbacks_model_loaded'], callback, name=name, category='model_loaded') -def on_ui_tabs(callback): +def on_ui_tabs(callback, *, name=None): """register a function to be called when the UI is creating new tabs. The function must either return a None, which means no new tabs to be added, or a list, where each element is a tuple: @@ -378,71 +480,71 @@ def on_ui_tabs(callback): title is tab text displayed to user in the UI elem_id is HTML id for the tab """ - add_callback(callback_map['callbacks_ui_tabs'], callback) + add_callback(callback_map['callbacks_ui_tabs'], callback, name=name, category='ui_tabs') -def on_ui_train_tabs(callback): +def on_ui_train_tabs(callback, *, name=None): """register a function to be called when the UI is creating new tabs for the train tab. Create your new tabs with gr.Tab. """ - add_callback(callback_map['callbacks_ui_train_tabs'], callback) + add_callback(callback_map['callbacks_ui_train_tabs'], callback, name=name, category='ui_train_tabs') -def on_ui_settings(callback): +def on_ui_settings(callback, *, name=None): """register a function to be called before UI settings are populated; add your settings by using shared.opts.add_option(shared.OptionInfo(...)) """ - add_callback(callback_map['callbacks_ui_settings'], callback) + add_callback(callback_map['callbacks_ui_settings'], callback, name=name, category='ui_settings') -def on_before_image_saved(callback): +def on_before_image_saved(callback, *, name=None): """register a function to be called before an image is saved to a file. The callback is called with one argument: - params: ImageSaveParams - parameters the image is to be saved with. You can change fields in this object. """ - add_callback(callback_map['callbacks_before_image_saved'], callback) + add_callback(callback_map['callbacks_before_image_saved'], callback, name=name, category='before_image_saved') -def on_image_saved(callback): +def on_image_saved(callback, *, name=None): """register a function to be called after an image is saved to a file. The callback is called with one argument: - params: ImageSaveParams - parameters the image was saved with. Changing fields in this object does nothing. """ - add_callback(callback_map['callbacks_image_saved'], callback) + add_callback(callback_map['callbacks_image_saved'], callback, name=name, category='image_saved') -def on_extra_noise(callback): +def on_extra_noise(callback, *, name=None): """register a function to be called before adding extra noise in img2img or hires fix; The callback is called with one argument: - params: ExtraNoiseParams - contains noise determined by seed and latent representation of image """ - add_callback(callback_map['callbacks_extra_noise'], callback) + add_callback(callback_map['callbacks_extra_noise'], callback, name=name, category='extra_noise') -def on_cfg_denoiser(callback): +def on_cfg_denoiser(callback, *, name=None): """register a function to be called in the kdiffussion cfg_denoiser method after building the inner model inputs. The callback is called with one argument: - params: CFGDenoiserParams - parameters to be passed to the inner model and sampling state details. """ - add_callback(callback_map['callbacks_cfg_denoiser'], callback) + add_callback(callback_map['callbacks_cfg_denoiser'], callback, name=name, category='cfg_denoiser') -def on_cfg_denoised(callback): +def on_cfg_denoised(callback, *, name=None): """register a function to be called in the kdiffussion cfg_denoiser method after building the inner model inputs. The callback is called with one argument: - params: CFGDenoisedParams - parameters to be passed to the inner model and sampling state details. """ - add_callback(callback_map['callbacks_cfg_denoised'], callback) + add_callback(callback_map['callbacks_cfg_denoised'], callback, name=name, category='cfg_denoised') -def on_cfg_after_cfg(callback): +def on_cfg_after_cfg(callback, *, name=None): """register a function to be called in the kdiffussion cfg_denoiser method after cfg calculations are completed. The callback is called with one argument: - params: AfterCFGCallbackParams - parameters to be passed to the script for post-processing after cfg calculation. """ - add_callback(callback_map['callbacks_cfg_after_cfg'], callback) + add_callback(callback_map['callbacks_cfg_after_cfg'], callback, name=name, category='cfg_after_cfg') -def on_before_component(callback): +def on_before_component(callback, *, name=None): """register a function to be called before a component is created. The callback is called with arguments: - component - gradio component that is about to be created. @@ -451,61 +553,61 @@ def on_before_component(callback): Use elem_id/label fields of kwargs to figure out which component it is. This can be useful to inject your own components somewhere in the middle of vanilla UI. """ - add_callback(callback_map['callbacks_before_component'], callback) + add_callback(callback_map['callbacks_before_component'], callback, name=name, category='before_component') -def on_after_component(callback): +def on_after_component(callback, *, name=None): """register a function to be called after a component is created. See on_before_component for more.""" - add_callback(callback_map['callbacks_after_component'], callback) + add_callback(callback_map['callbacks_after_component'], callback, name=name, category='after_component') -def on_image_grid(callback): +def on_image_grid(callback, *, name=None): """register a function to be called before making an image grid. The callback is called with one argument: - params: ImageGridLoopParams - parameters to be used for grid creation. Can be modified. """ - add_callback(callback_map['callbacks_image_grid'], callback) + add_callback(callback_map['callbacks_image_grid'], callback, name=name, category='image_grid') -def on_infotext_pasted(callback): +def on_infotext_pasted(callback, *, name=None): """register a function to be called before applying an infotext. The callback is called with two arguments: - infotext: str - raw infotext. - result: dict[str, any] - parsed infotext parameters. """ - add_callback(callback_map['callbacks_infotext_pasted'], callback) + add_callback(callback_map['callbacks_infotext_pasted'], callback, name=name, category='infotext_pasted') -def on_script_unloaded(callback): +def on_script_unloaded(callback, *, name=None): """register a function to be called before the script is unloaded. Any hooks/hijacks/monkeying about that the script did should be reverted here""" - add_callback(callback_map['callbacks_script_unloaded'], callback) + add_callback(callback_map['callbacks_script_unloaded'], callback, name=name, category='script_unloaded') -def on_before_ui(callback): +def on_before_ui(callback, *, name=None): """register a function to be called before the UI is created.""" - add_callback(callback_map['callbacks_before_ui'], callback) + add_callback(callback_map['callbacks_before_ui'], callback, name=name, category='before_ui') -def on_list_optimizers(callback): +def on_list_optimizers(callback, *, name=None): """register a function to be called when UI is making a list of cross attention optimization options. The function will be called with one argument, a list, and shall add objects of type modules.sd_hijack_optimizations.SdOptimization to it.""" - add_callback(callback_map['callbacks_list_optimizers'], callback) + add_callback(callback_map['callbacks_list_optimizers'], callback, name=name, category='list_optimizers') -def on_list_unets(callback): +def on_list_unets(callback, *, name=None): """register a function to be called when UI is making a list of alternative options for unet. The function will be called with one argument, a list, and shall add objects of type modules.sd_unet.SdUnetOption to it.""" - add_callback(callback_map['callbacks_list_unets'], callback) + add_callback(callback_map['callbacks_list_unets'], callback, name=name, category='list_unets') -def on_before_token_counter(callback): +def on_before_token_counter(callback, *, name=None): """register a function to be called when UI is counting tokens for a prompt. The function will be called with one argument of type BeforeTokenCounterParams, and should modify its fields if necessary.""" - add_callback(callback_map['callbacks_before_token_counter'], callback) + add_callback(callback_map['callbacks_before_token_counter'], callback, name=name, category='before_token_counter') diff --git a/modules/script_loading.py b/modules/script_loading.py index 0d55f193..cccb3096 100644 --- a/modules/script_loading.py +++ b/modules/script_loading.py @@ -4,11 +4,15 @@ import importlib.util from modules import errors +loaded_scripts = {} + + def load_module(path): module_spec = importlib.util.spec_from_file_location(os.path.basename(path), path) module = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) + loaded_scripts[path] = module return module diff --git a/modules/scripts.py b/modules/scripts.py index 94690a22..8eca396b 100644 --- a/modules/scripts.py +++ b/modules/scripts.py @@ -7,7 +7,9 @@ from dataclasses import dataclass import gradio as gr -from modules import shared, paths, script_callbacks, extensions, script_loading, scripts_postprocessing, errors, timer +from modules import shared, paths, script_callbacks, extensions, script_loading, scripts_postprocessing, errors, timer, util + +topological_sort = util.topological_sort AlwaysVisible = object() @@ -92,7 +94,7 @@ class Script: """If true, the script setup will only be run in Gradio UI, not in API""" controls = None - """A list of controls retured by the ui().""" + """A list of controls returned by the ui().""" def title(self): """this function should return the title of the script. This is what will be displayed in the dropdown menu.""" @@ -109,7 +111,7 @@ class Script: def show(self, is_img2img): """ - is_img2img is True if this function is called for the img2img interface, and Fasle otherwise + is_img2img is True if this function is called for the img2img interface, and False otherwise This function should return: - False if the script should not be shown in UI at all @@ -138,7 +140,6 @@ class Script: """ pass - def before_process(self, p, *args): """ This function is called very early during processing begins for AlwaysVisible scripts. @@ -186,6 +187,13 @@ class Script: """ pass + def process_before_every_sampling(self, p, *args, **kwargs): + """ + Similar to process(), called before every sampling. + If you use high-res fix, this will be called two times. + """ + pass + def process_batch(self, p, *args, **kwargs): """ Same as process(), but called for every batch. @@ -351,6 +359,9 @@ class ScriptBuiltinUI(Script): return f'{tabname}{item_id}' + def show(self, is_img2img): + return AlwaysVisible + current_basedir = paths.script_path @@ -369,29 +380,6 @@ scripts_data = [] postprocessing_scripts_data = [] ScriptClassData = namedtuple("ScriptClassData", ["script_class", "path", "basedir", "module"]) -def topological_sort(dependencies): - """Accepts a dictionary mapping name to its dependencies, returns a list of names ordered according to dependencies. - Ignores errors relating to missing dependeencies or circular dependencies - """ - - visited = {} - result = [] - - def inner(name): - visited[name] = True - - for dep in dependencies.get(name, []): - if dep in dependencies and dep not in visited: - inner(dep) - - result.append(name) - - for depname in dependencies: - if depname not in visited: - inner(depname) - - return result - @dataclass class ScriptWithDependencies: @@ -562,6 +550,25 @@ class ScriptRunner: self.paste_field_names = [] self.inputs = [None] + self.callback_map = {} + self.callback_names = [ + 'before_process', + 'process', + 'before_process_batch', + 'after_extra_networks_activate', + 'process_batch', + 'postprocess', + 'postprocess_batch', + 'postprocess_batch_list', + 'post_sample', + 'on_mask_blend', + 'postprocess_image', + 'postprocess_maskoverlay', + 'postprocess_image_after_composite', + 'before_component', + 'after_component', + ] + self.on_before_component_elem_id = {} """dict of callbacks to be called before an element is created; key=elem_id, value=list of callbacks""" @@ -600,6 +607,8 @@ class ScriptRunner: self.scripts.append(script) self.selectable_scripts.append(script) + self.callback_map.clear() + self.apply_on_before_component_callbacks() def apply_on_before_component_callbacks(self): @@ -737,12 +746,17 @@ class ScriptRunner: def onload_script_visibility(params): title = params.get('Script', None) if title: - title_index = self.titles.index(title) - visibility = title_index == self.script_load_ctr - self.script_load_ctr = (self.script_load_ctr + 1) % len(self.titles) - return gr.update(visible=visibility) - else: - return gr.update(visible=False) + try: + title_index = self.titles.index(title) + visibility = title_index == self.script_load_ctr + self.script_load_ctr = (self.script_load_ctr + 1) % len(self.titles) + return gr.update(visible=visibility) + except ValueError: + params['Script'] = None + massage = f'Cannot find Script: "{title}"' + print(massage) + gr.Warning(massage) + return gr.update(visible=False) self.infotext_fields.append((dropdown, lambda x: gr.update(value=x.get('Script', 'None')))) self.infotext_fields.extend([(script.group, onload_script_visibility) for script in self.selectable_scripts]) @@ -769,8 +783,42 @@ class ScriptRunner: return processed + def list_scripts_for_method(self, method_name): + if method_name in ('before_component', 'after_component'): + return self.scripts + else: + return self.alwayson_scripts + + def create_ordered_callbacks_list(self, method_name, *, enable_user_sort=True): + script_list = self.list_scripts_for_method(method_name) + category = f'script_{method_name}' + callbacks = [] + + for script in script_list: + if getattr(script.__class__, method_name, None) == getattr(Script, method_name, None): + continue + + script_callbacks.add_callback(callbacks, script, category=category, name=script.__class__.__name__, filename=script.filename) + + return script_callbacks.sort_callbacks(category, callbacks, enable_user_sort=enable_user_sort) + + def ordered_callbacks(self, method_name, *, enable_user_sort=True): + script_list = self.list_scripts_for_method(method_name) + category = f'script_{method_name}' + + scrpts_len, callbacks = self.callback_map.get(category, (-1, None)) + + if callbacks is None or scrpts_len != len(script_list): + callbacks = self.create_ordered_callbacks_list(method_name, enable_user_sort=enable_user_sort) + self.callback_map[category] = len(script_list), callbacks + + return callbacks + + def ordered_scripts(self, method_name): + return [x.callback for x in self.ordered_callbacks(method_name)] + def before_process(self, p): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('before_process'): try: script_args = p.script_args[script.args_from:script.args_to] script.before_process(p, *script_args) @@ -778,15 +826,23 @@ class ScriptRunner: errors.report(f"Error running before_process: {script.filename}", exc_info=True) def process(self, p): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('process'): try: script_args = p.script_args[script.args_from:script.args_to] script.process(p, *script_args) except Exception: errors.report(f"Error running process: {script.filename}", exc_info=True) + def process_before_every_sampling(self, p, **kwargs): + for script in self.ordered_scripts('process_before_every_sampling'): + try: + script_args = p.script_args[script.args_from:script.args_to] + script.process_before_every_sampling(p, *script_args, **kwargs) + except Exception: + errors.report(f"Error running process_before_every_sampling: {script.filename}", exc_info=True) + def before_process_batch(self, p, **kwargs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('before_process_batch'): try: script_args = p.script_args[script.args_from:script.args_to] script.before_process_batch(p, *script_args, **kwargs) @@ -794,7 +850,7 @@ class ScriptRunner: errors.report(f"Error running before_process_batch: {script.filename}", exc_info=True) def after_extra_networks_activate(self, p, **kwargs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('after_extra_networks_activate'): try: script_args = p.script_args[script.args_from:script.args_to] script.after_extra_networks_activate(p, *script_args, **kwargs) @@ -802,7 +858,7 @@ class ScriptRunner: errors.report(f"Error running after_extra_networks_activate: {script.filename}", exc_info=True) def process_batch(self, p, **kwargs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('process_batch'): try: script_args = p.script_args[script.args_from:script.args_to] script.process_batch(p, *script_args, **kwargs) @@ -810,7 +866,7 @@ class ScriptRunner: errors.report(f"Error running process_batch: {script.filename}", exc_info=True) def postprocess(self, p, processed): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('postprocess'): try: script_args = p.script_args[script.args_from:script.args_to] script.postprocess(p, processed, *script_args) @@ -818,7 +874,7 @@ class ScriptRunner: errors.report(f"Error running postprocess: {script.filename}", exc_info=True) def postprocess_batch(self, p, images, **kwargs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('postprocess_batch'): try: script_args = p.script_args[script.args_from:script.args_to] script.postprocess_batch(p, *script_args, images=images, **kwargs) @@ -826,7 +882,7 @@ class ScriptRunner: errors.report(f"Error running postprocess_batch: {script.filename}", exc_info=True) def postprocess_batch_list(self, p, pp: PostprocessBatchListArgs, **kwargs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('postprocess_batch_list'): try: script_args = p.script_args[script.args_from:script.args_to] script.postprocess_batch_list(p, pp, *script_args, **kwargs) @@ -834,7 +890,7 @@ class ScriptRunner: errors.report(f"Error running postprocess_batch_list: {script.filename}", exc_info=True) def post_sample(self, p, ps: PostSampleArgs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('post_sample'): try: script_args = p.script_args[script.args_from:script.args_to] script.post_sample(p, ps, *script_args) @@ -842,7 +898,7 @@ class ScriptRunner: errors.report(f"Error running post_sample: {script.filename}", exc_info=True) def on_mask_blend(self, p, mba: MaskBlendArgs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('on_mask_blend'): try: script_args = p.script_args[script.args_from:script.args_to] script.on_mask_blend(p, mba, *script_args) @@ -850,7 +906,7 @@ class ScriptRunner: errors.report(f"Error running post_sample: {script.filename}", exc_info=True) def postprocess_image(self, p, pp: PostprocessImageArgs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('postprocess_image'): try: script_args = p.script_args[script.args_from:script.args_to] script.postprocess_image(p, pp, *script_args) @@ -858,7 +914,7 @@ class ScriptRunner: errors.report(f"Error running postprocess_image: {script.filename}", exc_info=True) def postprocess_maskoverlay(self, p, ppmo: PostProcessMaskOverlayArgs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('postprocess_maskoverlay'): try: script_args = p.script_args[script.args_from:script.args_to] script.postprocess_maskoverlay(p, ppmo, *script_args) @@ -866,7 +922,7 @@ class ScriptRunner: errors.report(f"Error running postprocess_image: {script.filename}", exc_info=True) def postprocess_image_after_composite(self, p, pp: PostprocessImageArgs): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('postprocess_image_after_composite'): try: script_args = p.script_args[script.args_from:script.args_to] script.postprocess_image_after_composite(p, pp, *script_args) @@ -880,7 +936,7 @@ class ScriptRunner: except Exception: errors.report(f"Error running on_before_component: {script.filename}", exc_info=True) - for script in self.scripts: + for script in self.ordered_scripts('before_component'): try: script.before_component(component, **kwargs) except Exception: @@ -893,7 +949,7 @@ class ScriptRunner: except Exception: errors.report(f"Error running on_after_component: {script.filename}", exc_info=True) - for script in self.scripts: + for script in self.ordered_scripts('after_component'): try: script.after_component(component, **kwargs) except Exception: @@ -921,7 +977,7 @@ class ScriptRunner: self.scripts[si].args_to = args_to def before_hr(self, p): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('before_hr'): try: script_args = p.script_args[script.args_from:script.args_to] script.before_hr(p, *script_args) @@ -929,7 +985,7 @@ class ScriptRunner: errors.report(f"Error running before_hr: {script.filename}", exc_info=True) def setup_scrips(self, p, *, is_ui=True): - for script in self.alwayson_scripts: + for script in self.ordered_scripts('setup'): if not is_ui and script.setup_for_ui_only: continue diff --git a/modules/scripts_postprocessing.py b/modules/scripts_postprocessing.py index 901cad08..4b3b7afd 100644 --- a/modules/scripts_postprocessing.py +++ b/modules/scripts_postprocessing.py @@ -143,6 +143,7 @@ class ScriptPostprocessingRunner: self.initialize_scripts(modules.scripts.postprocessing_scripts_data) scripts_order = shared.opts.postprocessing_operation_order + scripts_filter_out = set(shared.opts.postprocessing_disable_in_extras) def script_score(name): for i, possible_match in enumerate(scripts_order): @@ -151,9 +152,10 @@ class ScriptPostprocessingRunner: return len(self.scripts) - script_scores = {script.name: (script_score(script.name), script.order, script.name, original_index) for original_index, script in enumerate(self.scripts)} + filtered_scripts = [script for script in self.scripts if script.name not in scripts_filter_out] + script_scores = {script.name: (script_score(script.name), script.order, script.name, original_index) for original_index, script in enumerate(filtered_scripts)} - return sorted(self.scripts, key=lambda x: script_scores[x.name]) + return sorted(filtered_scripts, key=lambda x: script_scores[x.name]) def setup_ui(self): inputs = [] diff --git a/modules/sd_emphasis.py b/modules/sd_emphasis.py index 654817b6..49ef1a6a 100644 --- a/modules/sd_emphasis.py +++ b/modules/sd_emphasis.py @@ -35,7 +35,7 @@ class EmphasisIgnore(Emphasis): class EmphasisOriginal(Emphasis): name = "Original" - description = "the orginal emphasis implementation" + description = "the original emphasis implementation" def after_transformers(self): original_mean = self.z.mean() @@ -48,7 +48,7 @@ class EmphasisOriginal(Emphasis): class EmphasisOriginalNoNorm(EmphasisOriginal): name = "No norm" - description = "same as orginal, but without normalization (seems to work better for SDXL)" + description = "same as original, but without normalization (seems to work better for SDXL)" def after_transformers(self): self.z = self.z * self.multipliers.reshape(self.multipliers.shape + (1,)).expand(self.z.shape) diff --git a/modules/sd_hijack.py b/modules/sd_hijack.py index e139d996..0de83054 100644 --- a/modules/sd_hijack.py +++ b/modules/sd_hijack.py @@ -325,7 +325,10 @@ class StableDiffusionModelHijack: if self.clip is None: return "-", "-" - _, token_count = self.clip.process_texts([text]) + if hasattr(self.clip, 'get_token_count'): + token_count = self.clip.get_token_count(text) + else: + _, token_count = self.clip.process_texts([text]) return token_count, self.clip.get_target_prompt_token_count(token_count) @@ -356,13 +359,28 @@ class EmbeddingsWithFixes(torch.nn.Module): vec = embedding.vec[self.textual_inversion_key] if isinstance(embedding.vec, dict) else embedding.vec emb = devices.cond_cast_unet(vec) emb_len = min(tensor.shape[0] - offset - 1, emb.shape[0]) - tensor = torch.cat([tensor[0:offset + 1], emb[0:emb_len], tensor[offset + 1 + emb_len:]]) + tensor = torch.cat([tensor[0:offset + 1], emb[0:emb_len], tensor[offset + 1 + emb_len:]]).to(dtype=inputs_embeds.dtype) vecs.append(tensor) return torch.stack(vecs) +class TextualInversionEmbeddings(torch.nn.Embedding): + def __init__(self, num_embeddings: int, embedding_dim: int, textual_inversion_key='clip_l', **kwargs): + super().__init__(num_embeddings, embedding_dim, **kwargs) + + self.embeddings = model_hijack + self.textual_inversion_key = textual_inversion_key + + @property + def wrapped(self): + return super().forward + + def forward(self, input_ids): + return EmbeddingsWithFixes.forward(self, input_ids) + + def add_circular_option_to_conv_2d(): conv2d_constructor = torch.nn.Conv2d.__init__ diff --git a/modules/sd_hijack_clip.py b/modules/sd_hijack_clip.py index 98350ac4..a479148f 100644 --- a/modules/sd_hijack_clip.py +++ b/modules/sd_hijack_clip.py @@ -23,28 +23,25 @@ class PromptChunk: PromptChunkFix = namedtuple('PromptChunkFix', ['offset', 'embedding']) """An object of this type is a marker showing that textual inversion embedding's vectors have to placed at offset in the prompt -chunk. Thos objects are found in PromptChunk.fixes and, are placed into FrozenCLIPEmbedderWithCustomWordsBase.hijack.fixes, and finally +chunk. Those objects are found in PromptChunk.fixes and, are placed into FrozenCLIPEmbedderWithCustomWordsBase.hijack.fixes, and finally are applied by sd_hijack.EmbeddingsWithFixes's forward function.""" -class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): - """A pytorch module that is a wrapper for FrozenCLIPEmbedder module. it enhances FrozenCLIPEmbedder, making it possible to - have unlimited prompt length and assign weights to tokens in prompt. - """ - - def __init__(self, wrapped, hijack): +class TextConditionalModel(torch.nn.Module): + def __init__(self): super().__init__() - self.wrapped = wrapped - """Original FrozenCLIPEmbedder module; can also be FrozenOpenCLIPEmbedder or xlmr.BertSeriesModelWithTransformation, - depending on model.""" - - self.hijack: sd_hijack.StableDiffusionModelHijack = hijack + self.hijack = sd_hijack.model_hijack self.chunk_length = 75 - self.is_trainable = getattr(wrapped, 'is_trainable', False) - self.input_key = getattr(wrapped, 'input_key', 'txt') - self.legacy_ucg_val = None + self.is_trainable = False + self.input_key = 'txt' + self.return_pooled = False + + self.comma_token = None + self.id_start = None + self.id_end = None + self.id_pad = None def empty_chunk(self): """creates an empty PromptChunk and returns it""" @@ -66,7 +63,7 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): def encode_with_transformers(self, tokens): """ - converts a batch of token ids (in python lists) into a single tensor with numeric respresentation of those tokens; + converts a batch of token ids (in python lists) into a single tensor with numeric representation of those tokens; All python lists with tokens are assumed to have same length, usually 77. if input is a list with B elements and each element has T tokens, expected output shape is (B, T, C), where C depends on model - can be 768 and 1024. @@ -136,7 +133,7 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): if token == self.comma_token: last_comma = len(chunk.tokens) - # this is when we are at the end of alloted 75 tokens for the current chunk, and the current token is not a comma. opts.comma_padding_backtrack + # this is when we are at the end of allotted 75 tokens for the current chunk, and the current token is not a comma. opts.comma_padding_backtrack # is a setting that specifies that if there is a comma nearby, the text after the comma should be moved out of this chunk and into the next. elif opts.comma_padding_backtrack != 0 and len(chunk.tokens) == self.chunk_length and last_comma != -1 and len(chunk.tokens) - last_comma <= opts.comma_padding_backtrack: break_location = last_comma + 1 @@ -206,14 +203,10 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): be a multiple of 77; and C is dimensionality of each token - for SD1 it's 768, for SD2 it's 1024, and for SDXL it's 1280. An example shape returned by this function can be: (2, 77, 768). For SDXL, instead of returning one tensor avobe, it returns a tuple with two: the other one with shape (B, 1280) with pooled values. - Webui usually sends just one text at a time through this function - the only time when texts is an array with more than one elemenet + Webui usually sends just one text at a time through this function - the only time when texts is an array with more than one element is when you do prompt editing: "a picture of a [cat:dog:0.4] eating ice cream" """ - if opts.use_old_emphasis_implementation: - import modules.sd_hijack_clip_old - return modules.sd_hijack_clip_old.forward_old(self, texts) - batch_chunks, token_count = self.process_texts(texts) used_embeddings = {} @@ -230,7 +223,7 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): for fixes in self.hijack.fixes: for _position, embedding in fixes: used_embeddings[embedding.name] = embedding - + devices.torch_npu_set_device() z = self.process_tokens(tokens, multipliers) zs.append(z) @@ -252,7 +245,7 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): if any(x for x in texts if "(" in x or "[" in x) and opts.emphasis != "Original": self.hijack.extra_generation_params["Emphasis"] = opts.emphasis - if getattr(self.wrapped, 'return_pooled', False): + if self.return_pooled: return torch.hstack(zs), zs[0].pooled else: return torch.hstack(zs) @@ -292,6 +285,34 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): return z +class FrozenCLIPEmbedderWithCustomWordsBase(TextConditionalModel): + """A pytorch module that is a wrapper for FrozenCLIPEmbedder module. it enhances FrozenCLIPEmbedder, making it possible to + have unlimited prompt length and assign weights to tokens in prompt. + """ + + def __init__(self, wrapped, hijack): + super().__init__() + + self.hijack = hijack + + self.wrapped = wrapped + """Original FrozenCLIPEmbedder module; can also be FrozenOpenCLIPEmbedder or xlmr.BertSeriesModelWithTransformation, + depending on model.""" + + self.is_trainable = getattr(wrapped, 'is_trainable', False) + self.input_key = getattr(wrapped, 'input_key', 'txt') + self.return_pooled = getattr(self.wrapped, 'return_pooled', False) + + self.legacy_ucg_val = None # for sgm codebase + + def forward(self, texts): + if opts.use_old_emphasis_implementation: + import modules.sd_hijack_clip_old + return modules.sd_hijack_clip_old.forward_old(self, texts) + + return super().forward(texts) + + class FrozenCLIPEmbedderWithCustomWords(FrozenCLIPEmbedderWithCustomWordsBase): def __init__(self, wrapped, hijack): super().__init__(wrapped, hijack) @@ -353,7 +374,9 @@ class FrozenCLIPEmbedderForSDXLWithCustomWords(FrozenCLIPEmbedderWithCustomWords def encode_with_transformers(self, tokens): outputs = self.wrapped.transformer(input_ids=tokens, output_hidden_states=self.wrapped.layer == "hidden") - if self.wrapped.layer == "last": + if opts.sdxl_clip_l_skip is True: + z = outputs.hidden_states[-opts.CLIP_stop_at_last_layers] + elif self.wrapped.layer == "last": z = outputs.last_hidden_state else: z = outputs.hidden_states[self.wrapped.layer_idx] diff --git a/modules/sd_hijack_optimizations.py b/modules/sd_hijack_optimizations.py index 7f9e328d..0269f1f5 100644 --- a/modules/sd_hijack_optimizations.py +++ b/modules/sd_hijack_optimizations.py @@ -486,7 +486,8 @@ def xformers_attention_forward(self, x, context=None, mask=None, **kwargs): k_in = self.to_k(context_k) v_in = self.to_v(context_v) - q, k, v = (rearrange(t, 'b n (h d) -> b n h d', h=h) for t in (q_in, k_in, v_in)) + q, k, v = (t.reshape(t.shape[0], t.shape[1], h, -1) for t in (q_in, k_in, v_in)) + del q_in, k_in, v_in dtype = q.dtype @@ -497,7 +498,8 @@ def xformers_attention_forward(self, x, context=None, mask=None, **kwargs): out = out.to(dtype) - out = rearrange(out, 'b n h d -> b n (h d)', h=h) + b, n, h, d = out.shape + out = out.reshape(b, n, h * d) return self.to_out(out) diff --git a/modules/sd_hijack_unet.py b/modules/sd_hijack_unet.py index 2101f1a0..b4f03b13 100644 --- a/modules/sd_hijack_unet.py +++ b/modules/sd_hijack_unet.py @@ -1,5 +1,7 @@ import torch from packaging import version +from einops import repeat +import math from modules import devices from modules.sd_hijack_utils import CondFunc @@ -36,7 +38,7 @@ th = TorchHijackForUnet() # Below are monkey patches to enable upcasting a float16 UNet for float32 sampling def apply_model(orig_func, self, x_noisy, t, cond, **kwargs): - + """Always make sure inputs to unet are in correct dtype.""" if isinstance(cond, dict): for y in cond.keys(): if isinstance(cond[y], list): @@ -45,7 +47,59 @@ def apply_model(orig_func, self, x_noisy, t, cond, **kwargs): cond[y] = cond[y].to(devices.dtype_unet) if isinstance(cond[y], torch.Tensor) else cond[y] with devices.autocast(): - return orig_func(self, x_noisy.to(devices.dtype_unet), t.to(devices.dtype_unet), cond, **kwargs).float() + result = orig_func(self, x_noisy.to(devices.dtype_unet), t.to(devices.dtype_unet), cond, **kwargs) + if devices.unet_needs_upcast: + return result.float() + else: + return result + + +# Monkey patch to create timestep embed tensor on device, avoiding a block. +def timestep_embedding(_, timesteps, dim, max_period=10000, repeat_only=False): + """ + Create sinusoidal timestep embeddings. + :param timesteps: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param dim: the dimension of the output. + :param max_period: controls the minimum frequency of the embeddings. + :return: an [N x dim] Tensor of positional embeddings. + """ + if not repeat_only: + half = dim // 2 + freqs = torch.exp( + -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32, device=timesteps.device) / half + ) + args = timesteps[:, None].float() * freqs[None] + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) + if dim % 2: + embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) + else: + embedding = repeat(timesteps, 'b -> b d', d=dim) + return embedding + + +# Monkey patch to SpatialTransformer removing unnecessary contiguous calls. +# Prevents a lot of unnecessary aten::copy_ calls +def spatial_transformer_forward(_, self, x: torch.Tensor, context=None): + # note: if no context is given, cross-attention defaults to self-attention + if not isinstance(context, list): + context = [context] + b, c, h, w = x.shape + x_in = x + x = self.norm(x) + if not self.use_linear: + x = self.proj_in(x) + x = x.permute(0, 2, 3, 1).reshape(b, h * w, c) + if self.use_linear: + x = self.proj_in(x) + for i, block in enumerate(self.transformer_blocks): + x = block(x, context=context[i]) + if self.use_linear: + x = self.proj_out(x) + x = x.view(b, h, w, c).permute(0, 3, 1, 2) + if not self.use_linear: + x = self.proj_out(x) + return x + x_in class GELUHijack(torch.nn.GELU, torch.nn.Module): @@ -64,12 +118,15 @@ def hijack_ddpm_edit(): if not ddpm_edit_hijack: CondFunc('modules.models.diffusion.ddpm_edit.LatentDiffusion.decode_first_stage', first_stage_sub, first_stage_cond) CondFunc('modules.models.diffusion.ddpm_edit.LatentDiffusion.encode_first_stage', first_stage_sub, first_stage_cond) - ddpm_edit_hijack = CondFunc('modules.models.diffusion.ddpm_edit.LatentDiffusion.apply_model', apply_model, unet_needs_upcast) + ddpm_edit_hijack = CondFunc('modules.models.diffusion.ddpm_edit.LatentDiffusion.apply_model', apply_model) unet_needs_upcast = lambda *args, **kwargs: devices.unet_needs_upcast CondFunc('ldm.models.diffusion.ddpm.LatentDiffusion.apply_model', apply_model, unet_needs_upcast) +CondFunc('ldm.modules.diffusionmodules.openaimodel.timestep_embedding', timestep_embedding) +CondFunc('ldm.modules.attention.SpatialTransformer.forward', spatial_transformer_forward) CondFunc('ldm.modules.diffusionmodules.openaimodel.timestep_embedding', lambda orig_func, timesteps, *args, **kwargs: orig_func(timesteps, *args, **kwargs).to(torch.float32 if timesteps.dtype == torch.int64 else devices.dtype_unet), unet_needs_upcast) + if version.parse(torch.__version__) <= version.parse("1.13.2") or torch.cuda.is_available(): CondFunc('ldm.modules.diffusionmodules.util.GroupNorm32.forward', lambda orig_func, self, *args, **kwargs: orig_func(self.float(), *args, **kwargs), unet_needs_upcast) CondFunc('ldm.modules.attention.GEGLU.forward', lambda orig_func, self, x: orig_func(self.float(), x.float()).to(devices.dtype_unet), unet_needs_upcast) @@ -81,5 +138,17 @@ CondFunc('ldm.models.diffusion.ddpm.LatentDiffusion.decode_first_stage', first_s CondFunc('ldm.models.diffusion.ddpm.LatentDiffusion.encode_first_stage', first_stage_sub, first_stage_cond) CondFunc('ldm.models.diffusion.ddpm.LatentDiffusion.get_first_stage_encoding', lambda orig_func, *args, **kwargs: orig_func(*args, **kwargs).float(), first_stage_cond) -CondFunc('sgm.modules.diffusionmodules.wrappers.OpenAIWrapper.forward', apply_model, unet_needs_upcast) -CondFunc('sgm.modules.diffusionmodules.openaimodel.timestep_embedding', lambda orig_func, timesteps, *args, **kwargs: orig_func(timesteps, *args, **kwargs).to(torch.float32 if timesteps.dtype == torch.int64 else devices.dtype_unet), unet_needs_upcast) +CondFunc('ldm.models.diffusion.ddpm.LatentDiffusion.apply_model', apply_model) +CondFunc('sgm.modules.diffusionmodules.wrappers.OpenAIWrapper.forward', apply_model) + + +def timestep_embedding_cast_result(orig_func, timesteps, *args, **kwargs): + if devices.unet_needs_upcast and timesteps.dtype == torch.int64: + dtype = torch.float32 + else: + dtype = devices.dtype_unet + return orig_func(timesteps, *args, **kwargs).to(dtype=dtype) + + +CondFunc('ldm.modules.diffusionmodules.openaimodel.timestep_embedding', timestep_embedding_cast_result) +CondFunc('sgm.modules.diffusionmodules.openaimodel.timestep_embedding', timestep_embedding_cast_result) diff --git a/modules/sd_hijack_utils.py b/modules/sd_hijack_utils.py index 79bf6e46..546f2eda 100644 --- a/modules/sd_hijack_utils.py +++ b/modules/sd_hijack_utils.py @@ -1,7 +1,11 @@ import importlib + +always_true_func = lambda *args, **kwargs: True + + class CondFunc: - def __new__(cls, orig_func, sub_func, cond_func): + def __new__(cls, orig_func, sub_func, cond_func=always_true_func): self = super(CondFunc, cls).__new__(cls) if isinstance(orig_func, str): func_path = orig_func.split('.') @@ -20,13 +24,13 @@ class CondFunc: print(f"Warning: Failed to resolve {orig_func} for CondFunc hijack") pass self.__init__(orig_func, sub_func, cond_func) - return lambda *args, **kwargs: self(*args, **kwargs) - def __init__(self, orig_func, sub_func, cond_func): - self.__orig_func = orig_func - self.__sub_func = sub_func - self.__cond_func = cond_func - def __call__(self, *args, **kwargs): - if not self.__cond_func or self.__cond_func(self.__orig_func, *args, **kwargs): - return self.__sub_func(self.__orig_func, *args, **kwargs) - else: - return self.__orig_func(*args, **kwargs) + return lambda *args, **kwargs: self(*args, **kwargs) + def __init__(self, orig_func, sub_func, cond_func): + self.__orig_func = orig_func + self.__sub_func = sub_func + self.__cond_func = cond_func + def __call__(self, *args, **kwargs): + if not self.__cond_func or self.__cond_func(self.__orig_func, *args, **kwargs): + return self.__sub_func(self.__orig_func, *args, **kwargs) + else: + return self.__orig_func(*args, **kwargs) diff --git a/modules/sd_models.py b/modules/sd_models.py index 747fc39e..55bd9ca5 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -1,18 +1,17 @@ import collections -import os.path +import importlib +import os import sys import threading +import enum import torch import re import safetensors.torch from omegaconf import OmegaConf, ListConfig -from os import mkdir from urllib import request import ldm.modules.midas as midas -from ldm.util import instantiate_from_config - from modules import paths, shared, modelloader, devices, script_callbacks, sd_vae, sd_disable_initialization, errors, hashes, sd_models_config, sd_unet, sd_models_xl, cache, extra_networks, processing, lowvram, sd_hijack, patches from modules.timer import Timer from modules.shared import opts @@ -28,6 +27,14 @@ checkpoint_alisases = checkpoint_aliases # for compatibility with old name checkpoints_loaded = collections.OrderedDict() +class ModelType(enum.Enum): + SD1 = 1 + SD2 = 2 + SDXL = 3 + SSD = 4 + SD3 = 5 + + def replace_key(d, key, new_key, value): keys = list(d.keys()) @@ -150,10 +157,12 @@ def list_models(): cmd_ckpt = shared.cmd_opts.ckpt if shared.cmd_opts.no_download_sd_model or cmd_ckpt != shared.sd_model_file or os.path.exists(cmd_ckpt): model_url = None + expected_sha256 = None else: - model_url = "https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors" + model_url = f"{shared.hf_endpoint}/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors" + expected_sha256 = '6ce0161689b3853acaa03779ec93eafe75a02f4ced659bee03f50797806fa2fa' - model_list = modelloader.load_models(model_path=model_path, model_url=model_url, command_path=shared.cmd_opts.ckpt_dir, ext_filter=[".ckpt", ".safetensors"], download_name="v1-5-pruned-emaonly.safetensors", ext_blacklist=[".vae.ckpt", ".vae.safetensors"]) + model_list = modelloader.load_models(model_path=model_path, model_url=model_url, command_path=shared.cmd_opts.ckpt_dir, ext_filter=[".ckpt", ".safetensors"], download_name="v1-5-pruned-emaonly.safetensors", ext_blacklist=[".vae.ckpt", ".vae.safetensors"], hash_prefix=expected_sha256) if os.path.exists(cmd_ckpt): checkpoint_info = CheckpointInfo(cmd_ckpt) @@ -281,17 +290,21 @@ def read_metadata_from_safetensors(filename): json_start = file.read(2) assert metadata_len > 2 and json_start in (b'{"', b"{'"), f"{filename} is not a safetensors file" - json_data = json_start + file.read(metadata_len-2) - json_obj = json.loads(json_data) res = {} - for k, v in json_obj.get("__metadata__", {}).items(): - res[k] = v - if isinstance(v, str) and v[0:1] == '{': - try: - res[k] = json.loads(v) - except Exception: - pass + + try: + json_data = json_start + file.read(metadata_len-2) + json_obj = json.loads(json_data) + for k, v in json_obj.get("__metadata__", {}).items(): + res[k] = v + if isinstance(v, str) and v[0:1] == '{': + try: + res[k] = json.loads(v) + except Exception: + pass + except Exception: + errors.report(f"Error reading metadata from file: {filename}", exc_info=True) return res @@ -363,6 +376,37 @@ def check_fp8(model): return enable_fp8 +def set_model_type(model, state_dict): + model.is_sd1 = False + model.is_sd2 = False + model.is_sdxl = False + model.is_ssd = False + model.is_sd3 = False + + if "model.diffusion_model.x_embedder.proj.weight" in state_dict: + model.is_sd3 = True + model.model_type = ModelType.SD3 + elif hasattr(model, 'conditioner'): + model.is_sdxl = True + + if 'model.diffusion_model.middle_block.1.transformer_blocks.0.attn1.to_q.weight' not in state_dict.keys(): + model.is_ssd = True + model.model_type = ModelType.SSD + else: + model.model_type = ModelType.SDXL + elif hasattr(model.cond_stage_model, 'model'): + model.is_sd2 = True + model.model_type = ModelType.SD2 + else: + model.is_sd1 = True + model.model_type = ModelType.SD1 + + +def set_model_fields(model): + if not hasattr(model, 'latent_channels'): + model.latent_channels = 4 + + def load_model_weights(model, checkpoint_info: CheckpointInfo, state_dict, timer): sd_model_hash = checkpoint_info.calculate_shorthash() timer.record("calculate hash") @@ -377,10 +421,9 @@ def load_model_weights(model, checkpoint_info: CheckpointInfo, state_dict, timer if state_dict is None: state_dict = get_checkpoint_state_dict(checkpoint_info, timer) - model.is_sdxl = hasattr(model, 'conditioner') - model.is_sd2 = not model.is_sdxl and hasattr(model.cond_stage_model, 'model') - model.is_sd1 = not model.is_sdxl and not model.is_sd2 - model.is_ssd = model.is_sdxl and 'model.diffusion_model.middle_block.1.transformer_blocks.0.attn1.to_q.weight' not in state_dict.keys() + set_model_type(model, state_dict) + set_model_fields(model) + if model.is_sdxl: sd_models_xl.extend_sdxl(model) @@ -391,11 +434,30 @@ def load_model_weights(model, checkpoint_info: CheckpointInfo, state_dict, timer # cache newly loaded model checkpoints_loaded[checkpoint_info] = state_dict.copy() + if hasattr(model, "before_load_weights"): + model.before_load_weights(state_dict) + model.load_state_dict(state_dict, strict=False) timer.record("apply weights to model") + if hasattr(model, "after_load_weights"): + model.after_load_weights(state_dict) + del state_dict + # Set is_sdxl_inpaint flag. + # Checks Unet structure to detect inpaint model. The inpaint model's + # checkpoint state_dict does not contain the key + # 'diffusion_model.input_blocks.0.0.weight'. + diffusion_model_input = model.model.state_dict().get( + 'diffusion_model.input_blocks.0.0.weight' + ) + model.is_sdxl_inpaint = ( + model.is_sdxl and + diffusion_model_input is not None and + diffusion_model_input.shape[1] == 9 + ) + if shared.cmd_opts.opt_channelslast: model.to(memory_format=torch.channels_last) timer.record("apply channels_last") @@ -404,6 +466,7 @@ def load_model_weights(model, checkpoint_info: CheckpointInfo, state_dict, timer model.float() model.alphas_cumprod_original = model.alphas_cumprod devices.dtype_unet = torch.float32 + assert shared.cmd_opts.precision != "half", "Cannot use --precision half with --no-half" timer.record("apply float()") else: vae = model.first_stage_model @@ -508,7 +571,7 @@ def enable_midas_autodownload(): path = midas.api.ISL_PATHS[model_type] if not os.path.exists(path): if not os.path.exists(midas_path): - mkdir(midas_path) + os.mkdir(midas_path) print(f"Downloading midas model weights for {model_type} to {path}") request.urlretrieve(midas_urls[model_type], path) @@ -533,25 +596,34 @@ def patch_given_betas(): original_register_schedule = patches.patch(__name__, ldm.models.diffusion.ddpm.DDPM, 'register_schedule', patched_register_schedule) -def repair_config(sd_config): - +def repair_config(sd_config, state_dict=None): if not hasattr(sd_config.model.params, "use_ema"): sd_config.model.params.use_ema = False if hasattr(sd_config.model.params, 'unet_config'): if shared.cmd_opts.no_half: sd_config.model.params.unet_config.params.use_fp16 = False - elif shared.cmd_opts.upcast_sampling: + elif shared.cmd_opts.upcast_sampling or shared.cmd_opts.precision == "half": sd_config.model.params.unet_config.params.use_fp16 = True - if getattr(sd_config.model.params.first_stage_config.params.ddconfig, "attn_type", None) == "vanilla-xformers" and not shared.xformers_available: - sd_config.model.params.first_stage_config.params.ddconfig.attn_type = "vanilla" + if hasattr(sd_config.model.params, 'first_stage_config'): + if getattr(sd_config.model.params.first_stage_config.params.ddconfig, "attn_type", None) == "vanilla-xformers" and not shared.xformers_available: + sd_config.model.params.first_stage_config.params.ddconfig.attn_type = "vanilla" # For UnCLIP-L, override the hardcoded karlo directory if hasattr(sd_config.model.params, "noise_aug_config") and hasattr(sd_config.model.params.noise_aug_config.params, "clip_stats_path"): karlo_path = os.path.join(paths.models_path, 'karlo') sd_config.model.params.noise_aug_config.params.clip_stats_path = sd_config.model.params.noise_aug_config.params.clip_stats_path.replace("checkpoints/karlo_models", karlo_path) + # Do not use checkpoint for inference. + # This helps prevent extra performance overhead on checking parameters. + # The perf overhead is about 100ms/it on 4090 for SDXL. + if hasattr(sd_config.model.params, "network_config"): + sd_config.model.params.network_config.params.use_checkpoint = False + if hasattr(sd_config.model.params, "unet_config"): + sd_config.model.params.unet_config.params.use_checkpoint = False + + def rescale_zero_terminal_snr_abar(alphas_cumprod): alphas_bar_sqrt = alphas_cumprod.sqrt() @@ -652,18 +724,23 @@ def get_empty_cond(sd_model): p = processing.StableDiffusionProcessingTxt2Img() extra_networks.activate(p, {}) - if hasattr(sd_model, 'conditioner'): + if hasattr(sd_model, 'get_learned_conditioning'): d = sd_model.get_learned_conditioning([""]) - return d['crossattn'] else: - return sd_model.cond_stage_model([""]) + d = sd_model.cond_stage_model([""]) + + if isinstance(d, dict): + d = d['crossattn'] + + return d def send_model_to_cpu(m): - if m.lowvram: - lowvram.send_everything_to_cpu() - else: - m.to(devices.cpu) + if m is not None: + if m.lowvram: + lowvram.send_everything_to_cpu() + else: + m.to(devices.cpu) devices.torch_gc() @@ -687,6 +764,25 @@ def send_model_to_trash(m): devices.torch_gc() +def instantiate_from_config(config, state_dict=None): + constructor = get_obj_from_str(config["target"]) + + params = {**config.get("params", {})} + + if state_dict and "state_dict" in params and params["state_dict"] is None: + params["state_dict"] = state_dict + + return constructor(**params) + + +def get_obj_from_str(string, reload=False): + module, cls = string.rsplit(".", 1) + if reload: + module_imp = importlib.import_module(module) + importlib.reload(module_imp) + return getattr(importlib.import_module(module, package=None), cls) + + def load_model(checkpoint_info=None, already_loaded_state_dict=None): from modules import sd_hijack checkpoint_info = checkpoint_info or select_checkpoint() @@ -711,7 +807,7 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None): timer.record("find config") sd_config = OmegaConf.load(checkpoint_config) - repair_config(sd_config) + repair_config(sd_config, state_dict) timer.record("load config") @@ -721,7 +817,7 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None): try: with sd_disable_initialization.DisableInitialization(disable_clip=clip_is_included_into_sd or shared.cmd_opts.do_not_download_clip): with sd_disable_initialization.InitializeOnMeta(): - sd_model = instantiate_from_config(sd_config.model) + sd_model = instantiate_from_config(sd_config.model, state_dict) except Exception as e: errors.display(e, "creating model quickly", full_traceback=True) @@ -730,7 +826,7 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None): print('Failed to create model quickly; will retry using slow method.', file=sys.stderr) with sd_disable_initialization.InitializeOnMeta(): - sd_model = instantiate_from_config(sd_config.model) + sd_model = instantiate_from_config(sd_config.model, state_dict) sd_model.used_config = checkpoint_config @@ -747,6 +843,7 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None): with sd_disable_initialization.LoadStateDictOnMeta(state_dict, device=model_target_device(sd_model), weight_dtype_conversion=weight_dtype_conversion): load_model_weights(sd_model, checkpoint_info, state_dict, timer) + timer.record("load weights from state dict") send_model_to_device(sd_model) @@ -784,9 +881,16 @@ def reuse_model_from_already_loaded(sd_model, checkpoint_info, timer): If it is loaded, returns that (moving it to GPU if necessary, and moving the currently loadded model to CPU if necessary). If not, returns the model that can be used to load weights from checkpoint_info's file. If no such model exists, returns None. - Additionaly deletes loaded models that are over the limit set in settings (sd_checkpoints_limit). + Additionally deletes loaded models that are over the limit set in settings (sd_checkpoints_limit). """ + if sd_model is not None and sd_model.sd_checkpoint_info.filename == checkpoint_info.filename: + return sd_model + + if shared.opts.sd_checkpoints_keep_in_cpu: + send_model_to_cpu(sd_model) + timer.record("send model to cpu") + already_loaded = None for i in reversed(range(len(model_data.loaded_sd_models))): loaded_model = model_data.loaded_sd_models[i] @@ -796,14 +900,10 @@ def reuse_model_from_already_loaded(sd_model, checkpoint_info, timer): if len(model_data.loaded_sd_models) > shared.opts.sd_checkpoints_limit > 0: print(f"Unloading model {len(model_data.loaded_sd_models)} over the limit of {shared.opts.sd_checkpoints_limit}: {loaded_model.sd_checkpoint_info.title}") - model_data.loaded_sd_models.pop() + del model_data.loaded_sd_models[i] send_model_to_trash(loaded_model) timer.record("send model to trash") - if shared.opts.sd_checkpoints_keep_in_cpu: - send_model_to_cpu(sd_model) - timer.record("send model to cpu") - if already_loaded is not None: send_model_to_device(already_loaded) timer.record("send model to device") diff --git a/modules/sd_models_config.py b/modules/sd_models_config.py index b38137eb..fb44c5a8 100644 --- a/modules/sd_models_config.py +++ b/modules/sd_models_config.py @@ -23,6 +23,8 @@ config_inpainting = os.path.join(sd_configs_path, "v1-inpainting-inference.yaml" config_instruct_pix2pix = os.path.join(sd_configs_path, "instruct-pix2pix.yaml") config_alt_diffusion = os.path.join(sd_configs_path, "alt-diffusion-inference.yaml") config_alt_diffusion_m18 = os.path.join(sd_configs_path, "alt-diffusion-m18-inference.yaml") +config_sd3 = os.path.join(sd_configs_path, "sd3-inference.yaml") + def is_using_v_parameterization_for_sd2(state_dict): """ @@ -31,11 +33,11 @@ def is_using_v_parameterization_for_sd2(state_dict): import ldm.modules.diffusionmodules.openaimodel - device = devices.cpu + device = devices.device with sd_disable_initialization.DisableInitialization(): unet = ldm.modules.diffusionmodules.openaimodel.UNetModel( - use_checkpoint=True, + use_checkpoint=False, use_fp16=False, image_size=32, in_channels=4, @@ -56,12 +58,13 @@ def is_using_v_parameterization_for_sd2(state_dict): with torch.no_grad(): unet_sd = {k.replace("model.diffusion_model.", ""): v for k, v in state_dict.items() if "model.diffusion_model." in k} unet.load_state_dict(unet_sd, strict=True) - unet.to(device=device, dtype=torch.float) + unet.to(device=device, dtype=devices.dtype_unet) test_cond = torch.ones((1, 2, 1024), device=device) * 0.5 x_test = torch.ones((1, 4, 8, 8), device=device) * 0.5 - out = (unet(x_test, torch.asarray([999], device=device), context=test_cond) - x_test).mean().item() + with devices.autocast(): + out = (unet(x_test, torch.asarray([999], device=device), context=test_cond) - x_test).mean().cpu().item() return out < -1 @@ -71,11 +74,15 @@ def guess_model_config_from_state_dict(sd, filename): diffusion_model_input = sd.get('model.diffusion_model.input_blocks.0.0.weight', None) sd2_variations_weight = sd.get('embedder.model.ln_final.weight', None) + if "model.diffusion_model.x_embedder.proj.weight" in sd: + return config_sd3 + if sd.get('conditioner.embedders.1.model.ln_final.weight', None) is not None: if diffusion_model_input.shape[1] == 9: return config_sdxl_inpainting else: return config_sdxl + if sd.get('conditioner.embedders.0.model.ln_final.weight', None) is not None: return config_sdxl_refiner elif sd.get('depth_model.model.pretrained.act_postprocess3.0.project.0.bias', None) is not None: @@ -99,7 +106,6 @@ def guess_model_config_from_state_dict(sd, filename): if diffusion_model_input.shape[1] == 8: return config_instruct_pix2pix - if sd.get('cond_stage_model.roberta.embeddings.word_embeddings.weight', None) is not None: if sd.get('cond_stage_model.transformation.weight').size()[0] == 1024: return config_alt_diffusion_m18 diff --git a/modules/sd_models_types.py b/modules/sd_models_types.py index f911fbb6..2fce2777 100644 --- a/modules/sd_models_types.py +++ b/modules/sd_models_types.py @@ -32,3 +32,9 @@ class WebuiSdModel(LatentDiffusion): is_sd1: bool """True if the model's architecture is SD 1.x""" + + is_sd3: bool + """True if the model's architecture is SD 3""" + + latent_channels: int + """number of layer in latent image representation; will be 16 in SD3 and 4 in other version""" diff --git a/modules/sd_models_xl.py b/modules/sd_models_xl.py index 0de17af3..1242a593 100644 --- a/modules/sd_models_xl.py +++ b/modules/sd_models_xl.py @@ -13,8 +13,8 @@ def get_learned_conditioning(self: sgm.models.diffusion.DiffusionEngine, batch: for embedder in self.conditioner.embedders: embedder.ucg_rate = 0.0 - width = getattr(batch, 'width', 1024) - height = getattr(batch, 'height', 1024) + width = getattr(batch, 'width', 1024) or 1024 + height = getattr(batch, 'height', 1024) or 1024 is_negative_prompt = getattr(batch, 'is_negative_prompt', False) aesthetic_score = shared.opts.sdxl_refiner_low_aesthetic_score if is_negative_prompt else shared.opts.sdxl_refiner_high_aesthetic_score @@ -35,11 +35,10 @@ def get_learned_conditioning(self: sgm.models.diffusion.DiffusionEngine, batch: def apply_model(self: sgm.models.diffusion.DiffusionEngine, x, t, cond): - sd = self.model.state_dict() - diffusion_model_input = sd.get('diffusion_model.input_blocks.0.0.weight', None) - if diffusion_model_input is not None: - if diffusion_model_input.shape[1] == 9: - x = torch.cat([x] + cond['c_concat'], dim=1) + """WARNING: This function is called once per denoising iteration. DO NOT add + expensive functionc calls such as `model.state_dict`. """ + if self.is_sdxl_inpaint: + x = torch.cat([x] + cond['c_concat'], dim=1) return self.model(x, t, cond) diff --git a/modules/sd_samplers.py b/modules/sd_samplers.py index a58528a0..963da5be 100644 --- a/modules/sd_samplers.py +++ b/modules/sd_samplers.py @@ -1,7 +1,12 @@ -from modules import sd_samplers_kdiffusion, sd_samplers_timesteps, sd_samplers_lcm, shared +from __future__ import annotations + +import functools +import logging +from modules import sd_samplers_kdiffusion, sd_samplers_timesteps, sd_samplers_lcm, shared, sd_samplers_common, sd_schedulers # imports for functions that previously were here and are used by other modules -from modules.sd_samplers_common import samples_to_image_grid, sample_to_image # noqa: F401 +samples_to_image_grid = sd_samplers_common.samples_to_image_grid +sample_to_image = sd_samplers_common.sample_to_image all_samplers = [ *sd_samplers_kdiffusion.samplers_data_k_diffusion, @@ -10,8 +15,8 @@ all_samplers = [ ] all_samplers_map = {x.name: x for x in all_samplers} -samplers = [] -samplers_for_img2img = [] +samplers: list[sd_samplers_common.SamplerData] = [] +samplers_for_img2img: list[sd_samplers_common.SamplerData] = [] samplers_map = {} samplers_hidden = {} @@ -57,4 +62,71 @@ def visible_sampler_names(): return [x.name for x in samplers if x.name not in samplers_hidden] +def visible_samplers(): + return [x for x in samplers if x.name not in samplers_hidden] + + +def get_sampler_from_infotext(d: dict): + return get_sampler_and_scheduler(d.get("Sampler"), d.get("Schedule type"))[0] + + +def get_scheduler_from_infotext(d: dict): + return get_sampler_and_scheduler(d.get("Sampler"), d.get("Schedule type"))[1] + + +def get_hr_sampler_and_scheduler(d: dict): + hr_sampler = d.get("Hires sampler", "Use same sampler") + sampler = d.get("Sampler") if hr_sampler == "Use same sampler" else hr_sampler + + hr_scheduler = d.get("Hires schedule type", "Use same scheduler") + scheduler = d.get("Schedule type") if hr_scheduler == "Use same scheduler" else hr_scheduler + + sampler, scheduler = get_sampler_and_scheduler(sampler, scheduler) + + sampler = sampler if sampler != d.get("Sampler") else "Use same sampler" + scheduler = scheduler if scheduler != d.get("Schedule type") else "Use same scheduler" + + return sampler, scheduler + + +def get_hr_sampler_from_infotext(d: dict): + return get_hr_sampler_and_scheduler(d)[0] + + +def get_hr_scheduler_from_infotext(d: dict): + return get_hr_sampler_and_scheduler(d)[1] + + +@functools.cache +def get_sampler_and_scheduler(sampler_name, scheduler_name, *, convert_automatic=True): + default_sampler = samplers[0] + found_scheduler = sd_schedulers.schedulers_map.get(scheduler_name, sd_schedulers.schedulers[0]) + + name = sampler_name or default_sampler.name + + for scheduler in sd_schedulers.schedulers: + name_options = [scheduler.label, scheduler.name, *(scheduler.aliases or [])] + + for name_option in name_options: + if name.endswith(" " + name_option): + found_scheduler = scheduler + name = name[0:-(len(name_option) + 1)] + break + + sampler = all_samplers_map.get(name, default_sampler) + + # revert back to Automatic if it's the default scheduler for the selected sampler + if convert_automatic and sampler.options.get('scheduler', None) == found_scheduler.name: + found_scheduler = sd_schedulers.schedulers[0] + + return sampler.name, found_scheduler.label + + +def fix_p_invalid_sampler_and_scheduler(p): + i_sampler_name, i_scheduler = p.sampler_name, p.scheduler + p.sampler_name, p.scheduler = get_sampler_and_scheduler(p.sampler_name, p.scheduler, convert_automatic=False) + if p.sampler_name != i_sampler_name or i_scheduler != p.scheduler: + logging.warning(f'Sampler Scheduler autocorrection: "{i_sampler_name}" -> "{p.sampler_name}", "{i_scheduler}" -> "{p.scheduler}"') + + set_samplers() diff --git a/modules/sd_samplers_cfg_denoiser.py b/modules/sd_samplers_cfg_denoiser.py index a73d3b03..b6fbf337 100644 --- a/modules/sd_samplers_cfg_denoiser.py +++ b/modules/sd_samplers_cfg_denoiser.py @@ -1,5 +1,5 @@ import torch -from modules import prompt_parser, devices, sd_samplers_common +from modules import prompt_parser, sd_samplers_common from modules.shared import opts, state import modules.shared as shared @@ -58,6 +58,11 @@ class CFGDenoiser(torch.nn.Module): self.model_wrap = None self.p = None + self.cond_scale_miltiplier = 1.0 + + self.need_last_noise_uncond = False + self.last_noise_uncond = None + # NOTE: masking before denoising can cause the original latents to be oversmoothed # as the original latents do not have noise self.mask_before_denoising = False @@ -152,7 +157,7 @@ class CFGDenoiser(torch.nn.Module): if state.interrupted or state.skipped: raise sd_samplers_common.InterruptedException - if sd_samplers_common.apply_refiner(self): + if sd_samplers_common.apply_refiner(self, sigma): cond = self.sampler.sampler_extra_args['cond'] uncond = self.sampler.sampler_extra_args['uncond'] @@ -212,9 +217,16 @@ class CFGDenoiser(torch.nn.Module): uncond = denoiser_params.text_uncond skip_uncond = False - # alternating uncond allows for higher thresholds without the quality loss normally expected from raising it - if self.step % 2 and s_min_uncond > 0 and sigma[0] < s_min_uncond and not is_edit_model: + if shared.opts.skip_early_cond != 0. and self.step / self.total_steps <= shared.opts.skip_early_cond: skip_uncond = True + self.p.extra_generation_params["Skip Early CFG"] = shared.opts.skip_early_cond + elif (self.step % 2 or shared.opts.s_min_uncond_all) and s_min_uncond > 0 and sigma[0] < s_min_uncond and not is_edit_model: + skip_uncond = True + self.p.extra_generation_params["NGMS"] = s_min_uncond + if shared.opts.s_min_uncond_all: + self.p.extra_generation_params["NGMS all steps"] = shared.opts.s_min_uncond_all + + if skip_uncond: x_in = x_in[:-batch_size] sigma_in = sigma_in[:-batch_size] @@ -266,14 +278,15 @@ class CFGDenoiser(torch.nn.Module): denoised_params = CFGDenoisedParams(x_out, state.sampling_step, state.sampling_steps, self.inner_model) cfg_denoised_callback(denoised_params) - devices.test_for_nans(x_out, "unet") + if self.need_last_noise_uncond: + self.last_noise_uncond = torch.clone(x_out[-uncond.shape[0]:]) if is_edit_model: - denoised = self.combine_denoised_for_edit_model(x_out, cond_scale) + denoised = self.combine_denoised_for_edit_model(x_out, cond_scale * self.cond_scale_miltiplier) elif skip_uncond: denoised = self.combine_denoised(x_out, conds_list, uncond, 1.0) else: - denoised = self.combine_denoised(x_out, conds_list, uncond, cond_scale) + denoised = self.combine_denoised(x_out, conds_list, uncond, cond_scale * self.cond_scale_miltiplier) # Blend in the original latents (after) if not self.mask_before_denoising and self.mask is not None: diff --git a/modules/sd_samplers_common.py b/modules/sd_samplers_common.py index 6bd38e12..c060cccb 100644 --- a/modules/sd_samplers_common.py +++ b/modules/sd_samplers_common.py @@ -54,7 +54,7 @@ def samples_to_images_tensor(sample, approximation=None, model=None): else: if model is None: model = shared.sd_model - with devices.without_autocast(): # fixes an issue with unstable VAEs that are flaky even in fp32 + with torch.no_grad(), devices.without_autocast(): # fixes an issue with unstable VAEs that are flaky even in fp32 x_sample = model.decode_first_stage(sample.to(model.first_stage_model.dtype)) return x_sample @@ -155,8 +155,19 @@ def replace_torchsde_browinan(): replace_torchsde_browinan() -def apply_refiner(cfg_denoiser): - completed_ratio = cfg_denoiser.step / cfg_denoiser.total_steps +def apply_refiner(cfg_denoiser, sigma=None): + if opts.refiner_switch_by_sample_steps or sigma is None: + completed_ratio = cfg_denoiser.step / cfg_denoiser.total_steps + cfg_denoiser.p.extra_generation_params["Refiner switch by sampling steps"] = True + + else: + # torch.max(sigma) only to handle rare case where we might have different sigmas in the same batch + try: + timestep = torch.argmin(torch.abs(cfg_denoiser.inner_model.sigmas.to(sigma.device) - torch.max(sigma))) + except AttributeError: # for samplers that don't use sigmas (DDIM) sigma is actually the timestep + timestep = torch.max(sigma).to(dtype=int) + completed_ratio = (999 - timestep) / 1000 + refiner_switch_at = cfg_denoiser.p.refiner_switch_at refiner_checkpoint_info = cfg_denoiser.p.refiner_checkpoint_info @@ -235,7 +246,7 @@ class Sampler: self.eta_infotext_field = 'Eta' self.eta_default = 1.0 - self.conditioning_key = shared.sd_model.model.conditioning_key + self.conditioning_key = getattr(shared.sd_model.model, 'conditioning_key', 'crossattn') self.p = None self.model_wrap_cfg = None diff --git a/modules/sd_samplers_kdiffusion.py b/modules/sd_samplers_kdiffusion.py index 337106c0..0c94d100 100644 --- a/modules/sd_samplers_kdiffusion.py +++ b/modules/sd_samplers_kdiffusion.py @@ -1,7 +1,7 @@ import torch import inspect import k_diffusion.sampling -from modules import sd_samplers_common, sd_samplers_extra, sd_samplers_cfg_denoiser +from modules import sd_samplers_common, sd_samplers_extra, sd_samplers_cfg_denoiser, sd_schedulers, devices from modules.sd_samplers_cfg_denoiser import CFGDenoiser # noqa: F401 from modules.script_callbacks import ExtraNoiseParams, extra_noise_callback @@ -9,32 +9,20 @@ from modules.shared import opts import modules.shared as shared samplers_k_diffusion = [ - ('DPM++ 2M Karras', 'sample_dpmpp_2m', ['k_dpmpp_2m_ka'], {'scheduler': 'karras'}), - ('DPM++ SDE Karras', 'sample_dpmpp_sde', ['k_dpmpp_sde_ka'], {'scheduler': 'karras', "second_order": True, "brownian_noise": True}), - ('DPM++ 2M SDE Exponential', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde_exp'], {'scheduler': 'exponential', "brownian_noise": True}), - ('DPM++ 2M SDE Karras', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde_ka'], {'scheduler': 'karras', "brownian_noise": True}), + ('DPM++ 2M', 'sample_dpmpp_2m', ['k_dpmpp_2m'], {'scheduler': 'karras'}), + ('DPM++ SDE', 'sample_dpmpp_sde', ['k_dpmpp_sde'], {'scheduler': 'karras', "second_order": True, "brownian_noise": True}), + ('DPM++ 2M SDE', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde'], {'scheduler': 'exponential', "brownian_noise": True}), + ('DPM++ 2M SDE Heun', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde_heun'], {'scheduler': 'exponential', "brownian_noise": True, "solver_type": "heun"}), + ('DPM++ 2S a', 'sample_dpmpp_2s_ancestral', ['k_dpmpp_2s_a'], {'scheduler': 'karras', "uses_ensd": True, "second_order": True}), + ('DPM++ 3M SDE', 'sample_dpmpp_3m_sde', ['k_dpmpp_3m_sde'], {'scheduler': 'exponential', 'discard_next_to_last_sigma': True, "brownian_noise": True}), ('Euler a', 'sample_euler_ancestral', ['k_euler_a', 'k_euler_ancestral'], {"uses_ensd": True}), ('Euler', 'sample_euler', ['k_euler'], {}), ('LMS', 'sample_lms', ['k_lms'], {}), ('Heun', 'sample_heun', ['k_heun'], {"second_order": True}), - ('DPM2', 'sample_dpm_2', ['k_dpm_2'], {'discard_next_to_last_sigma': True, "second_order": True}), - ('DPM2 a', 'sample_dpm_2_ancestral', ['k_dpm_2_a'], {'discard_next_to_last_sigma': True, "uses_ensd": True, "second_order": True}), - ('DPM++ 2S a', 'sample_dpmpp_2s_ancestral', ['k_dpmpp_2s_a'], {"uses_ensd": True, "second_order": True}), - ('DPM++ 2M', 'sample_dpmpp_2m', ['k_dpmpp_2m'], {}), - ('DPM++ SDE', 'sample_dpmpp_sde', ['k_dpmpp_sde'], {"second_order": True, "brownian_noise": True}), - ('DPM++ 2M SDE', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde_ka'], {"brownian_noise": True}), - ('DPM++ 2M SDE Heun', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde_heun'], {"brownian_noise": True, "solver_type": "heun"}), - ('DPM++ 2M SDE Heun Karras', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde_heun_ka'], {'scheduler': 'karras', "brownian_noise": True, "solver_type": "heun"}), - ('DPM++ 2M SDE Heun Exponential', 'sample_dpmpp_2m_sde', ['k_dpmpp_2m_sde_heun_exp'], {'scheduler': 'exponential', "brownian_noise": True, "solver_type": "heun"}), - ('DPM++ 3M SDE', 'sample_dpmpp_3m_sde', ['k_dpmpp_3m_sde'], {'discard_next_to_last_sigma': True, "brownian_noise": True}), - ('DPM++ 3M SDE Karras', 'sample_dpmpp_3m_sde', ['k_dpmpp_3m_sde_ka'], {'scheduler': 'karras', 'discard_next_to_last_sigma': True, "brownian_noise": True}), - ('DPM++ 3M SDE Exponential', 'sample_dpmpp_3m_sde', ['k_dpmpp_3m_sde_exp'], {'scheduler': 'exponential', 'discard_next_to_last_sigma': True, "brownian_noise": True}), + ('DPM2', 'sample_dpm_2', ['k_dpm_2'], {'scheduler': 'karras', 'discard_next_to_last_sigma': True, "second_order": True}), + ('DPM2 a', 'sample_dpm_2_ancestral', ['k_dpm_2_a'], {'scheduler': 'karras', 'discard_next_to_last_sigma': True, "uses_ensd": True, "second_order": True}), ('DPM fast', 'sample_dpm_fast', ['k_dpm_fast'], {"uses_ensd": True}), ('DPM adaptive', 'sample_dpm_adaptive', ['k_dpm_ad'], {"uses_ensd": True}), - ('LMS Karras', 'sample_lms', ['k_lms_ka'], {'scheduler': 'karras'}), - ('DPM2 Karras', 'sample_dpm_2', ['k_dpm_2_ka'], {'scheduler': 'karras', 'discard_next_to_last_sigma': True, "uses_ensd": True, "second_order": True}), - ('DPM2 a Karras', 'sample_dpm_2_ancestral', ['k_dpm_2_a_ka'], {'scheduler': 'karras', 'discard_next_to_last_sigma': True, "uses_ensd": True, "second_order": True}), - ('DPM++ 2S a Karras', 'sample_dpmpp_2s_ancestral', ['k_dpmpp_2s_a_ka'], {'scheduler': 'karras', "uses_ensd": True, "second_order": True}), ('Restart', sd_samplers_extra.restart_sampler, ['restart'], {'scheduler': 'karras', "second_order": True}), ] @@ -58,20 +46,20 @@ sampler_extra_params = { } k_diffusion_samplers_map = {x.name: x for x in samplers_data_k_diffusion} -k_diffusion_scheduler = { - 'Automatic': None, - 'karras': k_diffusion.sampling.get_sigmas_karras, - 'exponential': k_diffusion.sampling.get_sigmas_exponential, - 'polyexponential': k_diffusion.sampling.get_sigmas_polyexponential -} +k_diffusion_scheduler = {x.name: x.function for x in sd_schedulers.schedulers} class CFGDenoiserKDiffusion(sd_samplers_cfg_denoiser.CFGDenoiser): @property def inner_model(self): if self.model_wrap is None: - denoiser = k_diffusion.external.CompVisVDenoiser if shared.sd_model.parameterization == "v" else k_diffusion.external.CompVisDenoiser - self.model_wrap = denoiser(shared.sd_model, quantize=shared.opts.enable_quantization) + denoiser_constructor = getattr(shared.sd_model, 'create_denoiser', None) + + if denoiser_constructor is not None: + self.model_wrap = denoiser_constructor() + else: + denoiser = k_diffusion.external.CompVisVDenoiser if shared.sd_model.parameterization == "v" else k_diffusion.external.CompVisDenoiser + self.model_wrap = denoiser(shared.sd_model, quantize=shared.opts.enable_quantization) return self.model_wrap @@ -96,47 +84,52 @@ class KDiffusionSampler(sd_samplers_common.Sampler): steps += 1 if discard_next_to_last_sigma else 0 + scheduler_name = (p.hr_scheduler if p.is_hr_pass else p.scheduler) or 'Automatic' + if scheduler_name == 'Automatic': + scheduler_name = self.config.options.get('scheduler', None) + + scheduler = sd_schedulers.schedulers_map.get(scheduler_name) + + m_sigma_min, m_sigma_max = self.model_wrap.sigmas[0].item(), self.model_wrap.sigmas[-1].item() + sigma_min, sigma_max = (0.1, 10) if opts.use_old_karras_scheduler_sigmas else (m_sigma_min, m_sigma_max) + if p.sampler_noise_scheduler_override: sigmas = p.sampler_noise_scheduler_override(steps) - elif opts.k_sched_type != "Automatic": - m_sigma_min, m_sigma_max = (self.model_wrap.sigmas[0].item(), self.model_wrap.sigmas[-1].item()) - sigma_min, sigma_max = (0.1, 10) if opts.use_old_karras_scheduler_sigmas else (m_sigma_min, m_sigma_max) - sigmas_kwargs = { - 'sigma_min': sigma_min, - 'sigma_max': sigma_max, - } + elif scheduler is None or scheduler.function is None: + sigmas = self.model_wrap.get_sigmas(steps) + else: + sigmas_kwargs = {'sigma_min': sigma_min, 'sigma_max': sigma_max} - sigmas_func = k_diffusion_scheduler[opts.k_sched_type] - p.extra_generation_params["Schedule type"] = opts.k_sched_type + if scheduler.label != 'Automatic' and not p.is_hr_pass: + p.extra_generation_params["Schedule type"] = scheduler.label + elif scheduler.label != p.extra_generation_params.get("Schedule type"): + p.extra_generation_params["Hires schedule type"] = scheduler.label - if opts.sigma_min != m_sigma_min and opts.sigma_min != 0: + if opts.sigma_min != 0 and opts.sigma_min != m_sigma_min: sigmas_kwargs['sigma_min'] = opts.sigma_min p.extra_generation_params["Schedule min sigma"] = opts.sigma_min - if opts.sigma_max != m_sigma_max and opts.sigma_max != 0: + + if opts.sigma_max != 0 and opts.sigma_max != m_sigma_max: sigmas_kwargs['sigma_max'] = opts.sigma_max p.extra_generation_params["Schedule max sigma"] = opts.sigma_max - default_rho = 1. if opts.k_sched_type == "polyexponential" else 7. - - if opts.k_sched_type != 'exponential' and opts.rho != 0 and opts.rho != default_rho: + if scheduler.default_rho != -1 and opts.rho != 0 and opts.rho != scheduler.default_rho: sigmas_kwargs['rho'] = opts.rho p.extra_generation_params["Schedule rho"] = opts.rho - sigmas = sigmas_func(n=steps, **sigmas_kwargs, device=shared.device) - elif self.config is not None and self.config.options.get('scheduler', None) == 'karras': - sigma_min, sigma_max = (0.1, 10) if opts.use_old_karras_scheduler_sigmas else (self.model_wrap.sigmas[0].item(), self.model_wrap.sigmas[-1].item()) + if scheduler.need_inner_model: + sigmas_kwargs['inner_model'] = self.model_wrap - sigmas = k_diffusion.sampling.get_sigmas_karras(n=steps, sigma_min=sigma_min, sigma_max=sigma_max, device=shared.device) - elif self.config is not None and self.config.options.get('scheduler', None) == 'exponential': - m_sigma_min, m_sigma_max = (self.model_wrap.sigmas[0].item(), self.model_wrap.sigmas[-1].item()) - sigmas = k_diffusion.sampling.get_sigmas_exponential(n=steps, sigma_min=m_sigma_min, sigma_max=m_sigma_max, device=shared.device) - else: - sigmas = self.model_wrap.get_sigmas(steps) + if scheduler.label == 'Beta': + p.extra_generation_params["Beta schedule alpha"] = opts.beta_dist_alpha + p.extra_generation_params["Beta schedule beta"] = opts.beta_dist_beta + + sigmas = scheduler.function(n=steps, **sigmas_kwargs, device=devices.cpu) if discard_next_to_last_sigma: sigmas = torch.cat([sigmas[:-2], sigmas[-1:]]) - return sigmas + return sigmas.cpu() def sample_img2img(self, p, x, noise, conditioning, unconditional_conditioning, steps=None, image_conditioning=None): steps, t_enc = sd_samplers_common.setup_img2img_steps(p, steps) @@ -144,7 +137,10 @@ class KDiffusionSampler(sd_samplers_common.Sampler): sigmas = self.get_sigmas(p, steps) sigma_sched = sigmas[steps - t_enc - 1:] - xi = x + noise * sigma_sched[0] + if hasattr(shared.sd_model, 'add_noise_to_latent'): + xi = shared.sd_model.add_noise_to_latent(x, noise, sigma_sched[0]) + else: + xi = x + noise * sigma_sched[0] if opts.img2img_extra_noise > 0: p.extra_generation_params["Extra noise"] = opts.img2img_extra_noise diff --git a/modules/sd_samplers_timesteps.py b/modules/sd_samplers_timesteps.py index 8cc7d384..81edd67d 100644 --- a/modules/sd_samplers_timesteps.py +++ b/modules/sd_samplers_timesteps.py @@ -10,6 +10,7 @@ import modules.shared as shared samplers_timesteps = [ ('DDIM', sd_samplers_timesteps_impl.ddim, ['ddim'], {}), + ('DDIM CFG++', sd_samplers_timesteps_impl.ddim_cfgpp, ['ddim_cfgpp'], {}), ('PLMS', sd_samplers_timesteps_impl.plms, ['plms'], {}), ('UniPC', sd_samplers_timesteps_impl.unipc, ['unipc'], {}), ] diff --git a/modules/sd_samplers_timesteps_impl.py b/modules/sd_samplers_timesteps_impl.py index 930a64af..180e4389 100644 --- a/modules/sd_samplers_timesteps_impl.py +++ b/modules/sd_samplers_timesteps_impl.py @@ -5,13 +5,14 @@ import numpy as np from modules import shared from modules.models.diffusion.uni_pc import uni_pc +from modules.torch_utils import float64 @torch.no_grad() def ddim(model, x, timesteps, extra_args=None, callback=None, disable=None, eta=0.0): alphas_cumprod = model.inner_model.inner_model.alphas_cumprod alphas = alphas_cumprod[timesteps] - alphas_prev = alphas_cumprod[torch.nn.functional.pad(timesteps[:-1], pad=(1, 0))].to(torch.float64 if x.device.type != 'mps' and x.device.type != 'xpu' else torch.float32) + alphas_prev = alphas_cumprod[torch.nn.functional.pad(timesteps[:-1], pad=(1, 0))].to(float64(x)) sqrt_one_minus_alphas = torch.sqrt(1 - alphas) sigmas = eta * np.sqrt((1 - alphas_prev.cpu().numpy()) / (1 - alphas.cpu()) * (1 - alphas.cpu() / alphas_prev.cpu().numpy())) @@ -39,11 +40,51 @@ def ddim(model, x, timesteps, extra_args=None, callback=None, disable=None, eta= return x +@torch.no_grad() +def ddim_cfgpp(model, x, timesteps, extra_args=None, callback=None, disable=None, eta=0.0): + """ Implements CFG++: Manifold-constrained Classifier Free Guidance For Diffusion Models (2024). + Uses the unconditional noise prediction instead of the conditional noise to guide the denoising direction. + The CFG scale is divided by 12.5 to map CFG from [0.0, 12.5] to [0, 1.0]. + """ + alphas_cumprod = model.inner_model.inner_model.alphas_cumprod + alphas = alphas_cumprod[timesteps] + alphas_prev = alphas_cumprod[torch.nn.functional.pad(timesteps[:-1], pad=(1, 0))].to(float64(x)) + sqrt_one_minus_alphas = torch.sqrt(1 - alphas) + sigmas = eta * np.sqrt((1 - alphas_prev.cpu().numpy()) / (1 - alphas.cpu()) * (1 - alphas.cpu() / alphas_prev.cpu().numpy())) + + model.cond_scale_miltiplier = 1 / 12.5 + model.need_last_noise_uncond = True + + extra_args = {} if extra_args is None else extra_args + s_in = x.new_ones((x.shape[0])) + s_x = x.new_ones((x.shape[0], 1, 1, 1)) + for i in tqdm.trange(len(timesteps) - 1, disable=disable): + index = len(timesteps) - 1 - i + + e_t = model(x, timesteps[index].item() * s_in, **extra_args) + last_noise_uncond = model.last_noise_uncond + + a_t = alphas[index].item() * s_x + a_prev = alphas_prev[index].item() * s_x + sigma_t = sigmas[index].item() * s_x + sqrt_one_minus_at = sqrt_one_minus_alphas[index].item() * s_x + + pred_x0 = (x - sqrt_one_minus_at * e_t) / a_t.sqrt() + dir_xt = (1. - a_prev - sigma_t ** 2).sqrt() * last_noise_uncond + noise = sigma_t * k_diffusion.sampling.torch.randn_like(x) + x = a_prev.sqrt() * pred_x0 + dir_xt + noise + + if callback is not None: + callback({'x': x, 'i': i, 'sigma': 0, 'sigma_hat': 0, 'denoised': pred_x0}) + + return x + + @torch.no_grad() def plms(model, x, timesteps, extra_args=None, callback=None, disable=None): alphas_cumprod = model.inner_model.inner_model.alphas_cumprod alphas = alphas_cumprod[timesteps] - alphas_prev = alphas_cumprod[torch.nn.functional.pad(timesteps[:-1], pad=(1, 0))].to(torch.float64 if x.device.type != 'mps' and x.device.type != 'xpu' else torch.float32) + alphas_prev = alphas_cumprod[torch.nn.functional.pad(timesteps[:-1], pad=(1, 0))].to(float64(x)) sqrt_one_minus_alphas = torch.sqrt(1 - alphas) extra_args = {} if extra_args is None else extra_args diff --git a/modules/sd_schedulers.py b/modules/sd_schedulers.py new file mode 100644 index 00000000..f4d16e30 --- /dev/null +++ b/modules/sd_schedulers.py @@ -0,0 +1,145 @@ +import dataclasses +import torch +import k_diffusion +import numpy as np +from scipy import stats + +from modules import shared + + +def to_d(x, sigma, denoised): + """Converts a denoiser output to a Karras ODE derivative.""" + return (x - denoised) / sigma + + +k_diffusion.sampling.to_d = to_d + + +@dataclasses.dataclass +class Scheduler: + name: str + label: str + function: any + + default_rho: float = -1 + need_inner_model: bool = False + aliases: list = None + + +def uniform(n, sigma_min, sigma_max, inner_model, device): + return inner_model.get_sigmas(n).to(device) + + +def sgm_uniform(n, sigma_min, sigma_max, inner_model, device): + start = inner_model.sigma_to_t(torch.tensor(sigma_max)) + end = inner_model.sigma_to_t(torch.tensor(sigma_min)) + sigs = [ + inner_model.t_to_sigma(ts) + for ts in torch.linspace(start, end, n + 1)[:-1] + ] + sigs += [0.0] + return torch.FloatTensor(sigs).to(device) + + +def get_align_your_steps_sigmas(n, sigma_min, sigma_max, device): + # https://research.nvidia.com/labs/toronto-ai/AlignYourSteps/howto.html + def loglinear_interp(t_steps, num_steps): + """ + Performs log-linear interpolation of a given array of decreasing numbers. + """ + xs = np.linspace(0, 1, len(t_steps)) + ys = np.log(t_steps[::-1]) + + new_xs = np.linspace(0, 1, num_steps) + new_ys = np.interp(new_xs, xs, ys) + + interped_ys = np.exp(new_ys)[::-1].copy() + return interped_ys + + if shared.sd_model.is_sdxl: + sigmas = [14.615, 6.315, 3.771, 2.181, 1.342, 0.862, 0.555, 0.380, 0.234, 0.113, 0.029] + else: + # Default to SD 1.5 sigmas. + sigmas = [14.615, 6.475, 3.861, 2.697, 1.886, 1.396, 0.963, 0.652, 0.399, 0.152, 0.029] + + if n != len(sigmas): + sigmas = np.append(loglinear_interp(sigmas, n), [0.0]) + else: + sigmas.append(0.0) + + return torch.FloatTensor(sigmas).to(device) + + +def kl_optimal(n, sigma_min, sigma_max, device): + alpha_min = torch.arctan(torch.tensor(sigma_min, device=device)) + alpha_max = torch.arctan(torch.tensor(sigma_max, device=device)) + step_indices = torch.arange(n + 1, device=device) + sigmas = torch.tan(step_indices / n * alpha_min + (1.0 - step_indices / n) * alpha_max) + return sigmas + + +def simple_scheduler(n, sigma_min, sigma_max, inner_model, device): + sigs = [] + ss = len(inner_model.sigmas) / n + for x in range(n): + sigs += [float(inner_model.sigmas[-(1 + int(x * ss))])] + sigs += [0.0] + return torch.FloatTensor(sigs).to(device) + + +def normal_scheduler(n, sigma_min, sigma_max, inner_model, device, sgm=False, floor=False): + start = inner_model.sigma_to_t(torch.tensor(sigma_max)) + end = inner_model.sigma_to_t(torch.tensor(sigma_min)) + + if sgm: + timesteps = torch.linspace(start, end, n + 1)[:-1] + else: + timesteps = torch.linspace(start, end, n) + + sigs = [] + for x in range(len(timesteps)): + ts = timesteps[x] + sigs.append(inner_model.t_to_sigma(ts)) + sigs += [0.0] + return torch.FloatTensor(sigs).to(device) + + +def ddim_scheduler(n, sigma_min, sigma_max, inner_model, device): + sigs = [] + ss = max(len(inner_model.sigmas) // n, 1) + x = 1 + while x < len(inner_model.sigmas): + sigs += [float(inner_model.sigmas[x])] + x += ss + sigs = sigs[::-1] + sigs += [0.0] + return torch.FloatTensor(sigs).to(device) + + +def beta_scheduler(n, sigma_min, sigma_max, inner_model, device): + # From "Beta Sampling is All You Need" [arXiv:2407.12173] (Lee et. al, 2024) """ + alpha = shared.opts.beta_dist_alpha + beta = shared.opts.beta_dist_beta + timesteps = 1 - np.linspace(0, 1, n) + timesteps = [stats.beta.ppf(x, alpha, beta) for x in timesteps] + sigmas = [sigma_min + (x * (sigma_max-sigma_min)) for x in timesteps] + sigmas += [0.0] + return torch.FloatTensor(sigmas).to(device) + + +schedulers = [ + Scheduler('automatic', 'Automatic', None), + Scheduler('uniform', 'Uniform', uniform, need_inner_model=True), + Scheduler('karras', 'Karras', k_diffusion.sampling.get_sigmas_karras, default_rho=7.0), + Scheduler('exponential', 'Exponential', k_diffusion.sampling.get_sigmas_exponential), + Scheduler('polyexponential', 'Polyexponential', k_diffusion.sampling.get_sigmas_polyexponential, default_rho=1.0), + Scheduler('sgm_uniform', 'SGM Uniform', sgm_uniform, need_inner_model=True, aliases=["SGMUniform"]), + Scheduler('kl_optimal', 'KL Optimal', kl_optimal), + Scheduler('align_your_steps', 'Align Your Steps', get_align_your_steps_sigmas), + Scheduler('simple', 'Simple', simple_scheduler, need_inner_model=True), + Scheduler('normal', 'Normal', normal_scheduler, need_inner_model=True), + Scheduler('ddim', 'DDIM', ddim_scheduler, need_inner_model=True), + Scheduler('beta', 'Beta', beta_scheduler, need_inner_model=True), +] + +schedulers_map = {**{x.name: x for x in schedulers}, **{x.label: x for x in schedulers}} diff --git a/modules/sd_vae_approx.py b/modules/sd_vae_approx.py index 3965e223..c5dda743 100644 --- a/modules/sd_vae_approx.py +++ b/modules/sd_vae_approx.py @@ -8,9 +8,9 @@ sd_vae_approx_models = {} class VAEApprox(nn.Module): - def __init__(self): + def __init__(self, latent_channels=4): super(VAEApprox, self).__init__() - self.conv1 = nn.Conv2d(4, 8, (7, 7)) + self.conv1 = nn.Conv2d(latent_channels, 8, (7, 7)) self.conv2 = nn.Conv2d(8, 16, (5, 5)) self.conv3 = nn.Conv2d(16, 32, (3, 3)) self.conv4 = nn.Conv2d(32, 64, (3, 3)) @@ -40,7 +40,13 @@ def download_model(model_path, model_url): def model(): - model_name = "vaeapprox-sdxl.pt" if getattr(shared.sd_model, 'is_sdxl', False) else "model.pt" + if shared.sd_model.is_sd3: + model_name = "vaeapprox-sd3.pt" + elif shared.sd_model.is_sdxl: + model_name = "vaeapprox-sdxl.pt" + else: + model_name = "model.pt" + loaded_model = sd_vae_approx_models.get(model_name) if loaded_model is None: @@ -52,7 +58,7 @@ def model(): model_path = os.path.join(paths.models_path, "VAE-approx", model_name) download_model(model_path, 'https://github.com/AUTOMATIC1111/stable-diffusion-webui/releases/download/v1.0.0-pre/' + model_name) - loaded_model = VAEApprox() + loaded_model = VAEApprox(latent_channels=shared.sd_model.latent_channels) loaded_model.load_state_dict(torch.load(model_path, map_location='cpu' if devices.device.type != 'cuda' else None)) loaded_model.eval() loaded_model.to(devices.device, devices.dtype) @@ -64,7 +70,18 @@ def model(): def cheap_approximation(sample): # https://discuss.huggingface.co/t/decoding-latents-to-rgb-without-upscaling/23204/2 - if shared.sd_model.is_sdxl: + if shared.sd_model.is_sd3: + coeffs = [ + [-0.0645, 0.0177, 0.1052], [ 0.0028, 0.0312, 0.0650], + [ 0.1848, 0.0762, 0.0360], [ 0.0944, 0.0360, 0.0889], + [ 0.0897, 0.0506, -0.0364], [-0.0020, 0.1203, 0.0284], + [ 0.0855, 0.0118, 0.0283], [-0.0539, 0.0658, 0.1047], + [-0.0057, 0.0116, 0.0700], [-0.0412, 0.0281, -0.0039], + [ 0.1106, 0.1171, 0.1220], [-0.0248, 0.0682, -0.0481], + [ 0.0815, 0.0846, 0.1207], [-0.0120, -0.0055, -0.0867], + [-0.0749, -0.0634, -0.0456], [-0.1418, -0.1457, -0.1259], + ] + elif shared.sd_model.is_sdxl: coeffs = [ [ 0.3448, 0.4168, 0.4395], [-0.1953, -0.0290, 0.0250], diff --git a/modules/sd_vae_taesd.py b/modules/sd_vae_taesd.py index 808eb362..d06253d2 100644 --- a/modules/sd_vae_taesd.py +++ b/modules/sd_vae_taesd.py @@ -34,9 +34,9 @@ class Block(nn.Module): return self.fuse(self.conv(x) + self.skip(x)) -def decoder(): +def decoder(latent_channels=4): return nn.Sequential( - Clamp(), conv(4, 64), nn.ReLU(), + Clamp(), conv(latent_channels, 64), nn.ReLU(), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), @@ -44,13 +44,13 @@ def decoder(): ) -def encoder(): +def encoder(latent_channels=4): return nn.Sequential( conv(3, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), - conv(64, 4), + conv(64, latent_channels), ) @@ -58,10 +58,14 @@ class TAESDDecoder(nn.Module): latent_magnitude = 3 latent_shift = 0.5 - def __init__(self, decoder_path="taesd_decoder.pth"): + def __init__(self, decoder_path="taesd_decoder.pth", latent_channels=None): """Initialize pretrained TAESD on the given device from the given checkpoints.""" super().__init__() - self.decoder = decoder() + + if latent_channels is None: + latent_channels = 16 if "taesd3" in str(decoder_path) else 4 + + self.decoder = decoder(latent_channels) self.decoder.load_state_dict( torch.load(decoder_path, map_location='cpu' if devices.device.type != 'cuda' else None)) @@ -70,10 +74,14 @@ class TAESDEncoder(nn.Module): latent_magnitude = 3 latent_shift = 0.5 - def __init__(self, encoder_path="taesd_encoder.pth"): + def __init__(self, encoder_path="taesd_encoder.pth", latent_channels=None): """Initialize pretrained TAESD on the given device from the given checkpoints.""" super().__init__() - self.encoder = encoder() + + if latent_channels is None: + latent_channels = 16 if "taesd3" in str(encoder_path) else 4 + + self.encoder = encoder(latent_channels) self.encoder.load_state_dict( torch.load(encoder_path, map_location='cpu' if devices.device.type != 'cuda' else None)) @@ -87,7 +95,13 @@ def download_model(model_path, model_url): def decoder_model(): - model_name = "taesdxl_decoder.pth" if getattr(shared.sd_model, 'is_sdxl', False) else "taesd_decoder.pth" + if shared.sd_model.is_sd3: + model_name = "taesd3_decoder.pth" + elif shared.sd_model.is_sdxl: + model_name = "taesdxl_decoder.pth" + else: + model_name = "taesd_decoder.pth" + loaded_model = sd_vae_taesd_models.get(model_name) if loaded_model is None: @@ -106,7 +120,13 @@ def decoder_model(): def encoder_model(): - model_name = "taesdxl_encoder.pth" if getattr(shared.sd_model, 'is_sdxl', False) else "taesd_encoder.pth" + if shared.sd_model.is_sd3: + model_name = "taesd3_encoder.pth" + elif shared.sd_model.is_sdxl: + model_name = "taesdxl_encoder.pth" + else: + model_name = "taesd_encoder.pth" + loaded_model = sd_vae_taesd_models.get(model_name) if loaded_model is None: diff --git a/modules/shared.py b/modules/shared.py index ccdca4e7..2a3787f9 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -6,6 +6,10 @@ import gradio as gr from modules import shared_cmd_options, shared_gradio_themes, options, shared_items, sd_models_types from modules.paths_internal import models_path, script_path, data_path, sd_configs_path, sd_default_config, sd_model_file, default_sd_model_file, extensions_dir, extensions_builtin_dir # noqa: F401 from modules import util +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from modules import shared_state, styles, interrogate, shared_total_tqdm, memmon cmd_opts = shared_cmd_options.cmd_opts parser = shared_cmd_options.parser @@ -16,11 +20,11 @@ styles_filename = cmd_opts.styles_file = cmd_opts.styles_file if len(cmd_opts.st config_filename = cmd_opts.ui_settings_file hide_dirs = {"visible": not cmd_opts.hide_ui_dir_config} -demo = None +demo: gr.Blocks = None -device = None +device: str = None -weight_load_location = None +weight_load_location: str = None xformers_available = False @@ -28,22 +32,22 @@ hypernetworks = {} loaded_hypernetworks = [] -state = None +state: 'shared_state.State' = None -prompt_styles = None +prompt_styles: 'styles.StyleDatabase' = None -interrogator = None +interrogator: 'interrogate.InterrogateModels' = None face_restorers = [] -options_templates = None -opts = None -restricted_opts = None +options_templates: dict = None +opts: options.Options = None +restricted_opts: set[str] = None sd_model: sd_models_types.WebuiSdModel = None -settings_components = None -"""assinged from ui.py, a mapping on setting names to gradio components repsponsible for those settings""" +settings_components: dict = None +"""assigned from ui.py, a mapping on setting names to gradio components responsible for those settings""" tab_names = [] @@ -65,9 +69,9 @@ progress_print_out = sys.stdout gradio_theme = gr.themes.Base() -total_tqdm = None +total_tqdm: 'shared_total_tqdm.TotalTQDM' = None -mem_mon = None +mem_mon: 'memmon.MemUsageMonitor' = None options_section = options.options_section OptionInfo = options.OptionInfo @@ -86,3 +90,5 @@ list_checkpoint_tiles = shared_items.list_checkpoint_tiles refresh_checkpoints = shared_items.refresh_checkpoints list_samplers = shared_items.list_samplers reload_hypernetworks = shared_items.reload_hypernetworks + +hf_endpoint = os.getenv('HF_ENDPOINT', 'https://huggingface.co') diff --git a/modules/shared_gradio_themes.py b/modules/shared_gradio_themes.py index b6dc3145..b4e3f32b 100644 --- a/modules/shared_gradio_themes.py +++ b/modules/shared_gradio_themes.py @@ -69,3 +69,44 @@ def reload_gradio_theme(theme_name=None): # append additional values gradio_theme shared.gradio_theme.sd_webui_modal_lightbox_toolbar_opacity = shared.opts.sd_webui_modal_lightbox_toolbar_opacity shared.gradio_theme.sd_webui_modal_lightbox_icon_opacity = shared.opts.sd_webui_modal_lightbox_icon_opacity + + +def resolve_var(name: str, gradio_theme=None, history=None): + """ + Attempt to resolve a theme variable name to its value + + Parameters: + name (str): The name of the theme variable + ie "background_fill_primary", "background_fill_primary_dark" + spaces and asterisk (*) prefix is removed from name before lookup + gradio_theme (gradio.themes.ThemeClass): The theme object to resolve the variable from + blank to use the webui default shared.gradio_theme + history (list): A list of previously resolved variables to prevent circular references + for regular use leave blank + Returns: + str: The resolved value + + Error handling: + return either #000000 or #ffffff depending on initial name ending with "_dark" + """ + try: + if history is None: + history = [] + if gradio_theme is None: + gradio_theme = shared.gradio_theme + + name = name.strip() + name = name[1:] if name.startswith("*") else name + + if name in history: + raise ValueError(f'Circular references: name "{name}" in {history}') + + if value := getattr(gradio_theme, name, None): + return resolve_var(value, gradio_theme, history + [name]) + else: + return name + + except Exception: + name = history[0] if history else name + errors.report(f'resolve_color({name})', exc_info=True) + return '#000000' if name.endswith("_dark") else '#ffffff' diff --git a/modules/shared_init.py b/modules/shared_init.py index 935e3a21..a6ad0433 100644 --- a/modules/shared_init.py +++ b/modules/shared_init.py @@ -31,6 +31,14 @@ def initialize(): devices.dtype_vae = torch.float32 if cmd_opts.no_half or cmd_opts.no_half_vae else torch.float16 devices.dtype_inference = torch.float32 if cmd_opts.precision == 'full' else devices.dtype + if cmd_opts.precision == "half": + msg = "--no-half and --no-half-vae conflict with --precision half" + assert devices.dtype == torch.float16, msg + assert devices.dtype_vae == torch.float16, msg + assert devices.dtype_inference == torch.float16, msg + devices.force_fp16 = True + devices.force_model_fp16() + shared.device = devices.device shared.weight_load_location = None if cmd_opts.lowram else "cpu" diff --git a/modules/shared_items.py b/modules/shared_items.py index 88f63645..11f10b3f 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -1,5 +1,8 @@ +import html import sys +from modules import script_callbacks, scripts, ui_components +from modules.options import OptionHTML, OptionInfo from modules.shared_cmd_options import cmd_opts @@ -118,6 +121,45 @@ def ui_reorder_categories(): yield "scripts" +def callbacks_order_settings(): + options = { + "sd_vae_explanation": OptionHTML(""" + For categories below, callbacks added to dropdowns happen before others, in order listed. + """), + + } + + callback_options = {} + + for category, _ in script_callbacks.enumerate_callbacks(): + callback_options[category] = script_callbacks.ordered_callbacks(category, enable_user_sort=False) + + for method_name in scripts.scripts_txt2img.callback_names: + callback_options["script_" + method_name] = scripts.scripts_txt2img.create_ordered_callbacks_list(method_name, enable_user_sort=False) + + for method_name in scripts.scripts_img2img.callback_names: + callbacks = callback_options.get("script_" + method_name, []) + + for addition in scripts.scripts_img2img.create_ordered_callbacks_list(method_name, enable_user_sort=False): + if any(x.name == addition.name for x in callbacks): + continue + + callbacks.append(addition) + + callback_options["script_" + method_name] = callbacks + + for category, callbacks in callback_options.items(): + if not callbacks: + continue + + option_info = OptionInfo([], f"{category} callback priority", ui_components.DropdownMulti, {"choices": [x.name for x in callbacks]}) + option_info.needs_restart() + option_info.html("
Default order:
    " + "".join(f"
  1. {html.escape(x.name)}
  2. \n" for x in callbacks) + "
") + options['prioritized_callbacks_' + category] = option_info + + return options + + class Shared(sys.modules[__name__].__class__): """ this class is here to provide sd_model field as a property, so that it can be created and loaded on demand rather than diff --git a/modules/shared_options.py b/modules/shared_options.py index 64f8f196..9f452027 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -19,7 +19,9 @@ restricted_opts = { "outdir_grids", "outdir_txt2img_grids", "outdir_save", - "outdir_init_images" + "outdir_init_images", + "temp_dir", + "clean_temp_dir_at_start", } categories.register_category("saving", "Saving images") @@ -52,7 +54,7 @@ options_templates.update(options_section(('saving-images', "Saving images/grids" "save_images_before_color_correction": OptionInfo(False, "Save a copy of image before applying color correction to img2img results"), "save_mask": OptionInfo(False, "For inpainting, save a copy of the greyscale mask"), "save_mask_composite": OptionInfo(False, "For inpainting, save a masked composite"), - "jpeg_quality": OptionInfo(80, "Quality for saved jpeg images", gr.Slider, {"minimum": 1, "maximum": 100, "step": 1}), + "jpeg_quality": OptionInfo(80, "Quality for saved jpeg and avif images", gr.Slider, {"minimum": 1, "maximum": 100, "step": 1}), "webp_lossless": OptionInfo(False, "Use lossless compression for webp images"), "export_for_4chan": OptionInfo(True, "Save copy of large images as JPG").info("if the file size is above the limit, or either width or height are above the limit"), "img_downscale_threshold": OptionInfo(4.0, "File size limit for the above option, MB", gr.Number), @@ -62,6 +64,7 @@ options_templates.update(options_section(('saving-images', "Saving images/grids" "use_original_name_batch": OptionInfo(True, "Use original name for output filename during batch process in extras tab"), "use_upscaler_name_as_suffix": OptionInfo(False, "Use upscaler name as filename suffix in the extras tab"), "save_selected_only": OptionInfo(True, "When using 'Save' button, only save a single selected image"), + "save_write_log_csv": OptionInfo(True, "Write log.csv when saving images using 'Save' button"), "save_init_img": OptionInfo(False, "Save init images when using img2img"), "temp_dir": OptionInfo("", "Directory for temporary images; leave empty for default"), @@ -101,6 +104,7 @@ options_templates.update(options_section(('upscaling', "Upscaling", "postprocess "DAT_tile": OptionInfo(192, "Tile size for DAT upscalers.", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}).info("0 = no tiling"), "DAT_tile_overlap": OptionInfo(8, "Tile overlap for DAT upscalers.", gr.Slider, {"minimum": 0, "maximum": 48, "step": 1}).info("Low values = visible seam"), "upscaler_for_img2img": OptionInfo(None, "Upscaler for img2img", gr.Dropdown, lambda: {"choices": [x.name for x in shared.sd_upscalers]}), + "set_scale_by_when_changing_upscaler": OptionInfo(False, "Automatically set the Scale by factor based on the name of the selected Upscaler."), })) options_templates.update(options_section(('face-restoration', "Face restoration", "postprocessing"), { @@ -126,6 +130,22 @@ options_templates.update(options_section(('system', "System", "system"), { "dump_stacks_on_signal": OptionInfo(False, "Print stack traces before exiting the program with ctrl+c."), })) +options_templates.update(options_section(('profiler', "Profiler", "system"), { + "profiling_explanation": OptionHTML(""" +Those settings allow you to enable torch profiler when generating pictures. +Profiling allows you to see which code uses how much of computer's resources during generation. +Each generation writes its own profile to one file, overwriting previous. +The file can be viewed in Chrome, or on a Perfetto web site. +Warning: writing profile can take a lot of time, up to 30 seconds, and the file itelf can be around 500MB in size. +"""), + "profiling_enable": OptionInfo(False, "Enable profiling"), + "profiling_activities": OptionInfo(["CPU"], "Activities", gr.CheckboxGroup, {"choices": ["CPU", "CUDA"]}), + "profiling_record_shapes": OptionInfo(True, "Record shapes"), + "profiling_profile_memory": OptionInfo(True, "Profile memory"), + "profiling_with_stack": OptionInfo(True, "Include python stack"), + "profiling_filename": OptionInfo("trace.json", "Profile filename"), +})) + options_templates.update(options_section(('API', "API", "system"), { "api_enable_requests": OptionInfo(True, "Allow http:// and https:// URLs for input images in API", restrict_api=True), "api_forbid_local_requests": OptionInfo(True, "Forbid URLs to local resources", restrict_api=True), @@ -157,6 +177,7 @@ options_templates.update(options_section(('sd', "Stable Diffusion", "sd"), { "emphasis": OptionInfo("Original", "Emphasis mode", gr.Radio, lambda: {"choices": [x.name for x in sd_emphasis.options]}, infotext="Emphasis").info("makes it possible to make model to pay (more:1.1) or (less:0.9) attention to text when you use the syntax in prompt; " + sd_emphasis.get_options_descriptions()), "enable_batch_seeds": OptionInfo(True, "Make K-diffusion samplers produce same images in a batch as when making a single image"), "comma_padding_backtrack": OptionInfo(20, "Prompt word wrap length limit", gr.Slider, {"minimum": 0, "maximum": 74, "step": 1}).info("in tokens - for texts shorter than specified, if they don't fit into 75 token limit, move them to the next 75 token chunk"), + "sdxl_clip_l_skip": OptionInfo(False, "Clip skip SDXL", gr.Checkbox).info("Enable Clip skip for the secondary clip model in sdxl. Has no effect on SD 1.5 or SD 2.0/2.1."), "CLIP_stop_at_last_layers": OptionInfo(1, "Clip skip", gr.Slider, {"minimum": 1, "maximum": 12, "step": 1}, infotext="Clip skip").link("wiki", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#clip-skip").info("ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer"), "upcast_attn": OptionInfo(False, "Upcast cross attention layer to float32"), "randn_source": OptionInfo("GPU", "Random number generator source.", gr.Radio, {"choices": ["GPU", "CPU", "NV"]}, infotext="RNG").info("changes seeds drastically; use CPU to produce the same picture across different videocard vendors; use NV to produce same picture as on NVidia videocards"), @@ -171,6 +192,10 @@ options_templates.update(options_section(('sdxl', "Stable Diffusion XL", "sd"), "sdxl_refiner_high_aesthetic_score": OptionInfo(6.0, "SDXL high aesthetic score", gr.Number).info("used for refiner model prompt"), })) +options_templates.update(options_section(('sd3', "Stable Diffusion 3", "sd"), { + "sd3_enable_t5": OptionInfo(False, "Enable T5").info("load T5 text encoder; increases VRAM use by a lot, potentially improving quality of generation; requires model reload to apply"), +})) + options_templates.update(options_section(('vae', "VAE", "sd"), { "sd_vae_explanation": OptionHTML(""" VAE is a neural network that transforms a standard RGB @@ -206,14 +231,15 @@ options_templates.update(options_section(('img2img', "img2img", "sd"), { options_templates.update(options_section(('optimizations', "Optimizations", "sd"), { "cross_attention_optimization": OptionInfo("Automatic", "Cross attention optimization", gr.Dropdown, lambda: {"choices": shared_items.cross_attention_optimizations()}), - "s_min_uncond": OptionInfo(0.0, "Negative Guidance minimum sigma", gr.Slider, {"minimum": 0.0, "maximum": 15.0, "step": 0.01}).link("PR", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/9177").info("skip negative prompt for some steps when the image is almost ready; 0=disable, higher=faster"), + "s_min_uncond": OptionInfo(0.0, "Negative Guidance minimum sigma", gr.Slider, {"minimum": 0.0, "maximum": 15.0, "step": 0.01}, infotext='NGMS').link("PR", "https://github.com/AUTOMATIC1111/stablediffusion-webui/pull/9177").info("skip negative prompt for some steps when the image is almost ready; 0=disable, higher=faster"), + "s_min_uncond_all": OptionInfo(False, "Negative Guidance minimum sigma all steps", infotext='NGMS all steps').info("By default, NGMS above skips every other step; this makes it skip all steps"), "token_merging_ratio": OptionInfo(0.0, "Token merging ratio", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}, infotext='Token merging ratio').link("PR", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/9256").info("0=disable, higher=faster"), "token_merging_ratio_img2img": OptionInfo(0.0, "Token merging ratio for img2img", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}).info("only applies if non-zero and overrides above"), "token_merging_ratio_hr": OptionInfo(0.0, "Token merging ratio for high-res pass", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}, infotext='Token merging ratio hr').info("only applies if non-zero and overrides above"), "pad_cond_uncond": OptionInfo(False, "Pad prompt/negative prompt", infotext='Pad conds').info("improves performance when prompt and negative prompt have different lengths; changes seeds"), "pad_cond_uncond_v0": OptionInfo(False, "Pad prompt/negative prompt (v0)", infotext='Pad conds v0').info("alternative implementation for the above; used prior to 1.6.0 for DDIM sampler; overrides the above if set; WARNING: truncates negative prompt if it's too long; changes seeds"), "persistent_cond_cache": OptionInfo(True, "Persistent cond cache").info("do not recalculate conds from prompts if prompts have not changed since previous calculation"), - "batch_cond_uncond": OptionInfo(True, "Batch cond/uncond").info("do both conditional and unconditional denoising in one batch; uses a bit more VRAM during sampling, but improves speed; previously this was controlled by --always-batch-cond-uncond comandline argument"), + "batch_cond_uncond": OptionInfo(True, "Batch cond/uncond").info("do both conditional and unconditional denoising in one batch; uses a bit more VRAM during sampling, but improves speed; previously this was controlled by --always-batch-cond-uncond commandline argument"), "fp8_storage": OptionInfo("Disable", "FP8 weight", gr.Radio, {"choices": ["Disable", "Enable for SDXL", "Enable"]}).info("Use FP8 to store Linear/Conv layers' weight. Require pytorch>=2.1.0."), "cache_fp16_weight": OptionInfo(False, "Cache FP16 weight for LoRA").info("Cache fp16 weight when enabling FP8, will increase the quality of LoRA. Use more system ram."), })) @@ -224,10 +250,10 @@ options_templates.update(options_section(('compatibility', "Compatibility", "sd" "use_old_karras_scheduler_sigmas": OptionInfo(False, "Use old karras scheduler sigmas (0.1 to 10)."), "no_dpmpp_sde_batch_determinism": OptionInfo(False, "Do not make DPM++ SDE deterministic across different batch sizes."), "use_old_hires_fix_width_height": OptionInfo(False, "For hires fix, use width/height sliders to set final resolution rather than first pass (disables Upscale by, Resize width/height to)."), - "dont_fix_second_order_samplers_schedule": OptionInfo(False, "Do not fix prompt schedule for second order samplers."), "hires_fix_use_firstpass_conds": OptionInfo(False, "For hires fix, calculate conds of second pass using extra networks of first pass."), "use_old_scheduling": OptionInfo(False, "Use old prompt editing timelines.", infotext="Old prompt editing timelines").info("For [red:green:N]; old: If N < 1, it's a fraction of steps (and hires fix uses range from 0 to 1), if N >= 1, it's an absolute number of steps; new: If N has a decimal point in it, it's a fraction of steps (and hires fix uses range from 1 to 2), othewrwise it's an absolute number of steps"), - "use_downcasted_alpha_bar": OptionInfo(False, "Downcast model alphas_cumprod to fp16 before sampling. For reproducing old seeds.", infotext="Downcast alphas_cumprod") + "use_downcasted_alpha_bar": OptionInfo(False, "Downcast model alphas_cumprod to fp16 before sampling. For reproducing old seeds.", infotext="Downcast alphas_cumprod"), + "refiner_switch_by_sample_steps": OptionInfo(False, "Switch to refiner by sampling steps instead of model timesteps. Old behavior for refiner.", infotext="Refiner switch by sampling steps") })) options_templates.update(options_section(('interrogate', "Interrogate"), { @@ -257,7 +283,9 @@ options_templates.update(options_section(('extra_networks', "Extra Networks", "s "extra_networks_card_description_is_html": OptionInfo(False, "Treat card description as HTML"), "extra_networks_card_order_field": OptionInfo("Path", "Default order field for Extra Networks cards", gr.Dropdown, {"choices": ['Path', 'Name', 'Date Created', 'Date Modified']}).needs_reload_ui(), "extra_networks_card_order": OptionInfo("Ascending", "Default order for Extra Networks cards", gr.Dropdown, {"choices": ['Ascending', 'Descending']}).needs_reload_ui(), - "extra_networks_tree_view_default_enabled": OptionInfo(False, "Enables the Extra Networks directory tree view by default").needs_reload_ui(), + "extra_networks_tree_view_style": OptionInfo("Dirs", "Extra Networks directory view style", gr.Radio, {"choices": ["Tree", "Dirs"]}).needs_reload_ui(), + "extra_networks_tree_view_default_enabled": OptionInfo(True, "Show the Extra Networks directory view by default").needs_reload_ui(), + "extra_networks_tree_view_default_width": OptionInfo(180, "Default width for the Extra Networks directory tree view", gr.Number).needs_reload_ui(), "extra_networks_add_text_separator": OptionInfo(" ", "Extra networks separator").info("extra text to add before <...> when adding extra network to prompt"), "ui_extra_networks_tab_reorder": OptionInfo("", "Extra networks tab order").needs_reload_ui(), "textual_inversion_print_at_load": OptionInfo(False, "Print a list of Textual Inversion embeddings when loading model"), @@ -311,6 +339,8 @@ options_templates.update(options_section(('ui', "User interface", "ui"), { "show_progress_in_title": OptionInfo(True, "Show generation progress in window title."), "send_seed": OptionInfo(True, "Send seed when sending prompt or image to other interface"), "send_size": OptionInfo(True, "Send size when sending prompt or image to another interface"), + "enable_reloading_ui_scripts": OptionInfo(False, "Reload UI scripts when using Reload UI option").info("useful for developing: if you make changes to UI scripts code, it is applied when the UI is reloded."), + })) @@ -351,6 +381,7 @@ options_templates.update(options_section(('ui', "Live previews", "ui"), { "live_preview_refresh_period": OptionInfo(1000, "Progressbar and preview update period").info("in milliseconds"), "live_preview_fast_interrupt": OptionInfo(False, "Return image with chosen live preview method on interrupt").info("makes interrupts faster"), "js_live_preview_in_modal_lightbox": OptionInfo(False, "Show Live preview in full page image viewer"), + "prevent_screen_sleep_during_generation": OptionInfo(True, "Prevent screen sleep during generation"), })) options_templates.update(options_section(('sampler-params', "Sampler parameters", "sd"), { @@ -362,22 +393,25 @@ options_templates.update(options_section(('sampler-params', "Sampler parameters" 's_tmin': OptionInfo(0.0, "sigma tmin", gr.Slider, {"minimum": 0.0, "maximum": 10.0, "step": 0.01}, infotext='Sigma tmin').info('enable stochasticity; start value of the sigma range; only applies to Euler, Heun, and DPM2'), 's_tmax': OptionInfo(0.0, "sigma tmax", gr.Slider, {"minimum": 0.0, "maximum": 999.0, "step": 0.01}, infotext='Sigma tmax').info("0 = inf; end value of the sigma range; only applies to Euler, Heun, and DPM2"), 's_noise': OptionInfo(1.0, "sigma noise", gr.Slider, {"minimum": 0.0, "maximum": 1.1, "step": 0.001}, infotext='Sigma noise').info('amount of additional noise to counteract loss of detail during sampling'), - 'k_sched_type': OptionInfo("Automatic", "Scheduler type", gr.Dropdown, {"choices": ["Automatic", "karras", "exponential", "polyexponential"]}, infotext='Schedule type').info("lets you override the noise schedule for k-diffusion samplers; choosing Automatic disables the three parameters below"), 'sigma_min': OptionInfo(0.0, "sigma min", gr.Number, infotext='Schedule min sigma').info("0 = default (~0.03); minimum noise strength for k-diffusion noise scheduler"), 'sigma_max': OptionInfo(0.0, "sigma max", gr.Number, infotext='Schedule max sigma').info("0 = default (~14.6); maximum noise strength for k-diffusion noise scheduler"), 'rho': OptionInfo(0.0, "rho", gr.Number, infotext='Schedule rho').info("0 = default (7 for karras, 1 for polyexponential); higher values result in a steeper noise schedule (decreases faster)"), 'eta_noise_seed_delta': OptionInfo(0, "Eta noise seed delta", gr.Number, {"precision": 0}, infotext='ENSD').info("ENSD; does not improve anything, just produces different results for ancestral samplers - only useful for reproducing images"), 'always_discard_next_to_last_sigma': OptionInfo(False, "Always discard next-to-last sigma", infotext='Discard penultimate sigma').link("PR", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/6044"), - 'sgm_noise_multiplier': OptionInfo(False, "SGM noise multiplier", infotext='SGM noise multplier').link("PR", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/12818").info("Match initial noise to official SDXL implementation - only useful for reproducing images"), + 'sgm_noise_multiplier': OptionInfo(False, "SGM noise multiplier", infotext='SGM noise multiplier').link("PR", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/12818").info("Match initial noise to official SDXL implementation - only useful for reproducing images"), 'uni_pc_variant': OptionInfo("bh1", "UniPC variant", gr.Radio, {"choices": ["bh1", "bh2", "vary_coeff"]}, infotext='UniPC variant'), 'uni_pc_skip_type': OptionInfo("time_uniform", "UniPC skip type", gr.Radio, {"choices": ["time_uniform", "time_quadratic", "logSNR"]}, infotext='UniPC skip type'), 'uni_pc_order': OptionInfo(3, "UniPC order", gr.Slider, {"minimum": 1, "maximum": 50, "step": 1}, infotext='UniPC order').info("must be < sampling steps"), 'uni_pc_lower_order_final': OptionInfo(True, "UniPC lower order final", infotext='UniPC lower order final'), - 'sd_noise_schedule': OptionInfo("Default", "Noise schedule for sampling", gr.Radio, {"choices": ["Default", "Zero Terminal SNR"]}, infotext="Noise Schedule").info("for use with zero terminal SNR trained models") + 'sd_noise_schedule': OptionInfo("Default", "Noise schedule for sampling", gr.Radio, {"choices": ["Default", "Zero Terminal SNR"]}, infotext="Noise Schedule").info("for use with zero terminal SNR trained models"), + 'skip_early_cond': OptionInfo(0.0, "Ignore negative prompt during early sampling", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}, infotext="Skip Early CFG").info("disables CFG on a proportion of steps at the beginning of generation; 0=skip none; 1=skip all; can both improve sample diversity/quality and speed up sampling"), + 'beta_dist_alpha': OptionInfo(0.6, "Beta scheduler - alpha", gr.Slider, {"minimum": 0.01, "maximum": 1.0, "step": 0.01}, infotext='Beta scheduler alpha').info('Default = 0.6; the alpha parameter of the beta distribution used in Beta sampling'), + 'beta_dist_beta': OptionInfo(0.6, "Beta scheduler - beta", gr.Slider, {"minimum": 0.01, "maximum": 1.0, "step": 0.01}, infotext='Beta scheduler beta').info('Default = 0.6; the beta parameter of the beta distribution used in Beta sampling'), })) options_templates.update(options_section(('postprocessing', "Postprocessing", "postprocessing"), { 'postprocessing_enable_in_main_ui': OptionInfo([], "Enable postprocessing operations in txt2img and img2img tabs", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), + 'postprocessing_disable_in_extras': OptionInfo([], "Disable postprocessing operations in extras tab", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), 'postprocessing_operation_order': OptionInfo([], "Postprocessing operation order", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), 'upscaling_max_images_in_cache': OptionInfo(5, "Maximum number of images in upscaling cache", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), 'postprocessing_existing_caption_action': OptionInfo("Ignore", "Action for existing captions", gr.Radio, {"choices": ["Ignore", "Keep", "Prepend", "Append"]}).info("when generating captions using postprocessing; Ignore = use generated; Keep = use original; Prepend/Append = combine both"), diff --git a/modules/shared_state.py b/modules/shared_state.py index 33996691..4cd53af6 100644 --- a/modules/shared_state.py +++ b/modules/shared_state.py @@ -157,10 +157,12 @@ class State: self.current_image_sampling_step = self.sampling_step except Exception: - # when switching models during genration, VAE would be on CPU, so creating an image will fail. + # when switching models during generation, VAE would be on CPU, so creating an image will fail. # we silently ignore this error errors.record_exception() def assign_current_image(self, image): + if shared.opts.live_previews_image_format == 'jpeg' and image.mode in ('RGBA', 'P'): + image = image.convert('RGB') self.current_image = image self.id_live_preview += 1 diff --git a/modules/styles.py b/modules/styles.py index 60bd8a7f..25f22d3d 100644 --- a/modules/styles.py +++ b/modules/styles.py @@ -1,3 +1,4 @@ +from __future__ import annotations from pathlib import Path from modules import errors import csv @@ -42,7 +43,7 @@ def extract_style_text_from_prompt(style_text, prompt): stripped_style_text = style_text.strip() if "{prompt}" in stripped_style_text: - left, right = stripped_style_text.split("{prompt}", 2) + left, _, right = stripped_style_text.partition("{prompt}") if stripped_prompt.startswith(left) and stripped_prompt.endswith(right): prompt = stripped_prompt[len(left):len(stripped_prompt)-len(right)] return True, prompt diff --git a/modules/sysinfo.py b/modules/sysinfo.py index f336251e..e9a83d74 100644 --- a/modules/sysinfo.py +++ b/modules/sysinfo.py @@ -1,15 +1,13 @@ import json import os import sys - +import subprocess import platform import hashlib -import pkg_resources -import psutil import re +from pathlib import Path -import launch -from modules import paths_internal, timer, shared, extensions, errors +from modules import paths_internal, timer, shared_cmd_options, errors, launch_utils checksum_token = "DontStealMyGamePlz__WINNERS_DONT_USE_DRUGS__DONT_COPY_THAT_FLOPPY" environment_whitelist = { @@ -69,14 +67,46 @@ def check(x): return h.hexdigest() == m.group(1) -def get_dict(): - ram = psutil.virtual_memory() +def get_cpu_info(): + cpu_info = {"model": platform.processor()} + try: + import psutil + cpu_info["count logical"] = psutil.cpu_count(logical=True) + cpu_info["count physical"] = psutil.cpu_count(logical=False) + except Exception as e: + cpu_info["error"] = str(e) + return cpu_info + +def get_ram_info(): + try: + import psutil + ram = psutil.virtual_memory() + return {x: pretty_bytes(getattr(ram, x, 0)) for x in ["total", "used", "free", "active", "inactive", "buffers", "cached", "shared"] if getattr(ram, x, 0) != 0} + except Exception as e: + return str(e) + + +def get_packages(): + try: + return subprocess.check_output([sys.executable, '-m', 'pip', 'freeze', '--all']).decode("utf8").splitlines() + except Exception as pip_error: + try: + import importlib.metadata + packages = importlib.metadata.distributions() + return sorted([f"{package.metadata['Name']}=={package.version}" for package in packages]) + except Exception as e2: + return {'error pip': pip_error, 'error importlib': str(e2)} + + +def get_dict(): + config = get_config() res = { "Platform": platform.platform(), "Python": platform.python_version(), - "Version": launch.git_tag(), - "Commit": launch.commit_hash(), + "Version": launch_utils.git_tag(), + "Commit": launch_utils.commit_hash(), + "Git status": git_status(paths_internal.script_path), "Script path": paths_internal.script_path, "Data path": paths_internal.data_path, "Extensions dir": paths_internal.extensions_dir, @@ -84,20 +114,14 @@ def get_dict(): "Commandline": get_argv(), "Torch env info": get_torch_sysinfo(), "Exceptions": errors.get_exceptions(), - "CPU": { - "model": platform.processor(), - "count logical": psutil.cpu_count(logical=True), - "count physical": psutil.cpu_count(logical=False), - }, - "RAM": { - x: pretty_bytes(getattr(ram, x, 0)) for x in ["total", "used", "free", "active", "inactive", "buffers", "cached", "shared"] if getattr(ram, x, 0) != 0 - }, - "Extensions": get_extensions(enabled=True), - "Inactive extensions": get_extensions(enabled=False), + "CPU": get_cpu_info(), + "RAM": get_ram_info(), + "Extensions": get_extensions(enabled=True, fallback_disabled_extensions=config.get('disabled_extensions', [])), + "Inactive extensions": get_extensions(enabled=False, fallback_disabled_extensions=config.get('disabled_extensions', [])), "Environment": get_environment(), - "Config": get_config(), + "Config": config, "Startup": timer.startup_record, - "Packages": sorted([f"{pkg.key}=={pkg.version}" for pkg in pkg_resources.working_set]), + "Packages": get_packages(), } return res @@ -111,11 +135,11 @@ def get_argv(): res = [] for v in sys.argv: - if shared.cmd_opts.gradio_auth and shared.cmd_opts.gradio_auth == v: + if shared_cmd_options.cmd_opts.gradio_auth and shared_cmd_options.cmd_opts.gradio_auth == v: res.append("") continue - if shared.cmd_opts.api_auth and shared.cmd_opts.api_auth == v: + if shared_cmd_options.cmd_opts.api_auth and shared_cmd_options.cmd_opts.api_auth == v: res.append("") continue @@ -123,6 +147,7 @@ def get_argv(): return res + re_newline = re.compile(r"\r*\n") @@ -136,25 +161,55 @@ def get_torch_sysinfo(): return str(e) -def get_extensions(*, enabled): - +def run_git(path, *args): try: - def to_json(x: extensions.Extension): - return { - "name": x.name, - "path": x.path, - "version": x.version, - "branch": x.branch, - "remote": x.remote, - } + return subprocess.check_output([launch_utils.git, '-C', path, *args], shell=False, encoding='utf8').strip() + except Exception as e: + return str(e) - return [to_json(x) for x in extensions.extensions if not x.is_builtin and x.enabled == enabled] + +def git_status(path): + if (Path(path) / '.git').is_dir(): + return run_git(paths_internal.script_path, 'status') + + +def get_info_from_repo_path(path: Path): + is_repo = (path / '.git').is_dir() + return { + 'name': path.name, + 'path': str(path), + 'commit': run_git(path, 'rev-parse', 'HEAD') if is_repo else None, + 'branch': run_git(path, 'branch', '--show-current') if is_repo else None, + 'remote': run_git(path, 'remote', 'get-url', 'origin') if is_repo else None, + } + + +def get_extensions(*, enabled, fallback_disabled_extensions=None): + try: + from modules import extensions + if extensions.extensions: + def to_json(x: extensions.Extension): + return { + "name": x.name, + "path": x.path, + "commit": x.commit_hash, + "branch": x.branch, + "remote": x.remote, + } + return [to_json(x) for x in extensions.extensions if not x.is_builtin and x.enabled == enabled] + else: + return [get_info_from_repo_path(d) for d in Path(paths_internal.extensions_dir).iterdir() if d.is_dir() and enabled != (str(d.name) in fallback_disabled_extensions)] except Exception as e: return str(e) def get_config(): try: + from modules import shared return shared.opts.data - except Exception as e: - return str(e) + except Exception as _: + try: + with open(shared_cmd_options.cmd_opts.ui_settings_file, 'r') as f: + return json.load(f) + except Exception as e: + return str(e) diff --git a/modules/textual_inversion/autocrop.py b/modules/textual_inversion/autocrop.py index e223a2e0..ca858ef4 100644 --- a/modules/textual_inversion/autocrop.py +++ b/modules/textual_inversion/autocrop.py @@ -65,7 +65,7 @@ def crop_image(im, settings): rect[3] -= 1 d.rectangle(rect, outline=GREEN) results.append(im_debug) - if settings.destop_view_image: + if settings.desktop_view_image: im_debug.show() return results @@ -341,5 +341,5 @@ class Settings: self.entropy_points_weight = entropy_points_weight self.face_points_weight = face_points_weight self.annotate_image = annotate_image - self.destop_view_image = False + self.desktop_view_image = False self.dnn_model_path = dnn_model_path diff --git a/modules/textual_inversion/dataset.py b/modules/textual_inversion/dataset.py index 7ee05061..71c032df 100644 --- a/modules/textual_inversion/dataset.py +++ b/modules/textual_inversion/dataset.py @@ -2,7 +2,6 @@ import os import numpy as np import PIL import torch -from PIL import Image from torch.utils.data import Dataset, DataLoader, Sampler from torchvision import transforms from collections import defaultdict @@ -10,7 +9,7 @@ from random import shuffle, choices import random import tqdm -from modules import devices, shared +from modules import devices, shared, images import re from ldm.modules.distributions.distributions import DiagonalGaussianDistribution @@ -61,7 +60,7 @@ class PersonalizedBase(Dataset): if shared.state.interrupted: raise Exception("interrupted") try: - image = Image.open(path) + image = images.read(path) #Currently does not work for single color transparency #We would need to read image.info['transparency'] for that if use_weight and 'A' in image.getbands(): diff --git a/modules/textual_inversion/image_embedding.py b/modules/textual_inversion/image_embedding.py index 81cff7bf..eac0f976 100644 --- a/modules/textual_inversion/image_embedding.py +++ b/modules/textual_inversion/image_embedding.py @@ -1,12 +1,16 @@ import base64 import json +import os.path import warnings +import logging import numpy as np import zlib from PIL import Image, ImageDraw import torch +logger = logging.getLogger(__name__) + class EmbeddingEncoder(json.JSONEncoder): def default(self, obj): @@ -43,7 +47,7 @@ def lcg(m=2**32, a=1664525, c=1013904223, seed=0): def xor_block(block): g = lcg() - randblock = np.array([next(g) for _ in range(np.product(block.shape))]).astype(np.uint8).reshape(block.shape) + randblock = np.array([next(g) for _ in range(np.prod(block.shape))]).astype(np.uint8).reshape(block.shape) return np.bitwise_xor(block.astype(np.uint8), randblock & 0x0F) @@ -114,7 +118,7 @@ def extract_image_data_embed(image): outarr = crop_black(np.array(image.convert('RGB').getdata()).reshape(image.size[1], image.size[0], d).astype(np.uint8)) & 0x0F black_cols = np.where(np.sum(outarr, axis=(0, 2)) == 0) if black_cols[0].shape[0] < 2: - print('No Image data blocks found.') + logger.debug(f'{os.path.basename(getattr(image, "filename", "unknown image file"))}: no embedded information found.') return None data_block_lower = outarr[:, :black_cols[0].min(), :].astype(np.uint8) @@ -193,11 +197,11 @@ if __name__ == '__main__': embedded_image = insert_image_data_embed(cap_image, test_embed) - retrived_embed = extract_image_data_embed(embedded_image) + retrieved_embed = extract_image_data_embed(embedded_image) - assert str(retrived_embed) == str(test_embed) + assert str(retrieved_embed) == str(test_embed) - embedded_image2 = insert_image_data_embed(cap_image, retrived_embed) + embedded_image2 = insert_image_data_embed(cap_image, retrieved_embed) assert embedded_image == embedded_image2 diff --git a/modules/textual_inversion/logging.py b/modules/textual_inversion/saving_settings.py similarity index 100% rename from modules/textual_inversion/logging.py rename to modules/textual_inversion/saving_settings.py diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index 6d815c0b..dc7833e9 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -17,7 +17,7 @@ import modules.textual_inversion.dataset from modules.textual_inversion.learn_schedule import LearnRateScheduler from modules.textual_inversion.image_embedding import embedding_to_b64, embedding_from_b64, insert_image_data_embed, extract_image_data_embed, caption_image_overlay -from modules.textual_inversion.logging import save_settings_to_file +from modules.textual_inversion.saving_settings import save_settings_to_file TextualInversionTemplate = namedtuple("TextualInversionTemplate", ["name", "path"]) @@ -172,7 +172,7 @@ class EmbeddingDatabase: if data: name = data.get('name', name) else: - # if data is None, means this is not an embeding, just a preview image + # if data is None, means this is not an embedding, just a preview image return elif ext in ['.BIN', '.PT']: data = torch.load(path, map_location="cpu") @@ -181,12 +181,16 @@ class EmbeddingDatabase: else: return - embedding = create_embedding_from_data(data, name, filename=filename, filepath=path) + if data is not None: + embedding = create_embedding_from_data(data, name, filename=filename, filepath=path) - if self.expected_shape == -1 or self.expected_shape == embedding.shape: - self.register_embedding(embedding, shared.sd_model) + if self.expected_shape == -1 or self.expected_shape == embedding.shape: + self.register_embedding(embedding, shared.sd_model) + else: + self.skipped_embeddings[name] = embedding else: - self.skipped_embeddings[name] = embedding + print(f"Unable to load Textual inversion embedding due to data issue: '{name}'.") + def load_from_dir(self, embdir): if not os.path.isdir(embdir.path): diff --git a/modules/torch_utils.py b/modules/torch_utils.py index e5b52393..5ea3da09 100644 --- a/modules/torch_utils.py +++ b/modules/torch_utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import torch.nn +import torch def get_param(model) -> torch.nn.Parameter: @@ -15,3 +16,10 @@ def get_param(model) -> torch.nn.Parameter: return param raise ValueError(f"No parameters found in model {model!r}") + + +def float64(t: torch.Tensor): + """return torch.float64 if device is not mps or xpu, else return torch.float32""" + if t.device.type in ['mps', 'xpu']: + return torch.float32 + return torch.float64 diff --git a/modules/txt2img.py b/modules/txt2img.py index fc56b8a8..6f20253a 100644 --- a/modules/txt2img.py +++ b/modules/txt2img.py @@ -11,7 +11,7 @@ from PIL import Image import gradio as gr -def txt2img_create_processing(id_task: str, request: gr.Request, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_name: str, n_iter: int, batch_size: int, cfg_scale: float, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, hr_checkpoint_name: str, hr_sampler_name: str, hr_prompt: str, hr_negative_prompt, override_settings_texts, *args, force_enable_hr=False): +def txt2img_create_processing(id_task: str, request: gr.Request, prompt: str, negative_prompt: str, prompt_styles, n_iter: int, batch_size: int, cfg_scale: float, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, hr_checkpoint_name: str, hr_sampler_name: str, hr_scheduler: str, hr_prompt: str, hr_negative_prompt, override_settings_texts, *args, force_enable_hr=False): override_settings = create_override_settings_dict(override_settings_texts) if force_enable_hr: @@ -24,10 +24,8 @@ def txt2img_create_processing(id_task: str, request: gr.Request, prompt: str, ne prompt=prompt, styles=prompt_styles, negative_prompt=negative_prompt, - sampler_name=sampler_name, batch_size=batch_size, n_iter=n_iter, - steps=steps, cfg_scale=cfg_scale, width=width, height=height, @@ -40,6 +38,7 @@ def txt2img_create_processing(id_task: str, request: gr.Request, prompt: str, ne hr_resize_y=hr_resize_y, hr_checkpoint_name=None if hr_checkpoint_name == 'Use same checkpoint' else hr_checkpoint_name, hr_sampler_name=None if hr_sampler_name == 'Use same sampler' else hr_sampler_name, + hr_scheduler=None if hr_scheduler == 'Use same scheduler' else hr_scheduler, hr_prompt=hr_prompt, hr_negative_prompt=hr_negative_prompt, override_settings=override_settings, diff --git a/modules/ui.py b/modules/ui.py index dcba8e88..f48638f6 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -10,9 +10,9 @@ import gradio as gr import gradio.utils import numpy as np from PIL import Image, PngImagePlugin # noqa: F401 -from modules.call_queue import wrap_gradio_gpu_call, wrap_queued_call, wrap_gradio_call +from modules.call_queue import wrap_gradio_gpu_call, wrap_queued_call, wrap_gradio_call, wrap_gradio_call_no_job # noqa: F401 -from modules import gradio_extensons # noqa: F401 +from modules import gradio_extensons, sd_schedulers # noqa: F401 from modules import sd_hijack, sd_models, script_callbacks, ui_extensions, deepbooru, extra_networks, ui_common, ui_postprocessing, progress, ui_loadsave, shared_items, ui_settings, timer, sysinfo, ui_checkpoint_merger, scripts, sd_samplers, processing, ui_extra_networks, ui_toprow, launch_utils from modules.ui_components import FormRow, FormGroup, ToolButton, FormHTML, InputAccordion, ResizeHandleRow from modules.paths import script_path @@ -38,9 +38,11 @@ warnings.filterwarnings("default" if opts.show_gradio_deprecation_warnings else # this is a fix for Windows users. Without it, javascript files will be served with text/html content-type and the browser will not show any UI mimetypes.init() mimetypes.add_type('application/javascript', '.js') +mimetypes.add_type('application/javascript', '.mjs') # Likewise, add explicit content-type header for certain missing image types mimetypes.add_type('image/webp', '.webp') +mimetypes.add_type('image/avif', '.avif') if not cmd_opts.share and not cmd_opts.listen: # fix gradio phoning home @@ -229,19 +231,6 @@ def create_output_panel(tabname, outdir, toprow=None): return ui_common.create_output_panel(tabname, outdir, toprow) -def create_sampler_and_steps_selection(choices, tabname): - if opts.samplers_in_dropdown: - with FormRow(elem_id=f"sampler_selection_{tabname}"): - sampler_name = gr.Dropdown(label='Sampling method', elem_id=f"{tabname}_sampling", choices=choices, value=choices[0]) - steps = gr.Slider(minimum=1, maximum=150, step=1, elem_id=f"{tabname}_steps", label="Sampling steps", value=20) - else: - with FormGroup(elem_id=f"sampler_selection_{tabname}"): - steps = gr.Slider(minimum=1, maximum=150, step=1, elem_id=f"{tabname}_steps", label="Sampling steps", value=20) - sampler_name = gr.Radio(label='Sampling method', elem_id=f"{tabname}_sampling", choices=choices, value=choices[0]) - - return steps, sampler_name - - def ordered_ui_categories(): user_order = {x.strip(): i * 2 + 1 for i, x in enumerate(shared.opts.ui_reorder_list)} @@ -269,6 +258,9 @@ def create_ui(): parameters_copypaste.reset() + settings = ui_settings.UiSettings() + settings.register_settings() + scripts.scripts_current = scripts.scripts_txt2img scripts.scripts_txt2img.initialize_scripts(is_img2img=False) @@ -292,9 +284,6 @@ def create_ui(): if category == "prompt": toprow.create_inline_toprow_prompts() - if category == "sampler": - steps, sampler_name = create_sampler_and_steps_selection(sd_samplers.visible_sampler_names(), "txt2img") - elif category == "dimensions": with FormRow(): with gr.Column(elem_id="txt2img_column_size", scale=4): @@ -335,10 +324,11 @@ def create_ui(): with FormRow(elem_id="txt2img_hires_fix_row3", variant="compact", visible=opts.hires_fix_show_sampler) as hr_sampler_container: - hr_checkpoint_name = gr.Dropdown(label='Hires checkpoint', elem_id="hr_checkpoint", choices=["Use same checkpoint"] + modules.sd_models.checkpoint_tiles(use_short=True), value="Use same checkpoint") + hr_checkpoint_name = gr.Dropdown(label='Checkpoint', elem_id="hr_checkpoint", choices=["Use same checkpoint"] + modules.sd_models.checkpoint_tiles(use_short=True), value="Use same checkpoint") create_refresh_button(hr_checkpoint_name, modules.sd_models.list_models, lambda: {"choices": ["Use same checkpoint"] + modules.sd_models.checkpoint_tiles(use_short=True)}, "hr_checkpoint_refresh") hr_sampler_name = gr.Dropdown(label='Hires sampling method', elem_id="hr_sampler", choices=["Use same sampler"] + sd_samplers.visible_sampler_names(), value="Use same sampler") + hr_scheduler = gr.Dropdown(label='Hires schedule type', elem_id="hr_scheduler", choices=["Use same scheduler"] + [x.label for x in sd_schedulers.schedulers], value="Use same scheduler") with FormRow(elem_id="txt2img_hires_fix_row4", variant="compact", visible=opts.hires_fix_show_prompts) as hr_prompts_container: with gr.Column(scale=80): @@ -393,8 +383,6 @@ def create_ui(): toprow.prompt, toprow.negative_prompt, toprow.ui_styles.dropdown, - steps, - sampler_name, batch_count, batch_size, cfg_scale, @@ -409,6 +397,7 @@ def create_ui(): hr_resize_y, hr_checkpoint_name, hr_sampler_name, + hr_scheduler, hr_prompt, hr_negative_prompt, override_settings, @@ -458,8 +447,6 @@ def create_ui(): txt2img_paste_fields = [ PasteField(toprow.prompt, "Prompt", api="prompt"), PasteField(toprow.negative_prompt, "Negative prompt", api="negative_prompt"), - PasteField(steps, "Steps", api="steps"), - PasteField(sampler_name, "Sampler", api="sampler_name"), PasteField(cfg_scale, "CFG scale", api="cfg_scale"), PasteField(width, "Size-1", api="width"), PasteField(height, "Size-2", api="height"), @@ -473,8 +460,9 @@ def create_ui(): PasteField(hr_resize_x, "Hires resize-1", api="hr_resize_x"), PasteField(hr_resize_y, "Hires resize-2", api="hr_resize_y"), PasteField(hr_checkpoint_name, "Hires checkpoint", api="hr_checkpoint_name"), - PasteField(hr_sampler_name, "Hires sampler", api="hr_sampler_name"), - PasteField(hr_sampler_container, lambda d: gr.update(visible=True) if d.get("Hires sampler", "Use same sampler") != "Use same sampler" or d.get("Hires checkpoint", "Use same checkpoint") != "Use same checkpoint" else gr.update()), + PasteField(hr_sampler_name, sd_samplers.get_hr_sampler_from_infotext, api="hr_sampler_name"), + PasteField(hr_scheduler, sd_samplers.get_hr_scheduler_from_infotext, api="hr_scheduler"), + PasteField(hr_sampler_container, lambda d: gr.update(visible=True) if d.get("Hires sampler", "Use same sampler") != "Use same sampler" or d.get("Hires checkpoint", "Use same checkpoint") != "Use same checkpoint" or d.get("Hires schedule type", "Use same scheduler") != "Use same scheduler" else gr.update()), PasteField(hr_prompt, "Hires prompt", api="hr_prompt"), PasteField(hr_negative_prompt, "Hires negative prompt", api="hr_negative_prompt"), PasteField(hr_prompts_container, lambda d: gr.update(visible=True) if d.get("Hires prompt", "") != "" or d.get("Hires negative prompt", "") != "" else gr.update()), @@ -485,11 +473,13 @@ def create_ui(): paste_button=toprow.paste, tabname="txt2img", source_text_component=toprow.prompt, source_image_component=None, )) + steps = scripts.scripts_txt2img.script('Sampler').steps + txt2img_preview_params = [ toprow.prompt, toprow.negative_prompt, steps, - sampler_name, + scripts.scripts_txt2img.script('Sampler').sampler_name, cfg_scale, scripts.scripts_txt2img.script('Seed').seed, width, @@ -578,18 +568,25 @@ def create_ui(): init_mask_inpaint = gr.Image(label="Mask", source="upload", interactive=True, type="pil", image_mode="RGBA", elem_id="img_inpaint_mask") with gr.TabItem('Batch', id='batch', elem_id="img2img_batch_tab") as tab_batch: - hidden = '
Disabled when launched with --hide-ui-dir-config.' if shared.cmd_opts.hide_ui_dir_config else '' - gr.HTML( - "

Process images in a directory on the same machine where the server is running." + - "
Use an empty output directory to save pictures normally instead of writing to the output directory." + - f"
Add inpaint batch mask directory to enable inpaint batch processing." - f"{hidden}

" - ) - img2img_batch_input_dir = gr.Textbox(label="Input directory", **shared.hide_dirs, elem_id="img2img_batch_input_dir") - img2img_batch_output_dir = gr.Textbox(label="Output directory", **shared.hide_dirs, elem_id="img2img_batch_output_dir") - img2img_batch_inpaint_mask_dir = gr.Textbox(label="Inpaint batch mask directory (required for inpaint batch processing only)", **shared.hide_dirs, elem_id="img2img_batch_inpaint_mask_dir") + with gr.Tabs(elem_id="img2img_batch_source"): + img2img_batch_source_type = gr.Textbox(visible=False, value="upload") + with gr.TabItem('Upload', id='batch_upload', elem_id="img2img_batch_upload_tab") as tab_batch_upload: + img2img_batch_upload = gr.Files(label="Files", interactive=True, elem_id="img2img_batch_upload") + with gr.TabItem('From directory', id='batch_from_dir', elem_id="img2img_batch_from_dir_tab") as tab_batch_from_dir: + hidden = '
Disabled when launched with --hide-ui-dir-config.' if shared.cmd_opts.hide_ui_dir_config else '' + gr.HTML( + "

Process images in a directory on the same machine where the server is running." + + "
Use an empty output directory to save pictures normally instead of writing to the output directory." + + f"
Add inpaint batch mask directory to enable inpaint batch processing." + f"{hidden}

" + ) + img2img_batch_input_dir = gr.Textbox(label="Input directory", **shared.hide_dirs, elem_id="img2img_batch_input_dir") + img2img_batch_output_dir = gr.Textbox(label="Output directory", **shared.hide_dirs, elem_id="img2img_batch_output_dir") + img2img_batch_inpaint_mask_dir = gr.Textbox(label="Inpaint batch mask directory (required for inpaint batch processing only)", **shared.hide_dirs, elem_id="img2img_batch_inpaint_mask_dir") + tab_batch_upload.select(fn=lambda: "upload", inputs=[], outputs=[img2img_batch_source_type]) + tab_batch_from_dir.select(fn=lambda: "from dir", inputs=[], outputs=[img2img_batch_source_type]) with gr.Accordion("PNG info", open=False): - img2img_batch_use_png_info = gr.Checkbox(label="Append png info to prompts", **shared.hide_dirs, elem_id="img2img_batch_use_png_info") + img2img_batch_use_png_info = gr.Checkbox(label="Append png info to prompts", elem_id="img2img_batch_use_png_info") img2img_batch_png_info_dir = gr.Textbox(label="PNG info directory", **shared.hide_dirs, placeholder="Leave empty to use input directory", elem_id="img2img_batch_png_info_dir") img2img_batch_png_info_props = gr.CheckboxGroup(["Prompt", "Negative prompt", "Seed", "CFG scale", "Sampler", "Steps", "Model hash"], label="Parameters to take from png info", info="Prompts from png info will be appended to prompts set in ui.") @@ -620,16 +617,13 @@ def create_ui(): with FormRow(): resize_mode = gr.Radio(label="Resize mode", elem_id="resize_mode", choices=["Just resize", "Crop and resize", "Resize and fill", "Just resize (latent upscale)"], type="index", value="Just resize") - if category == "sampler": - steps, sampler_name = create_sampler_and_steps_selection(sd_samplers.visible_sampler_names(), "img2img") - elif category == "dimensions": with FormRow(): with gr.Column(elem_id="img2img_column_size", scale=4): selected_scale_tab = gr.Number(value=0, visible=False) - with gr.Tabs(): - with gr.Tab(label="Resize to", elem_id="img2img_tab_resize_to") as tab_scale_to: + with gr.Tabs(elem_id="img2img_tabs_resize"): + with gr.Tab(label="Resize to", id="to", elem_id="img2img_tab_resize_to") as tab_scale_to: with FormRow(): with gr.Column(elem_id="img2img_column_size", scale=4): width = gr.Slider(minimum=64, maximum=2048, step=8, label="Width", value=512, elem_id="img2img_width") @@ -638,7 +632,7 @@ def create_ui(): res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="img2img_res_switch_btn", tooltip="Switch width/height") detect_image_size_btn = ToolButton(value=detect_image_size_symbol, elem_id="img2img_detect_image_size_btn", tooltip="Auto detect size from img2img") - with gr.Tab(label="Resize by", elem_id="img2img_tab_resize_by") as tab_scale_by: + with gr.Tab(label="Resize by", id="by", elem_id="img2img_tab_resize_by") as tab_scale_by: scale_by = gr.Slider(minimum=0.05, maximum=4.0, step=0.05, label="Scale", value=1.0, elem_id="img2img_scale") with FormRow(): @@ -751,8 +745,6 @@ def create_ui(): inpaint_color_sketch_orig, init_img_inpaint, init_mask_inpaint, - steps, - sampler_name, mask_blur, mask_alpha, inpainting_fill, @@ -776,6 +768,8 @@ def create_ui(): img2img_batch_use_png_info, img2img_batch_png_info_props, img2img_batch_png_info_dir, + img2img_batch_source_type, + img2img_batch_upload, ] + custom_inputs, outputs=[ output_panel.gallery, @@ -837,6 +831,8 @@ def create_ui(): **interrogate_args, ) + steps = scripts.scripts_img2img.script('Sampler').steps + toprow.ui_styles.dropdown.change(fn=wrap_queued_call(update_token_counter), inputs=[toprow.prompt, steps, toprow.ui_styles.dropdown], outputs=[toprow.token_counter]) toprow.ui_styles.dropdown.change(fn=wrap_queued_call(update_negative_prompt_token_counter), inputs=[toprow.negative_prompt, steps, toprow.ui_styles.dropdown], outputs=[toprow.negative_token_counter]) toprow.token_button.click(fn=update_token_counter, inputs=[toprow.prompt, steps, toprow.ui_styles.dropdown], outputs=[toprow.token_counter]) @@ -845,8 +841,6 @@ def create_ui(): img2img_paste_fields = [ (toprow.prompt, "Prompt"), (toprow.negative_prompt, "Negative prompt"), - (steps, "Steps"), - (sampler_name, "Sampler"), (cfg_scale, "CFG scale"), (image_cfg_scale, "Image CFG scale"), (width, "Size-1"), @@ -895,7 +889,7 @@ def create_ui(): )) image.change( - fn=wrap_gradio_call(modules.extras.run_pnginfo), + fn=wrap_gradio_call_no_job(modules.extras.run_pnginfo), inputs=[image], outputs=[html, generation_info, html2], ) @@ -1116,7 +1110,6 @@ def create_ui(): loadsave = ui_loadsave.UiLoadsave(cmd_opts.ui_config_file) ui_settings_from_file = loadsave.ui_settings.copy() - settings = ui_settings.UiSettings() settings.create_ui(loadsave, dummy_component) interfaces = [ diff --git a/modules/ui_common.py b/modules/ui_common.py index cf1b8b32..395bb3b6 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -3,13 +3,11 @@ import dataclasses import json import html import os -import platform -import sys +from contextlib import nullcontext import gradio as gr -import subprocess as sp -from modules import call_queue, shared, ui_tempdir +from modules import call_queue, shared, ui_tempdir, util from modules.infotext_utils import image_from_url_text import modules.images from modules.ui_components import ToolButton @@ -105,15 +103,16 @@ def save_files(js_data, images, do_make_zip, index): logfile_path = os.path.join(shared.opts.outdir_save, "log.csv") # NOTE: ensure csv integrity when fields are added by - # updating headers and padding with delimeters where needed - if os.path.exists(logfile_path): + # updating headers and padding with delimiters where needed + if shared.opts.save_write_log_csv and os.path.exists(logfile_path): update_logfile(logfile_path, fields) - with open(logfile_path, "a", encoding="utf8", newline='') as file: - at_start = file.tell() == 0 - writer = csv.writer(file) - if at_start: - writer.writerow(fields) + with (open(logfile_path, "a", encoding="utf8", newline='') if shared.opts.save_write_log_csv else nullcontext()) as file: + if file: + at_start = file.tell() == 0 + writer = csv.writer(file) + if at_start: + writer.writerow(fields) for image_index, filedata in enumerate(images, start_index): image = image_from_url_text(filedata) @@ -133,7 +132,8 @@ def save_files(js_data, images, do_make_zip, index): filenames.append(os.path.basename(txt_fullfn)) fullfns.append(txt_fullfn) - writer.writerow([parsed_infotexts[0]['Prompt'], parsed_infotexts[0]['Seed'], data["width"], data["height"], data["sampler_name"], data["cfg_scale"], data["steps"], filenames[0], parsed_infotexts[0]['Negative prompt'], data["sd_model_name"], data["sd_model_hash"]]) + if file: + writer.writerow([parsed_infotexts[0]['Prompt'], parsed_infotexts[0]['Seed'], data["width"], data["height"], data["sampler_name"], data["cfg_scale"], data["steps"], filenames[0], parsed_infotexts[0]['Negative prompt'], data["sd_model_name"], data["sd_model_hash"]]) # Make Zip if do_make_zip: @@ -176,31 +176,7 @@ def create_output_panel(tabname, outdir, toprow=None): except Exception: pass - if not os.path.exists(f): - msg = f'Folder "{f}" does not exist. After you create an image, the folder will be created.' - print(msg) - gr.Info(msg) - return - elif not os.path.isdir(f): - msg = f""" -WARNING -An open_folder request was made with an argument that is not a folder. -This could be an error or a malicious attempt to run code on your computer. -Requested path was: {f} -""" - print(msg, file=sys.stderr) - gr.Warning(msg) - return - - path = os.path.normpath(f) - if platform.system() == "Windows": - os.startfile(path) - elif platform.system() == "Darwin": - sp.Popen(["open", path]) - elif "microsoft-standard-WSL2" in platform.uname().release: - sp.Popen(["wsl-open", path]) - else: - sp.Popen(["xdg-open", path]) + util.open_folder(f) with gr.Column(elem_id=f"{tabname}_results"): if toprow: @@ -255,7 +231,7 @@ Requested path was: {f} ) save.click( - fn=call_queue.wrap_gradio_call(save_files), + fn=call_queue.wrap_gradio_call_no_job(save_files), _js="(x, y, z, w) => [x, y, false, selected_gallery_index()]", inputs=[ res.generation_info, @@ -271,7 +247,7 @@ Requested path was: {f} ) save_zip.click( - fn=call_queue.wrap_gradio_call(save_files), + fn=call_queue.wrap_gradio_call_no_job(save_files), _js="(x, y, z, w) => [x, y, true, selected_gallery_index()]", inputs=[ res.generation_info, diff --git a/modules/ui_components.py b/modules/ui_components.py index 55979f62..9cf67722 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -88,7 +88,7 @@ class DropdownEditable(FormComponent, gr.Dropdown): class InputAccordion(gr.Checkbox): """A gr.Accordion that can be used as an input - returns True if open, False if closed. - Actaully just a hidden checkbox, but creates an accordion that follows and is followed by the state of the checkbox. + Actually just a hidden checkbox, but creates an accordion that follows and is followed by the state of the checkbox. """ global_index = 0 diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index a24ea32e..23aff709 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -58,8 +58,9 @@ def apply_and_restart(disable_list, update_list, disable_all): def save_config_state(name): current_config_state = config_states.get_config() - if not name: - name = "Config" + + name = os.path.basename(name or "Config") + current_config_state["name"] = name timestamp = datetime.now().strftime('%Y_%m_%d-%H_%M_%S') filename = os.path.join(config_states_dir, f"{timestamp}_{name}.json") @@ -380,7 +381,7 @@ def install_extension_from_url(dirname, url, branch_name=None): except OSError as err: if err.errno == errno.EXDEV: # Cross device link, typical in docker or when tmp/ and extensions/ are on different file systems - # Since we can't use a rename, do the slower but more versitile shutil.move() + # Since we can't use a rename, do the slower but more versatile shutil.move() shutil.move(tmpdir, target_dir) else: # Something else, not enough free space, permissions, etc. rethrow it so that it gets handled. @@ -395,15 +396,15 @@ def install_extension_from_url(dirname, url, branch_name=None): shutil.rmtree(tmpdir, True) -def install_extension_from_index(url, hide_tags, sort_column, filter_text): +def install_extension_from_index(url, selected_tags, showing_type, filtering_type, sort_column, filter_text): ext_table, message = install_extension_from_url(None, url) - code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text) + code, _ = refresh_available_extensions_from_data(selected_tags, showing_type, filtering_type, sort_column, filter_text) return code, ext_table, message, '' -def refresh_available_extensions(url, hide_tags, sort_column): +def refresh_available_extensions(url, selected_tags, showing_type, filtering_type, sort_column): global available_extensions import urllib.request @@ -412,19 +413,19 @@ def refresh_available_extensions(url, hide_tags, sort_column): available_extensions = json.loads(text) - code, tags = refresh_available_extensions_from_data(hide_tags, sort_column) + code, tags = refresh_available_extensions_from_data(selected_tags, showing_type, filtering_type, sort_column) return url, code, gr.CheckboxGroup.update(choices=tags), '', '' -def refresh_available_extensions_for_tags(hide_tags, sort_column, filter_text): - code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text) +def refresh_available_extensions_for_tags(selected_tags, showing_type, filtering_type, sort_column, filter_text): + code, _ = refresh_available_extensions_from_data(selected_tags, showing_type, filtering_type, sort_column, filter_text) return code, '' -def search_extensions(filter_text, hide_tags, sort_column): - code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text) +def search_extensions(filter_text, selected_tags, showing_type, filtering_type, sort_column): + code, _ = refresh_available_extensions_from_data(selected_tags, showing_type, filtering_type, sort_column, filter_text) return code, '' @@ -449,13 +450,13 @@ def get_date(info: dict, key): return '' -def refresh_available_extensions_from_data(hide_tags, sort_column, filter_text=""): +def refresh_available_extensions_from_data(selected_tags, showing_type, filtering_type, sort_column, filter_text=""): extlist = available_extensions["extensions"] installed_extensions = {extension.name for extension in extensions.extensions} installed_extension_urls = {normalize_git_url(extension.remote) for extension in extensions.extensions if extension.remote is not None} tags = available_extensions.get("tags", {}) - tags_to_hide = set(hide_tags) + selected_tags = set(selected_tags) hidden = 0 code = f""" @@ -488,9 +489,19 @@ def refresh_available_extensions_from_data(hide_tags, sort_column, filter_text=" existing = get_extension_dirname_from_url(url) in installed_extensions or normalize_git_url(url) in installed_extension_urls extension_tags = extension_tags + ["installed"] if existing else extension_tags - if any(x for x in extension_tags if x in tags_to_hide): - hidden += 1 - continue + if len(selected_tags) > 0: + matched_tags = [x for x in extension_tags if x in selected_tags] + if filtering_type == 'or': + need_hide = len(matched_tags) > 0 + else: + need_hide = len(matched_tags) == len(selected_tags) + + if showing_type == 'show': + need_hide = not need_hide + + if need_hide: + hidden += 1 + continue if filter_text and filter_text.strip(): if filter_text.lower() not in html.escape(name).lower() and filter_text.lower() not in html.escape(description).lower(): @@ -593,8 +604,12 @@ def create_ui(): install_extension_button = gr.Button(elem_id="install_extension_button", visible=False) with gr.Row(): - hide_tags = gr.CheckboxGroup(value=["ads", "localization", "installed"], label="Hide extensions with tags", choices=["script", "ads", "localization", "installed"]) - sort_column = gr.Radio(value="newest first", label="Order", choices=["newest first", "oldest first", "a-z", "z-a", "internal order",'update time', 'create time', "stars"], type="index") + selected_tags = gr.CheckboxGroup(value=["ads", "localization", "installed"], label="Extension tags", choices=["script", "ads", "localization", "installed"], elem_classes=['compact-checkbox-group']) + sort_column = gr.Radio(value="newest first", label="Order", choices=["newest first", "oldest first", "a-z", "z-a", "internal order",'update time', 'create time', "stars"], type="index", elem_classes=['compact-checkbox-group']) + + with gr.Row(): + showing_type = gr.Radio(value="hide", label="Showing type", choices=["hide", "show"], elem_classes=['compact-checkbox-group']) + filtering_type = gr.Radio(value="or", label="Filtering type", choices=["or", "and"], elem_classes=['compact-checkbox-group']) with gr.Row(): search_extensions_text = gr.Text(label="Search", container=False) @@ -604,31 +619,43 @@ def create_ui(): refresh_available_extensions_button.click( fn=modules.ui.wrap_gradio_call(refresh_available_extensions, extra_outputs=[gr.update(), gr.update(), gr.update(), gr.update()]), - inputs=[available_extensions_index, hide_tags, sort_column], - outputs=[available_extensions_index, available_extensions_table, hide_tags, search_extensions_text, install_result], + inputs=[available_extensions_index, selected_tags, showing_type, filtering_type, sort_column], + outputs=[available_extensions_index, available_extensions_table, selected_tags, search_extensions_text, install_result], ) install_extension_button.click( - fn=modules.ui.wrap_gradio_call(install_extension_from_index, extra_outputs=[gr.update(), gr.update()]), - inputs=[extension_to_install, hide_tags, sort_column, search_extensions_text], + fn=modules.ui.wrap_gradio_call_no_job(install_extension_from_index, extra_outputs=[gr.update(), gr.update()]), + inputs=[extension_to_install, selected_tags, showing_type, filtering_type, sort_column, search_extensions_text], outputs=[available_extensions_table, extensions_table, install_result], ) search_extensions_text.change( - fn=modules.ui.wrap_gradio_call(search_extensions, extra_outputs=[gr.update()]), - inputs=[search_extensions_text, hide_tags, sort_column], + fn=modules.ui.wrap_gradio_call_no_job(search_extensions, extra_outputs=[gr.update()]), + inputs=[search_extensions_text, selected_tags, showing_type, filtering_type, sort_column], outputs=[available_extensions_table, install_result], ) - hide_tags.change( - fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]), - inputs=[hide_tags, sort_column, search_extensions_text], + selected_tags.change( + fn=modules.ui.wrap_gradio_call_no_job(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]), + inputs=[selected_tags, showing_type, filtering_type, sort_column, search_extensions_text], + outputs=[available_extensions_table, install_result] + ) + + showing_type.change( + fn=modules.ui.wrap_gradio_call_no_job(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]), + inputs=[selected_tags, showing_type, filtering_type, sort_column, search_extensions_text], + outputs=[available_extensions_table, install_result] + ) + + filtering_type.change( + fn=modules.ui.wrap_gradio_call_no_job(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]), + inputs=[selected_tags, showing_type, filtering_type, sort_column, search_extensions_text], outputs=[available_extensions_table, install_result] ) sort_column.change( - fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]), - inputs=[hide_tags, sort_column, search_extensions_text], + fn=modules.ui.wrap_gradio_call_no_job(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]), + inputs=[selected_tags, showing_type, filtering_type, sort_column, search_extensions_text], outputs=[available_extensions_table, install_result] ) @@ -640,7 +667,7 @@ def create_ui(): install_result = gr.HTML(elem_id="extension_install_result") install_button.click( - fn=modules.ui.wrap_gradio_call(lambda *args: [gr.update(), *install_extension_from_url(*args)], extra_outputs=[gr.update(), gr.update()]), + fn=modules.ui.wrap_gradio_call_no_job(lambda *args: [gr.update(), *install_extension_from_url(*args)], extra_outputs=[gr.update(), gr.update()]), inputs=[install_dirname, install_url, install_branch], outputs=[install_url, extensions_table, install_result], ) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 34c46ed4..6e9ec164 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -1,6 +1,8 @@ import functools import os.path import urllib.parse +from base64 import b64decode +from io import BytesIO from pathlib import Path from typing import Optional, Union from dataclasses import dataclass @@ -11,6 +13,7 @@ import gradio as gr import json import html from fastapi.exceptions import HTTPException +from PIL import Image from modules.infotext_utils import image_from_url_text @@ -108,6 +111,31 @@ def fetch_file(filename: str = ""): return FileResponse(filename, headers={"Accept-Ranges": "bytes"}) +def fetch_cover_images(page: str = "", item: str = "", index: int = 0): + from starlette.responses import Response + + page = next(iter([x for x in extra_pages if x.name == page]), None) + if page is None: + raise HTTPException(status_code=404, detail="File not found") + + metadata = page.metadata.get(item) + if metadata is None: + raise HTTPException(status_code=404, detail="File not found") + + cover_images = json.loads(metadata.get('ssmd_cover_images', {})) + image = cover_images[index] if index < len(cover_images) else None + if not image: + raise HTTPException(status_code=404, detail="File not found") + + try: + image = Image.open(BytesIO(b64decode(image))) + buffer = BytesIO() + image.save(buffer, format=image.format) + return Response(content=buffer.getvalue(), media_type=image.get_format_mimetype()) + except Exception as err: + raise ValueError(f"File cannot be fetched: {item}. Failed to load cover image.") from err + + def get_metadata(page: str = "", item: str = ""): from starlette.responses import JSONResponse @@ -119,6 +147,8 @@ def get_metadata(page: str = "", item: str = ""): if metadata is None: return JSONResponse({}) + metadata = {i:metadata[i] for i in metadata if i != 'ssmd_cover_images'} # those are cover images, and they are too big to display in UI as text + return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)}) @@ -142,6 +172,7 @@ def get_single_card(page: str = "", tabname: str = "", name: str = ""): def add_pages_to_demo(app): app.add_api_route("/sd_extra_networks/thumb", fetch_file, methods=["GET"]) + app.add_api_route("/sd_extra_networks/cover-images", fetch_cover_images, methods=["GET"]) app.add_api_route("/sd_extra_networks/metadata", get_metadata, methods=["GET"]) app.add_api_route("/sd_extra_networks/get-single-card", get_single_card, methods=["GET"]) @@ -151,6 +182,7 @@ def quote_js(s): s = s.replace('"', '\\"') return f'"{s}"' + class ExtraNetworksPage: def __init__(self, title): self.title = title @@ -164,6 +196,8 @@ class ExtraNetworksPage: self.lister = util.MassFileLister() # HTML Templates self.pane_tpl = shared.html("extra-networks-pane.html") + self.pane_content_tree_tpl = shared.html("extra-networks-pane-tree.html") + self.pane_content_dirs_tpl = shared.html("extra-networks-pane-dirs.html") self.card_tpl = shared.html("extra-networks-card.html") self.btn_tree_tpl = shared.html("extra-networks-tree-button.html") self.btn_copy_path_tpl = shared.html("extra-networks-copy-path-button.html") @@ -243,14 +277,12 @@ class ExtraNetworksPage: btn_metadata = self.btn_metadata_tpl.format( **{ "extra_networks_tabname": self.extra_networks_tabname, - "name": html.escape(item["name"]), } ) btn_edit_item = self.btn_edit_item_tpl.format( **{ "tabname": tabname, "extra_networks_tabname": self.extra_networks_tabname, - "name": html.escape(item["name"]), } ) @@ -476,6 +508,47 @@ class ExtraNetworksPage: return f"
    {res}
" + def create_dirs_view_html(self, tabname: str) -> str: + """Generates HTML for displaying folders.""" + + subdirs = {} + for parentdir in [os.path.abspath(x) for x in self.allowed_directories_for_previews()]: + for root, dirs, _ in sorted(os.walk(parentdir, followlinks=True), key=lambda x: shared.natural_sort_key(x[0])): + for dirname in sorted(dirs, key=shared.natural_sort_key): + x = os.path.join(root, dirname) + + if not os.path.isdir(x): + continue + + subdir = os.path.abspath(x)[len(parentdir):] + + if shared.opts.extra_networks_dir_button_function: + if not subdir.startswith(os.path.sep): + subdir = os.path.sep + subdir + else: + while subdir.startswith(os.path.sep): + subdir = subdir[1:] + + is_empty = len(os.listdir(x)) == 0 + if not is_empty and not subdir.endswith(os.path.sep): + subdir = subdir + os.path.sep + + if (os.path.sep + "." in subdir or subdir.startswith(".")) and not shared.opts.extra_networks_show_hidden_directories: + continue + + subdirs[subdir] = 1 + + if subdirs: + subdirs = {"": 1, **subdirs} + + subdirs_html = "".join([f""" + + """ for subdir in subdirs]) + + return subdirs_html + def create_card_view_html(self, tabname: str, *, none_message) -> str: """Generates HTML for the network Card View section for a tab. @@ -489,15 +562,15 @@ class ExtraNetworksPage: Returns: HTML formatted string. """ - res = "" + res = [] for item in self.items.values(): - res += self.create_item_html(tabname, item, self.card_tpl) + res.append(self.create_item_html(tabname, item, self.card_tpl)) - if res == "": + if not res: dirs = "".join([f"
  • {x}
  • " for x in self.allowed_directories_for_previews()]) - res = none_message or shared.html("extra-networks-no-cards.html").format(dirs=dirs) + res = [none_message or shared.html("extra-networks-no-cards.html").format(dirs=dirs)] - return res + return "".join(res) def create_html(self, tabname, *, empty=False): """Generates an HTML string for the current pane. @@ -526,28 +599,28 @@ class ExtraNetworksPage: if "user_metadata" not in item: self.read_user_metadata(item) - data_sortdir = shared.opts.extra_networks_card_order - data_sortmode = shared.opts.extra_networks_card_order_field.lower().replace("sort", "").replace(" ", "_").rstrip("_").strip() - data_sortkey = f"{data_sortmode}-{data_sortdir}-{len(self.items)}" - tree_view_btn_extra_class = "" - tree_view_div_extra_class = "hidden" - if shared.opts.extra_networks_tree_view_default_enabled: - tree_view_btn_extra_class = "extra-network-control--enabled" - tree_view_div_extra_class = "" + show_tree = shared.opts.extra_networks_tree_view_default_enabled - return self.pane_tpl.format( - **{ - "tabname": tabname, - "extra_networks_tabname": self.extra_networks_tabname, - "data_sortmode": data_sortmode, - "data_sortkey": data_sortkey, - "data_sortdir": data_sortdir, - "tree_view_btn_extra_class": tree_view_btn_extra_class, - "tree_view_div_extra_class": tree_view_div_extra_class, - "tree_html": self.create_tree_view_html(tabname), - "items_html": self.create_card_view_html(tabname, none_message="Loading..." if empty else None), - } - ) + page_params = { + "tabname": tabname, + "extra_networks_tabname": self.extra_networks_tabname, + "data_sortdir": shared.opts.extra_networks_card_order, + "sort_path_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Path' else '', + "sort_name_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Name' else '', + "sort_date_created_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Date Created' else '', + "sort_date_modified_active": ' extra-network-control--enabled' if shared.opts.extra_networks_card_order_field == 'Date Modified' else '', + "tree_view_btn_extra_class": "extra-network-control--enabled" if show_tree else "", + "items_html": self.create_card_view_html(tabname, none_message="Loading..." if empty else None), + "extra_networks_tree_view_default_width": shared.opts.extra_networks_tree_view_default_width, + "tree_view_div_default_display_class": "" if show_tree else "extra-network-dirs-hidden", + } + + if shared.opts.extra_networks_tree_view_style == "Tree": + pane_content = self.pane_content_tree_tpl.format(**page_params, tree_html=self.create_tree_view_html(tabname)) + else: + pane_content = self.pane_content_dirs_tpl.format(**page_params, dirs_html=self.create_dirs_view_html(tabname)) + + return self.pane_tpl.format(**page_params, pane_content=pane_content) def create_item(self, name, index=None): raise NotImplementedError() @@ -584,6 +657,17 @@ class ExtraNetworksPage: return None + def find_embedded_preview(self, path, name, metadata): + """ + Find if embedded preview exists in safetensors metadata and return endpoint for it. + """ + + file = f"{path}.safetensors" + if self.lister.exists(file) and 'ssmd_cover_images' in metadata and len(list(filter(None, json.loads(metadata['ssmd_cover_images'])))) > 0: + return f"./sd_extra_networks/cover-images?page={self.extra_networks_tabname}&item={name}" + + return None + def find_description(self, path): """ Find and read a description file for a given path (without extension). @@ -693,7 +777,7 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): return ui.pages_contents button_refresh = gr.Button("Refresh", elem_id=f"{tabname}_{page.extra_networks_tabname}_extra_refresh_internal", visible=False) - button_refresh.click(fn=refresh, inputs=[], outputs=ui.pages).then(fn=lambda: None, _js="function(){ " + f"applyExtraNetworkFilter('{tabname}_{page.extra_networks_tabname}');" + " }") + button_refresh.click(fn=refresh, inputs=[], outputs=ui.pages).then(fn=lambda: None, _js="function(){ " + f"applyExtraNetworkFilter('{tabname}_{page.extra_networks_tabname}');" + " }").then(fn=lambda: None, _js='setupAllResizeHandles') def create_html(): ui.pages_contents = [pg.create_html(ui.tabname) for pg in ui.stored_extra_pages] @@ -703,7 +787,7 @@ def create_ui(interface: gr.Blocks, unrelated_tabs, tabname): create_html() return ui.pages_contents - interface.load(fn=pages_html, inputs=[], outputs=ui.pages) + interface.load(fn=pages_html, inputs=[], outputs=ui.pages).then(fn=lambda: None, _js='setupAllResizeHandles') return ui diff --git a/modules/ui_extra_networks_user_metadata.py b/modules/ui_extra_networks_user_metadata.py index 2ca937fd..3a07db10 100644 --- a/modules/ui_extra_networks_user_metadata.py +++ b/modules/ui_extra_networks_user_metadata.py @@ -133,8 +133,10 @@ class UserMetadataEditor: filename = item.get("filename", None) basename, ext = os.path.splitext(filename) - with open(basename + '.json', "w", encoding="utf8") as file: + metadata_path = basename + '.json' + with open(metadata_path, "w", encoding="utf8") as file: json.dump(metadata, file, indent=4, ensure_ascii=False) + self.page.lister.update_file_entry(metadata_path) def save_user_metadata(self, name, desc, notes): user_metadata = self.get_user_metadata(name) @@ -185,13 +187,14 @@ class UserMetadataEditor: geninfo, items = images.read_info_from_image(image) images.save_image_with_geninfo(image, geninfo, item["local_preview"]) - + self.page.lister.update_file_entry(item["local_preview"]) + item['preview'] = self.page.find_preview(item["local_preview"]) return self.get_card_html(name), '' def setup_ui(self, gallery): self.button_replace_preview.click( fn=self.save_preview, - _js="function(x, y, z){return [selected_gallery_index(), y, z]}", + _js=f"function(x, y, z){{return [selected_gallery_index_id('{self.tabname + '_gallery_container'}'), y, z]}}", inputs=[self.edit_name_input, gallery, self.edit_name_input], outputs=[self.html_preview, self.html_status] ).then( @@ -200,6 +203,3 @@ class UserMetadataEditor: inputs=[self.edit_name_input], outputs=[] ) - - - diff --git a/modules/ui_gradio_extensions.py b/modules/ui_gradio_extensions.py index f5278d22..ed57c1e9 100644 --- a/modules/ui_gradio_extensions.py +++ b/modules/ui_gradio_extensions.py @@ -41,6 +41,11 @@ def css_html(): if os.path.exists(user_css): head += stylesheet(user_css) + from modules.shared_gradio_themes import resolve_var + light = resolve_var('background_fill_primary') + dark = resolve_var('background_fill_primary_dark') + head += f'' + return head @@ -50,7 +55,7 @@ def reload_javascript(): def template_response(*args, **kwargs): res = shared.GradioTemplateResponseOriginal(*args, **kwargs) - res.body = res.body.replace(b'', f'{js}'.encode("utf8")) + res.body = res.body.replace(b'', f'{js}'.encode("utf8")) res.body = res.body.replace(b'', f'{css}'.encode("utf8")) res.init_headers() return res diff --git a/modules/ui_loadsave.py b/modules/ui_loadsave.py index 2555cdb6..0cc1ab82 100644 --- a/modules/ui_loadsave.py +++ b/modules/ui_loadsave.py @@ -104,6 +104,8 @@ class UiLoadsave: apply_field(x, 'value', check_dropdown, getattr(x, 'init_field', None)) if type(x) == InputAccordion: + if hasattr(x, 'custom_script_source'): + x.accordion.custom_script_source = x.custom_script_source if x.accordion.visible: apply_field(x.accordion, 'visible') apply_field(x, 'value') diff --git a/modules/ui_postprocessing.py b/modules/ui_postprocessing.py index 7261c2df..dc08350d 100644 --- a/modules/ui_postprocessing.py +++ b/modules/ui_postprocessing.py @@ -12,7 +12,7 @@ def create_ui(): with gr.Column(variant='compact'): with gr.Tabs(elem_id="mode_extras"): with gr.TabItem('Single Image', id="single_image", elem_id="extras_single_tab") as tab_single: - extras_image = gr.Image(label="Source", source="upload", interactive=True, type="pil", elem_id="extras_image") + extras_image = gr.Image(label="Source", source="upload", interactive=True, type="pil", elem_id="extras_image", image_mode="RGBA") with gr.TabItem('Batch Process', id="batch_process", elem_id="extras_batch_process_tab") as tab_batch: image_batch = gr.Files(label="Batch Process", interactive=True, elem_id="extras_image_batch") diff --git a/modules/ui_prompt_styles.py b/modules/ui_prompt_styles.py index d67e3f17..f71b40c4 100644 --- a/modules/ui_prompt_styles.py +++ b/modules/ui_prompt_styles.py @@ -67,7 +67,7 @@ class UiPromptStyles: with gr.Row(): self.selection = gr.Dropdown(label="Styles", elem_id=f"{tabname}_styles_edit_select", choices=list(shared.prompt_styles.styles), value=[], allow_custom_value=True, info="Styles allow you to add custom text to prompt. Use the {prompt} token in style text, and it will be replaced with user's prompt when applying style. Otherwise, style's text will be added to the end of the prompt.") ui_common.create_refresh_button([self.dropdown, self.selection], shared.prompt_styles.reload, lambda: {"choices": list(shared.prompt_styles.styles)}, f"refresh_{tabname}_styles") - self.materialize = ui_components.ToolButton(value=styles_materialize_symbol, elem_id=f"{tabname}_style_apply_dialog", tooltip="Apply all selected styles from the style selction dropdown in main UI to the prompt.") + self.materialize = ui_components.ToolButton(value=styles_materialize_symbol, elem_id=f"{tabname}_style_apply_dialog", tooltip="Apply all selected styles from the style selection dropdown in main UI to the prompt.") self.copy = ui_components.ToolButton(value=styles_copy_symbol, elem_id=f"{tabname}_style_copy", tooltip="Copy main UI prompt to style.") with gr.Row(): diff --git a/modules/ui_settings.py b/modules/ui_settings.py index e054d00a..e53ad50f 100644 --- a/modules/ui_settings.py +++ b/modules/ui_settings.py @@ -1,7 +1,8 @@ import gradio as gr -from modules import ui_common, shared, script_callbacks, scripts, sd_models, sysinfo, timer -from modules.call_queue import wrap_gradio_call +from modules import ui_common, shared, script_callbacks, scripts, sd_models, sysinfo, timer, shared_items +from modules.call_queue import wrap_gradio_call_no_job +from modules.options import options_section from modules.shared import opts from modules.ui_components import FormRow from modules.ui_gradio_extensions import reload_javascript @@ -98,6 +99,9 @@ class UiSettings: return get_value_for_setting(key), opts.dumpjson() + def register_settings(self): + script_callbacks.ui_settings_callback() + def create_ui(self, loadsave, dummy_component): self.components = [] self.component_dict = {} @@ -105,7 +109,11 @@ class UiSettings: shared.settings_components = self.component_dict - script_callbacks.ui_settings_callback() + # we add this as late as possible so that scripts have already registered their callbacks + opts.data_labels.update(options_section(('callbacks', "Callbacks", "system"), { + **shared_items.callbacks_order_settings(), + })) + opts.reorder() with gr.Blocks(analytics_enabled=False) as settings_interface: @@ -287,7 +295,7 @@ class UiSettings: def add_functionality(self, demo): self.submit.click( - fn=wrap_gradio_call(lambda *args: self.run_settings(*args), extra_outputs=[gr.update()]), + fn=wrap_gradio_call_no_job(lambda *args: self.run_settings(*args), extra_outputs=[gr.update()]), inputs=self.components, outputs=[self.text_settings, self.result], ) diff --git a/modules/upscaler.py b/modules/upscaler.py index 3aee69db..507881fe 100644 --- a/modules/upscaler.py +++ b/modules/upscaler.py @@ -20,7 +20,7 @@ class Upscaler: filter = None model = None user_path = None - scalers: [] + scalers: list tile = True def __init__(self, create_dirs=False): @@ -56,8 +56,11 @@ class Upscaler: dest_w = int((img.width * scale) // 8 * 8) dest_h = int((img.height * scale) // 8 * 8) - for _ in range(3): - if img.width >= dest_w and img.height >= dest_h: + for i in range(3): + if img.width >= dest_w and img.height >= dest_h and (i > 0 or scale != 1): + break + + if shared.state.interrupted: break shape = (img.width, img.height) diff --git a/modules/upscaler_utils.py b/modules/upscaler_utils.py index b5e5a80c..a8408f05 100644 --- a/modules/upscaler_utils.py +++ b/modules/upscaler_utils.py @@ -41,7 +41,7 @@ def upscale_pil_patch(model, img: Image.Image) -> Image.Image: """ param = torch_utils.get_param(model) - with torch.no_grad(): + with torch.inference_mode(): tensor = pil_image_to_torch_bgr(img).unsqueeze(0) # add batch dimension tensor = tensor.to(device=param.device, dtype=param.dtype) with devices.without_autocast(): @@ -69,10 +69,10 @@ def upscale_with_model( for y, h, row in grid.tiles: newrow = [] for x, w, tile in row: - logger.debug("Tile (%d, %d) %s...", x, y, tile) + if shared.state.interrupted: + return img output = upscale_pil_patch(model, tile) scale_factor = output.width // tile.width - logger.debug("=> %s (scale factor %s)", output, scale_factor) newrow.append([x * scale_factor, w * scale_factor, output]) p.update(1) newtiles.append([y * scale_factor, h * scale_factor, newrow]) diff --git a/modules/util.py b/modules/util.py index 8d1aea44..7911b0db 100644 --- a/modules/util.py +++ b/modules/util.py @@ -81,6 +81,17 @@ class MassFileListerCachedDir: self.files = {x[0].lower(): x for x in files} self.files_cased = {x[0]: x for x in files} + def update_entry(self, filename): + """Add a file to the cache""" + file_path = os.path.join(self.dirname, filename) + try: + stat = os.stat(file_path) + entry = (filename, stat.st_mtime, stat.st_ctime) + self.files[filename.lower()] = entry + self.files_cased[filename] = entry + except FileNotFoundError as e: + print(f'MassFileListerCachedDir.add_entry: "{file_path}" {e}') + class MassFileLister: """A class that provides a way to check for the existence and mtime/ctile of files without doing more than one stat call per file.""" @@ -136,3 +147,67 @@ class MassFileLister: def reset(self): """Clear the cache of all directories.""" self.cached_dirs.clear() + + def update_file_entry(self, path): + """Update the cache for a specific directory.""" + dirname, filename = os.path.split(path) + if cached_dir := self.cached_dirs.get(dirname): + cached_dir.update_entry(filename) + +def topological_sort(dependencies): + """Accepts a dictionary mapping name to its dependencies, returns a list of names ordered according to dependencies. + Ignores errors relating to missing dependencies or circular dependencies + """ + + visited = {} + result = [] + + def inner(name): + visited[name] = True + + for dep in dependencies.get(name, []): + if dep in dependencies and dep not in visited: + inner(dep) + + result.append(name) + + for depname in dependencies: + if depname not in visited: + inner(depname) + + return result + + +def open_folder(path): + """Open a folder in the file manager of the respect OS.""" + # import at function level to avoid potential issues + import gradio as gr + import platform + import sys + import subprocess + + if not os.path.exists(path): + msg = f'Folder "{path}" does not exist. after you save an image, the folder will be created.' + print(msg) + gr.Info(msg) + return + elif not os.path.isdir(path): + msg = f""" +WARNING +An open_folder request was made with an path that is not a folder. +This could be an error or a malicious attempt to run code on your computer. +Requested path was: {path} +""" + print(msg, file=sys.stderr) + gr.Warning(msg) + return + + path = os.path.normpath(path) + if platform.system() == "Windows": + os.startfile(path) + elif platform.system() == "Darwin": + subprocess.Popen(["open", path]) + elif "microsoft-standard-WSL2" in platform.uname().release: + subprocess.Popen(["explorer.exe", subprocess.check_output(["wslpath", "-w", path])]) + else: + subprocess.Popen(["xdg-open", path]) diff --git a/package.json b/package.json index c0ba4067..bd71a5a1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "stable-diffusion-webui", "version": "0.0.0", "devDependencies": { - "eslint": "^8.40.0" + "eslint": "^8.57.1" }, "scripts": { "lint": "eslint .", diff --git a/pyproject.toml b/pyproject.toml index d03036e7..10ebc84b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,8 @@ target-version = "py39" +[tool.ruff.lint] + extend-select = [ "B", "C", @@ -25,10 +27,10 @@ ignore = [ "W605", # invalid escape sequence, messes with some docstrings ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "webui.py" = ["E402"] # Module level import not at top of file -[tool.ruff.flake8-bugbear] +[tool.ruff.lint.flake8-bugbear] # Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`. extend-immutable-calls = ["fastapi.Depends", "fastapi.security.HTTPBasic"] diff --git a/requirements.txt b/requirements.txt index 731a1be7..0d6bac60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ accelerate blendmodes clean-fid +diskcache einops facexlib fastapi>=0.90.1 @@ -17,6 +18,7 @@ omegaconf open-clip-torch piexif +protobuf==3.20.0 psutil pytorch_lightning requests @@ -29,3 +31,4 @@ torch torchdiffeq torchsde transformers==4.30.2 +pillow-avif-plugin==1.4.3 \ No newline at end of file diff --git a/requirements_versions.txt b/requirements_versions.txt index 5e30b5ea..0306ce94 100644 --- a/requirements_versions.txt +++ b/requirements_versions.txt @@ -1,8 +1,10 @@ +setuptools==69.5.1 # temp fix for compatibility with some old packages GitPython==3.1.32 Pillow==9.5.0 accelerate==0.21.0 blendmodes==2022 clean-fid==0.1.35 +diskcache==5.6.3 einops==0.4.1 facexlib==0.3.0 fastapi==0.94.0 @@ -16,15 +18,18 @@ numpy==1.26.2 omegaconf==2.2.3 open-clip-torch==2.20.0 piexif==1.1.3 +protobuf==3.20.0 psutil==5.9.5 pytorch_lightning==1.9.4 resize-right==0.0.2 safetensors==0.4.2 scikit-image==0.21.0 -spandrel==0.1.6 +spandrel==0.3.4 +spandrel-extra-arches==0.1.1 tomesd==0.1.3 torch torchdiffeq==0.2.3 torchsde==0.2.6 transformers==4.30.2 httpx==0.24.1 +pillow-avif-plugin==1.4.3 diff --git a/script.js b/script.js index f069b1ef..de1a9000 100644 --- a/script.js +++ b/script.js @@ -29,6 +29,7 @@ var uiAfterUpdateCallbacks = []; var uiLoadedCallbacks = []; var uiTabChangeCallbacks = []; var optionsChangedCallbacks = []; +var optionsAvailableCallbacks = []; var uiAfterUpdateTimeout = null; var uiCurrentTab = null; @@ -77,6 +78,20 @@ function onOptionsChanged(callback) { optionsChangedCallbacks.push(callback); } +/** + * Register callback to be called when the options (in opts global variable) are available. + * The callback receives no arguments. + * If you register the callback after the options are available, it's just immediately called. + */ +function onOptionsAvailable(callback) { + if (Object.keys(opts).length != 0) { + callback(); + return; + } + + optionsAvailableCallbacks.push(callback); +} + function executeCallbacks(queue, arg) { for (const callback of queue) { try { diff --git a/scripts/outpainting_mk_2.py b/scripts/outpainting_mk_2.py index c98ab480..5df9dff9 100644 --- a/scripts/outpainting_mk_2.py +++ b/scripts/outpainting_mk_2.py @@ -102,7 +102,7 @@ def get_matched_noise(_np_src_image, np_mask_rgb, noise_q=1, color_variation=0.0 shaped_noise_fft = _fft2(noise_rgb) shaped_noise_fft[:, :, :] = np.absolute(shaped_noise_fft[:, :, :]) ** 2 * (src_dist ** noise_q) * src_phase # perform the actual shaping - brightness_variation = 0. # color_variation # todo: temporarily tieing brightness variation to color variation for now + brightness_variation = 0. # color_variation # todo: temporarily tying brightness variation to color variation for now contrast_adjusted_np_src = _np_src_image[:] * (brightness_variation + 1.) - brightness_variation * 2. # scikit-image is used for histogram matching, very convenient! diff --git a/scripts/postprocessing_codeformer.py b/scripts/postprocessing_codeformer.py index e1e156dd..53a0cc44 100644 --- a/scripts/postprocessing_codeformer.py +++ b/scripts/postprocessing_codeformer.py @@ -25,7 +25,7 @@ class ScriptPostprocessingCodeFormer(scripts_postprocessing.ScriptPostprocessing if codeformer_visibility == 0 or not enable: return - restored_img = codeformer_model.codeformer.restore(np.array(pp.image, dtype=np.uint8), w=codeformer_weight) + restored_img = codeformer_model.codeformer.restore(np.array(pp.image.convert("RGB"), dtype=np.uint8), w=codeformer_weight) res = Image.fromarray(restored_img) if codeformer_visibility < 1.0: diff --git a/scripts/postprocessing_gfpgan.py b/scripts/postprocessing_gfpgan.py index 6e756605..57e36239 100644 --- a/scripts/postprocessing_gfpgan.py +++ b/scripts/postprocessing_gfpgan.py @@ -22,7 +22,7 @@ class ScriptPostprocessingGfpGan(scripts_postprocessing.ScriptPostprocessing): if gfpgan_visibility == 0 or not enable: return - restored_img = gfpgan_model.gfpgan_fix_faces(np.array(pp.image, dtype=np.uint8)) + restored_img = gfpgan_model.gfpgan_fix_faces(np.array(pp.image.convert("RGB"), dtype=np.uint8)) res = Image.fromarray(restored_img) if gfpgan_visibility < 1.0: diff --git a/scripts/postprocessing_upscale.py b/scripts/postprocessing_upscale.py index e269682d..2409fd20 100644 --- a/scripts/postprocessing_upscale.py +++ b/scripts/postprocessing_upscale.py @@ -1,15 +1,28 @@ +import re + from PIL import Image import numpy as np from modules import scripts_postprocessing, shared import gradio as gr -from modules.ui_components import FormRow, ToolButton +from modules.ui_components import FormRow, ToolButton, InputAccordion from modules.ui import switch_values_symbol upscale_cache = {} +def limit_size_by_one_dimention(w, h, limit): + if h > w and h > limit: + w = limit * w // h + h = limit + elif w > limit: + h = limit * h // w + w = limit + + return int(w), int(h) + + class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): name = "Upscale" order = 1000 @@ -17,11 +30,22 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): def ui(self): selected_tab = gr.Number(value=0, visible=False) - with gr.Column(): + with InputAccordion(True, label="Upscale", elem_id="extras_upscale") as upscale_enabled: + with FormRow(): + extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + + with FormRow(): + extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility") + with FormRow(): with gr.Tabs(elem_id="extras_resize_mode"): with gr.TabItem('Scale by', elem_id="extras_scale_by_tab") as tab_scale_by: - upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") + with gr.Row(): + with gr.Column(scale=4): + upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") + with gr.Column(scale=1, min_width=160): + max_side_length = gr.Number(label="Max side length", value=0, elem_id="extras_upscale_max_side_length", tooltip="If any of two sides of the image ends up larger than specified, will downscale it to fit. 0 = no limit.", min_width=160, step=8, minimum=0) with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to: with FormRow(): @@ -32,20 +56,27 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): upscaling_res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="upscaling_res_switch_btn", tooltip="Switch width/height") upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop") - with FormRow(): - extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + def on_selected_upscale_method(upscale_method): + if not shared.opts.set_scale_by_when_changing_upscaler: + return gr.update() - with FormRow(): - extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) - extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility") + match = re.search(r'(\d)[xX]|[xX](\d)', upscale_method) + if not match: + return gr.update() + + return gr.update(value=int(match.group(1) or match.group(2))) upscaling_res_switch_btn.click(lambda w, h: (h, w), inputs=[upscaling_resize_w, upscaling_resize_h], outputs=[upscaling_resize_w, upscaling_resize_h], show_progress=False) tab_scale_by.select(fn=lambda: 0, inputs=[], outputs=[selected_tab]) tab_scale_to.select(fn=lambda: 1, inputs=[], outputs=[selected_tab]) + extras_upscaler_1.change(on_selected_upscale_method, inputs=[extras_upscaler_1], outputs=[upscaling_resize], show_progress="hidden") + return { + "upscale_enabled": upscale_enabled, "upscale_mode": selected_tab, "upscale_by": upscaling_resize, + "max_side_length": max_side_length, "upscale_to_width": upscaling_resize_w, "upscale_to_height": upscaling_resize_h, "upscale_crop": upscaling_crop, @@ -54,12 +85,18 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): "upscaler_2_visibility": extras_upscaler_2_visibility, } - def upscale(self, image, info, upscaler, upscale_mode, upscale_by, upscale_to_width, upscale_to_height, upscale_crop): + def upscale(self, image, info, upscaler, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop): if upscale_mode == 1: upscale_by = max(upscale_to_width/image.width, upscale_to_height/image.height) info["Postprocess upscale to"] = f"{upscale_to_width}x{upscale_to_height}" else: info["Postprocess upscale by"] = upscale_by + if max_side_length != 0 and max(*image.size)*upscale_by > max_side_length: + upscale_mode = 1 + upscale_crop = False + upscale_to_width, upscale_to_height = limit_size_by_one_dimention(image.width*upscale_by, image.height*upscale_by, max_side_length) + upscale_by = max(upscale_to_width/image.width, upscale_to_height/image.height) + info["Max side length"] = max_side_length cache_key = (hash(np.array(image.getdata()).tobytes()), upscaler.name, upscale_mode, upscale_by, upscale_to_width, upscale_to_height, upscale_crop) cached_image = upscale_cache.pop(cache_key, None) @@ -81,7 +118,7 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): return image - def process_firstpass(self, pp: scripts_postprocessing.PostprocessedImage, upscale_mode=1, upscale_by=2.0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): + def process_firstpass(self, pp: scripts_postprocessing.PostprocessedImage, upscale_enabled=True, upscale_mode=1, upscale_by=2.0, max_side_length=0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): if upscale_mode == 1: pp.shared.target_width = upscale_to_width pp.shared.target_height = upscale_to_height @@ -89,7 +126,13 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): pp.shared.target_width = int(pp.image.width * upscale_by) pp.shared.target_height = int(pp.image.height * upscale_by) - def process(self, pp: scripts_postprocessing.PostprocessedImage, upscale_mode=1, upscale_by=2.0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): + pp.shared.target_width, pp.shared.target_height = limit_size_by_one_dimention(pp.shared.target_width, pp.shared.target_height, max_side_length) + + def process(self, pp: scripts_postprocessing.PostprocessedImage, upscale_enabled=True, upscale_mode=1, upscale_by=2.0, max_side_length=0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): + if not upscale_enabled: + return + + upscaler_1_name = upscaler_1_name if upscaler_1_name == "None": upscaler_1_name = None @@ -99,17 +142,20 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): if not upscaler1: return + upscaler_2_name = upscaler_2_name if upscaler_2_name == "None": upscaler_2_name = None upscaler2 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_2_name and x.name != "None"]), None) assert upscaler2 or (upscaler_2_name is None), f'could not find upscaler named {upscaler_2_name}' - upscaled_image = self.upscale(pp.image, pp.info, upscaler1, upscale_mode, upscale_by, upscale_to_width, upscale_to_height, upscale_crop) + upscaled_image = self.upscale(pp.image, pp.info, upscaler1, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop) pp.info["Postprocess upscaler"] = upscaler1.name if upscaler2 and upscaler_2_visibility > 0: - second_upscale = self.upscale(pp.image, pp.info, upscaler2, upscale_mode, upscale_by, upscale_to_width, upscale_to_height, upscale_crop) + second_upscale = self.upscale(pp.image, pp.info, upscaler2, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop) + if upscaled_image.mode != second_upscale.mode: + second_upscale = second_upscale.convert(upscaled_image.mode) upscaled_image = Image.blend(upscaled_image, second_upscale, upscaler_2_visibility) pp.info["Postprocess upscaler 2"] = upscaler2.name @@ -145,5 +191,5 @@ class ScriptPostprocessingUpscaleSimple(ScriptPostprocessingUpscale): upscaler1 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_name]), None) assert upscaler1, f'could not find upscaler named {upscaler_name}' - pp.image = self.upscale(pp.image, pp.info, upscaler1, 0, upscale_by, 0, 0, False) + pp.image = self.upscale(pp.image, pp.info, upscaler1, 0, upscale_by, 0, 0, 0, False) pp.info["Postprocess upscaler"] = upscaler1.name diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 6d3e42c0..6a42a04d 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -11,7 +11,7 @@ import numpy as np import modules.scripts as scripts import gradio as gr -from modules import images, sd_samplers, processing, sd_models, sd_vae, sd_samplers_kdiffusion, errors +from modules import images, sd_samplers, processing, sd_models, sd_vae, sd_schedulers, errors from modules.processing import process_images, Processed, StableDiffusionProcessingTxt2Img from modules.shared import opts, state import modules.shared as shared @@ -45,7 +45,7 @@ def apply_prompt(p, x, xs): def apply_order(p, x, xs): token_order = [] - # Initally grab the tokens from the prompt, so they can be replaced in order of earliest seen + # Initially grab the tokens from the prompt, so they can be replaced in order of earliest seen for token in x: token_order.append((p.prompt.find(token), token)) @@ -95,33 +95,38 @@ def confirm_checkpoints_or_none(p, xs): raise RuntimeError(f"Unknown checkpoint: {x}") -def apply_clip_skip(p, x, xs): - opts.data["CLIP_stop_at_last_layers"] = x +def confirm_range(min_val, max_val, axis_label): + """Generates a AxisOption.confirm() function that checks all values are within the specified range.""" + + def confirm_range_fun(p, xs): + for x in xs: + if not (max_val >= x >= min_val): + raise ValueError(f'{axis_label} value "{x}" out of range [{min_val}, {max_val}]') + + return confirm_range_fun -def apply_upscale_latent_space(p, x, xs): - if x.lower().strip() != '0': - opts.data["use_scale_latent_for_hires_fix"] = True - else: - opts.data["use_scale_latent_for_hires_fix"] = False +def apply_size(p, x: str, xs) -> None: + try: + width, _, height = x.partition('x') + width = int(width.strip()) + height = int(height.strip()) + p.width = width + p.height = height + except ValueError: + print(f"Invalid size in XYZ plot: {x}") def find_vae(name: str): - if name.lower() in ['auto', 'automatic']: - return modules.sd_vae.unspecified - if name.lower() == 'none': - return None - else: - choices = [x for x in sorted(modules.sd_vae.vae_dict, key=lambda x: len(x)) if name.lower().strip() in x.lower()] - if len(choices) == 0: - print(f"No VAE found for {name}; using automatic") - return modules.sd_vae.unspecified - else: - return modules.sd_vae.vae_dict[choices[0]] + if (name := name.strip().lower()) in ('auto', 'automatic'): + return 'Automatic' + elif name == 'none': + return 'None' + return next((k for k in modules.sd_vae.vae_dict if k.lower() == name), print(f'No VAE found for {name}; using Automatic') or 'Automatic') def apply_vae(p, x, xs): - modules.sd_vae.reload_vae_weights(shared.sd_model, vae_file=find_vae(x)) + p.override_settings['sd_vae'] = find_vae(x) def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): @@ -129,7 +134,7 @@ def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): def apply_uni_pc_order(p, x, xs): - opts.data["uni_pc_order"] = min(x, p.steps - 1) + p.override_settings['uni_pc_order'] = min(x, p.steps - 1) def apply_face_restore(p, opt, x): @@ -151,12 +156,14 @@ def apply_override(field, boolean: bool = False): if boolean: x = True if x.lower() == "true" else False p.override_settings[field] = x + return fun def boolean_choice(reverse: bool = False): def choice(): return ["False", "True"] if reverse else ["True", "False"] + return choice @@ -201,7 +208,7 @@ def list_to_csv_string(data_list): def csv_string_to_list_strip(data_str): - return list(map(str.strip, chain.from_iterable(csv.reader(StringIO(data_str))))) + return list(map(str.strip, chain.from_iterable(csv.reader(StringIO(data_str), skipinitialspace=True)))) class AxisOption: @@ -248,18 +255,20 @@ axis_options = [ AxisOption("Sigma min", float, apply_field("s_tmin")), AxisOption("Sigma max", float, apply_field("s_tmax")), AxisOption("Sigma noise", float, apply_field("s_noise")), - AxisOption("Schedule type", str, apply_override("k_sched_type"), choices=lambda: list(sd_samplers_kdiffusion.k_diffusion_scheduler)), + AxisOption("Schedule type", str, apply_field("scheduler"), choices=lambda: [x.label for x in sd_schedulers.schedulers]), AxisOption("Schedule min sigma", float, apply_override("sigma_min")), AxisOption("Schedule max sigma", float, apply_override("sigma_max")), AxisOption("Schedule rho", float, apply_override("rho")), + AxisOption("Beta schedule alpha", float, apply_override("beta_dist_alpha")), + AxisOption("Beta schedule beta", float, apply_override("beta_dist_beta")), AxisOption("Eta", float, apply_field("eta")), - AxisOption("Clip skip", int, apply_clip_skip), + AxisOption("Clip skip", int, apply_override('CLIP_stop_at_last_layers')), AxisOption("Denoising", float, apply_field("denoising_strength")), AxisOption("Initial noise multiplier", float, apply_field("initial_noise_multiplier")), AxisOption("Extra noise", float, apply_override("img2img_extra_noise")), AxisOptionTxt2Img("Hires upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), AxisOptionImg2Img("Cond. Image Mask Weight", float, apply_field("inpainting_mask_weight")), - AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: ['None'] + list(sd_vae.vae_dict)), + AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: ['Automatic', 'None'] + list(sd_vae.vae_dict)), AxisOption("Styles", str, apply_styles, choices=lambda: list(shared.prompt_styles.styles)), AxisOption("UniPC Order", int, apply_uni_pc_order, cost=0.5), AxisOption("Face restore", str, apply_face_restore, format_value=format_value), @@ -271,6 +280,7 @@ axis_options = [ AxisOption("Refiner switch at", float, apply_field('refiner_switch_at')), AxisOption("RNG source", str, apply_override("randn_source"), choices=lambda: ["GPU", "CPU", "NV"]), AxisOption("FP8 mode", str, apply_override("fp8_storage"), cost=0.9, choices=lambda: ["Disable", "Enable for SDXL", "Enable"]), + AxisOption("Size", str, apply_size), ] @@ -366,16 +376,17 @@ def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend end_index = start_index + len(xs) * len(ys) grid = images.image_grid(processed_result.images[start_index:end_index], rows=len(ys)) if draw_legend: - grid = images.draw_grid_annotations(grid, processed_result.images[start_index].size[0], processed_result.images[start_index].size[1], hor_texts, ver_texts, margin_size) + grid_max_w, grid_max_h = map(max, zip(*(img.size for img in processed_result.images[start_index:end_index]))) + grid = images.draw_grid_annotations(grid, grid_max_w, grid_max_h, hor_texts, ver_texts, margin_size) processed_result.images.insert(i, grid) processed_result.all_prompts.insert(i, processed_result.all_prompts[start_index]) processed_result.all_seeds.insert(i, processed_result.all_seeds[start_index]) processed_result.infotexts.insert(i, processed_result.infotexts[start_index]) - sub_grid_size = processed_result.images[0].size z_grid = images.image_grid(processed_result.images[:z_count], rows=1) + z_sub_grid_max_w, z_sub_grid_max_h = map(max, zip(*(img.size for img in processed_result.images[:z_count]))) if draw_legend: - z_grid = images.draw_grid_annotations(z_grid, sub_grid_size[0], sub_grid_size[1], title_texts, [[images.GridAnnotation()]]) + z_grid = images.draw_grid_annotations(z_grid, z_sub_grid_max_w, z_sub_grid_max_h, title_texts, [[images.GridAnnotation()]]) processed_result.images.insert(0, z_grid) # TODO: Deeper aspects of the program rely on grid info being misaligned between metadata arrays, which is not ideal. # processed_result.all_prompts.insert(0, processed_result.all_prompts[0]) @@ -387,18 +398,12 @@ def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend class SharedSettingsStackHelper(object): def __enter__(self): - self.CLIP_stop_at_last_layers = opts.CLIP_stop_at_last_layers - self.vae = opts.sd_vae - self.uni_pc_order = opts.uni_pc_order + pass def __exit__(self, exc_type, exc_value, tb): - opts.data["sd_vae"] = self.vae - opts.data["uni_pc_order"] = self.uni_pc_order modules.sd_models.reload_model_weights() modules.sd_vae.reload_vae_weights() - opts.data["CLIP_stop_at_last_layers"] = self.CLIP_stop_at_last_layers - re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*") re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*") @@ -560,7 +565,7 @@ class Script(scripts.Script): mc = re_range_count.fullmatch(val) if m is not None: start = int(m.group(1)) - end = int(m.group(2))+1 + end = int(m.group(2)) + 1 step = int(m.group(3)) if m.group(3) is not None else 1 valslist_ext += list(range(start, end, step)) @@ -713,11 +718,11 @@ class Script(scripts.Script): ydim = len(ys) if vary_seeds_y else 1 if vary_seeds_x: - pc.seed += ix + pc.seed += ix if vary_seeds_y: - pc.seed += iy * xdim + pc.seed += iy * xdim if vary_seeds_z: - pc.seed += iz * xdim * ydim + pc.seed += iz * xdim * ydim try: res = process_images(pc) @@ -785,18 +790,18 @@ class Script(scripts.Script): z_count = len(zs) # Set the grid infotexts to the real ones with extra_generation_params (1 main grid + z_count sub-grids) - processed.infotexts[:1+z_count] = grid_infotext[:1+z_count] + processed.infotexts[:1 + z_count] = grid_infotext[:1 + z_count] if not include_lone_images: # Don't need sub-images anymore, drop from list: - processed.images = processed.images[:z_count+1] + processed.images = processed.images[:z_count + 1] if opts.grid_save: # Auto-save main and sub-grids: grid_count = z_count + 1 if z_count > 1 else 1 for g in range(grid_count): # TODO: See previous comment about intentional data misalignment. - adj_g = g-1 if g > 0 else g + adj_g = g - 1 if g > 0 else g images.save_image(processed.images[g], p.outpath_grids, "xyz_grid", info=processed.infotexts[g], extension=opts.grid_format, prompt=processed.all_prompts[adj_g], seed=processed.all_seeds[adj_g], grid=True, p=processed) if not include_sub_grids: # if not include_sub_grids then skip saving after the first grid break diff --git a/style.css b/style.css index 8ce78ff0..64ef61ba 100644 --- a/style.css +++ b/style.css @@ -279,7 +279,7 @@ input[type="checkbox"].input-accordion-checkbox{ display: inline-block; } -.html-log .performance p.time, .performance p.vram, .performance p.time abbr, .performance p.vram abbr { +.html-log .performance p.time, .performance p.vram, .performance p.profile, .performance p.time abbr, .performance p.vram abbr { margin-bottom: 0; color: var(--block-title-text-color); } @@ -291,6 +291,10 @@ input[type="checkbox"].input-accordion-checkbox{ margin-left: auto; } +.html-log .performance p.profile { + margin-left: 0.5em; +} + .html-log .performance .measurement{ color: var(--body-text-color); font-weight: bold; @@ -528,6 +532,10 @@ table.popup-table .link{ opacity: 0.75; } +.settings-comment .info ol{ + margin: 0.4em 0 0.8em 1em; +} + #sysinfo_download a.sysinfo_big_link{ font-size: 24pt; } @@ -776,9 +784,9 @@ table.popup-table .link{ position:absolute; display:block; padding:0px 0; - border:2px solid #a55000; + border:2px solid var(--primary-800); border-radius:8px; - box-shadow:1px 1px 2px #CE6400; + box-shadow:1px 1px 2px var(--primary-500); width: 200px; } @@ -795,7 +803,7 @@ table.popup-table .link{ } .context-menu-items a:hover{ - background: #a55000; + background: var(--primary-700); } @@ -803,6 +811,8 @@ table.popup-table .link{ #tab_extensions table{ border-collapse: collapse; + overflow-x: auto; + display: block; } #tab_extensions table td, #tab_extensions table th{ @@ -850,6 +860,10 @@ table.popup-table .link{ display: inline-block; } +.compact-checkbox-group div label { + padding: 0.1em 0.3em !important; +} + /* extensions tab table row hover highlight */ #extensions tr:hover td, @@ -1205,12 +1219,24 @@ body.resizing .resize-handle { overflow: hidden; } -.extra-network-pane .extra-network-pane-content { +.extra-network-pane .extra-network-pane-content-dirs { + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; +} + +.extra-network-pane .extra-network-pane-content-tree { display: flex; flex: 1; overflow: hidden; } +.extra-network-dirs-hidden .extra-network-dirs{ display: none; } +.extra-network-dirs-hidden .extra-network-tree{ display: none; } +.extra-network-dirs-hidden .resize-handle { display: none; } +.extra-network-dirs-hidden .resize-handle-row { display: flex !important; } + .extra-network-pane .extra-network-tree { flex: 1; font-size: 1rem; @@ -1260,7 +1286,7 @@ body.resizing .resize-handle { .extra-network-control { position: relative; - display: grid; + display: flex; width: 100%; padding: 0 !important; margin-top: 0 !important; @@ -1277,6 +1303,12 @@ body.resizing .resize-handle { align-items: start; } +.extra-network-control small{ + color: var(--input-placeholder-color); + line-height: 2.2rem; + margin: 0 0.5rem 0 0.75rem; +} + .extra-network-tree .tree-list--tree {} /* Remove auto indentation from tree. Will be overridden later. */ @@ -1424,6 +1456,12 @@ body.resizing .resize-handle { line-height: 1rem; } + +.extra-network-control .extra-network-control--search .extra-network-control--search-text::placeholder { + color: var(--input-placeholder-color); +} + + /* clear button (x on right side) styling */ .extra-network-control .extra-network-control--search .extra-network-control--search-text::-webkit-search-cancel-button { -webkit-appearance: none; @@ -1456,19 +1494,19 @@ body.resizing .resize-handle { background-color: var(--input-placeholder-color); } -.extra-network-control .extra-network-control--sort[data-sortmode="path"] .extra-network-control--sort-icon { +.extra-network-control .extra-network-control--sort[data-sortkey="default"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } -.extra-network-control .extra-network-control--sort[data-sortmode="name"] .extra-network-control--sort-icon { +.extra-network-control .extra-network-control--sort[data-sortkey="name"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } -.extra-network-control .extra-network-control--sort[data-sortmode="date_created"] .extra-network-control--sort-icon { +.extra-network-control .extra-network-control--sort[data-sortkey="date_created"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } -.extra-network-control .extra-network-control--sort[data-sortmode="date_modified"] .extra-network-control--sort-icon { +.extra-network-control .extra-network-control--sort[data-sortkey="date_modified"] .extra-network-control--sort-icon { mask-image: url('data:image/svg+xml,'); } @@ -1518,13 +1556,18 @@ body.resizing .resize-handle { } .extra-network-control .extra-network-control--enabled { - background-color: rgba(0, 0, 0, 0.15); + background-color: rgba(0, 0, 0, 0.1); + border-radius: 0.25rem; } .dark .extra-network-control .extra-network-control--enabled { background-color: rgba(255, 255, 255, 0.15); } +.extra-network-control .extra-network-control--enabled .extra-network-control--icon{ + background-color: var(--button-secondary-text-color); +} + /* ==== REFRESH ICON ACTIONS ==== */ .extra-network-control .extra-network-control--refresh { padding: 0.25rem; @@ -1615,9 +1658,10 @@ body.resizing .resize-handle { display: inline-flex; visibility: hidden; color: var(--button-secondary-text-color); - + width: 0; } .extra-network-tree .tree-list-content:hover .button-row { visibility: visible; + width: auto; } diff --git a/webui-macos-env.sh b/webui-macos-env.sh index db7e8b1a..00a36e17 100644 --- a/webui-macos-env.sh +++ b/webui-macos-env.sh @@ -4,14 +4,14 @@ # Please modify webui-user.sh to change these instead of this file # #################################################################### -if [[ -x "$(command -v python3.10)" ]] -then - python_cmd="python3.10" -fi - export install_dir="$HOME" export COMMANDLINE_ARGS="--skip-torch-cuda-test --upcast-sampling --no-half-vae --use-cpu interrogate" -export TORCH_COMMAND="pip install torch==2.1.0 torchvision==0.16.0" export PYTORCH_ENABLE_MPS_FALLBACK=1 +if [[ "$(sysctl -n machdep.cpu.brand_string)" =~ ^.*"Intel".*$ ]]; then + export TORCH_COMMAND="pip install torch==2.1.2 torchvision==0.16.2" +else + export TORCH_COMMAND="pip install torch==2.3.1 torchvision==0.18.1" +fi + #################################################################### diff --git a/webui.bat b/webui.bat index e2c9079d..7b162ce2 100644 --- a/webui.bat +++ b/webui.bat @@ -37,12 +37,18 @@ if %ERRORLEVEL% == 0 goto :activate_venv for /f "delims=" %%i in ('CALL %PYTHON% -c "import sys; print(sys.executable)"') do set PYTHON_FULLNAME="%%i" echo Creating venv in directory %VENV_DIR% using python %PYTHON_FULLNAME% %PYTHON_FULLNAME% -m venv "%VENV_DIR%" >tmp/stdout.txt 2>tmp/stderr.txt -if %ERRORLEVEL% == 0 goto :activate_venv +if %ERRORLEVEL% == 0 goto :upgrade_pip echo Unable to create venv in directory "%VENV_DIR%" goto :show_stdout_stderr +:upgrade_pip +"%VENV_DIR%\Scripts\Python.exe" -m pip install --upgrade pip +if %ERRORLEVEL% == 0 goto :activate_venv +echo Warning: Failed to upgrade PIP version + :activate_venv set PYTHON="%VENV_DIR%\Scripts\Python.exe" +call "%VENV_DIR%\Scripts\activate.bat" echo venv %PYTHON% :skip_venv diff --git a/webui.sh b/webui.sh index f116376f..89dae163 100755 --- a/webui.sh +++ b/webui.sh @@ -44,7 +44,11 @@ fi # python3 executable if [[ -z "${python_cmd}" ]] then - python_cmd="python3" + python_cmd="python3.10" +fi +if [[ ! -x "$(command -v "${python_cmd}")" ]] +then + python_cmd="python3" fi # git executable @@ -113,13 +117,13 @@ then exit 1 fi -if [[ -d .git ]] +if [[ -d "$SCRIPT_DIR/.git" ]] then printf "\n%s\n" "${delimiter}" printf "Repo already cloned, using it as install directory" printf "\n%s\n" "${delimiter}" - install_dir="${PWD}/../" - clone_dir="${PWD##*/}" + install_dir="${SCRIPT_DIR}/../" + clone_dir="${SCRIPT_DIR##*/}" fi # Check prerequisites @@ -129,13 +133,19 @@ case "$gpu_info" in export HSA_OVERRIDE_GFX_VERSION=10.3.0 if [[ -z "${TORCH_COMMAND}" ]] then - pyv="$(${python_cmd} -c 'import sys; print(".".join(map(str, sys.version_info[0:2])))')" - if [[ $(bc <<< "$pyv <= 3.10") -eq 1 ]] + pyv="$(${python_cmd} -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]:02d}")')" + # Using an old nightly compiled against rocm 5.2 for Navi1, see https://github.com/pytorch/pytorch/issues/106728#issuecomment-1749511711 + if [[ $pyv == "3.8" ]] then - # Navi users will still use torch 1.13 because 2.0 does not seem to work. - export TORCH_COMMAND="pip install --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm5.6" + export TORCH_COMMAND="pip install https://download.pytorch.org/whl/nightly/rocm5.2/torch-2.0.0.dev20230209%2Brocm5.2-cp38-cp38-linux_x86_64.whl https://download.pytorch.org/whl/nightly/rocm5.2/torchvision-0.15.0.dev20230209%2Brocm5.2-cp38-cp38-linux_x86_64.whl" + elif [[ $pyv == "3.9" ]] + then + export TORCH_COMMAND="pip install https://download.pytorch.org/whl/nightly/rocm5.2/torch-2.0.0.dev20230209%2Brocm5.2-cp39-cp39-linux_x86_64.whl https://download.pytorch.org/whl/nightly/rocm5.2/torchvision-0.15.0.dev20230209%2Brocm5.2-cp39-cp39-linux_x86_64.whl" + elif [[ $pyv == "3.10" ]] + then + export TORCH_COMMAND="pip install https://download.pytorch.org/whl/nightly/rocm5.2/torch-2.0.0.dev20230209%2Brocm5.2-cp310-cp310-linux_x86_64.whl https://download.pytorch.org/whl/nightly/rocm5.2/torchvision-0.15.0.dev20230209%2Brocm5.2-cp310-cp310-linux_x86_64.whl" else - printf "\e[1m\e[31mERROR: RX 5000 series GPUs must be using at max python 3.10, aborting...\e[0m" + printf "\e[1m\e[31mERROR: RX 5000 series GPUs python version must be between 3.8 and 3.10, aborting...\e[0m" exit 1 fi fi @@ -143,7 +153,7 @@ case "$gpu_info" in *"Navi 2"*) export HSA_OVERRIDE_GFX_VERSION=10.3.0 ;; *"Navi 3"*) [[ -z "${TORCH_COMMAND}" ]] && \ - export TORCH_COMMAND="pip install --pre torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm5.7" + export TORCH_COMMAND="pip install torch torchvision --index-url https://download.pytorch.org/whl/nightly/rocm5.7" ;; *"Renoir"*) export HSA_OVERRIDE_GFX_VERSION=9.0.0 printf "\n%s\n" "${delimiter}" @@ -157,11 +167,10 @@ if ! echo "$gpu_info" | grep -q "NVIDIA"; then if echo "$gpu_info" | grep -q "AMD" && [[ -z "${TORCH_COMMAND}" ]] then - export TORCH_COMMAND="pip install torch==2.0.1+rocm5.4.2 torchvision==0.15.2+rocm5.4.2 --index-url https://download.pytorch.org/whl/rocm5.4.2" - elif echo "$gpu_info" | grep -q "Huawei" && [[ -z "${TORCH_COMMAND}" ]] + export TORCH_COMMAND="pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm5.7" + elif npu-smi info 2>/dev/null then - export TORCH_COMMAND="pip install torch==2.1.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu; pip install torch_npu" - + export TORCH_COMMAND="pip install torch==2.1.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu; pip install torch_npu==2.1.0" fi fi @@ -205,12 +214,15 @@ then if [[ ! -d "${venv_dir}" ]] then "${python_cmd}" -m venv "${venv_dir}" + "${venv_dir}"/bin/python -m pip install --upgrade pip first_launch=1 fi # shellcheck source=/dev/null if [[ -f "${venv_dir}"/bin/activate ]] then source "${venv_dir}"/bin/activate + # ensure use of python from venv + python_cmd="${venv_dir}"/bin/python else printf "\n%s\n" "${delimiter}" printf "\e[1m\e[31mERROR: Cannot activate python venv, aborting...\e[0m" @@ -238,7 +250,7 @@ prepare_tcmalloc() { for lib in "${TCMALLOC_LIBS[@]}" do # Determine which type of tcmalloc library the library supports - TCMALLOC="$(PATH=/usr/sbin:$PATH ldconfig -p | grep -P $lib | head -n 1)" + TCMALLOC="$(PATH=/sbin:/usr/sbin:$PATH ldconfig -p | grep -P $lib | head -n 1)" TC_INFO=(${TCMALLOC//=>/}) if [[ ! -z "${TC_INFO}" ]]; then echo "Check TCMalloc: ${TC_INFO}"