elismasilva commited on
Commit
7ebbf86
·
0 Parent(s):

initial commit

Browse files
Files changed (44) hide show
  1. .gitattributes +37 -0
  2. .gitignore +15 -0
  3. CHANGELOG.md +51 -0
  4. LICENSE +177 -0
  5. NOTICE +62 -0
  6. README.md +149 -0
  7. app.py +405 -0
  8. assets/devaixp_logo-white.png +3 -0
  9. assets/samples/1.jpg +3 -0
  10. assets/samples/2.jpg +3 -0
  11. assets/samples/3.jpg +3 -0
  12. assets/samples/band.png +3 -0
  13. assets/samples/band_restored.png +3 -0
  14. assets/samples/boy.jpg +3 -0
  15. assets/samples/woman.png +3 -0
  16. assets/samples/woman_restored.png +3 -0
  17. configs/settings.json +29 -0
  18. outputs/examples/1_FaithDiff_Restore_1x_CFGOnly.png +3 -0
  19. outputs/examples/1_FaithDiff_Restore_1x_FaithDiff_Upscale_2x_CFGOnly.png +3 -0
  20. outputs/examples/1_FaithDiff_Restore_1x_FaithDiff_Upscale_2x_PAG.png +3 -0
  21. outputs/examples/1_FaithDiff_Restore_1x_PAG.png +3 -0
  22. outputs/examples/1_FaithDiff_Restore_1x_SUPIR_Upscale_2x.png +3 -0
  23. outputs/examples/1_SUPIR_Restore_1x_CFGOnly.png +3 -0
  24. outputs/examples/1_SUPIR_Restore_1x_FaithDiff_Upscale_2x.png +3 -0
  25. outputs/examples/1_SUPIR_Restore_1x_PAGOnly.png +3 -0
  26. outputs/examples/1_SUPIR_Restore_1x_SUPIR_Upscale_2x_CFGOnly.png +3 -0
  27. outputs/examples/1_SUPIR_Restore_1x_SUPIR_Upscale_2x_PAG.png +3 -0
  28. outputs/examples/2_FaithDiff_Restore_1x_CFGOnly.png +3 -0
  29. outputs/examples/2_SUPIR_Restore_1x.png +3 -0
  30. outputs/examples/2_SUPIR_Restore_1x_ControlNetTIle_Upscale_2x.png +3 -0
  31. outputs/examples/2_SUPIR_Restore_1x_ControlNetTIle_Upscale_4x.png +3 -0
  32. outputs/examples/3_ControlnetTile_Upscale_2x.png +3 -0
  33. outputs/examples/4_ControlnetTile_Upscale_8x.jpg +3 -0
  34. outputs/examples/5_ControlnetTile_Upscale_4x.png +3 -0
  35. requirements.txt +25 -0
  36. ui/globals.py +8 -0
  37. ui/script.js +194 -0
  38. ui/style.css +301 -0
  39. ui/ui_config.py +1360 -0
  40. ui/ui_data.py +525 -0
  41. ui/ui_events.py +1789 -0
  42. ui/ui_layout.py +684 -0
  43. ui/ui_state.py +38 -0
  44. ui/util/dataclass_helpers.py +300 -0
.gitattributes ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.jpg filter=lfs diff=lfs merge=lfs -text
37
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ /outputs/restored
4
+ /outputs/upscaled
5
+ /models
6
+ /dist
7
+ /.vs
8
+ .vscode/
9
+ .idea/
10
+ /venv
11
+ .venv/
12
+ *.log
13
+ Makefile
14
+ .DS_Store
15
+ .gradio
CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-11-10
9
+
10
+ This marks the first major stable release after a significant architectural refactor, focusing on robustness, maintainability, and developer experience.
11
+
12
+ ### Added
13
+
14
+ - **Modular Architecture:**
15
+ - Introduced a clean, multi-file architecture separating responsibilities:
16
+ - `app.py`: The main application orchestrator.
17
+ - `ui_layout.py`: Defines the complete UI structure and components.
18
+ - `ui_events.py`: Contains all event handling logic and business rules.
19
+ - `ui_state.py`: Provides a centralized `AppState` dataclass for managing shared application state.
20
+ - **State Management:**
21
+ - Implemented a shared `AppState` container to manage application state (e.g., `uidata`, `cancel_event`, pipeline instances) cleanly, removing all global variables.
22
+ - Event logic is now encapsulated within an `EventHandlers` class, which receives the shared state and UI components via dependency injection.
23
+ - **Developer Experience:**
24
+ - Created a `UIComponents` dataclass to hold all Gradio component instances, providing full static type checking and editor Intellisense (auto-completion) for component access.
25
+ - Refactored event bindings in `app.py` to use direct attribute access (e.g., `c.run_btn.click`) instead of error-prone string-based dictionary keys.
26
+
27
+ ### Changed
28
+
29
+ - **Event Handling Logic:**
30
+ - Reverted the primary image generation event from using the `@livelog` decorator back to the original, more robust `threading` and `queue` pattern. This ensures the progress bar and logs update correctly even when the browser tab is not in focus.
31
+ - The `generate` method now acts as a generator that spawns a `process_image` worker thread, yielding updates from a queue to the UI.
32
+ - **Event Binding:**
33
+ - The main application class (`GradioApp`) is now solely responsible for orchestrating the app's lifecycle: initializing state, building the UI, and binding events. All implementation logic has been moved to `EventHandlers`.
34
+ - Simplified event binding calls by removing unnecessary `lambda` and `partial` wrappers where methods could be called directly. `partial` is now only used for its intended purpose of pre-filling static arguments.
35
+ - **Code Quality & Style:**
36
+ - Integrated `ruff` as the primary tool for linting and formatting, replacing `isort` and `black`.
37
+ - Refactored code to resolve all major `ruff` warnings, including:
38
+ - Replacing unnecessary list comprehensions with more efficient set comprehensions (e.g., `set([...])` -> `{...}`).
39
+ - Replacing list comprehensions inside `any()` with memory-efficient generator expressions.
40
+ - Eliminating all "bare `except`" blocks by specifying `except Exception` to prevent hiding critical system errors.
41
+
42
+ ### Fixed
43
+
44
+ - **Component State Synchronization:**
45
+ - Fixed a critical bug where event handlers were reading stale state from component instances instead of receiving the latest values from the UI. All event handler methods now correctly receive up-to-date component values as arguments from the `inputs` list of the event trigger.
46
+ - **Gradio Context Errors:**
47
+ - Resolved `AttributeError: Cannot call .click outside of a gradio.Blocks context` by centralizing the `gr.Blocks` context management within the `GradioApp` constructor, ensuring both UI creation and event binding occur within the same active context.
48
+ - **`gr.EventData` Injection:**
49
+ - Fixed a bug where `gr.EventData` was being passed as `None` to event handlers. Implemented a robust wrapper pattern for event callbacks that require `EventData`, ensuring Gradio's event data injection mechanism works correctly with the class-based architecture.
50
+ - **Preset Loading Logic:**
51
+ - Corrected a bug in the `load_preset` function where it was mutating the global `RESTORER_CONFIG_MAPPING` dictionary with component instances instead of classes, which caused a `TypeError` on subsequent UI interactions. The function now correctly updates the application state without modifying the original class mapping.
LICENSE ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
NOTICE ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DEVAIEXP Scaling-UP ToolBox Application
2
+ Copyright 2025, DEVAIEXP
3
+
4
+ This product is licensed under the Apache License, Version 2.0.
5
+ You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ This product includes or is built upon software developed by third parties.
8
+ The following is a list of these components and their respective licenses.
9
+ Full license texts for each component can be found within their respective module directories.
10
+
11
+ ===================================================
12
+ Component: SUPIR
13
+ ---------------------------------------------------
14
+ This product includes an adaptation of the SUPIR project.
15
+ Copyright (c) 2024 by SupPixel Pty Ltd. All rights reserved.
16
+ Original Project: https://github.com/Fanghua-Yu/SUPIR
17
+
18
+ The use of this component is strictly governed by the SUPIR Software
19
+ License Agreement. This license PROHIBITS ANY COMMERCIAL USE of the
20
+ SUPIR pipeline or its outputs. For commercial licensing, please contact
21
+ the original authors.
22
+
23
+ ===================================================
24
+ Component: FaithDiff
25
+ ---------------------------------------------------
26
+ This product includes a derivative work based on the FaithDiff project.
27
+ The original work is Copyright (c) 2025, Junyang Chen, and is licensed
28
+ under the MIT License.
29
+ Original Project: https://github.com/JyChen9811/FaithDiff
30
+
31
+ ===================================================
32
+ Component: IPAdapter
33
+ ---------------------------------------------------
34
+ This product utilizes software from the IP-Adapter project.
35
+ The original work is Copyright (c) 2023, TencentARC, and is licensed
36
+ under the Apache License, Version 2.0.
37
+ Original Project: https://github.com/tencent-ailab/IP-Adapter
38
+
39
+ ===================================================
40
+ Component: ControlNet Tile Pipeline
41
+ ---------------------------------------------------
42
+ The code for this pipeline is a derivative work based on the ControlNet
43
+ pipeline implementations found in the HuggingFace Diffusers library.
44
+
45
+ The original Diffusers library is Copyright 2024, The HuggingFace Team,
46
+ and is licensed under the Apache License, Version 2.0.
47
+
48
+ ===================================================
49
+ Component: Mixture of Diffusers Technique
50
+ ---------------------------------------------------
51
+ This product includes an implementation of the "Mixture of Diffusers"
52
+ technique. The original work is Copyright (c) 2022, Álvaro Barbero Jiménez,
53
+ and is licensed under the MIT License.
54
+ Original Project: https://github.com/albarji/mixture-of-diffusers
55
+
56
+ ===================================================
57
+ Component: Diffusers Library
58
+ ---------------------------------------------------
59
+ This product is built upon and includes software from the Diffusers library.
60
+ The original work is Copyright 2024, The HuggingFace Team, and is licensed
61
+ under the Apache License, Version 2.0.
62
+ Original Project: https://github.com/huggingface/diffusers
README.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Sup Toolbox App
3
+ emoji: ⚡
4
+ colorFrom: gray
5
+ colorTo: yellow
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ short_description: SUP Toolbox Gradio Demo
12
+ ---
13
+
14
+ <h1 align="center">SUP Toolbox App 🎨</h1>
15
+ <p align="center">
16
+ A user-friendly Gradio web interface for the powerful <strong>SUP-Toolbox</strong> image restoration and upscaling library.
17
+ </p>
18
+
19
+ <p align="center">
20
+ <img src="https://huggingface.co/datasets/DEVAIEXP/assets/resolve/main/screen_sup_toolbox.PNG" alt="SUP Toolbox App Interface" width="800"/>
21
+ </p>
22
+
23
+ <p align="center">
24
+ <a href="https://github.com/DEVAIEXP/sup-toolbox-app/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License"></a>
25
+ <a href="https://github.com/DEVAIEXP/sup-toolbox/blob/main/pyproject.toml"><img src="https://img.shields.io/badge/Python-3.10%2B-blue.svg" alt="Python Version"></a>
26
+ <a href="https://github.com/DEVAIEXP/sup-toolbox-app/stargazers"><img src="https://img.shields.io/github/stars/DEVAIEXP/sup-toolbox-app?style=social" alt="GitHub Stars"></a>
27
+ <a href="https://github.com/DEVAIEXP/sup-toolbox"><img src="https://img.shields.io/badge/Powered%20by-SUP--Toolbox-orange" alt="Powered by SUP-Toolbox"></a>
28
+ <a href="https://huggingface.co/spaces/YOUR_HF_USERNAME/sup-toolbox-app"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-yellow" alt="Hugging Face Spaces"></a>
29
+ </p>
30
+
31
+ ## About
32
+
33
+ The **SUP Toolbox App** provides an intuitive and interactive web interface for the [SUP-Toolbox library](https://github.com/DEVAIEXP/sup-toolbox). It allows you to harness the power of advanced image enhancement models like **SUPIR**, **FaithDiff**, and **ControlNetTile** through a user-friendly Gradio application, without needing to use the command line.
34
+
35
+ This application is designed for artists, photographers, and enthusiasts who want to visually experiment with different settings, manage presets, and process images one by one with real-time feedback.
36
+
37
+ ## Features
38
+
39
+ - **Interactive UI:** A clean and organized Gradio interface for easy access to all features.
40
+ - **Full Engine Support:** Access and combine all restoration (`SUPIR`, `FaithDiff`) and upscaling (`SUPIR`, `FaithDiff`, `ControlNetTile`) engines from the core library.
41
+ - **Preset Management:** Create, save, and load your custom workflows as `.json` presets directly from the UI.
42
+ - **Live Previews & Comparison:** Instantly see the results of your settings with before-and-after image sliders.
43
+ - **Metadata Handling:** Automatically saves all generation parameters within the output image. Load settings from a previously generated image by simply dropping it into the app.
44
+ - **Real-time Logging:** A live log panel shows the progress of the image generation process.
45
+
46
+ ## Requirements
47
+
48
+ This application depends on the `sup-toolbox` library. Therefore, it shares the same hardware and software requirements. For a detailed breakdown, please refer to the core library's documentation:
49
+
50
+ ➡️ **[View Full Requirements in the SUP-Toolbox README](https://github.com/DEVAIEXP/sup-toolbox#requirements)**
51
+
52
+ In summary:
53
+ - **Python 3.10+**, Git
54
+ - **NVIDIA GPU** with 12GB+ VRAM recommended.
55
+ - **~83 GB** of disk space for models.
56
+
57
+ ## Installation
58
+
59
+ The setup process is designed to be simple. It will create a virtual environment and install both the `sup-toolbox` library from its GitHub repository and all other necessary Python packages.
60
+
61
+ ### 1. Clone the Application Repository
62
+ ```bash
63
+ git clone https://github.com/DEVAIEXP/sup-toolbox-app.git
64
+ cd sup-toolbox-app
65
+ ```
66
+
67
+ ### 2. Run the Setup Script
68
+
69
+ Choose the script that matches your operating system and terminal.
70
+
71
+ * **On Windows (using PowerShell):**
72
+ This is the recommended method for modern Windows systems.
73
+
74
+ 1. **Allow script execution (one-time setup):**
75
+ If you haven't run PowerShell scripts before, open PowerShell **as an Administrator** and run:
76
+ ```powershell
77
+ Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
78
+ ```
79
+ Press `Y` and `Enter` to confirm.
80
+
81
+ 2. **Run the script:**
82
+ In a regular PowerShell terminal, run:
83
+ ```powershell
84
+ .\setup.ps1
85
+ ```
86
+
87
+ * **On Windows (using Command Prompt):**
88
+ ```batch
89
+ .\setup.bat
90
+ ```
91
+
92
+ * **On Linux or macOS:**
93
+ ```bash
94
+ chmod +x setup.sh
95
+ ./setup.sh
96
+ ```
97
+
98
+ This will set up everything you need. The `requirements.txt` file in this repository is configured to automatically download and install the correct version of the `sup-toolbox` library.
99
+
100
+ ## Usage
101
+
102
+ After a successful installation, you can easily start the application using the provided start scripts.
103
+
104
+ * **On Windows:** Double-click `start.bat` or `start.ps1`, or run them from your terminal:
105
+ ```powershell
106
+ # In PowerShell
107
+ .\start.ps1
108
+ ```
109
+ ```batch
110
+ :: In Command Prompt
111
+ .\start.bat
112
+ ```
113
+
114
+ * **On Linux or macOS:**
115
+ ```bash
116
+ ./start.sh
117
+ ```
118
+
119
+ This will activate the virtual environment and launch the Gradio application. Open your web browser and navigate to the local URL provided in the terminal (usually `http://127.0.0.1:7860`).
120
+
121
+ ## Relationship with `sup-toolbox`
122
+
123
+ This project is the official Gradio UI front-end for the `sup-toolbox` library.
124
+
125
+ - **[sup-toolbox (Library)](https://github.com/DEVAIEXP/sup-toolbox):** The core engine. It's a Python library and CLI tool that contains all the processing logic, model management, and configuration system. It's designed for programmatic use and automation.
126
+ - **sup-toolbox-app (This Project):** A user-friendly graphical interface that *uses* the `sup-toolbox` library as its backend. It provides no image processing logic itself but offers a convenient way to access the library's features.
127
+
128
+ ## Releases and Changelog
129
+
130
+ For a detailed list of changes, new features, and bug fixes for each version of this application, please see the **[CHANGELOG.md](CHANGELOG.md)** file.
131
+
132
+ ## Licensing
133
+
134
+ The **SUP Toolbox App** (this project) is licensed under the **Apache License, Version 2.0**. You can find the full license text in the `LICENSE` file in this repository.
135
+
136
+ Please be aware that this application is a front-end for the `sup-toolbox` library, which integrates several third-party components, some of which are under **non-commercial licenses** (such as SUPIR).
137
+
138
+ **By using this application, you acknowledge and agree to comply with all the licensing terms of the underlying `sup-toolbox` library and its components.** For a complete breakdown of these licenses, please refer to the library's documentation:
139
+
140
+ ➡️ **[View Full Licensing Details in the SUP-Toolbox README](https://github.com/DEVAIEXP/sup-toolbox#licensing)**
141
+
142
+ ## Star History
143
+
144
+ [![Star History Chart](https://api.star-history.com/svg?repos=DEVAIEXP/sup-toolbox-app&type=Date)](https://star-history.com/#DEVAIEXP/sup-toolbox-app&Date)
145
+
146
+ ## Contact
147
+ For questions or issues related to this Gradio application, please open an issue in this repository. For issues related to the core processing logic, please refer to the [`sup-toolbox`](https://github.com/DEVAIEXP/sup-toolbox/issues) library's repository.
148
+
149
+ Project Contact: contact@devaiexp.com
app.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The DEVAIEXP Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import spaces
16
+ import gradio as gr
17
+ import torch
18
+ import argparse
19
+ from functools import partial
20
+ from typing import Any
21
+
22
+ from gradio_folderexplorer.helpers import load_media_from_folder
23
+ from gradio_livelog.utils import livelog
24
+
25
+ from ui.ui_data import UIData
26
+ from ui.ui_events import AppState, EventHandlers
27
+ from ui.ui_layout import UIComponents, create_ui_components
28
+
29
+
30
+ class GradioApp:
31
+ def __init__(self, args):
32
+ self.args = args
33
+ self.state = AppState(uidata=UIData(always_download_models=args.always_download_models))
34
+
35
+ self.app = gr.Blocks(theme=self.state.uidata.theme, title="Scaling-UP ToolBox")
36
+ with self.app:
37
+ components_dict = create_ui_components(self.state.uidata)
38
+ self.components = UIComponents(**components_dict)
39
+ self.event_handlers = EventHandlers(self.state, self.components)
40
+ self._bind_events()
41
+
42
+ def _bind_events(self):
43
+ c = self.components
44
+ ui_inputs, output_fields = c._get_ui_inputs_and_outputs()
45
+ js_update_flyout = "(jsonData) => { update_flyout_from_state(jsonData); }"
46
+ flyout_data_event = {"fn": None, "inputs": [c.js_data_bridge], "js": js_update_flyout}
47
+
48
+ generate_inputs = list(ui_inputs.values())[4:]
49
+
50
+ @spaces.GPU(duration=60)
51
+ # Here we don't use the @livelog decorator function to ensure the progress bar is synchronized when the browser is not in focus.
52
+ def on_generate_wrapper(*args):
53
+ """
54
+ This wrapper is decorated by @spaces.GPU and is passed to Gradio.
55
+ It calls the actual logic handler from the EventHandlers instance.
56
+ """
57
+ yield from self.event_handlers.on_generate(*args)
58
+
59
+ @spaces.GPU(duration=60)
60
+ def on_refresh_mask_gpu_wrapper(*args, **kwargs):
61
+ """Wrapper for the mask generation."""
62
+
63
+ livelog_decorated_func = livelog(
64
+ log_names=["suptoolbox_app", "suptoolbox"],
65
+ outputs_for_yield=[c.restoration_mask, c.livelog_viewer],
66
+ log_output_index=1,
67
+ result_output_index=0,
68
+ use_tracker=False,
69
+ )(self.event_handlers.on_refresh_restoration_mask)
70
+
71
+ yield from livelog_decorated_func(*args, **kwargs)
72
+
73
+ @spaces.GPU(duration=60)
74
+ def on_generate_caption_gpu_wrapper(*args, **kwargs):
75
+ """
76
+ This wrapper handles the @spaces.GPU decorator and then applies
77
+ the @livelog decorator to the actual event handler.
78
+ """
79
+
80
+ livelog_decorated_func = livelog(
81
+ log_names=["suptoolbox_app", "suptoolbox"],
82
+ outputs_for_yield=[kwargs.pop("output_component_1"), kwargs.pop("output_component_2")],
83
+ log_output_index=1,
84
+ result_output_index=0,
85
+ use_tracker=False,
86
+ )(self.event_handlers.on_generate_caption)
87
+
88
+ yield from livelog_decorated_func(*args, **kwargs)
89
+
90
+ def on_load_metadata_from_gallery_wrapper(folder_explorer_value: Any, image_data: gr.EventData):
91
+ return self.event_handlers.on_load_metadata_from_gallery(folder_explorer_value, image_data)
92
+
93
+ # Settings Tab Events
94
+ c.reset_settings_btn.click(fn=self.event_handlers.on_reset_settings).then(fn=self.event_handlers.restart, js="restart_ui")
95
+ c.save_settings_btn.click(fn=self.event_handlers.on_save_settings, inputs=c.settings_sheet).then(fn=self.event_handlers.restart, js="restart_ui")
96
+
97
+ # Flyout Events
98
+ c.flyout_sheet.change(fn=self.event_handlers.on_flyout_change, inputs=[c.flyout_sheet, c.active_anchor_id])
99
+ c.flyout_property_sheet_close_btn.click(
100
+ partial(self.event_handlers.on_close_the_flyout, "flyout_property_sheet_panel_target"),
101
+ outputs=[c.flyout_visible, c.active_anchor_id, c.js_data_bridge],
102
+ ).then(**flyout_data_event)
103
+ c.flyout_restoration_image_close_btn.click(
104
+ partial(self.event_handlers.on_close_the_flyout, "flyout_restoration_mask_panel_target"),
105
+ outputs=[c.flyout_visible, c.active_anchor_id, c.js_data_bridge],
106
+ ).then(**flyout_data_event)
107
+ c.restorer_sampler.change(
108
+ partial(self.event_handlers.on_update_ear_visibility, c.restorer_sampler.elem_id),
109
+ outputs=[c.restorer_sampler_ear_btn],
110
+ ).then(
111
+ partial(self.event_handlers.on_close_the_flyout, "flyout_property_sheet_panel_target"),
112
+ outputs=[c.flyout_visible, c.active_anchor_id, c.js_data_bridge],
113
+ ).then(**flyout_data_event)
114
+ c.restorer_sampler_ear_btn.click(
115
+ partial(
116
+ self.event_handlers.on_handle_flyout_toggle,
117
+ clicked_elem_id=c.restorer_sampler.elem_id,
118
+ target_elem_id="flyout_property_sheet_panel_target",
119
+ ),
120
+ inputs=[c.flyout_visible, c.active_anchor_id],
121
+ outputs=[c.flyout_visible, c.active_anchor_id, c.flyout_sheet, c.js_data_bridge],
122
+ ).then(**flyout_data_event)
123
+ c.upscaler_sampler.change(
124
+ partial(self.event_handlers.on_update_ear_visibility, c.upscaler_sampler.elem_id),
125
+ outputs=[c.upscaler_sampler_ear_btn],
126
+ ).then(
127
+ partial(self.event_handlers.on_close_the_flyout, "flyout_property_sheet_panel_target"),
128
+ outputs=[c.flyout_visible, c.active_anchor_id, c.js_data_bridge],
129
+ ).then(**flyout_data_event)
130
+ c.upscaler_sampler_ear_btn.click(
131
+ partial(
132
+ self.event_handlers.on_handle_flyout_toggle,
133
+ clicked_elem_id=c.upscaler_sampler.elem_id,
134
+ target_elem_id="flyout_property_sheet_panel_target",
135
+ ),
136
+ inputs=[c.flyout_visible, c.active_anchor_id],
137
+ outputs=[c.flyout_visible, c.active_anchor_id, c.flyout_sheet, c.js_data_bridge],
138
+ ).then(**flyout_data_event)
139
+ c.preview_restoration_mask_chk.select(
140
+ lambda is_checked: (gr.update(interactive=is_checked), gr.update(interactive=is_checked)),
141
+ inputs=[c.preview_restoration_mask_chk],
142
+ outputs=[c.restoration_mask_prompt, c.preview_restoration_mask_btn],
143
+ )
144
+
145
+ c.preview_restoration_mask_btn.click(
146
+ self.event_handlers.on_check_inputs,
147
+ inputs=[
148
+ c.restorer_engine,
149
+ c.upscaler_engine,
150
+ c.restorer_model,
151
+ c.upscaler_model,
152
+ gr.State("generation_mask"),
153
+ ],
154
+ outputs=[c.bottom_bar, c.livelog_viewer],
155
+ show_progress="hidden",
156
+ ).success(
157
+ fn=on_refresh_mask_gpu_wrapper,
158
+ inputs=[c.restoration_mask_prompt],
159
+ outputs=[c.restoration_mask, c.livelog_viewer],
160
+ ).success(
161
+ partial(
162
+ self.event_handlers.on_handle_flyout_toggle,
163
+ clicked_elem_id=c.preview_restoration_mask_btn.elem_id,
164
+ target_elem_id="flyout_restoration_mask_panel_target",
165
+ ),
166
+ inputs=[c.flyout_visible, c.active_anchor_id],
167
+ outputs=[c.flyout_visible, c.active_anchor_id, c.restoration_mask, c.js_data_bridge],
168
+ ).success(**flyout_data_event)
169
+
170
+ c.res_prompt_generate_btn.click(
171
+ self.event_handlers.on_check_inputs,
172
+ inputs=[
173
+ c.restorer_engine,
174
+ c.upscaler_engine,
175
+ c.restorer_model,
176
+ c.upscaler_model,
177
+ gr.State("caption_generation"),
178
+ ],
179
+ outputs=[c.bottom_bar, c.livelog_viewer],
180
+ show_progress="hidden",
181
+ ).success(
182
+ fn=partial(on_generate_caption_gpu_wrapper, output_component_1=c.res_prompt, output_component_2=c.livelog_viewer),
183
+ outputs=[c.res_prompt, c.livelog_viewer],
184
+ )
185
+
186
+ c.ups_prompt_generate_btn.click(
187
+ self.event_handlers.on_check_inputs,
188
+ inputs=[
189
+ c.restorer_engine,
190
+ c.upscaler_engine,
191
+ c.restorer_model,
192
+ c.upscaler_model,
193
+ gr.State("caption_generation"),
194
+ ],
195
+ outputs=[c.bottom_bar, c.livelog_viewer],
196
+ show_progress="hidden",
197
+ ).success(
198
+ fn=partial(on_generate_caption_gpu_wrapper, output_component_1=c.ups_prompt, output_component_2=c.livelog_viewer),
199
+ outputs=[c.ups_prompt, c.livelog_viewer],
200
+ )
201
+ c.restorer_engine.select(
202
+ self.event_handlers.on_restore_engine_change,
203
+ inputs=[c.restorer_engine, c.upscaler_engine],
204
+ outputs=[c.restorer_tab, c.restorer_sheet_supir_advanced, c.restorer_sheet, c.ec_accordion, c.config_tabs],
205
+ ).success(
206
+ self.event_handlers.on_set_default_prompts,
207
+ inputs=[
208
+ c.restorer_engine,
209
+ c.upscaler_engine,
210
+ c.res_prompt,
211
+ c.res_prompt_2,
212
+ c.res_negative_prompt,
213
+ c.ups_prompt,
214
+ c.ups_prompt_2,
215
+ c.ups_negative_prompt,
216
+ ],
217
+ outputs=[
218
+ c.res_prompt,
219
+ c.res_prompt_2,
220
+ c.res_negative_prompt,
221
+ c.ups_prompt,
222
+ c.ups_prompt_2,
223
+ c.ups_negative_prompt,
224
+ ],
225
+ )
226
+ c.restorer_engine.change(
227
+ self.event_handlers.on_restore_engine_change,
228
+ inputs=[c.restorer_engine, c.upscaler_engine],
229
+ outputs=[c.restorer_tab, c.restorer_sheet_supir_advanced, c.restorer_sheet, c.ec_accordion, c.config_tabs],
230
+ )
231
+ c.upscaler_engine.change(
232
+ self.event_handlers.on_upscaler_engine_change,
233
+ inputs=[c.restorer_engine, c.upscaler_engine],
234
+ outputs=[
235
+ c.upscaler_tab,
236
+ c.upscaler_sheet_supir_advanced,
237
+ c.upscaler_sheet,
238
+ c.ec_accordion,
239
+ c.config_tabs,
240
+ c.ups_prompt_method,
241
+ ],
242
+ )
243
+ c.upscaler_engine.select(
244
+ self.event_handlers.on_upscaler_engine_change,
245
+ inputs=[c.restorer_engine, c.upscaler_engine],
246
+ outputs=[
247
+ c.upscaler_tab,
248
+ c.upscaler_sheet_supir_advanced,
249
+ c.upscaler_sheet,
250
+ c.ec_accordion,
251
+ c.config_tabs,
252
+ c.ups_prompt_method,
253
+ ],
254
+ ).success(
255
+ self.event_handlers.on_set_default_prompts,
256
+ inputs=[
257
+ c.restorer_engine,
258
+ c.upscaler_engine,
259
+ c.res_prompt,
260
+ c.res_prompt_2,
261
+ c.res_negative_prompt,
262
+ c.ups_prompt,
263
+ c.ups_prompt_2,
264
+ c.ups_negative_prompt,
265
+ ],
266
+ outputs=[
267
+ c.res_prompt,
268
+ c.res_prompt_2,
269
+ c.res_negative_prompt,
270
+ c.ups_prompt,
271
+ c.ups_prompt_2,
272
+ c.ups_negative_prompt,
273
+ ],
274
+ )
275
+ c.restorer_sheet.change(self.event_handlers.on_restorer_sheet_change, inputs=[c.restorer_sheet], outputs=[c.restorer_sheet])
276
+ c.upscaler_sheet.change(self.event_handlers.on_upscaler_sheet_change, inputs=[c.upscaler_sheet], outputs=[c.upscaler_sheet])
277
+ c.settings_sheet.change(self.event_handlers.on_settings_sheet_change, inputs=[c.settings_sheet], outputs=[c.settings_sheet])
278
+ c.restorer_sheet_supir_advanced.change(
279
+ self.event_handlers.on_restorer_supir_advanced_sheet_change, inputs=[c.restorer_sheet_supir_advanced], outputs=[c.restorer_sheet_supir_advanced]
280
+ )
281
+ c.upscaler_sheet_supir_advanced.change(
282
+ self.event_handlers.on_upscaler_supir_advanced_sheet_change, inputs=[c.upscaler_sheet_supir_advanced], outputs=[c.upscaler_sheet_supir_advanced]
283
+ )
284
+ c.settings_tab.select(self.event_handlers.on_settings_tab_select, outputs=[c.settings_sheet])
285
+ update_prompt_helper_from_tab_event = {
286
+ "fn": self.event_handlers.update_prompt_helper_from_tab,
287
+ "outputs": [c.tag_helper_pos, c.tag_helper_neg],
288
+ }
289
+ c.restorer_tab.select(**update_prompt_helper_from_tab_event)
290
+ c.upscaler_tab.select(**update_prompt_helper_from_tab_event)
291
+ c.res_prompt.change(
292
+ self.event_handlers.update_positive_tokenizer,
293
+ inputs=[c.res_prompt, c.res_prompt_2],
294
+ outputs=c.res_tokenizer_pos,
295
+ )
296
+ c.res_prompt_2.change(
297
+ self.event_handlers.update_positive_tokenizer,
298
+ inputs=[c.res_prompt, c.res_prompt_2],
299
+ outputs=c.res_tokenizer_pos,
300
+ )
301
+ c.ups_prompt.change(
302
+ self.event_handlers.update_positive_tokenizer,
303
+ inputs=[c.ups_prompt, c.ups_prompt_2],
304
+ outputs=c.ups_tokenizer_pos,
305
+ )
306
+ c.ups_prompt_2.change(
307
+ self.event_handlers.update_positive_tokenizer,
308
+ inputs=[c.ups_prompt, c.ups_prompt_2],
309
+ outputs=c.ups_tokenizer_pos,
310
+ )
311
+ c.res_negative_prompt.change(lambda p: gr.update(value=p), inputs=c.res_negative_prompt, outputs=c.res_tokenizer_neg)
312
+ c.ups_negative_prompt.change(lambda p: gr.update(value=p), inputs=c.ups_negative_prompt, outputs=c.ups_tokenizer_neg)
313
+ c.res_prompt.focus(self.event_handlers.update_positive_prompt_helper, outputs=c.tag_helper_pos)
314
+ c.res_prompt_2.focus(self.event_handlers.update_positive_prompt_helper, outputs=c.tag_helper_pos)
315
+ c.ups_prompt.focus(self.event_handlers.update_positive_prompt_helper, outputs=c.tag_helper_pos)
316
+ c.ups_prompt_2.focus(self.event_handlers.update_positive_prompt_helper, outputs=c.tag_helper_pos)
317
+
318
+ c.run_btn.click(
319
+ self.event_handlers.on_check_inputs,
320
+ inputs=[c.restorer_engine, c.upscaler_engine, c.restorer_model, c.upscaler_model],
321
+ outputs=[c.bottom_bar, c.livelog_viewer],
322
+ show_progress="hidden",
323
+ ).success(
324
+ self.event_handlers.calculate_total_steps,
325
+ inputs=[c.restorer_sheet, c.upscaler_sheet, c.restorer_engine, c.upscaler_engine],
326
+ outputs=c.total_inference_steps,
327
+ ).success(
328
+ fn=on_generate_wrapper,
329
+ inputs=[c.total_inference_steps, *generate_inputs],
330
+ outputs=[c.result_slider, c.livelog_viewer, c.run_btn, c.cancel_btn, c.bottom_bar],
331
+ show_progress="hidden",
332
+ )
333
+ c.cancel_btn.click(self.event_handlers.on_cancel_click)
334
+ c.save_preset_btn.click(
335
+ fn=self.event_handlers.save_preset,
336
+ inputs=[c.preset_name, *c.ALL_UI_COMPONENTS.values()],
337
+ ).then(self.event_handlers.update_preset_list, inputs=c.preset_name, outputs=[c.presets])
338
+ c.load_preset_btn.click(
339
+ fn=self.event_handlers.load_preset,
340
+ inputs=[c.presets],
341
+ outputs=list(c.ALL_UI_COMPONENTS.values()),
342
+ )
343
+ c.input_image.load_metadata(self.event_handlers.on_load_metadata_from_single_image, inputs=c.input_image, outputs=output_fields)
344
+ c.input_image.change(self.event_handlers.on_input_image_change, inputs=c.input_image)
345
+ c.folder_explorer.change(load_media_from_folder, inputs=c.folder_explorer, outputs=c.generated_image_viewer)
346
+ c.generated_image_viewer.load_metadata(fn=on_load_metadata_from_gallery_wrapper, inputs=[c.folder_explorer], outputs=output_fields).then(
347
+ lambda: gr.update(selected="process-tab"), outputs=c.main_tabs
348
+ )
349
+ c.livelog_viewer.clear(fn=self.event_handlers.on_clear_log_output, outputs=c.livelog_viewer)
350
+ flyout_setup = self.event_handlers.initial_flyout_setup()
351
+ self.app.load(fn=self.state.uidata.inject_assets, inputs=None, outputs=[c.html_injector]).then(
352
+ self.event_handlers.on_restore_engine_change,
353
+ inputs=[c.restorer_engine, c.upscaler_engine],
354
+ outputs=[c.restorer_tab, c.restorer_sheet_supir_advanced, c.restorer_sheet, c.ec_accordion, c.config_tabs],
355
+ ).then(
356
+ self.event_handlers.on_upscaler_engine_change,
357
+ inputs=[c.restorer_engine, c.upscaler_engine],
358
+ outputs=[
359
+ c.upscaler_tab,
360
+ c.upscaler_sheet_supir_advanced,
361
+ c.upscaler_sheet,
362
+ c.ec_accordion,
363
+ c.config_tabs,
364
+ c.ups_prompt_method,
365
+ ],
366
+ ).then(self.event_handlers.on_restorer_sheet_change, inputs=[c.restorer_sheet], outputs=[c.restorer_sheet]).then(
367
+ self.event_handlers.update_positive_tokenizer,
368
+ inputs=[c.res_prompt, c.res_prompt_2],
369
+ outputs=c.res_tokenizer_pos,
370
+ ).then(
371
+ self.event_handlers.update_positive_tokenizer,
372
+ inputs=[c.ups_prompt, c.ups_prompt_2],
373
+ outputs=c.ups_tokenizer_pos,
374
+ ).then(lambda p: gr.update(value=p), inputs=c.res_negative_prompt, outputs=c.res_tokenizer_neg).then(
375
+ lambda p: gr.update(value=p), inputs=c.ups_negative_prompt, outputs=c.ups_tokenizer_neg
376
+ ).then(
377
+ lambda: [flyout_setup["restorer_sampler_ear_btn"], flyout_setup["upscaler_sampler_ear_btn"]],
378
+ outputs=[c.restorer_sampler_ear_btn, c.upscaler_sampler_ear_btn],
379
+ ).then(
380
+ fn=None,
381
+ inputs=None,
382
+ outputs=None,
383
+ js="() => { setTimeout(reparent_flyout(['flyout_property_sheet_panel', 'flyout_restoration_mask_panel']), 200); }",
384
+ )
385
+
386
+ def launch(self):
387
+ self.app.queue().launch(debug=True, inbrowser=True, share=self.args.share, server_port=self.args.port, server_name=self.args.listen)
388
+
389
+
390
+ if __name__ == "__main__":
391
+ parser = argparse.ArgumentParser(description="SUP-Toolbox initialization args")
392
+ parser.add_argument(
393
+ "--always-download-models",
394
+ action="store_true",
395
+ default=False,
396
+ help="If specified, forces a full scan and download of the models if necessary.",
397
+ )
398
+ parser.add_argument("-s", "--share", action="store_true", help="Create a public link")
399
+ parser.add_argument("--port", default=7860, type=int, help="Port to run the server on")
400
+ parser.add_argument("--listen", default="0.0.0.0", help="IP address to listen on")
401
+
402
+ args = parser.parse_args()
403
+
404
+ app_instance = GradioApp(args)
405
+ app_instance.launch()
assets/devaixp_logo-white.png ADDED

Git LFS Details

  • SHA256: d53d41fe1640dba2d1c31b5940cf851bda1ed07aa194af03b8e951511c80f1cb
  • Pointer size: 130 Bytes
  • Size of remote file: 40.3 kB
assets/samples/1.jpg ADDED

Git LFS Details

  • SHA256: cd3e7cf153b8c23d88fc301704a973ca62660b130b2496a57211f45d1377d0e8
  • Pointer size: 131 Bytes
  • Size of remote file: 763 kB
assets/samples/2.jpg ADDED

Git LFS Details

  • SHA256: be182901f7691b5b1dddf6123cb455f6e53bad2ad8b9350b20ff077ae77faa26
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
assets/samples/3.jpg ADDED

Git LFS Details

  • SHA256: 2228889b840b0bd3a2e986bc09d1cde47eb201dc8f99ce84646b291bf1d4bbd5
  • Pointer size: 131 Bytes
  • Size of remote file: 167 kB
assets/samples/band.png ADDED

Git LFS Details

  • SHA256: b4a856e478429b5be8a7bac1e24dcb07e4446e770e9cea7b984eb1756b32787a
  • Pointer size: 131 Bytes
  • Size of remote file: 948 kB
assets/samples/band_restored.png ADDED

Git LFS Details

  • SHA256: c572bf220328d01d48eee69450bb745c6e0894a6e90088cade54e05d561b0cb7
  • Pointer size: 132 Bytes
  • Size of remote file: 1.41 MB
assets/samples/boy.jpg ADDED

Git LFS Details

  • SHA256: b8d9d9b9f15118519ce9e53b84bc0edc83bef885e5c3f05b60df6278d6c53666
  • Pointer size: 131 Bytes
  • Size of remote file: 114 kB
assets/samples/woman.png ADDED

Git LFS Details

  • SHA256: de38493e4420a13c6cd1b18c65a95c039c8f5de4c4460c6ccff4fce933c8af6b
  • Pointer size: 131 Bytes
  • Size of remote file: 213 kB
assets/samples/woman_restored.png ADDED

Git LFS Details

  • SHA256: daace3f27e886bfdb71357cad09c38c7c96f5ad0a3508b088b44b45ea209f3b1
  • Pointer size: 132 Bytes
  • Size of remote file: 1.47 MB
configs/settings.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "save_image_format": ".jpg",
3
+ "latest_preset": "Default",
4
+ "cache_dir": "models",
5
+ "checkpoints_dir": "F:\\models\\Stable-diffusion",
6
+ "vae_dir": "models/VAE",
7
+ "output_dir": "./outputs",
8
+ "save_image_on_upscaling_passes": true,
9
+ "weight_dtype": "Float16",
10
+ "vae_weight_dtype": "Float16",
11
+ "device": "cuda",
12
+ "generator_device": "cpu",
13
+ "enable_cpu_offload": false,
14
+ "enable_vae_tiling": true,
15
+ "enable_vae_slicing": false,
16
+ "memory_attention": "sdp",
17
+ "quantization_method": "Layerwise & Bnb",
18
+ "quantization_mode": "FP8",
19
+ "allow_cuda_tf32": false,
20
+ "allow_cudnn_tf32": false,
21
+ "disable_mmap": true,
22
+ "always_offload_models": true,
23
+ "run_vae_on_cpu": false,
24
+ "enable_llava_quantization": true,
25
+ "llava_quantization_mode": "INT4",
26
+ "llava_offload_model": false,
27
+ "llava_weight_dtype": "Float16",
28
+ "llava_question_prompt": "Describe what you see in this image and put it in prompt format limited to 77 tokens:"
29
+ }
outputs/examples/1_FaithDiff_Restore_1x_CFGOnly.png ADDED

Git LFS Details

  • SHA256: 76435ff7f2c2a84885cfc670d38ca51b504dfaf25dc565718efbfff68215b62b
  • Pointer size: 132 Bytes
  • Size of remote file: 1.61 MB
outputs/examples/1_FaithDiff_Restore_1x_FaithDiff_Upscale_2x_CFGOnly.png ADDED

Git LFS Details

  • SHA256: 812397873b5c872f00aa1ac3d342bed7f8a5bb280817d320eb8479e492c84c88
  • Pointer size: 132 Bytes
  • Size of remote file: 5.48 MB
outputs/examples/1_FaithDiff_Restore_1x_FaithDiff_Upscale_2x_PAG.png ADDED

Git LFS Details

  • SHA256: 6b0f07f14a58612547bcda57f174f34a14d65dc88e10ae1ab1a4b216ae121cc7
  • Pointer size: 132 Bytes
  • Size of remote file: 5.44 MB
outputs/examples/1_FaithDiff_Restore_1x_PAG.png ADDED

Git LFS Details

  • SHA256: 7eccabc132de8ea29e9fe4efe6f96021306e0e9c281024fc6f269e101371cad2
  • Pointer size: 132 Bytes
  • Size of remote file: 1.65 MB
outputs/examples/1_FaithDiff_Restore_1x_SUPIR_Upscale_2x.png ADDED

Git LFS Details

  • SHA256: 1c9096309937e1c2eec09b6ea10bad806546a64a24fa09181d11951397e88be6
  • Pointer size: 132 Bytes
  • Size of remote file: 5.26 MB
outputs/examples/1_SUPIR_Restore_1x_CFGOnly.png ADDED

Git LFS Details

  • SHA256: d3e9693ad27cd1fa3979e68dc99545f8efc830122a05963a6f6d096999728a8d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.43 MB
outputs/examples/1_SUPIR_Restore_1x_FaithDiff_Upscale_2x.png ADDED

Git LFS Details

  • SHA256: 6d3c5fe3998771121fa6553182a07058d75acba4e0d772904e552573b6e3d62c
  • Pointer size: 132 Bytes
  • Size of remote file: 5.87 MB
outputs/examples/1_SUPIR_Restore_1x_PAGOnly.png ADDED

Git LFS Details

  • SHA256: 87b49adf1e4236e6cd151ba192bc085b4e0550e08a85eb14b3fdd1ffb4b45783
  • Pointer size: 132 Bytes
  • Size of remote file: 1.41 MB
outputs/examples/1_SUPIR_Restore_1x_SUPIR_Upscale_2x_CFGOnly.png ADDED

Git LFS Details

  • SHA256: f9ce13684736abbb0424f5067cc362913d02bb53346f99e2a31cdc276dc72a47
  • Pointer size: 132 Bytes
  • Size of remote file: 5.12 MB
outputs/examples/1_SUPIR_Restore_1x_SUPIR_Upscale_2x_PAG.png ADDED

Git LFS Details

  • SHA256: df47d0916b0529637f15af05d484a4259e437a3e454eeeead14855ea4be2eac4
  • Pointer size: 132 Bytes
  • Size of remote file: 5.03 MB
outputs/examples/2_FaithDiff_Restore_1x_CFGOnly.png ADDED

Git LFS Details

  • SHA256: f9d9f211908a92dd91e0bf0981defac04b929b6c380029dc014b2490ce22d332
  • Pointer size: 132 Bytes
  • Size of remote file: 1.47 MB
outputs/examples/2_SUPIR_Restore_1x.png ADDED

Git LFS Details

  • SHA256: 5effac07ecf2e77dd782a69794c07d4e788f817b6919f6c203b0431049c3c913
  • Pointer size: 131 Bytes
  • Size of remote file: 499 kB
outputs/examples/2_SUPIR_Restore_1x_ControlNetTIle_Upscale_2x.png ADDED

Git LFS Details

  • SHA256: 4df9c9f6657461fd0e490e2abc1202fac4dd484b069619f4d359557267edcca9
  • Pointer size: 132 Bytes
  • Size of remote file: 1.61 MB
outputs/examples/2_SUPIR_Restore_1x_ControlNetTIle_Upscale_4x.png ADDED

Git LFS Details

  • SHA256: 46c837a1757d92f46c353174762ddbe3708880ff13579b464d5bace4ec354064
  • Pointer size: 132 Bytes
  • Size of remote file: 5.23 MB
outputs/examples/3_ControlnetTile_Upscale_2x.png ADDED

Git LFS Details

  • SHA256: 39163abe7dee94ad9f1bec99ea51a29a9a4137901b23a2222c001aae129bc350
  • Pointer size: 132 Bytes
  • Size of remote file: 5.25 MB
outputs/examples/4_ControlnetTile_Upscale_8x.jpg ADDED

Git LFS Details

  • SHA256: 0364b5985deb81bbf4de6024b617409c8b94ab292202065091325abf6b7e954c
  • Pointer size: 132 Bytes
  • Size of remote file: 6.03 MB
outputs/examples/5_ControlnetTile_Upscale_4x.png ADDED

Git LFS Details

  • SHA256: a06690b2488fcc5ccb9a33cd66d211f460c74c933bd70b6c7ff1051c19458885
  • Pointer size: 132 Bytes
  • Size of remote file: 3.47 MB
requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.44.1
2
+ gradio_tokenizertextbox
3
+ gradio_taggrouphelper
4
+ gradio_propertysheet
5
+ gradio_htmlinjector
6
+ gradio_imagemeta
7
+ gradio_livelog
8
+ gradio_bottombar
9
+ gradio_topbar
10
+ gradio_textboxplus
11
+ gradio_dropdownplus
12
+ gradio_buttonplus
13
+ gradio_folderexplorer
14
+ gradio_mediagallery
15
+ gradio_creditspanel
16
+ spaces
17
+ optimum-quanto==0.2.7
18
+ torch==2.8.0
19
+ torchvision==0.23.0
20
+ torchaudio==2.8.0
21
+ transformers==4.56.0
22
+ bitsandbytes==0.48.1
23
+ xformers==0.0.32.post2
24
+ hf_xet
25
+ git+https://github.com/DEVAIEXP/sup-toolbox.git@main#egg=sup-toolbox
ui/globals.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import threading
2
+
3
+
4
+ # These variables live at the module level.
5
+ # When multiple workers import this module, they will all reference
6
+ # the same objects in the parent process's memory (due to how Python forks processes on Linux).
7
+ sup_toolbox_pipe = None
8
+ pipeline_lock = threading.Lock()
ui/script.js ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function isJsonString(str) {
2
+ if (typeof str !== 'string' || str.trim() === '') {
3
+ return false;
4
+ }
5
+ try {
6
+ const result = JSON.parse(str);
7
+ return typeof result === 'object' && result !== null;
8
+ } catch (e) {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ // Copied and adapted from https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/82a973c04367123ae98bd9abdf80d9eda9b910e2/javascript/extraNetworks.js#L582
14
+ function requestGet(url, data, handler, errorHandler) {
15
+ if (typeof url !== 'string' || url.trim() === '') {
16
+ console.error("requestGet: Invalid or empty URL provided.");
17
+ if (typeof errorHandler === 'function') errorHandler(new Error("Invalid URL"));
18
+ return;
19
+ }
20
+
21
+ const requestData = (data && typeof data === 'object') ? data : {};
22
+ const successHandler = typeof handler === 'function' ? handler : () => { };
23
+ const failureHandler = typeof errorHandler === 'function' ? errorHandler : (err) => { console.error("requestGet failed:", err); };
24
+
25
+ let args = '';
26
+ try {
27
+ args = Object.keys(requestData).map(function (k) {
28
+ const key = encodeURIComponent(k);
29
+ const value = encodeURIComponent(requestData[k]);
30
+ return `${key}=${value}`;
31
+ }).join('&');
32
+ } catch (e) {
33
+ console.error("requestGet: Failed to serialize data object.", e);
34
+ failureHandler(e);
35
+ return;
36
+ }
37
+
38
+ const fullUrl = args ? `${url}?${args}` : url;
39
+
40
+ const xhr = new XMLHttpRequest();
41
+ xhr.open("GET", fullUrl, true);
42
+
43
+ xhr.onreadystatechange = function () {
44
+ if (xhr.readyState === 4) {
45
+ if (xhr.status === 200) {
46
+ try {
47
+ const responseJson = isJsonString(xhr.responseText) ? JSON.parse(xhr.responseText) : {};
48
+ successHandler(responseJson);
49
+ } catch (error) {
50
+ console.error("requestGet: Error parsing JSON response.", error);
51
+ failureHandler(error);
52
+ }
53
+ } else {
54
+ failureHandler(new Error(`Request failed with status ${xhr.status}: ${xhr.statusText}`));
55
+ }
56
+ }
57
+ };
58
+
59
+ xhr.onerror = function () {
60
+ failureHandler(new Error("Network error occurred."));
61
+ };
62
+
63
+ xhr.send();
64
+ }
65
+
66
+ // copied and adapted from https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/82a973c04367123ae98bd9abdf80d9eda9b910e2/javascript/ui.js#L348
67
+ function restart_ui() {
68
+ const overlay = document.createElement('div');
69
+ overlay.innerHTML = '<h1 style="font-family:monospace; margin-top:20%; color:lightgray; text-align:center;">Reloading...</h1>';
70
+ overlay.style.position = 'fixed';
71
+ overlay.style.top = '0';
72
+ overlay.style.left = '0';
73
+ overlay.style.width = '100%';
74
+ overlay.style.height = '100%';
75
+ overlay.style.backgroundColor = 'rgba(20, 20, 20, 0.9)';
76
+ overlay.style.zIndex = '10000';
77
+ document.body.appendChild(overlay);
78
+
79
+ const requestPing = function () {
80
+ requestGet(
81
+ window.location.href,
82
+ {},
83
+ function (data) {
84
+ window.location.reload();
85
+ },
86
+ function (error) {
87
+ console.log("Ping failed, retrying...");
88
+ setTimeout(requestPing, 500);
89
+ }
90
+ );
91
+ };
92
+
93
+ setTimeout(requestPing, 2000);
94
+ }
95
+
96
+ /**
97
+ * Moves all child elements from one or more source containers to their corresponding target containers.
98
+ * By convention, the target ID for a source 'my-source-id' is expected to be 'my-source-id_target'.
99
+ * After moving, the source containers are removed from the DOM by default.
100
+ *
101
+ * @param {string | string[]} source_ids - A single source element ID or an array of IDs.
102
+ * @param {object} [options] - Optional settings.
103
+ * @param {boolean} [options.remove_sources=true] - If true, removes the source elements after their content is moved.
104
+ */
105
+ function reparent_flyout(source_ids, options = { remove_sources: true }) {
106
+ // Ensure the input is always an array to simplify the logic.
107
+ const sourceArray = Array.isArray(source_ids) ? source_ids : [source_ids];
108
+
109
+ // Iterate over each provided source ID.
110
+ sourceArray.forEach(source_id => {
111
+ // Find the source element.
112
+ const source = document.getElementById(source_id);
113
+
114
+ // If the source element doesn't exist, we can't do anything for this ID.
115
+ if (!source) {
116
+ console.warn(`WARNING: Source element with ID '${source_id}' not found. Skipping.`);
117
+ return; // 'return' inside a forEach acts like 'continue' in a for loop.
118
+ }
119
+
120
+ // Derive the target ID from the source ID
121
+ const target_id = `${source_id}_target`;
122
+ const target = document.getElementById(target_id);
123
+
124
+ // If the corresponding target element doesn't exist, log an error.
125
+ if (!target) {
126
+ console.error(`ERROR: Reparenting for '#${source_id}' failed. Corresponding target '#${target_id}' not found.`);
127
+ return;
128
+ }
129
+
130
+ // Move each child node from the source to the target.
131
+ // The `while (source.firstChild)` loop is efficient because `appendChild`
132
+ // automatically removes the child from its original parent.
133
+ while (source.firstChild) {
134
+ target.appendChild(source.firstChild);
135
+ }
136
+
137
+ // Remove the now-empty source container if the option is enabled.
138
+ if (options.remove_sources) {
139
+ source.remove();
140
+ }
141
+
142
+ console.log(`SUCCESS: Content from '#${source_id}' has been reparented to '#${target_id}'.`);
143
+ });
144
+ }
145
+
146
+ function position_flyout(anchorId, target_id) {
147
+ if (!anchorId) { return; }
148
+
149
+ const anchorElem = document.getElementById(anchorId);
150
+ const flyoutElem = document.getElementById(target_id);
151
+
152
+ if (anchorElem && flyoutElem) {
153
+ //console.log("JS: Positioning flyout relative to:", anchorId);
154
+ const anchorRect = anchorElem.getBoundingClientRect();
155
+ const flyoutWidth = flyoutElem.offsetWidth;
156
+ const flyoutHeight = flyoutElem.offsetHeight;
157
+
158
+ let topPosition = anchorRect.top + (anchorRect.height / 2) - (flyoutHeight / 2);
159
+ let leftPosition = anchorRect.left + (anchorRect.width / 2) - (flyoutWidth / 2);
160
+
161
+ const windowWidth = window.innerWidth;
162
+ const windowHeight = window.innerHeight;
163
+ if (leftPosition < 8) leftPosition = 8;
164
+ if (topPosition < 8) topPosition = 8;
165
+ if (leftPosition + flyoutWidth > windowWidth) leftPosition = windowWidth - flyoutWidth - 8;
166
+ if (topPosition + flyoutHeight > windowHeight) topPosition = windowHeight - flyoutHeight - 8;
167
+
168
+ flyoutElem.style.top = `${topPosition}px`;
169
+ flyoutElem.style.left = `${leftPosition}px`;
170
+ }
171
+ }
172
+
173
+ // This is the new main function called by Gradio's .then() event
174
+ function update_flyout_from_state(jsonData) {
175
+ //console.log("JS: update_flyout_from_state() called with data:", jsonData);
176
+
177
+ if (!jsonData) return;
178
+
179
+ const state = JSON.parse(jsonData);
180
+ const { isVisible, anchorId, targetId } = state;
181
+ const flyout = document.getElementById(targetId);
182
+
183
+ if (!flyout) {
184
+ console.error("ERROR: Cannot update UI. Flyout container not found.");
185
+ return;
186
+ }
187
+ //console.log("JS: Parsed state:", { isVisible, anchorId });
188
+ if (isVisible) {
189
+ flyout.style.display = 'flex';
190
+ position_flyout(anchorId, targetId);
191
+ } else {
192
+ flyout.style.display = 'none';
193
+ }
194
+ }
ui/style.css ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ 1. GLOBAL & LAYOUT STYLES
3
+ ========================================================================== */
4
+
5
+ body {
6
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
7
+ margin: 0;
8
+ padding: 0;
9
+ }
10
+
11
+ /* Main application container styling */
12
+ .gradio-container {
13
+ border-radius: 15px;
14
+ padding: 10px 10px;
15
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
16
+ margin: 40px 350px;
17
+ }
18
+
19
+ /* Text shadow for main headings for better readability */
20
+ .gradio-container h1 {
21
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
22
+ }
23
+
24
+ /* Utility class to make elements fill their container's width */
25
+ .fillable {
26
+ width: 100% !important;
27
+ max-width: unset !important;
28
+ }
29
+ .fillable .sidebar-parent {
30
+ padding-left: 10px !important;
31
+ padding-right: 10px !important;
32
+ }
33
+
34
+ .credit-panel .outer-credits-wrapper .credits-panel-wrapper {
35
+ min-height: 80vh !important;
36
+ max-height: 700px;
37
+ }
38
+ .media-gallery-row {
39
+ min-height: 80vh !important;
40
+ max-height: 700px;
41
+ }
42
+ /* ==========================================================================
43
+ 2. CUSTOM SCROLLBAR STYLES (GLOBAL & SIDEBAR)
44
+ ========================================================================== */
45
+
46
+ /* --- For WebKit browsers (Chrome, Safari, Edge, etc.) --- */
47
+ .sidebar-content::-webkit-scrollbar,
48
+ body::-webkit-scrollbar {
49
+ width: 8px;
50
+ height: 8px;
51
+ background-color: transparent;
52
+ }
53
+
54
+ .sidebar-content::-webkit-scrollbar-track,
55
+ body::-webkit-scrollbar-track {
56
+ background: transparent;
57
+ border-radius: 10px;
58
+ }
59
+
60
+ .sidebar-content::-webkit-scrollbar-thumb,
61
+ body::-webkit-scrollbar-thumb {
62
+ background-color: rgba(136, 136, 136, 0.4);
63
+ border-radius: 10px;
64
+ border: 2px solid transparent;
65
+ background-clip: content-box;
66
+ }
67
+
68
+ .sidebar-content::-webkit-scrollbar-thumb:hover,
69
+ body::-webkit-scrollbar-thumb:hover {
70
+ background-color: rgba(136, 136, 136, 0.7);
71
+ }
72
+
73
+ /* --- For Firefox --- */
74
+ .sidebar-content,
75
+ html {
76
+ scrollbar-width: thin;
77
+ scrollbar-color: rgba(136, 136, 136, 0.7) transparent;
78
+ }
79
+
80
+
81
+ /* ==========================================================================
82
+ 3. SIDEBAR STYLES
83
+ ========================================================================== */
84
+
85
+ .sidebar {
86
+ border-radius: 10px;
87
+ padding: 10px;
88
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
89
+ }
90
+ /* .top-bar, .bottom-bar {
91
+ border-radius: 10px;
92
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
93
+ } */
94
+ /* Overrides for the content padding inside the sidebar */
95
+ .sidebar .sidebar-content {
96
+ padding-left: 10px !important;
97
+ padding-right: 10px !important;
98
+ }
99
+
100
+ /* Centers text within Markdown blocks inside the sidebar */
101
+ .sidebar .sidebar-content .column .block div .prose {
102
+ text-align: center;
103
+ }
104
+
105
+ /* Stylish override for the sidebar toggle button */
106
+ .sidebar .toggle-button {
107
+ background: linear-gradient(90deg, #34d399, #10b981) !important;
108
+ border: none;
109
+ padding: 12px 18px;
110
+ text-transform: uppercase;
111
+ font-weight: bold;
112
+ letter-spacing: 1px;
113
+ border-radius: 5px;
114
+ position: absolute;
115
+ top: 50%;
116
+ right: -28px !important;
117
+ left: auto !important;
118
+ transform: unset !important;
119
+ }
120
+
121
+ .sidebar.right .toggle-button {
122
+ left: -28px !important;
123
+ right: auto !important;
124
+ transform: rotate(180deg) !important;
125
+ }
126
+
127
+ .sidebar.open .chevron-left {
128
+ transform: rotate(-135deg);
129
+ }
130
+
131
+ .top-bar .toggle-top-button, .bottom-bar .toggle-bottom-button {
132
+ background: linear-gradient(90deg, #34d399, #10b981) !important;
133
+ border: none;
134
+ padding: 12px 18px;
135
+ text-transform: uppercase;
136
+ font-weight: bold;
137
+ letter-spacing: 1px;
138
+ border-radius: 5px;
139
+ cursor: pointer;
140
+ transition: transform 0.2s ease-in-out;
141
+ /* right: -20px !important; */
142
+ }
143
+ .toggle-button:hover {
144
+ transform: scale(1.05);
145
+ }
146
+
147
+
148
+ /* ==========================================================================
149
+ 4. FLYOUT POPOVER STYLES
150
+ ========================================================================== */
151
+
152
+ /* The context area from which the flyout is positioned */
153
+ .flyout-context-area {
154
+ position: relative !important;
155
+ overflow: visible !important; /* Allows flyout to render outside its parent */
156
+ }
157
+
158
+ /* The main flyout container (the popover itself) */
159
+ .flyout-container {
160
+ position: fixed !important;
161
+ z-index: 1000;
162
+ width: 350px !important;
163
+ background: var(--panel-background-fill, white);
164
+ border-radius: var(--radius-lg);
165
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
166
+ padding: var(--spacing-lg) !important;
167
+ /* Reset flex properties that might be inherited */
168
+ flex-grow: 0 !important;
169
+ min-width: unset !important;
170
+ align-self: center !important;
171
+ }
172
+
173
+ /* Allows tooltips inside a PropertySheet within a flyout to be visible */
174
+ .flyout-container .propertysheet-wrapper .content-wrapper .container {
175
+ overflow: visible !important;
176
+ }
177
+
178
+ /* The hidden container for the flyout's initial content */
179
+ .flyout-source-hidden {
180
+ display: none !important;
181
+ }
182
+
183
+ /* The close (X) button for the flyout */
184
+ .flyout-close-btn {
185
+ position: absolute;
186
+ top: 5px;
187
+ right: 8px;
188
+ z-index: 1001; /* Should be on top of flyout content */
189
+ background: transparent !important;
190
+ border: none !important;
191
+ box-shadow: none !important;
192
+ color: #ef0000 !important;
193
+ min-width: 24px !important;
194
+ width: 24px !important;
195
+ height: 24px !important;
196
+ padding: 0 !important;
197
+ font-size: 1.2em !important;
198
+ line-height: 1;
199
+ }
200
+
201
+ /* .flyout-close-btn button:hover {
202
+ color: var(--body-text-color) !important;
203
+ } */
204
+ .flyout-container .block .image-container .icon-button-wrapper {
205
+ right: 20px !important
206
+ }
207
+ .flyout-container .block .image-container > button {
208
+ border: none !important;
209
+ border-radius: 0 !important;
210
+ }
211
+ #flyout_sheet > div.content-wrapper > div > div > div > div {
212
+ color: var(--button-secondary-text-color);
213
+ }
214
+ /* ==========================================================================
215
+ 5. COMPONENT-SPECIFIC STYLES & OVERRIDES
216
+ ========================================================================== */
217
+
218
+ /* Custom styling for the cancel button */
219
+ #cancel-button { /* Corrected the typo from 'cancell' to 'cancel' */
220
+ background: linear-gradient(120deg, var(--neutral-500) 0%, var(--neutral-600) 60%, var(--neutral-700) 100%) !important;
221
+ }
222
+ .custom-dropdown .wrap-inner input {
223
+ padding-right: 22px;
224
+ }
225
+ /* --- Styles for creating dropdowns with integrated "ear" buttons --- */
226
+
227
+ /* Container that looks like a single input field */
228
+ .fake-input-container {
229
+ border: var(--input-border-width) solid var(--border-color-primary);
230
+ border-radius: var(--input-radius);
231
+ background: var(--block-background-fill) !important;
232
+ box-shadow: var(--input-shadow);
233
+ transition: box-shadow 0.2s, border-color 0.2s;
234
+ padding: 0 !important;
235
+ align-items: stretch !important;
236
+ gap: 0 !important;
237
+ }
238
+
239
+ .fake-input-container:focus-within {
240
+ box-shadow: var(--input-shadow-focus);
241
+ border-color: var(--input-border-color-focus);
242
+ }
243
+
244
+ /* Removes the default border from a Gradio dropdown when it's inside our fake container */
245
+ .no-border-dropdown .form {
246
+ border: none !important;
247
+ box-shadow: none !important;
248
+ background: transparent !important;
249
+ }
250
+
251
+ /* The settings (gear) button next to the dropdown */
252
+ .integrated-ear-btn {
253
+ align-self: center;
254
+ background: none !important;
255
+ border: none !important;
256
+ box-shadow: none !important;
257
+ color: var(--body-text-color-subdued) !important;
258
+ font-size: 1.1em;
259
+ min-width: 28px !important;
260
+ width: 28px !important;
261
+ height: 100% !important;
262
+ padding: 20px var(--spacing-sm) 0 0 !important;
263
+ transition: color 0.2s ease-in-out;
264
+ flex-grow: 0 !important;
265
+ }
266
+
267
+ .integrated-ear-btn:hover {
268
+ color: var(--body-text-color) !important;
269
+ }
270
+
271
+ .row-form-size .form {
272
+ flex-grow: 0 !important;
273
+ min-width: 50% !important;
274
+ }
275
+
276
+ .row-form-size-2 .form {
277
+ flex-grow: 0 !important;
278
+ min-width: 97% !important;
279
+ }
280
+
281
+ /* --- Misc Component Tweaks --- */
282
+
283
+ /* Removes the gap from a specific group of settings */
284
+ .group {
285
+ gap: unset !important;
286
+ }
287
+
288
+ /* Targets a styler inside the sampler group to reset form gap */
289
+ .group .styler {
290
+ --form-gap-width: 0px !important;
291
+ }
292
+
293
+ /* Example of how to center content in Gradio's example components */
294
+ #examples_container {
295
+ margin: auto;
296
+ width: 90%;
297
+ }
298
+
299
+ #examples_row {
300
+ justify-content: center;
301
+ }
ui/ui_config.py ADDED
@@ -0,0 +1,1360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The DEVAIEXP Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import List, Literal
17
+
18
+ import sup_toolbox.modules.FaithDiff
19
+ import sup_toolbox.modules.IPAdapter
20
+ import sup_toolbox.modules.MoDControlTile
21
+ import sup_toolbox.modules.SUPIR
22
+ from sup_toolbox.config import PAG_LAYERS
23
+ from sup_toolbox.enums import ImageSizeFixMode, SUPIRModel
24
+ from sup_toolbox.utils.system import find_dist_info_files, get_module_file
25
+
26
+
27
+ LICENSE_PATHS = {
28
+ "NOTICE": "./NOTICE",
29
+ "SUP-ToolBox License": "./LICENSE",
30
+ "SUPIR Component": get_module_file(sup_toolbox.modules.SUPIR, "LICENSE_SUPIR.md"),
31
+ "FaithDiff Component": get_module_file(sup_toolbox.modules.FaithDiff, "LICENSE_FAITHDIFF.md"),
32
+ "MoDControlTile Component": get_module_file(sup_toolbox.modules.MoDControlTile, "LICENSE_MIXTURE_OF_DIFFUSERS.md"),
33
+ "IPAdapter Component": get_module_file(sup_toolbox.modules.IPAdapter, "LICENSE_IPADAPTER.md"),
34
+ "Diffusers": find_dist_info_files("diffusers", "LICENSE"),
35
+ "SUP-ToolBox App Changelog": "./CHANGELOG.md",
36
+ "SUP-ToolBox CLI Changelog": get_module_file(sup_toolbox, "CHANGELOG.md"),
37
+ }
38
+
39
+ CREDIT_LIST = [
40
+ # Publisher/Developer Credit
41
+ {"title": "Developed By", "name": "DEVAIEXP"},
42
+ # Core Development Section
43
+ {"section_title": "Core Development"},
44
+ {"title": "Principal Developer & Solution Architect", "name": "Eliseu Silva"},
45
+ # SUPIR
46
+ {"section_title": "SUPIR Restoration Pipeline"},
47
+ {"title": "Original work by", "name": "SupPixel Pty Ltd"},
48
+ {"title": "Adapted for Diffusers by", "name": "DEVAIEXP"},
49
+ {"section_title": "SUPIR - Original Authors"},
50
+ {"title": "Lead Researcher", "name": "Dr. Jinjin Gu"},
51
+ {"title": "Contributing Author", "name": "Fanghua Yu"},
52
+ {"title": "Contributing Author", "name": "Zheyuan Li"},
53
+ {"title": "Contributing Author", "name": "Jinfan Hu"},
54
+ {"title": "Contributing Author", "name": "Xiangtao Kong"},
55
+ {"title": "Contributing Author", "name": "Xintao Wang"},
56
+ {"title": "Contributing Author", "name": "Jingwen He"},
57
+ {"title": "Contributing Author", "name": "Yu Qiao"},
58
+ {"title": "Contributing Author", "name": "Chao Dong"},
59
+ # FaithDiff
60
+ {"section_title": "FaithDiff Super-Resolution Pipeline"},
61
+ {"title": "Original work by", "name": "Junyang Chen"},
62
+ {"title": "Adapted and improved by", "name": "DEVAIEXP"},
63
+ {"section_title": "FaithDiff - Original Authors"},
64
+ {"title": "Contributing Author", "name": "Junyang Chen"},
65
+ {"title": "Contributing Author", "name": "Jinshan Pan"},
66
+ {"title": "Contributing Author", "name": "Jiangxin Dong"},
67
+ # MoD ControlNet Tile
68
+ {"section_title": "Mixture of Diffusers for ControlNet Tile Pipeline"},
69
+ {"title": "Original Pipeline Concept by", "name": "Álvaro Barbero Jiménez"},
70
+ {"title": "Adapted and improved for ControlNet Union by", "name": "DEVAIEXP"},
71
+ {"title": "ControlNet Union Model", "name": "Trained by xinsir"},
72
+ # Key Models & Components Section
73
+ {"section_title": "Key Models & Components"},
74
+ {"title": "IP-Adapter Model", "name": "TencentARC"},
75
+ # Foundational Frameworks Section
76
+ # Gradio
77
+ {"section_title": "Gradio Framework"},
78
+ {"section_title": "Gradio - Original Development Team"},
79
+ {"title": "Core Contributor", "name": "Abubakar Abid"},
80
+ {"title": "Core Contributor", "name": "Ali Abdalla"},
81
+ {"title": "Core Contributor", "name": "Ali Abid"},
82
+ {"title": "Core Contributor", "name": "Dawood Khan"},
83
+ {"title": "Core Contributor", "name": "Abdulrahman Alfozan"},
84
+ {"title": "Core Contributor", "name": "James Zou"},
85
+ # Diffusers
86
+ {"section_title": "Diffusers Framework"},
87
+ {"section_title": "Diffusers - Original Development Team"},
88
+ {"title": "Core Contributor", "name": "Patrick von Platen"},
89
+ {"title": "Core Contributor", "name": "Suraj Patil"},
90
+ {"title": "Core Contributor", "name": "Anton Lozhkov"},
91
+ {"title": "Core Contributor", "name": "Pedro Cuenca"},
92
+ {"title": "Core Contributor", "name": "Nathan Lambert"},
93
+ {"title": "Core Contributor", "name": "Kashif Rasul"},
94
+ {"title": "Core Contributor", "name": "Mishig Davaadorj"},
95
+ {"title": "Core Contributor", "name": "Dhruv Nair"},
96
+ {"title": "Core Contributor", "name": "Sayak Paul"},
97
+ {"title": "Core Contributor", "name": "William Berman"},
98
+ {"title": "Core Contributor", "name": "Yiyi Xu"},
99
+ {"title": "Core Contributor", "name": "Steven Liu"},
100
+ {"title": "Core Contributor", "name": "Thomas Wolf"},
101
+ ]
102
+
103
+
104
+ # Dataclass for Application Settings
105
+ @dataclass
106
+ class AppSettings:
107
+ """Dataclass to hold all general application settings, replacing the old dictionary."""
108
+
109
+ # General
110
+ # civitai_token: str = field(default="", metadata={"component": "password", "label": "CivitAI API Token", "help": "CivitAI API token for downloading models."})
111
+ save_image_format: Literal[".png", ".jpg", ".webp"] = field(
112
+ default=".png",
113
+ metadata={
114
+ "component": "dropdown",
115
+ "label": "Save Image Format",
116
+ "help": "Default format for saving generated images.",
117
+ },
118
+ )
119
+ latest_preset: str = field(
120
+ default="Default",
121
+ metadata={"label": "Latest Preset", "help": "The most recently used preset."},
122
+ )
123
+
124
+ # Paths
125
+ cache_dir: str = field(
126
+ default="models",
127
+ metadata={
128
+ "label": "Cache Path",
129
+ "help": "Path to a directory to download and cache pre-trained model weights.",
130
+ },
131
+ )
132
+ checkpoints_dir: str = field(
133
+ default="models/Checkpoints",
134
+ metadata={
135
+ "label": "Checkpoints Path",
136
+ "help": "Directory path for local '.safetensors' model weights.",
137
+ },
138
+ )
139
+ vae_dir: str = field(
140
+ default="models/VAE",
141
+ metadata={
142
+ "label": "VAE Models Path",
143
+ "help": "Directory path for local VAE '.safetensors' model weights.",
144
+ },
145
+ )
146
+ output_dir: str = field(
147
+ default="./outputs",
148
+ metadata={
149
+ "label": "Output Path",
150
+ "help": "Directory where generated images will be saved.",
151
+ },
152
+ )
153
+ save_image_on_upscaling_passes: bool = field(
154
+ default=False,
155
+ metadata={
156
+ "label": "Save image in upscaling passes",
157
+ "help": "Saves intermediate images between image upscaling passes",
158
+ },
159
+ )
160
+
161
+ # Hardware & Optimizations
162
+ weight_dtype: Literal["Float16", "Bfloat16", "Float32"] = field(
163
+ default="Float16",
164
+ metadata={
165
+ "component": "dropdown",
166
+ "label": "Weight Precision",
167
+ "help": "Precision for loading model weights (e.g., UNet, ControlNet).",
168
+ },
169
+ )
170
+ vae_weight_dtype: Literal["Float16", "Float32"] = field(
171
+ default="Float16",
172
+ metadata={
173
+ "component": "dropdown",
174
+ "label": "VAE Precision",
175
+ "help": "Precision for loading the VAE weights. FP16 is often faster.",
176
+ },
177
+ )
178
+ device: Literal["cpu", "cuda", "mps"] = field(
179
+ default="cuda",
180
+ metadata={
181
+ "component": "dropdown",
182
+ "label": "Computation Device",
183
+ "help": "The primary device (GPU/CPU) for running the pipelines.",
184
+ },
185
+ )
186
+ generator_device: Literal["cpu", "cuda", "mps"] = field(
187
+ default="cpu",
188
+ metadata={
189
+ "component": "dropdown",
190
+ "label": "Generator Device",
191
+ "help": "The primary device (GPU/CPU) for generator seeds.",
192
+ },
193
+ )
194
+ enable_cpu_offload: bool = field(
195
+ default=True,
196
+ metadata={
197
+ "interactive_if": {"field": "device", "neq": "cpu"},
198
+ "label": "Enable CPU Offload",
199
+ "help": "Saves VRAM by keeping models on the CPU and moving them to the GPU only when needed during inference.",
200
+ },
201
+ )
202
+ enable_vae_tiling: bool = field(
203
+ default=True,
204
+ metadata={
205
+ "label": "Enable VAE Tiling",
206
+ "help": "Processes the VAE in tiles to decode large images with less VRAM.",
207
+ },
208
+ )
209
+ enable_vae_slicing: bool = field(
210
+ default=False,
211
+ metadata={
212
+ "label": "Enable VAE Slicing",
213
+ "help": "Processes image batches one slice at a time through the VAE to save VRAM.",
214
+ },
215
+ )
216
+ memory_attention: Literal["xformers", "sdp"] = field(
217
+ default="xformers",
218
+ metadata={
219
+ "component": "dropdown",
220
+ "label": "Memory Attention",
221
+ "help": "Optimized attention mechanism to save VRAM and increase speed (xformers recommended).",
222
+ },
223
+ )
224
+ quantization_method: Literal["None", "Quanto Library", "Layerwise & Bnb"] = field(
225
+ default="Layerwise & Bnb",
226
+ metadata={
227
+ "component": "radio",
228
+ "label": "Quantization Method",
229
+ "help": "Quantization mechanism to save VRAM and increase speed.",
230
+ },
231
+ )
232
+ quantization_mode: Literal["FP8", "NF4"] = field(
233
+ default="FP8",
234
+ metadata={
235
+ "interactive_if": {"field": "quantization_method", "neq": "None"},
236
+ "component": "radio",
237
+ "label": "Quantization Mode",
238
+ "help": "Quantization mechanism to save VRAM and increase speed.",
239
+ },
240
+ )
241
+ allow_cuda_tf32: bool = field(
242
+ default=False,
243
+ metadata={
244
+ "label": "Enable cuda TF32",
245
+ "help": "Enable TensorFloat-32 tensor cores to be used in matrix multiplications on Ampere or newer GPUs. Enable it only if your card is Ampere+ and weight precision type is Float32",
246
+ },
247
+ )
248
+ allow_cudnn_tf32: bool = field(
249
+ default=False,
250
+ metadata={
251
+ "label": "Enable cudnn TF32",
252
+ "help": "Enable TensorFloat-32 tensor cores to be used in cuDNN convolutions on Ampere or newer GPU. Enable it only if your card is Ampere+ and weight precision type is Float32",
253
+ },
254
+ )
255
+ disable_mmap: bool = field(
256
+ default=False,
257
+ metadata={
258
+ "label": "Disable mmap",
259
+ "help": "Disable memmapping for loading model files. Enable this if you encounter issues loading models on shared drives or network locations.",
260
+ },
261
+ )
262
+ always_offload_models: bool = field(
263
+ default=False,
264
+ metadata={
265
+ "label": "Always Offload Models",
266
+ "help": "Offload models to CPU immediately after finishing inference to free up GPU memory. May slow down performance switching between pipelines.",
267
+ },
268
+ )
269
+ run_vae_on_cpu: bool = field(
270
+ default=False,
271
+ metadata={
272
+ "label": "Run VAE on CPU",
273
+ "help": "Run the VAE on CPU to save GPU memory. This may slow down performance.",
274
+ },
275
+ )
276
+ enable_llava_quantization: bool = field(
277
+ default=True,
278
+ metadata={
279
+ "label": "Enable LLaVA Quantization",
280
+ "help": "Enable quantization for LLaVA models to save VRAM.",
281
+ },
282
+ )
283
+ llava_quantization_mode: Literal["INT4", "INT8"] = field(
284
+ default="INT4",
285
+ metadata={
286
+ "interactive_if": {"field": "enable_llava_quantization", "value": True},
287
+ "component": "radio",
288
+ "label": "LLaVA Quantization Mode",
289
+ "help": "Quantization mode used for LLaVA model.",
290
+ },
291
+ )
292
+ llava_offload_model: bool = field(
293
+ default=False,
294
+ metadata={
295
+ "label": "LLaVA Offload Model",
296
+ "help": "Offload the LLaVA model to CPU to save GPU memory. It may slow down performance.",
297
+ },
298
+ )
299
+ llava_weight_dtype: Literal["Float16", "Bfloat16", "Float32"] = field(
300
+ default="Float16",
301
+ metadata={
302
+ "component": "dropdown",
303
+ "label": "LLaVA Weight Precision",
304
+ "help": "Precision for loading LLaVA model weights.",
305
+ },
306
+ )
307
+ llava_question_prompt: str = field(
308
+ default="Describe what you see in this image and put it in prompt format limited to 77 tokens:",
309
+ metadata={
310
+ "label": "LLaVA Question Prompt",
311
+ "help": "Prompt used to query LLaVA for image descriptions.",
312
+ },
313
+ )
314
+
315
+
316
+ # UI-Specific Dataclass for SUPIR Injection
317
+ @dataclass
318
+ class InjectionScaleConfig:
319
+ """Configuration for a single dynamic injection scale."""
320
+
321
+ scale_end: float = field(
322
+ default=1.0,
323
+ metadata={
324
+ "interactive_if": {"field": "enable_custom_scale", "value": True},
325
+ "component": "slider",
326
+ "minimum": 0.0,
327
+ "maximum": 2.0,
328
+ "step": 0.1,
329
+ "label": "ControlNet Scale",
330
+ "help": "The weight of the ControlNet guidance.",
331
+ },
332
+ )
333
+ linear: bool = field(
334
+ default=False,
335
+ metadata={
336
+ "interactive_if": {"field": "enable_custom_scale", "value": True},
337
+ "label": "Use Linear Control",
338
+ "help": "Linearly increase Control scale during sampling.",
339
+ },
340
+ )
341
+ scale_start: float = field(
342
+ default=1.0,
343
+ metadata={
344
+ "interactive_if": {"field": "linear", "value": True},
345
+ "component": "slider",
346
+ "minimum": 0.0,
347
+ "maximum": 20.0,
348
+ "step": 0.1,
349
+ "label": "ControlNet Scale Start",
350
+ "help": "The starting value for linear ControlNet guidance.",
351
+ },
352
+ )
353
+ reverse: bool = field(
354
+ default=False,
355
+ metadata={
356
+ "interactive_if": {"field": "linear", "value": True},
357
+ "label": "Reverse Linear Control",
358
+ "help": "Linearly decrease ControlNet scale during sampling.",
359
+ },
360
+ )
361
+
362
+
363
+ @dataclass
364
+ class SUPIRInjectionConfig(InjectionScaleConfig):
365
+ sft_active: bool = field(
366
+ default=True,
367
+ metadata={
368
+ "label": "Enable injection",
369
+ "help": "Enable or disable SFT Injection on this stage.",
370
+ },
371
+ )
372
+ enable_custom_scale: bool = field(
373
+ default=False,
374
+ metadata={
375
+ "interactive_if": {"field": "sft_active", "value": True},
376
+ "label": "Enable custom scale",
377
+ "help": "Use customizable controlnet scale at this stage.",
378
+ },
379
+ )
380
+
381
+
382
+ # Reusable Building Blocks for PropertySheet
383
+ @dataclass
384
+ class GenerationSettings:
385
+ """Defines the core generation parameters, reused in each pipeline config."""
386
+
387
+ seed: int = field(
388
+ default=-1,
389
+ metadata={
390
+ "component": "number_integer",
391
+ "label": "Seed",
392
+ "help": "The random seed for generation. -1 means a random seed.",
393
+ },
394
+ )
395
+ randomize_seed: bool = field(default=True, metadata={"label": "Randomize Seed"})
396
+ num_steps: int = field(
397
+ default=30,
398
+ metadata={
399
+ "component": "slider",
400
+ "minimum": 1,
401
+ "maximum": 150,
402
+ "step": 1,
403
+ "label": "Inference Steps",
404
+ "help": "Number of denoising steps. More steps can improve quality but take longer.",
405
+ },
406
+ )
407
+ guidance_scale: float = field(
408
+ default=7.0,
409
+ metadata={
410
+ "component": "slider",
411
+ "minimum": 0.0,
412
+ "maximum": 20.0,
413
+ "step": 0.1,
414
+ "label": "Guidance Scale (CFG)",
415
+ "help": "How strongly the prompt is adhered to.",
416
+ },
417
+ )
418
+ guidance_rescale: float = field(
419
+ default=0.5,
420
+ metadata={
421
+ "component": "slider",
422
+ "minimum": 0.0,
423
+ "maximum": 1.0,
424
+ "step": 0.01,
425
+ "label": "Guidance Rescale",
426
+ "help": "Guidance rescale factor (phi).",
427
+ },
428
+ )
429
+ num_images: int = field(
430
+ default=1,
431
+ metadata={
432
+ "component": "slider",
433
+ "minimum": 1,
434
+ "maximum": 16,
435
+ "step": 1,
436
+ "label": "Number of Images",
437
+ "help": "Number of output images.",
438
+ },
439
+ )
440
+ image_size_fix_mode: Literal[tuple(member.value for member in ImageSizeFixMode)] = field(
441
+ default=ImageSizeFixMode.ProgressiveResize.value,
442
+ metadata={
443
+ "component": "dropdown",
444
+ "label": "Image Size Fix Mode",
445
+ "help": "Method to handle aspect ratio mismatches during processing.",
446
+ },
447
+ )
448
+ tile_size: Literal[1024, 1280] = field(
449
+ default=1280,
450
+ metadata={
451
+ "component": "dropdown",
452
+ "label": "Tile Size",
453
+ "help": "The size (in pixels) of the square tiles to use when latent tiling is enabled.",
454
+ },
455
+ )
456
+ upscaling_mode: Literal["Progressive", "Direct"] = field(
457
+ default="Direct",
458
+ metadata={
459
+ "component": "radio",
460
+ "label": "Upscaling Mode",
461
+ "help": "Choose 'Progressive' for maximum final quality, especially at high scaling factors. Choose 'Direct' for faster processing and previews.",
462
+ },
463
+ )
464
+ upscale_factor: Literal[
465
+ "2x",
466
+ "3x",
467
+ "4x",
468
+ "5x",
469
+ "6x",
470
+ "7x",
471
+ "8x",
472
+ "9x",
473
+ "10x",
474
+ "11x",
475
+ "12x",
476
+ "13x",
477
+ "14x",
478
+ "15x",
479
+ "16x",
480
+ ] = field(
481
+ default="2x",
482
+ metadata={
483
+ "component": "radio",
484
+ "label": "Upscale Factor",
485
+ "help": "Resolution upscale x1 to x10",
486
+ },
487
+ )
488
+ cfg_decay_rate: float = field(
489
+ default=0.5,
490
+ metadata={
491
+ "interactive_if": {"field": "upscaling_mode", "value": "Progressive"},
492
+ "component": "slider",
493
+ "label": "CFG Decay Rate",
494
+ "minimum": 0.0,
495
+ "maximum": 0.9,
496
+ "step": 0.05,
497
+ "help": (
498
+ "The percentage to reduce the Guidance Scale (CFG) at each progressive "
499
+ "upscaling pass. A value of 0.5 means the CFG is halved at each step. "
500
+ "Set to 0.0 to keep the CFG constant."
501
+ ),
502
+ },
503
+ )
504
+ strength_decay_rate: float = field(
505
+ default=0.5,
506
+ metadata={
507
+ "interactive_if": {"field": "upscaling_mode", "value": "Progressive"},
508
+ "component": "slider",
509
+ "label": "Strength Decay Rate",
510
+ "minimum": 0.0,
511
+ "maximum": 0.9,
512
+ "step": 0.05,
513
+ "help": (
514
+ "The percentage to reduce the Denoising Strength at each progressive "
515
+ "upscaling pass. A value of 0.5 means the strength is halved at each step. "
516
+ "Set to 0.0 to keep the strength constant."
517
+ ),
518
+ },
519
+ )
520
+
521
+
522
+ @dataclass
523
+ class CFGSettings:
524
+ """Defines advanced settings for Classifier-Free Guidance."""
525
+
526
+ use_linear_CFG: bool = field(
527
+ default=False,
528
+ metadata={
529
+ "label": "Use Linear CFG",
530
+ "help": "Linearly increase CFG scale during sampling.",
531
+ },
532
+ )
533
+ reverse_linear_CFG: bool = field(
534
+ default=False,
535
+ metadata={
536
+ "interactive_if": {"field": "use_linear_CFG", "value": True},
537
+ "label": "Reverse Linear CFG",
538
+ "help": "Linearly decrease CFG scale during sampling.",
539
+ },
540
+ )
541
+ guidance_scale_start: float = field(
542
+ default=1.0,
543
+ metadata={
544
+ "interactive_if": {"field": "use_linear_CFG", "value": True},
545
+ "component": "slider",
546
+ "minimum": 0.0,
547
+ "maximum": 20.0,
548
+ "step": 0.1,
549
+ "label": "Guidance Scale Start",
550
+ "help": "The starting value for linear CFG scaling.",
551
+ },
552
+ )
553
+
554
+
555
+ @dataclass
556
+ class ControlNetScaleSettings:
557
+ """Defines settings for ControlNet conditioning scale."""
558
+
559
+ controlnet_conditioning_scale: float = field(
560
+ default=1.0,
561
+ metadata={
562
+ "component": "slider",
563
+ "minimum": 0.0,
564
+ "maximum": 2.0,
565
+ "step": 0.05,
566
+ "label": "ControlNet Scale",
567
+ "help": "The weight of the ControlNet guidance.",
568
+ },
569
+ )
570
+ use_linear_control_scale: bool = field(
571
+ default=False,
572
+ metadata={
573
+ "label": "Use Linear Control Scale",
574
+ "help": "Linearly increase ControlNet scale during sampling.",
575
+ },
576
+ )
577
+ reverse_linear_control_scale: bool = field(
578
+ default=False,
579
+ metadata={
580
+ "interactive_if": {"field": "use_linear_control_scale", "value": True},
581
+ "label": "Reverse Linear Control Scale",
582
+ "help": "Linearly decrease ControlNet scale during sampling.",
583
+ },
584
+ )
585
+ control_scale_start: float = field(
586
+ default=0.7,
587
+ metadata={
588
+ "interactive_if": {"field": "use_linear_control_scale", "value": True},
589
+ "component": "slider",
590
+ "minimum": 0.0,
591
+ "maximum": 1.0,
592
+ "step": 0.05,
593
+ "label": "Control Scale Start",
594
+ "help": "The starting value for linear ControlNet scaling.",
595
+ },
596
+ )
597
+
598
+
599
+ @dataclass
600
+ class PAGSettings:
601
+ """Defines settings for Perturbed Attention Guidance."""
602
+
603
+ enable_PAG: bool = field(default=False, metadata={"label": "Enable PAG"})
604
+ pag_scale: float = field(
605
+ default=3.0,
606
+ metadata={
607
+ "component": "slider",
608
+ "minimum": 0.0,
609
+ "maximum": 10.0,
610
+ "step": 0.1,
611
+ "label": "PAG Scale",
612
+ "interactive_if": {"field": "enable_PAG", "value": True},
613
+ "help": "The scale factor for the perturbed attention guidance.",
614
+ },
615
+ )
616
+ use_linear_PAG: bool = field(
617
+ default=False,
618
+ metadata={
619
+ "label": "Use Linear PAG",
620
+ "interactive_if": {"field": "enable_PAG", "value": True},
621
+ "help": "Linearly increase PAG scale during sampling.",
622
+ },
623
+ )
624
+ reverse_linear_PAG: bool = field(
625
+ default=False,
626
+ metadata={
627
+ "label": "Reverse Linear PAG",
628
+ "interactive_if": {"field": "enable_PAG", "value": True},
629
+ "help": "Linearly decrease PAG scale during sampling.",
630
+ },
631
+ )
632
+ pag_scale_start: float = field(
633
+ default=1.0,
634
+ metadata={
635
+ "component": "slider",
636
+ "minimum": 0.0,
637
+ "maximum": 10.0,
638
+ "step": 0.1,
639
+ "label": "PAG Scale Start",
640
+ "interactive_if": {"field": "enable_PAG", "value": True},
641
+ "help": "The starting value for linear PAG scaling.",
642
+ },
643
+ )
644
+ pag_layers: List[str] = field(
645
+ default_factory=lambda: ["mid"],
646
+ metadata={
647
+ "component": "multiselect_checkbox",
648
+ "choices": list(PAG_LAYERS.keys()),
649
+ "label": "PAG Layers",
650
+ "interactive_if": {"field": "enable_PAG", "value": True},
651
+ "help": "Select the UNet layers where Perturbed Attention Guidance should be applied.",
652
+ },
653
+ )
654
+
655
+
656
+ @dataclass
657
+ class PostprocessingSettings:
658
+ color_fix_mode: Literal["None", "Adain", "Wavelet"] = field(
659
+ default="None",
660
+ metadata={
661
+ "component": "radio",
662
+ "label": "Color Fix Mode",
663
+ "help": "Applies the color profile of the input image to the generated image using one of the chosen algorithms.",
664
+ },
665
+ )
666
+
667
+
668
+ # Main Configuration Dataclasses for Each Pipeline
669
+ @dataclass
670
+ class SUPIR_Config:
671
+ """Complete configuration for a SUPIR pipeline run."""
672
+
673
+ apply_prompt_2: bool = field(
674
+ default=True,
675
+ metadata={
676
+ "label": "Apply Prompt 2",
677
+ "help": "If enabled, concatenates prompt 2 with prompt 1.",
678
+ },
679
+ )
680
+ supir_model: Literal[tuple(member.value for member in SUPIRModel)] = field(
681
+ default=SUPIRModel.Quality.value,
682
+ metadata={
683
+ "component": "radio",
684
+ "label": "Model Type",
685
+ "help": "Weights of the SUPIR model used. Quality or Fidelity.",
686
+ },
687
+ )
688
+ restoration_scale: float = field(
689
+ default=4.0,
690
+ metadata={
691
+ "component": "slider",
692
+ "label": "Restoration Scale",
693
+ "minimum": 0.0,
694
+ "maximum": 4.0,
695
+ "step": 0.1,
696
+ "help": "Strength of the SUPIR restoration guidance.",
697
+ },
698
+ )
699
+ s_churn: float = field(
700
+ default=0.0,
701
+ metadata={
702
+ "component": "slider",
703
+ "label": "S Churn",
704
+ "minimum": 0.0,
705
+ "maximum": 1.0,
706
+ "step": 0.01,
707
+ "help": "Stochasticity churn factor.",
708
+ },
709
+ )
710
+ s_noise: float = field(
711
+ default=1.003,
712
+ metadata={
713
+ "component": "slider",
714
+ "label": "S Noise",
715
+ "minimum": 1.0,
716
+ "maximum": 1.01,
717
+ "step": 0.001,
718
+ "help": "Stochasticity noise factor.",
719
+ },
720
+ )
721
+ start_point: Literal["lr", "noise"] = field(
722
+ default="lr",
723
+ metadata={
724
+ "component": "dropdown",
725
+ "label": "Start Point",
726
+ "help": "Start from low-res latents ('lr') or pure noise ('noise').",
727
+ },
728
+ )
729
+ strength: float = field(
730
+ default=1.0,
731
+ metadata={
732
+ "component": "slider",
733
+ "minimum": 0.0,
734
+ "maximum": 1.0,
735
+ "step": 0.01,
736
+ "label": "Denoising Strength",
737
+ "help": "How much to denoise the original image. 1.0 is full denoising.",
738
+ },
739
+ )
740
+ general: GenerationSettings = field(default_factory=GenerationSettings, metadata={"label": "Image Generation"})
741
+ cfg_settings: CFGSettings = field(default_factory=CFGSettings, metadata={"label": "Guidance Scale (CFG) - Advanced"})
742
+ controlnet_settings: ControlNetScaleSettings = field(default_factory=ControlNetScaleSettings, metadata={"label": "ControlNet - Advanced"})
743
+ pag_settings: PAGSettings = field(default_factory=PAGSettings, metadata={"label": "PAG - Perturbed Attention Guidance"})
744
+ post_processsing_settings: PostprocessingSettings = field(default_factory=PostprocessingSettings, metadata={"label": "Image Post-Processing"})
745
+
746
+
747
+ @dataclass
748
+ class SUPIRAdvanced_Config:
749
+ """SFT Injection advanced configuration settings."""
750
+
751
+ sft_post_mid: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Post Mid"})
752
+ sft_up_block_0_stage0: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 0 Stage 0"})
753
+ sft_up_block_0_stage1: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 0 Stage 1"})
754
+ sft_up_block_0_stage2: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 0 Stage 2"})
755
+ sft_up_block_1_stage0: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 1 Stage 0"})
756
+ sft_up_block_1_stage1: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 1 Stage 1"})
757
+ sft_up_block_1_stage2: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 1 Stage 2"})
758
+ sft_up_block_2_stage0: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 2 Stage 0"})
759
+ sft_up_block_2_stage1: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 2 Stage 1"})
760
+ sft_up_block_2_stage2: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "SFT Up Block 2 Stage 2"})
761
+ cross_up_block_0_stage1: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "Cross Up Block 0 Stage 1"})
762
+ cross_up_block_0_stage2: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "Cross Up Block 0 Stage 2"})
763
+ cross_up_block_1_stage1: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "Cross Up Block 1 Stage 1"})
764
+ cross_up_block_1_stage2: SUPIRInjectionConfig = field(default_factory=SUPIRInjectionConfig, metadata={"label": "Cross Up Block 1 Stage 2"})
765
+
766
+
767
+ @dataclass
768
+ class FaithDiff_Config:
769
+ """Complete configuration for a FaithDiff pipeline run."""
770
+
771
+ apply_prompt_2: bool = field(
772
+ default=True,
773
+ metadata={
774
+ "label": "Apply Prompt 2",
775
+ "help": "If enabled, concatenates prompt 2 with prompt 1.",
776
+ },
777
+ )
778
+ invert_prompts: bool = field(
779
+ default=True,
780
+ metadata={
781
+ "label": "Invert Prompts",
782
+ "help": "Starts the send prompt with prompt 2 and ends with prompt 1.",
783
+ },
784
+ )
785
+ apply_ipa_embeds: bool = field(
786
+ default=True,
787
+ metadata={
788
+ "label": "Apply IPA Embeds",
789
+ "help": "Apply IP-Adapter embeddings during diffusion.",
790
+ },
791
+ )
792
+ s_churn: float = field(
793
+ default=0.0,
794
+ metadata={
795
+ "component": "slider",
796
+ "label": "S Churn",
797
+ "minimum": 0.0,
798
+ "maximum": 1.0,
799
+ "step": 0.01,
800
+ "help": "Stochasticity churn factor.",
801
+ },
802
+ )
803
+ s_noise: float = field(
804
+ default=1.003,
805
+ metadata={
806
+ "component": "slider",
807
+ "label": "S Noise",
808
+ "minimum": 1.0,
809
+ "maximum": 1.01,
810
+ "step": 0.001,
811
+ "help": "Stochasticity noise factor.",
812
+ },
813
+ )
814
+ start_point: Literal["lr", "noise"] = field(
815
+ default="lr",
816
+ metadata={
817
+ "component": "dropdown",
818
+ "label": "Start Point",
819
+ "help": "Start from low-res latents ('lr') or pure noise ('noise').",
820
+ },
821
+ )
822
+ strength: float = field(
823
+ default=1.0,
824
+ metadata={
825
+ "component": "slider",
826
+ "minimum": 0.0,
827
+ "maximum": 1.0,
828
+ "step": 0.01,
829
+ "label": "Denoising Strength",
830
+ "help": "How much to denoise the original image. 1.0 is full denoising.",
831
+ },
832
+ )
833
+ general: GenerationSettings = field(default_factory=GenerationSettings, metadata={"label": "Image Generation"})
834
+ controlnet_settings: ControlNetScaleSettings = field(default_factory=ControlNetScaleSettings, metadata={"label": "ControlNet - Advanced"})
835
+ pag_settings: PAGSettings = field(default_factory=PAGSettings, metadata={"label": "PAG - Perturbed Attention Guidance"})
836
+ post_processsing_settings: PostprocessingSettings = field(default_factory=PostprocessingSettings, metadata={"label": "Image Post-Processing"})
837
+
838
+
839
+ @dataclass
840
+ class ControlNetTile_Config:
841
+ """Complete configuration for a ControlNetTile pipeline run."""
842
+
843
+ apply_prompt_2: bool = field(
844
+ default=True,
845
+ metadata={
846
+ "label": "Apply Prompt 2",
847
+ "help": "If enabled, concatenates prompt 2 with prompt 1.",
848
+ },
849
+ )
850
+ controlnet_conditioning_scale: float = field(
851
+ default=1.0,
852
+ metadata={
853
+ "component": "slider",
854
+ "minimum": 0.0,
855
+ "maximum": 2.0,
856
+ "step": 0.05,
857
+ "label": "ControlNet Scale",
858
+ "help": "The weight of the ControlNet Tile guidance.",
859
+ },
860
+ )
861
+ tile_overlap: int = field(
862
+ default=128,
863
+ metadata={
864
+ "component": "slider",
865
+ "label": "Tile Overlap",
866
+ "minimum": 64,
867
+ "maximum": 512,
868
+ "step": 16,
869
+ "help": "Overlap in pixels between tiles to reduce seams.",
870
+ },
871
+ )
872
+ tile_weighting_method: Literal["Cosine", "Gaussian"] = field(
873
+ default="Cosine",
874
+ metadata={
875
+ "component": "radio",
876
+ "label": "Tile Weighting Method",
877
+ "help": "Method for blending tile edges.",
878
+ },
879
+ )
880
+ tile_gaussian_sigma: float = field(
881
+ default=0.3,
882
+ metadata={
883
+ "interactive_if": {"field": "tile_weighting_method", "value": "Gaussian"},
884
+ "component": "slider",
885
+ "label": "Tile Guassian Sigma",
886
+ "minimum": 0.05,
887
+ "maximum": 2.0,
888
+ "step": 0.01,
889
+ "help": "Sigma parameter for Gaussian weighting of tiles.",
890
+ },
891
+ )
892
+ strength: float = field(
893
+ default=0.65,
894
+ metadata={
895
+ "component": "slider",
896
+ "minimum": 0.0,
897
+ "maximum": 1.0,
898
+ "step": 0.01,
899
+ "label": "Denoising Strength",
900
+ "help": "How much to denoise the original image. 1.0 is full denoising.",
901
+ },
902
+ )
903
+ general: GenerationSettings = field(default_factory=GenerationSettings, metadata={"label": "Image Generation"})
904
+ post_processsing_settings: PostprocessingSettings = field(default_factory=PostprocessingSettings, metadata={"label": "Image Post-Processing"})
905
+
906
+
907
+ # Data for Flyout popups
908
+ @dataclass
909
+ class SchedulerSettings:
910
+ """Settings to the sampler."""
911
+
912
+ scale_linear_exponent: float = field(
913
+ default=1.93,
914
+ metadata={
915
+ "component": "slider",
916
+ "label": "Scale Linear Exponent",
917
+ "minimum": 0.0,
918
+ "maximum": 2.0,
919
+ "step": 0.01,
920
+ "help": "Exponent for the scale linear beta schedule.",
921
+ },
922
+ )
923
+
924
+
925
+ # PropertySheet value Mappings
926
+ RESTORER_CONFIG_MAPPING = {
927
+ "SUPIR": SUPIR_Config(),
928
+ "SUPIRAdvanced": SUPIRAdvanced_Config(),
929
+ "FaithDiff": FaithDiff_Config(),
930
+ }
931
+
932
+ UPSCALER_CONFIG_MAPPING = {
933
+ "SUPIR": SUPIR_Config(),
934
+ "SUPIRAdvanced": SUPIRAdvanced_Config(),
935
+ "FaithDiff": FaithDiff_Config(),
936
+ "ControlNetTile": ControlNetTile_Config(),
937
+ }
938
+ SAMPLER_MAPPING = {
939
+ "restorer_sampler": SchedulerSettings(scale_linear_exponent=1.93),
940
+ "upscaler_sampler": SchedulerSettings(scale_linear_exponent=2.0),
941
+ }
942
+
943
+ # PropertySheet change rules mapping
944
+ APPSETTINGS_SHEET_DEPENDENCY_RULES = {
945
+ "dynamic_dependencies": {
946
+ "quantization_method": {
947
+ "actions": [
948
+ {
949
+ "type": "update_options",
950
+ "target_field_path": "quantization_mode",
951
+ "mapping": {
952
+ "None": {"options": ["FP8", "NF4"], "new_default": "FP8"},
953
+ "Layerwise & Bnb": {"options": ["FP8", "NF4"], "new_default": "FP8"},
954
+ "Quanto Library": {"options": ["INT8", "INT4"], "new_default": "INT8"},
955
+ },
956
+ },
957
+ ]
958
+ },
959
+ "device": {
960
+ "actions": [
961
+ {
962
+ "type": "update_options",
963
+ "target_field_path": "weight_dtype",
964
+ "mapping": {
965
+ "balanced": {
966
+ "options": ["Float16", "Bfloat16", "Float32"],
967
+ "new_default": None,
968
+ },
969
+ "cpu": {"options": ["Float32"], "new_default": "Float32"},
970
+ "cuda": {
971
+ "options": ["Float16", "Bfloat16", "Float32"],
972
+ "new_default": None,
973
+ },
974
+ "mps": {"options": ["Float16", "Bfloat16", "Float32"], "new_default": None},
975
+ },
976
+ },
977
+ {
978
+ "type": "update_options",
979
+ "target_field_path": "vae_weight_dtype",
980
+ "mapping": {
981
+ "balanced": {"options": ["Float16", "Float32"], "new_default": None},
982
+ "cpu": {"options": ["Float32"], "new_default": "Float32"},
983
+ "cuda": {"options": ["Float16", "Float32"], "new_default": None},
984
+ "mps": {"options": ["Float16", "Float32"], "new_default": None},
985
+ },
986
+ },
987
+ {
988
+ "type": "set_value",
989
+ "target_field_path": "enable_cpu_offload",
990
+ "mapping": {"cpu": False},
991
+ },
992
+ ]
993
+ },
994
+ },
995
+ "on_load_actions": [],
996
+ }
997
+
998
+ UPSCALER_SHEET_DEPENDENCY_RULES = {
999
+ "dynamic_dependencies": {
1000
+ "general.upscaling_mode": {
1001
+ "actions": [
1002
+ {
1003
+ "type": "update_options",
1004
+ "target_field_path": "general.upscale_factor",
1005
+ "mapping": {
1006
+ "Progressive": {
1007
+ "options": ["2x", "4x", "6x", "8x", "16x"],
1008
+ "new_default": None,
1009
+ },
1010
+ "Direct": {
1011
+ "options": [
1012
+ "2x",
1013
+ "3x",
1014
+ "4x",
1015
+ "5x",
1016
+ "6x",
1017
+ "7x",
1018
+ "8x",
1019
+ "9x",
1020
+ "10x",
1021
+ "11x",
1022
+ "12x",
1023
+ "13x",
1024
+ "14x",
1025
+ "15x",
1026
+ "16x",
1027
+ ],
1028
+ "new_default": None,
1029
+ },
1030
+ },
1031
+ },
1032
+ ]
1033
+ }
1034
+ },
1035
+ "on_load_actions": [],
1036
+ }
1037
+ RESTORER_SHEET_DEPENDENCY_RULES = {
1038
+ "dynamic_dependencies": {
1039
+ "general.upscaling_mode": {
1040
+ "actions": [
1041
+ {
1042
+ "type": "update_options",
1043
+ "target_field_path": "general.upscale_factor",
1044
+ "mapping": {
1045
+ "Progressive": {
1046
+ "options": ["2x", "4x", "6x", "8x", "16x"],
1047
+ "new_default": None,
1048
+ },
1049
+ "Direct": {"options": ["1x", "2x", "3x", "4x"], "new_default": "1x"},
1050
+ },
1051
+ },
1052
+ ]
1053
+ }
1054
+ },
1055
+ "on_load_actions": [
1056
+ # Each item in the list is an action to perform.
1057
+ {
1058
+ "type": "update_visibility",
1059
+ "target_field_path": "general.upscaling_mode",
1060
+ "visible": False,
1061
+ },
1062
+ {
1063
+ "type": "update_visibility",
1064
+ "target_field_path": "general.cfg_decay_rate",
1065
+ "visible": False,
1066
+ },
1067
+ {
1068
+ "type": "update_visibility",
1069
+ "target_field_path": "general.strength_decay_rate",
1070
+ "visible": False,
1071
+ },
1072
+ {
1073
+ "type": "update_visibility",
1074
+ "target_field_path": "general.upscale_factor",
1075
+ "visible": False,
1076
+ },
1077
+ # You could add more direct actions here..
1078
+ ],
1079
+ }
1080
+ SUPIR_ADVANCED_RULES = {"dynamic_dependencies": {}, "on_load_actions": []}
1081
+
1082
+ # Default prompt dictionary for each engine type
1083
+ DEFAULT_PROMPTS = {
1084
+ "Restorer": {
1085
+ "SUPIR": {
1086
+ "prompt": "Direct flash photography. [subject].",
1087
+ "prompt_2": "Extremely detailed faces, flawless natural skin texture, skin pore detailing, 4k, 8k, clean image, no noise, shot on Fujifilm Superia 400, sharp focus, faithful colors.",
1088
+ "negative_prompt": "low-res, disfigured, analog artifacts, smudged, animate, (out of focus:1.2), catchlights, over-smooth, extra eyes, worst quality, unreal engine, art, aberrations, surreal, pastel drawing, harsh lighting, dead eyes, deformed fingers, undistinct fingers outlines",
1089
+ },
1090
+ "FaithDiff": {
1091
+ "prompt": "[subject].",
1092
+ "prompt_2": "Ultra-quality photography, flawless natural skin texture, shot on Fujifilm Superia 400, clean image, sharp focus.",
1093
+ "negative_prompt": "low res, worst quality, blurry, out of focus, analog artifacts, oil painting, illustration, art, anime, cartoon, CG Style, unreal engine, render, artwork, (wrinkles:1.4), fine lines, smile lines, age spots, (blemishes:1.2), acne, scars, freckles, blotchy skin, deformed mouth, catchlights",
1094
+ },
1095
+ },
1096
+ "Upscaler": {
1097
+ "SUPIR": {
1098
+ "prompt": "Extremely detailed faces, flawless natural skin texture, skin pore detailing, 4k, 8k, clean image, no noise, shot on Fujifilm Superia 400, sharp focus, faithful colors.",
1099
+ "prompt_2": "",
1100
+ "negative_prompt": "blurry, pixelated, noisy, low resolution, artifacts, poor details, (oversaturated:2.5), (overexposed:2.5)",
1101
+ },
1102
+ "FaithDiff": {
1103
+ "prompt": "Ultra-quality photography, flawless natural skin texture, shot on Fujifilm Superia 400, clean image, sharp focus.",
1104
+ "prompt_2": "",
1105
+ "negative_prompt": "low res, worst quality, blurry, out of focus, analog artifacts, oil painting, illustration, art, anime, cartoon, CG Style, unreal engine, render, artwork, (wrinkles:1.4), fine lines, smile lines, age spots, (blemishes:1.2), acne, scars, freckles, blotchy skin, deformed mouth, catchlights",
1106
+ },
1107
+ "ControlNetTile": {
1108
+ "prompt": "high-quality, noise-free edges, high quality, 4k, hd, 8k",
1109
+ "prompt_2": "",
1110
+ "negative_prompt": "blurry, pixelated, noisy, low resolution, artifacts, poor details",
1111
+ },
1112
+ },
1113
+ }
1114
+
1115
+
1116
+ TAG_DATA_POSITIVE = {
1117
+ # POSITIVE PROMPTS
1118
+ "Quality": [
1119
+ "best quality",
1120
+ "high quality",
1121
+ "masterpiece",
1122
+ "ultra high resolution",
1123
+ "high resolution",
1124
+ "4k",
1125
+ "8k",
1126
+ "extremely detailed",
1127
+ "ultra-detailed",
1128
+ "hyper detailed",
1129
+ "intricate details",
1130
+ "sharp focus",
1131
+ "crisp details",
1132
+ "sharp",
1133
+ "hyper sharpness",
1134
+ ],
1135
+ "Style": [
1136
+ "photorealistic",
1137
+ "realistic photo",
1138
+ "photograph",
1139
+ "digital photograph",
1140
+ "professional photography",
1141
+ "direct flash photography",
1142
+ "soft studio lighting",
1143
+ "natural lighting",
1144
+ "softbox lighting",
1145
+ "cinematic",
1146
+ "filmic",
1147
+ "shot on [Camera Name]",
1148
+ "Fujifilm Superia 400",
1149
+ "Kodak Portra 400",
1150
+ "early 2000s aesthetic",
1151
+ "modern look",
1152
+ "editorial fashion photo",
1153
+ "beauty portrait",
1154
+ ],
1155
+ "Subject & Details": [
1156
+ "natural skin texture",
1157
+ "realistic skin",
1158
+ "subtle skin variations",
1159
+ "visible micro-details",
1160
+ "matte skin finish",
1161
+ "poreless skin",
1162
+ "flawless skin",
1163
+ "realistic eyes",
1164
+ "detailed eyes",
1165
+ "natural catchlights",
1166
+ "crisp whiskers",
1167
+ "dense fur",
1168
+ "realistic fur texture",
1169
+ "individual hair strands",
1170
+ "multi-layered fur texture",
1171
+ "detailed clothing",
1172
+ "realistic fabric texture",
1173
+ "clothing folds",
1174
+ ],
1175
+ }
1176
+
1177
+ TAG_DATA_NEGATIVE = {
1178
+ # NEGATIVE PROMPTS
1179
+ "Negative - General Quality & Artifacts": [
1180
+ "worst quality",
1181
+ "low quality",
1182
+ "normal quality",
1183
+ "bad quality",
1184
+ "low-res",
1185
+ "low resolution",
1186
+ "jpeg artifacts",
1187
+ "compression artifacts",
1188
+ "grain",
1189
+ "grainy",
1190
+ "noisy",
1191
+ "blurry",
1192
+ "unsharp",
1193
+ "blur",
1194
+ "out of focus",
1195
+ "unfocused",
1196
+ "smudged",
1197
+ "hazy",
1198
+ "watermark",
1199
+ "signature",
1200
+ "username",
1201
+ "artist name",
1202
+ "text",
1203
+ "logo",
1204
+ "symbol",
1205
+ "hieroglyph",
1206
+ "script",
1207
+ "printed words",
1208
+ "written language",
1209
+ ],
1210
+ "Negative - Unrealistic Styles": [
1211
+ "cartoon",
1212
+ "3d",
1213
+ "3d render",
1214
+ "cgi",
1215
+ "2d",
1216
+ "sketch",
1217
+ "drawing",
1218
+ "illustration",
1219
+ "bad art",
1220
+ "bad illustration",
1221
+ "painting",
1222
+ "oil painting",
1223
+ "pastel drawing",
1224
+ "art",
1225
+ "unreal engine",
1226
+ "cinema 4d",
1227
+ "artstation",
1228
+ "octane render",
1229
+ "photoshop",
1230
+ "video game",
1231
+ "surreal",
1232
+ "kitsch",
1233
+ "abstract",
1234
+ "creative",
1235
+ ],
1236
+ "Negative - Anatomy & Deformities (General)": [
1237
+ "ugly",
1238
+ "disfigured",
1239
+ "deformed",
1240
+ "mutation",
1241
+ "mutated",
1242
+ "mutilated",
1243
+ "mangled",
1244
+ "bad anatomy",
1245
+ "incorrect physiology",
1246
+ "bad proportions",
1247
+ "gross proportions",
1248
+ "disproportioned",
1249
+ "morbid",
1250
+ "disgusting",
1251
+ "repellent",
1252
+ "revolting dimensions",
1253
+ "corpse",
1254
+ "2 heads",
1255
+ "2 faces",
1256
+ "conjoined",
1257
+ "long neck",
1258
+ "long body",
1259
+ "childish",
1260
+ "old",
1261
+ ],
1262
+ "Negative - Limbs, Hands & Fingers": [
1263
+ "extra limbs",
1264
+ "missing limb",
1265
+ "extra legs",
1266
+ "extra arms",
1267
+ "missing arms",
1268
+ "missing legs",
1269
+ "malformed limbs",
1270
+ "floating limbs",
1271
+ "disconnected limbs",
1272
+ "disembodied limb",
1273
+ "linked limb",
1274
+ "connected limb",
1275
+ "interconnected limb",
1276
+ "split limbs",
1277
+ "split arms",
1278
+ "split hands",
1279
+ "severed",
1280
+ "dismembered",
1281
+ "amputee",
1282
+ "extra knee",
1283
+ "extra elbow",
1284
+ "three crus",
1285
+ "extra crus",
1286
+ "fused crus",
1287
+ "three feet",
1288
+ "fused feet",
1289
+ "worst feet",
1290
+ "poorly drawn feet",
1291
+ "fused thigh",
1292
+ "three thigh",
1293
+ "extra thigh",
1294
+ "worst thigh",
1295
+ "bad hands",
1296
+ "poorly drawn hands",
1297
+ "poorly rendered hands",
1298
+ "mutated hands",
1299
+ "malformed hands",
1300
+ "disfigured hand",
1301
+ "fused hands",
1302
+ "three hands",
1303
+ "missing hand",
1304
+ "no thumb",
1305
+ "broken hand",
1306
+ "broken wrist",
1307
+ "broken leg",
1308
+ "extra fingers",
1309
+ "missing fingers",
1310
+ "fused fingers",
1311
+ "too many fingers",
1312
+ "extra digit",
1313
+ "fewer digits",
1314
+ "ugly fingers",
1315
+ "long fingers",
1316
+ "twisted fingers",
1317
+ "bad digit",
1318
+ "missing digit",
1319
+ "broken finger",
1320
+ "six fingers per hand",
1321
+ "four fingers per hand",
1322
+ ],
1323
+ "Negative - Face & Eyes": [
1324
+ "poorly drawn face",
1325
+ "bad face",
1326
+ "fused face",
1327
+ "cloned face",
1328
+ "worst face",
1329
+ "deformed face",
1330
+ "ugly face",
1331
+ "irregular face",
1332
+ "asymmetrical",
1333
+ "squint",
1334
+ "extra eyes",
1335
+ "huge eyes",
1336
+ "ugly eyes",
1337
+ "deformed pupils",
1338
+ "deformed iris",
1339
+ "cross-eye",
1340
+ ],
1341
+ "Negative - Composition & Color": [
1342
+ "out of frame",
1343
+ "body out of frame",
1344
+ "poorly framed",
1345
+ "cut off",
1346
+ "trimmed",
1347
+ "cropped",
1348
+ "canvas frame",
1349
+ "picture frame",
1350
+ "storyboard",
1351
+ "split image",
1352
+ "tiling",
1353
+ "b&w",
1354
+ "black and white",
1355
+ "monochrome",
1356
+ "weird colors",
1357
+ "oversaturated",
1358
+ "low saturation",
1359
+ ],
1360
+ }
ui/ui_data.py ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The DEVAIEXP Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import os
17
+ import traceback
18
+ from dataclasses import fields
19
+ from enum import Enum
20
+ from importlib import resources
21
+ from importlib.resources import files
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional, get_type_hints
24
+
25
+ import gradio as gr
26
+ import numpy as np
27
+
28
+ # Project Imports
29
+ from sup_toolbox.config import (
30
+ SAMPLERS_OTHERS,
31
+ SAMPLERS_SUPIR,
32
+ Config,
33
+ SchedulerConfig,
34
+ )
35
+ from sup_toolbox.enums import ModelType, PromptMethod
36
+ from sup_toolbox.modules.model_manager import ModelManager
37
+ from sup_toolbox.modules.SUPIR.pipeline_supir_stable_diffusion_xl import (
38
+ InjectionConfigs,
39
+ InjectionFlags,
40
+ InjectionScaleConfig,
41
+ )
42
+ from sup_toolbox.utils.logging import logger
43
+ from ui.ui_config import AppSettings, SchedulerSettings, SUPIRAdvanced_Config, SUPIRInjectionConfig
44
+
45
+
46
+ class UIData:
47
+ """
48
+ This class acts as a service provider for the Gradio UI.
49
+ It loads dynamic data like model lists, manages application settings and presets,
50
+ and contains helper functions for UI data conversion.
51
+ """
52
+
53
+ # Symbols and Constants
54
+ folder_symbol = "\U0001f4c2"
55
+ refresh_symbol = "\U0001f504"
56
+ save_symbol = "\U0001f4be"
57
+ add_symbol = "\U00002795"
58
+ remove_symbol = "\U0000274c"
59
+ clean_symbol = "\U0001f9f9"
60
+ download_symbol = "\U0001f4e5"
61
+ engine_symbol = "\U00002699"
62
+ tools_symbol = "\U0001f6e0"
63
+ settings_symbol = "⚙️"
64
+ process_symbol = "\U00002728"
65
+ stop_symbol = "\U0001f6d1"
66
+ preview_symbol = "\U0001f441"
67
+ about_symbol = "ℹ️"
68
+ caption_symbol = "🤖"
69
+
70
+ APPLICATION_TITLE = "SUP Toolbox UI"
71
+ MAX_SEED = np.iinfo(np.int32).max
72
+
73
+ # Static UI Option Lists
74
+ PROMPT_METHODS = [e.value for e in PromptMethod]
75
+ SAMPLER_SUPIR_LIST = sorted(SAMPLERS_SUPIR.keys())
76
+ SAMPLER_OTHERS_LIST = sorted(SAMPLERS_OTHERS.keys())
77
+
78
+ # Dynamically Populated Lists
79
+ MODEL_CHOICES: list[str] = []
80
+ VAE_CHOICES: list[str] = []
81
+ RESTORER_ENGINE_CHOICES: list[str] = []
82
+ UPSCALER_ENGINE_CHOICES: list[str] = []
83
+ PRESETS_LIST: list[str] = []
84
+ PRETRAINED_MODELS_LIST: list[str] = []
85
+ PRETRAINED_VAE_MODELS_LIST: list[str] = []
86
+
87
+ APP_ROOT_DIR = Path(__file__).resolve().parent.parent
88
+
89
+ def __init__(self, **kwargs):
90
+ self.USER_PRESETS_DIR = Path("./presets")
91
+ self.USER_PRESETS_DIR.mkdir(exist_ok=True)
92
+ self.PRESETS_LIST = []
93
+ self.config = Config(models_root_path=self.APP_ROOT_DIR)
94
+ Path(self.config.output_dir).mkdir(exist_ok=True)
95
+ self.settings = AppSettings()
96
+ self.model_manager = ModelManager(self.config)
97
+ self.loaded_preset = {}
98
+ always_download_models = kwargs.pop("always_download_models", None)
99
+
100
+ # Main initialization sequence
101
+ self.load_settings()
102
+
103
+ # Prepare models
104
+ self.model_manager.prepare_models(always_download_models)
105
+
106
+ # Populate dynamic lists based on loaded settings
107
+ self.get_model_list()
108
+ self.get_vae_model_list()
109
+ self.get_preset_list()
110
+
111
+ # Populate engine choices from the config mappings
112
+ from ui.ui_config import RESTORER_CONFIG_MAPPING, UPSCALER_CONFIG_MAPPING
113
+
114
+ self.RESTORER_ENGINE_CHOICES = ["None"] + [k for k in RESTORER_CONFIG_MAPPING.keys() if k != "SUPIRAdvanced"]
115
+ self.UPSCALER_ENGINE_CHOICES = ["None"] + [k for k in UPSCALER_CONFIG_MAPPING.keys() if k != "SUPIRAdvanced"]
116
+
117
+ # Theme
118
+ self.theme = gr.themes.Ocean()
119
+
120
+ def load_settings(self):
121
+ """Loads settings from a JSON file into the AppSettings dataclass and the main Config object."""
122
+ print("Loading settings...")
123
+
124
+ settings_file_path = "configs/settings.json"
125
+ if os.path.exists(settings_file_path):
126
+ with open(settings_file_path, "r") as f:
127
+ data = json.load(f)
128
+ # Populate the dataclass with only the keys that exist in it
129
+ valid_keys = {f.name for f in fields(AppSettings)}
130
+ filtered_data = {k: v for k, v in data.items() if k in valid_keys}
131
+ self.settings = AppSettings(**filtered_data)
132
+ config_class = type(self.config)
133
+ type_hints = get_type_hints(config_class)
134
+
135
+ # Populate the backend config
136
+ for key, value in data.items():
137
+ if hasattr(self.config, key):
138
+ expected_type = type_hints.get(key)
139
+ final_value = value
140
+ if expected_type and isinstance(expected_type, type) and issubclass(expected_type, Enum):
141
+ try:
142
+ if hasattr(expected_type, "from_str") and callable(getattr(expected_type, "from_str")):
143
+ final_value = expected_type.from_str(value)
144
+ else:
145
+ final_value = expected_type(value)
146
+ except ValueError:
147
+ print(f"Warning: '{value}' is not a valid member of {expected_type.__name__} for field '{key}'. Skipping update.")
148
+ continue
149
+ setattr(self.config, key, final_value)
150
+
151
+ # This part is crucial for making the dynamic lists work
152
+ self.config.checkpoints_dir = self.settings.checkpoints_dir
153
+ self.config.vae_dir = self.settings.vae_dir
154
+ pass
155
+
156
+ def load_defaults(self):
157
+ default_settings_path = files("sup_toolbox.configs").joinpath("settings.json")
158
+ if not os.path.exists(default_settings_path):
159
+ print(f"Settings configuration file {default_settings_path} does not exist. Loading will be aborted!")
160
+ return
161
+
162
+ with open(default_settings_path, "r") as f:
163
+ config_dict = json.load(f)
164
+
165
+ self.save_settings(config_dict)
166
+
167
+ def save_settings(self, values_dict: dict):
168
+ status = "Settings was updated!"
169
+ try:
170
+ if not os.path.exists("configs"):
171
+ os.makedirs("configs")
172
+ settings_path = "configs/settings.json"
173
+ with open(settings_path, "w") as f:
174
+ json.dump(values_dict, f, indent=2)
175
+
176
+ except Exception as e:
177
+ logger.error(f"Error in save_settings: {str(e)}")
178
+ print(traceback.format_exc())
179
+ status = "There was an error saving the settings!"
180
+ pass
181
+ return status
182
+
183
+ def get_preset_list(self):
184
+ """
185
+ Scans for both default library presets and local user presets, then updates the instance's PRESETS_LIST.
186
+
187
+ This method performs a comprehensive scan to build a unified list of available presets for the UI.
188
+ It follows these steps:
189
+ 1. Scans the user-defined presets directory (`self.USER_PRESETS_DIR`) for any custom `.json` files.
190
+ This directory is created if it does not exist.
191
+ 2. Uses the modern `importlib.resources` library to safely locate and scan the `presets`
192
+ directory bundled within the installed `sup_toolbox` package. This approach works
193
+ reliably across all installation types (standard, editable, etc.).
194
+ 3. Prefixes the names of default presets with "Default: " to distinguish them in the UI.
195
+ 4. Merges the two lists (user and default), sorts them alphabetically, and stores the
196
+ final list in `self.PRESETS_LIST`.
197
+
198
+ Raises:
199
+ - Logs an error via the `logging` module if scanning the user presets directory fails.
200
+ - Logs a warning if the `sup_toolbox` package or its presets directory cannot be found.
201
+ - Logs an error for any other unexpected exceptions during the process.
202
+ """
203
+
204
+ print("Scanning for user and default presets...")
205
+
206
+ user_presets = set()
207
+ default_presets = set()
208
+
209
+ # 1. Load User-Defined Presets
210
+ try:
211
+ # Ensure the user presets directory exists before scanning.
212
+ self.USER_PRESETS_DIR.mkdir(parents=True, exist_ok=True)
213
+ for f in self.USER_PRESETS_DIR.glob("*.json"):
214
+ user_presets.add(f.stem)
215
+ except Exception as e:
216
+ logger.error(f"Could not scan user presets directory at '{self.USER_PRESETS_DIR}': {e}")
217
+
218
+ # 2. Load Bundled Default Presets
219
+ try:
220
+ # Use `importlib.resources.files` to get a traversable path object
221
+ # to the 'presets' directory inside the installed 'sup_toolbox' package.
222
+ # This is the modern, standard way to access package data.
223
+ preset_path = resources.files("sup_toolbox").joinpath("presets")
224
+
225
+ # Iterate through the contents of the bundled presets directory.
226
+ for item in preset_path.iterdir():
227
+ if item.is_file() and item.name.endswith(".json"):
228
+ # Add with "Default: " prefix for UI clarity.
229
+ default_presets.add(f"Default: {item.stem}")
230
+
231
+ except ModuleNotFoundError:
232
+ # This occurs if the `sup_toolbox` package is not installed in the environment.
233
+ logger.warning("Could not find default presets because the 'sup-toolbox' library is not installed.")
234
+ except FileNotFoundError:
235
+ # This might occur if the package is installed but the 'presets' folder is missing.
236
+ logger.warning("Located 'sup-toolbox' library, but its 'presets' directory could not be found.")
237
+ except Exception as e:
238
+ # Catch any other unexpected errors during resource loading.
239
+ logger.error(f"An unexpected error occurred while loading default presets: {e}")
240
+
241
+ # 3. Merge, Sort, and Finalize the List
242
+ # Convert sets to lists, sort each one, and then combine.
243
+ # User presets are listed first.
244
+ sorted_user_presets = sorted(user_presets)
245
+ sorted_default_presets = sorted(default_presets)
246
+
247
+ self.PRESETS_LIST = sorted_user_presets + sorted_default_presets
248
+ print(f"Finished loading presets. Found {len(user_presets)} user preset(s) and {len(default_presets)} default preset(s).")
249
+
250
+ def load_preset(self, preset_name: str) -> dict:
251
+ """
252
+ Loads a specific preset from a JSON file, returning its content as a dictionary.
253
+
254
+ This method intelligently searches for the preset in two locations:
255
+ 1. If the `preset_name` starts with "Default: ", it uses `importlib.resources`
256
+ to securely read the corresponding bundled preset file from within the
257
+ installed `sup_toolbox` package.
258
+ 2. Otherwise, it assumes the preset is a user-defined file located in the
259
+ `self.USER_PRESETS_DIR` directory.
260
+
261
+ In case of any errors (e.g., file not found, invalid JSON), it logs the
262
+ error and returns an empty dictionary to ensure the application can
263
+ continue gracefully.
264
+
265
+ Args:
266
+ preset_name (str): The name of the preset to load. Default presets
267
+ should be prefixed with "Default: ".
268
+
269
+ Returns:
270
+ dict: A dictionary containing the loaded preset data, or an empty
271
+ dictionary if the preset could not be loaded.
272
+ """
273
+
274
+ if not preset_name or not preset_name.strip():
275
+ logger.warning("`load_preset` called with an empty name.")
276
+ return {}
277
+
278
+ if preset_name.startswith("Default: "):
279
+ # Handle Bundled Default Presets
280
+ base_name = preset_name.replace("Default: ", "")
281
+ logger.info(f"Loading default preset: '{base_name}'")
282
+ try:
283
+ # Use `importlib.resources` to safely access the package data file.
284
+ # `files()` returns a traversable object representing the package.
285
+ preset_file_path = resources.files("sup_toolbox").joinpath("presets").joinpath(f"{base_name}.json")
286
+
287
+ # `read_text()` is a convenient and safe way to read the file content.
288
+ json_content = preset_file_path.read_text(encoding="utf-8")
289
+ return json.loads(json_content)
290
+
291
+ except ModuleNotFoundError:
292
+ logger.error("Cannot load default preset because the 'sup-toolbox' library is not installed.")
293
+ return {}
294
+ except FileNotFoundError:
295
+ logger.error(f"Default preset file '{base_name}.json' not found within the 'sup_toolbox' package.")
296
+ return {}
297
+ except json.JSONDecodeError as e:
298
+ logger.error(f"Failed to parse default preset '{base_name}.json'. Invalid JSON: {e}")
299
+ return {}
300
+ except Exception as e:
301
+ logger.error(f"An unexpected error occurred while loading default preset '{base_name}': {e}")
302
+ return {}
303
+ else:
304
+ # Handle User-Defined Presets
305
+ logger.info(f"Loading user preset: '{preset_name}'")
306
+ preset_path = self.USER_PRESETS_DIR / f"{preset_name}.json"
307
+
308
+ if not preset_path.is_file():
309
+ logger.warning(f"User preset '{preset_name}' not found at path: {preset_path}")
310
+ return {}
311
+
312
+ try:
313
+ with open(preset_path, "r", encoding="utf-8") as file:
314
+ return json.load(file)
315
+ except json.JSONDecodeError as e:
316
+ logger.error(f"Failed to parse user preset '{preset_name}.json'. Invalid JSON: {e}")
317
+ return {}
318
+ except Exception as e:
319
+ logger.error(f"An unexpected error occurred while loading user preset '{preset_name}': {e}")
320
+ return {}
321
+
322
+ def save_preset(self, preset_name: str, values_dict: dict) -> str:
323
+ """Saves a preset dictionary to the local user presets directory."""
324
+
325
+ if not preset_name or not preset_name.strip():
326
+ raise gr.Error("Preset name cannot be empty.")
327
+
328
+ if preset_name.startswith("Default: "):
329
+ raise gr.Error("Cannot overwrite default presets. Please choose a name without the 'Default:' prefix.")
330
+
331
+ status = f"Preset '{preset_name}' was saved!"
332
+ preset_path = self.USER_PRESETS_DIR / f"{preset_name}.json"
333
+ try:
334
+ with open(preset_path, "w", encoding="utf-8") as f:
335
+ json.dump(values_dict, f, indent=4)
336
+ self.get_preset_list()
337
+ except Exception as e:
338
+ logger.error(f"Error in save_preset: {e}")
339
+ traceback.print_exc()
340
+ status = f"Error saving preset {preset_name}"
341
+ return status
342
+
343
+ def get_model_list(self):
344
+ """Populates MODEL_CHOICES from pretrained list and local checkpoint directory."""
345
+ print("Loading model list...")
346
+ try:
347
+ self.PRETRAINED_MODELS_LIST = self.model_manager.filter_models_by_model_type(ModelType.Diffusers.value)
348
+ pretrained_names = [m["model_name"] for m in self.PRETRAINED_MODELS_LIST]
349
+ local_files = []
350
+ if self.settings.checkpoints_dir and os.path.isdir(self.settings.checkpoints_dir):
351
+ local_files = sorted([f for f in os.listdir(self.settings.checkpoints_dir) if f.endswith((".safetensors", ".ckpt"))])
352
+ self.MODEL_CHOICES = ["None"] + pretrained_names + local_files
353
+ except Exception as e:
354
+ logger.error(f"Error in get_model_list: {e}")
355
+ self.MODEL_CHOICES = ["Error: Check checkpoints_dir path in settings."]
356
+
357
+ def get_vae_model_list(self):
358
+ """Populates VAE_CHOICES from pretrained list and local VAE directory."""
359
+ print("Loading VAE list...")
360
+ try:
361
+ self.PRETRAINED_VAE_MODELS_LIST = self.model_manager.filter_models_by_model_type(ModelType.VAE.value)
362
+ pretrained_names = [m["model_name"] for m in self.PRETRAINED_VAE_MODELS_LIST]
363
+ local_files = []
364
+ if self.settings.vae_dir and os.path.isdir(self.settings.vae_dir):
365
+ local_files = sorted([f for f in os.listdir(self.settings.vae_dir) if f.endswith((".safetensors", ".pt"))])
366
+ self.VAE_CHOICES = ["Default"] + pretrained_names + local_files
367
+ except Exception as e:
368
+ logger.error(f"Error in get_vae_model_list: {e}")
369
+ self.VAE_CHOICES = ["Error: Check vae_dir path in settings."]
370
+
371
+ def inject_assets(self):
372
+ """
373
+ This function prepares the payload of CSS and JS code. It's called by the
374
+ app.load() event listener when the Gradio app starts.
375
+ """
376
+ # Inline code
377
+ css_code = ""
378
+ js_code = ""
379
+ popup_html = """
380
+ <div id="flyout_property_sheet_panel_target" class="flyout-container" style="display: none;">
381
+ </div>
382
+ <div id="flyout_restoration_mask_panel_target" class="flyout-container" style="display: none;">
383
+ </div>
384
+ """
385
+ # Read from files
386
+ try:
387
+ with open("ui/style.css", "r", encoding="utf-8") as f:
388
+ css_code += f.read() + "\n"
389
+ with open("ui/script.js", "r", encoding="utf-8") as f:
390
+ js_code += f.read() + "\n"
391
+ except FileNotFoundError as e:
392
+ print(f"Warning: Could not read asset file: {e}")
393
+
394
+ return {"js": js_code, "css": css_code, "body_html": popup_html}
395
+
396
+ def map_ui_supir_injection_to_pipeline_params(self, ui_config: SUPIRAdvanced_Config) -> tuple[Optional[InjectionConfigs], Optional[InjectionFlags]]:
397
+ """
398
+ Maps the configuration from the PropertySheet UI to the pipeline's
399
+ InjectionConfigs and InjectionFlags dataclasses by modifying their fields in place.
400
+
401
+ Args:
402
+ ui_config: An instance of SUPIRAdvanced_Config, as returned by the PropertySheet.
403
+
404
+ Returns:
405
+ A tuple containing populated instances of (InjectionConfigs, InjectionFlags),
406
+ or (None, None) if SFT settings are not applied.
407
+ """
408
+
409
+ # 1. Initialize the target dataclasses. Their nested fields are also initialized.
410
+ injection_configs = InjectionConfigs()
411
+ injection_flags = InjectionFlags()
412
+
413
+ # 2. Iterate through the fields of the source UI config (SUPIRAdvanced_Config).
414
+ for ui_field in fields(ui_config):
415
+ # Skip fields that are not part of the mapping logic
416
+ if not isinstance(getattr(ui_config, ui_field.name), SUPIRInjectionConfig):
417
+ continue
418
+
419
+ target_field_name = ui_field.name # e.g., "sft_post_mid"
420
+ source_injection_config: SUPIRInjectionConfig = getattr(ui_config, target_field_name)
421
+
422
+ # 3. Map the activation flag.
423
+ flag_name = f"{target_field_name}_active"
424
+ if hasattr(injection_flags, flag_name):
425
+ setattr(injection_flags, flag_name, source_injection_config.sft_active)
426
+
427
+ # 4. Map the scale configuration by modifying the existing nested object.
428
+ if hasattr(injection_configs, target_field_name):
429
+ # Get a direct reference to the nested dataclass instance
430
+ target_scale_config: InjectionScaleConfig = getattr(injection_configs, target_field_name)
431
+
432
+ # If custom scale is enabled, update the fields of the existing instance.
433
+ # Otherwise, it will keep its default values from initialization.
434
+ if source_injection_config.enable_custom_scale:
435
+ target_scale_config.scale_end = float(source_injection_config.scale_end)
436
+ target_scale_config.linear = source_injection_config.linear
437
+ target_scale_config.scale_start = float(source_injection_config.scale_start)
438
+ target_scale_config.reverse = source_injection_config.reverse
439
+ # No 'else' is needed, as the defaults are already set.
440
+
441
+ return injection_configs, injection_flags
442
+
443
+ def map_scheduler_settings_to_config(self, ui_settings: SchedulerSettings) -> SchedulerConfig:
444
+ """
445
+ Maps the UI-facing SchedulerSettings to the internal SchedulerConfig.
446
+
447
+ This function iterates through the fields of the source `ui_settings`
448
+ and transfers the values to a new `SchedulerConfig` instance if a field
449
+ with the same name exists in the destination. Fields present in
450
+ `SchedulerConfig` but not in `SchedulerSettings` will retain their
451
+ default values.
452
+
453
+ Args:
454
+ ui_settings: An instance of SchedulerSettings, as configured by the user.
455
+
456
+ Returns:
457
+ A populated instance of SchedulerConfig ready for use in the pipeline.
458
+ """
459
+ # 1. Initialize the target dataclass. This populates it with default values.
460
+ pipeline_config = SchedulerConfig()
461
+
462
+ # 2. Iterate through all fields defined in the source UI settings dataclass.
463
+ for source_field in fields(ui_settings):
464
+ field_name = source_field.name
465
+
466
+ # 3. Check if the destination dataclass has a field with the same name.
467
+ if hasattr(pipeline_config, field_name):
468
+ # 4. If it exists, get the value from the source and set it on the destination.
469
+ source_value = getattr(ui_settings, field_name)
470
+ setattr(pipeline_config, field_name, source_value)
471
+
472
+ return pipeline_config
473
+
474
+ def add_visibility_rules(self, rules_dict: Dict[str, Any], new_visibility_rules: List[Dict[str, bool]]) -> Dict[str, Any]:
475
+ """
476
+ Adds or updates unconditional visibility rules in the "on_load_actions" list.
477
+
478
+ This function takes a main UI rules dictionary and a list of new
479
+ visibility settings. It then iterates through the list and updates or adds
480
+ "update_visibility" actions to the `on_load_actions` section.
481
+
482
+ If an action for a specific field path already exists, its visibility
483
+ rule will be updated. If it doesn't exist, a new action will be appended.
484
+
485
+ Args:
486
+ rules_dict:
487
+ The main UI rules dictionary, expected to have keys like
488
+ `"dynamic_dependencies"` and `"on_load_actions"`.
489
+ new_visibility_rules:
490
+ A list of dictionaries, where each dictionary contains a single
491
+ key-value pair: the field path (e.g., "general.upscaling_mode")
492
+ and its desired visibility (True or False).
493
+
494
+ Returns:
495
+ The modified rules_dict with the new visibility rules applied.
496
+ """
497
+ # Safely get the 'on_load_actions' list.
498
+ # If it doesn't exist, create it as an empty list to avoid errors.
499
+ on_load_actions = rules_dict.setdefault("on_load_actions", [])
500
+
501
+ # Iterate through the list of new rules provided by the user.
502
+ for rule_item in new_visibility_rules:
503
+ # Each 'rule_item' is a dictionary like {"general.upscaling_mode": False}.
504
+ for field_path, is_visible in rule_item.items():
505
+ found_existing_rule = False
506
+ # Check if an action for this field_path already exists.
507
+ for action in on_load_actions:
508
+ if action.get("target_field_path") == field_path and action.get("type") == "update_visibility":
509
+ # If it exists, update its "visible" value.
510
+ action["visible"] = is_visible
511
+ found_existing_rule = True
512
+ break # Stop searching once found
513
+
514
+ # If no existing rule was found after checking the whole list...
515
+ if not found_existing_rule:
516
+ # append a new action dictionary in the correct format.
517
+ on_load_actions.append(
518
+ {
519
+ "type": "update_visibility",
520
+ "target_field_path": field_path,
521
+ "visible": is_visible,
522
+ }
523
+ )
524
+
525
+ return rules_dict
ui/ui_events.py ADDED
@@ -0,0 +1,1789 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The DEVAIEXP Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import dataclasses
16
+ import json
17
+ import logging
18
+ import math
19
+ import os
20
+ import queue
21
+ import random
22
+ import sys
23
+ import threading
24
+ from dataclasses import asdict, fields, is_dataclass
25
+ from pathlib import Path
26
+ from typing import Any, Union, cast
27
+
28
+ import gradio as gr
29
+ from gradio_imagemeta.helpers import extract_metadata, transfer_metadata
30
+ from gradio_livelog.utils import ProgressTracker, Tee, TqdmToQueueWriter, capture_logs
31
+ from gradio_propertysheet import PropertySheet
32
+ from gradio_propertysheet.helpers import flatten_dataclass_with_labels
33
+ from PIL import Image
34
+
35
+ from sup_toolbox.enums import (
36
+ ColorFix,
37
+ ImageSizeFixMode,
38
+ PromptMethod,
39
+ RestorerEngine,
40
+ Sampler,
41
+ StartPoint,
42
+ SUPIRModel,
43
+ UpscalerEngine,
44
+ UpscalingMode,
45
+ WeightingMethod,
46
+ )
47
+ from sup_toolbox.utils.system import infer_type
48
+ from ui.globals import pipeline_lock, sup_toolbox_pipe
49
+ from ui.ui_config import (
50
+ APPSETTINGS_SHEET_DEPENDENCY_RULES,
51
+ DEFAULT_PROMPTS,
52
+ RESTORER_CONFIG_MAPPING,
53
+ RESTORER_SHEET_DEPENDENCY_RULES,
54
+ SAMPLER_MAPPING,
55
+ SUPIR_ADVANCED_RULES,
56
+ UPSCALER_CONFIG_MAPPING,
57
+ UPSCALER_SHEET_DEPENDENCY_RULES,
58
+ ControlNetTile_Config,
59
+ FaithDiff_Config,
60
+ SUPIR_Config,
61
+ SUPIRAdvanced_Config,
62
+ )
63
+ from ui.ui_layout import UIComponents
64
+ from ui.ui_state import AppState
65
+ from ui.util.dataclass_helpers import (
66
+ apply_dynamic_changes,
67
+ dataclass_from_dict,
68
+ get_nested_attr,
69
+ )
70
+
71
+
72
+ class EventHandlers:
73
+ def __init__(self, state: AppState, components: UIComponents):
74
+ self.state = state
75
+ self.components = components
76
+
77
+ def update_pipeline(self, log_callback=None, progress_bar_handler=None):
78
+ """
79
+ Initializes or updates the shared SUPToolBoxPipeline instance in a thread-safe manner.
80
+
81
+ This method ensures that the module-level pipeline object (`sup_toolbox_pipe`)
82
+ is created and configured according to the current application state. It uses a
83
+ thread lock (`pipeline_lock`) to guarantee that the pipeline is initialized
84
+ only once, even if multiple threads or processes attempt to call this method
85
+ concurrently.
86
+
87
+ If a pipeline instance does not yet exist, it constructs a new `SUPToolBoxPipeline`
88
+ using the configuration from `self.state.uidata.config` and the provided callbacks.
89
+ If an instance already exists, it simply updates the instance's configuration and
90
+ callback attributes with the latest values from the application state.
91
+
92
+ Parameters
93
+ ----------
94
+ log_callback : callable, optional
95
+ A callback function used by the pipeline to emit log messages. Defaults to `None`.
96
+ progress_bar_handler : callable, optional
97
+ A handler for reporting progress, typically for UI progress bars. Defaults to `None`.
98
+
99
+ Accesses
100
+ --------
101
+ self.state.uidata.config : Config
102
+ The main configuration object used to initialize or update the pipeline.
103
+ self.state.cancel_event : threading.Event
104
+ The cancellation event object passed to the pipeline.
105
+
106
+ Side Effects
107
+ ------------
108
+ - Initializes the module-level `sup_toolbox_pipe` variable if it is `None`.
109
+ - Mutates the attributes (`config`, `log_callback`, etc.) of an existing
110
+ `sup_toolbox_pipe` instance.
111
+ - The import of `SUPToolBoxPipeline` is done locally within the function
112
+ to support the lazy-loading architecture.
113
+
114
+ Returns
115
+ -------
116
+ None
117
+ """
118
+ global sup_toolbox_pipe
119
+ from sup_toolbox.sup_toolbox_pipeline import (
120
+ SUPToolBoxPipeline,
121
+ )
122
+
123
+ with pipeline_lock:
124
+ if sup_toolbox_pipe is None:
125
+ sup_toolbox_pipe = SUPToolBoxPipeline(
126
+ self.state.uidata.config,
127
+ log_callback=log_callback,
128
+ progress_bar_handler=progress_bar_handler,
129
+ cancel_event=self.state.cancel_event,
130
+ )
131
+ else:
132
+ sup_toolbox_pipe.config = self.state.uidata.config
133
+ sup_toolbox_pipe.log_callback = log_callback
134
+ sup_toolbox_pipe.progress_bar_handler = progress_bar_handler
135
+ sup_toolbox_pipe.cancel_event = self.state.cancel_event
136
+
137
+ def get_supir_advanced_values(self):
138
+ """
139
+ Create and return a SUPIRAdvanced_Config instance with specific advanced flags disabled.
140
+
141
+ This function constructs a new SUPIRAdvanced_Config object initialized with its
142
+ default settings and then explicitly disables the "sft_active" flag for two
143
+ cross-up blocks used in stage 1:
144
+
145
+ - cross_up_block_0_stage1.sft_active is set to False
146
+ - cross_up_block_1_stage1.sft_active is set to False
147
+
148
+ Returns:
149
+ SUPIRAdvanced_Config: A configuration object reflecting the default advanced
150
+ settings with the two stage-1 cross-up block SFT flags disabled.
151
+
152
+ Notes:
153
+ - The function does not modify any global state; it returns a freshly
154
+ created configuration instance.
155
+ - It assumes SUPIRAdvanced_Config and its nested attributes
156
+ (cross_up_block_0_stage1, cross_up_block_1_stage1, and their sft_active
157
+ attributes) are defined and accessible.
158
+ - No parameters are required.
159
+
160
+ Example:
161
+ cfg = get_supir_advanced_values()
162
+ assert cfg.cross_up_block_0_stage1.sft_active is False
163
+ assert cfg.cross_up_block_1_stage1.sft_active is False
164
+ """
165
+ initial_supir_advanced_settings = SUPIRAdvanced_Config()
166
+ initial_supir_advanced_settings.cross_up_block_0_stage1.sft_active = False
167
+ initial_supir_advanced_settings.cross_up_block_1_stage1.sft_active = False
168
+ return initial_supir_advanced_settings
169
+
170
+ def calculate_effective_steps(
171
+ self,
172
+ config: Union[SUPIR_Config, FaithDiff_Config, ControlNetTile_Config],
173
+ is_upscaler: bool,
174
+ ) -> int:
175
+ """
176
+ Calculates the total effective number of inference steps for a given engine configuration.
177
+
178
+ For upscalers in 'Progressive' mode, it simulates the distributed decay of the 'strength'
179
+ parameter across multiple passes to provide an accurate total step count.
180
+
181
+ Args:
182
+ config: The configuration object for the engine.
183
+ is_upscaler: A boolean flag indicating if this config is for an upscaler.
184
+
185
+ Returns:
186
+ The total calculated effective number of steps for the configuration.
187
+ """
188
+ if not config or not hasattr(config, "general"):
189
+ return 0
190
+
191
+ num_images = getattr(config.general, "num_images", 1)
192
+ num_steps = getattr(config.general, "num_steps", 0)
193
+ initial_strength = getattr(config, "strength", 1.0)
194
+ total_steps_for_one_image = 0
195
+
196
+ # 1. Calculate steps for the first (or only) pass.
197
+ # The strength used is the initial strength.
198
+ first_pass_steps = min(int(num_steps * initial_strength), num_steps)
199
+ total_steps_for_one_image += first_pass_steps
200
+
201
+ # 2. If it's an upscaler in progressive mode, simulate subsequent passes with distributed decay.
202
+ is_progressive_mode = is_upscaler and hasattr(config.general, "upscaling_mode") and config.general.upscaling_mode == UpscalingMode.Progressive.value
203
+
204
+ if is_progressive_mode:
205
+ scale_factor_str = getattr(config.general, "upscale_factor", "1x")
206
+ # Ensure the string is not empty before trying to slice
207
+ if scale_factor_str:
208
+ try:
209
+ target_scale_factor = int(scale_factor_str[:-1])
210
+ except (ValueError, IndexError):
211
+ target_scale_factor = 1
212
+ else:
213
+ target_scale_factor = 1
214
+
215
+ # Only calculate decay if there are multiple passes
216
+ if target_scale_factor > 1: # Changed from > 2 to handle 2x upscale correctly
217
+ num_2x_passes = math.ceil(math.log2(target_scale_factor))
218
+
219
+ if num_2x_passes > 1:
220
+ strength_decay_rate = getattr(config.general, "strength_decay_rate", 0.5)
221
+
222
+ # Calculate the total amount of strength to be reduced over all passes
223
+ total_strength_decay = initial_strength * strength_decay_rate
224
+
225
+ # Distribute this total decay amount over the subsequent passes
226
+ num_decay_steps = num_2x_passes - 1
227
+ strength_decay_per_step = total_strength_decay / num_decay_steps if num_decay_steps > 0 else 0
228
+
229
+ current_strength = initial_strength
230
+
231
+ # Simulate the remaining passes (first pass is already counted)
232
+ for _ in range(num_decay_steps):
233
+ # Apply the linear decay step
234
+ current_strength -= strength_decay_per_step
235
+
236
+ # Apply the safety floor, same as in the pipeline
237
+ final_strength_for_pass = round(max(current_strength, 0.1), 2)
238
+
239
+ # Calculate steps for this pass and add to total
240
+ pass_steps = min(int(num_steps * final_strength_for_pass), num_steps)
241
+ total_steps_for_one_image += pass_steps
242
+
243
+ # 3. Multiply by the number of images to get the overall total
244
+ total_effective_steps = num_images * total_steps_for_one_image
245
+
246
+ return total_effective_steps
247
+
248
+ def prepare_engine_configs(
249
+ self,
250
+ restorer_engine: str,
251
+ restorer_config: dict,
252
+ upscaler_engine: str,
253
+ upscaler_config: dict,
254
+ ):
255
+ """
256
+ Prepares and casts the engine configuration objects based on the selected engine names.
257
+ This function is responsible only for creating the correct dataclass instances.
258
+
259
+ Args:
260
+ restorer_engine: The name of the selected restorer engine.
261
+ restorer_config: The raw configuration data for the restorer from the UI.
262
+ upscaler_engine: The name of the selected upscaler engine.
263
+ upscaler_config: The raw configuration data for the upscaler from the UI.
264
+
265
+ Returns:
266
+ A tuple containing the prepared restorer config and upscaler config objects,
267
+ which may be None if the corresponding engine is not selected.
268
+ """
269
+ res_config, ups_config = None, None
270
+ if restorer_engine in [
271
+ RestorerEngine.SUPIR.value,
272
+ RestorerEngine.FaithDiff.value,
273
+ ]:
274
+ res_config = cast(Union[SUPIR_Config, FaithDiff_Config], restorer_config)
275
+
276
+ if upscaler_engine in [
277
+ UpscalerEngine.SUPIR.value,
278
+ UpscalerEngine.FaithDiff.value,
279
+ UpscalerEngine.ControlNetTile.value,
280
+ ]:
281
+ if upscaler_engine == UpscalerEngine.ControlNetTile.value:
282
+ ups_config = cast(ControlNetTile_Config, upscaler_config)
283
+ else:
284
+ ups_config = cast(Union[SUPIR_Config, FaithDiff_Config], upscaler_config)
285
+
286
+ return res_config, ups_config
287
+
288
+ def calculate_total_steps(self, res_config, ups_config, res_engine_name, ups_engine_name):
289
+ """
290
+ Calculates the total effective inference steps based on the prepared engine configurations.
291
+
292
+ Args:
293
+ res_config: The prepared configuration object for the restorer.
294
+ ups_config: The prepared configuration object for the upscaler.
295
+ res_engine_name: The selected restorer engine name.
296
+ ups_engine_name: The selected upscaler engine name.
297
+
298
+ Returns:
299
+ The total number of effective inference steps.
300
+ """
301
+
302
+ total_inference_steps = 0
303
+ if res_config and res_engine_name != "None":
304
+ total_inference_steps += self.calculate_effective_steps(res_config, is_upscaler=False)
305
+
306
+ if ups_config and ups_engine_name != "None":
307
+ total_inference_steps += self.calculate_effective_steps(ups_config, is_upscaler=True)
308
+
309
+ return total_inference_steps
310
+
311
+ def generate_image_metadata(
312
+ self,
313
+ res_config_class,
314
+ ups_config_class,
315
+ res_supir_advanced_config_class,
316
+ ups_supir_advanced_config_class,
317
+ input_params,
318
+ res_sampler_config_class,
319
+ ups_sampler_config_class,
320
+ ):
321
+ """
322
+ Generates metadata by aggregating configuration values from various sources.
323
+ This function takes multiple configuration classes and input parameters, flattens them,
324
+ and combines them into a single dictionary with prefixed keys for metadata tracking.
325
+ Args:
326
+ res_config_class (dict): Configuration for the image restoration.
327
+ ups_config_class (dict): Configuration for the image upscaling.
328
+ res_supir_advanced_config_class (dict): Advanced SUPIR configuration for restoration.
329
+ ups_supir_advanced_config_class (dict): Advanced SUPIR configuration for upscaling.
330
+ input_params (dict): User input parameters including engine selections.
331
+ res_sampler_config_class (dict): Sampler configuration for restoration.
332
+ ups_sampler_config_class (dict): Sampler configuration for upscaling.
333
+ Returns:
334
+ Dict[str, Any]: A flattened dictionary containing all configuration values with
335
+ prefixed keys that identify their source and purpose. For example:
336
+ {
337
+ "Restorer - Engine1 - param1": value1,
338
+ "Upscaler - Engine2 - param2": value2,
339
+ ...
340
+ }
341
+ Notes:
342
+ - Keys are prefixed based on their source (Restorer/Upscaler) and engine type
343
+ - Processing only occurs if the respective engine is selected and not "none"
344
+ - Input parameters are preserved in the output dictionary
345
+ """
346
+ res_engine, ups_engine = (
347
+ input_params["Image Restore Engine"],
348
+ input_params["Image Upscale Engine"],
349
+ )
350
+
351
+ all_values = input_params.copy()
352
+
353
+ def process_and_prefix(instance: Any, prefix: str):
354
+ """Helper to flatten a dataclass and add a final prefix to its keys."""
355
+ if instance:
356
+ for key, value in flatten_dataclass_with_labels(instance).items():
357
+ all_values[f"{prefix} - {key}"] = value
358
+
359
+ if res_engine and res_engine.lower() != "none":
360
+ process_and_prefix(res_config_class, f"Restorer - {res_engine}")
361
+ process_and_prefix(res_supir_advanced_config_class, "Restorer")
362
+ process_and_prefix(res_sampler_config_class, "Restorer - Sampler")
363
+
364
+ if ups_engine and ups_engine.lower() != "none":
365
+ process_and_prefix(ups_config_class, f"Upscaler - {ups_engine}")
366
+ process_and_prefix(ups_supir_advanced_config_class, "Upscaler")
367
+ process_and_prefix(ups_sampler_config_class, "Upscaler - Sampler")
368
+
369
+ return all_values
370
+
371
+ def update_preset_list(self, preset):
372
+ """
373
+ Updates the preset list in the UI dropdown with the latest presets and sets a specific value.
374
+
375
+ Args:
376
+ preset (str): The preset value to be selected in the dropdown after updating the list
377
+
378
+ Returns:
379
+ gr.update: A Gradio update object containing the new preset choices and selected value
380
+ """
381
+ self.state.uidata.get_preset_list()
382
+ return gr.update(choices=self.state.uidata.PRESETS_LIST, value=preset)
383
+
384
+ def save_preset(self, preset_name: str, *all_component_values):
385
+ """
386
+ Parameters:
387
+ preset_name (str): The name of the preset to be saved. Must not be empty or a default preset name.
388
+ *all_component_values: A variable number of values representing the current state of UI components.
389
+ Raises:
390
+ gr.Error: If the preset name is empty or if an attempt is made to overwrite a default preset.
391
+ Notes:
392
+ - Maps the received values back to the components using the order of the input list passed to the .click() event.
393
+ - It is safer to map by elem_id.
394
+ - Removes 'restorer_supir_advanced_settings' if the 'restorer_engine' is not set to SUPIR.
395
+ - Removes 'upscaler_supir_advanced_settings' if the 'upscaler_engine' is not set to SUPIR.
396
+ - For PropertySheets, if the value is a dataclass, it is converted to a dictionary before saving.
397
+ """
398
+ if not preset_name or not preset_name.strip():
399
+ raise gr.Error("Please enter a preset name.")
400
+
401
+ if preset_name in self.state.uidata.PRESETS_LIST and "Default:" in preset_name:
402
+ raise gr.Error("A default preset cannot be overwritten; please set a different name.")
403
+
404
+ preset_data = {}
405
+ ALL_UI_COMPONENTS = self.components.ALL_UI_COMPONENTS
406
+ component_values = dict(zip(ALL_UI_COMPONENTS.keys(), all_component_values))
407
+ component_values["restorer_sampler_settings"] = SAMPLER_MAPPING["restorer_sampler"]
408
+ component_values["upscaler_sampler_settings"] = SAMPLER_MAPPING["upscaler_sampler"]
409
+
410
+ if component_values.get("restorer_engine") != RestorerEngine.SUPIR.value:
411
+ component_values.pop("restorer_supir_advanced_settings", None)
412
+
413
+ if component_values.get("upscaler_engine") != RestorerEngine.SUPIR.value:
414
+ component_values.pop("upscaler_supir_advanced_settings", None)
415
+
416
+ for elem_id, value in component_values.items():
417
+ if dataclasses.is_dataclass(value):
418
+ preset_data[elem_id] = asdict(value)
419
+ else:
420
+ preset_data[elem_id] = value
421
+
422
+ self.state.uidata.save_preset(preset_name.strip(), preset_data)
423
+ gr.Info(f"Preset '{preset_name}' saved successfully!")
424
+
425
+ def load_preset(self, preset_name: str):
426
+ """
427
+ Load Presets to UI components. It also updates the backend state, such as
428
+ SAMPLER_MAPPING, based on the loaded preset.
429
+ Parameters:
430
+ preset_name (str): The name of the preset to load. Must be a non-empty string.
431
+ Returns:
432
+ List[gr.Update]: A list of updates for the UI components, where each update
433
+ corresponds to the state of a component after loading the preset.
434
+ Raises:
435
+ gr.Error: If no preset is selected, if the preset is empty or cannot be found,
436
+ or if there is an error during the loading process.
437
+ The function handles different types of UI components, including PropertySheets,
438
+ and updates their values based on the data retrieved from the preset. It also
439
+ ensures that the backend state is synchronized with the loaded preset data.
440
+ """
441
+
442
+ ALL_UI_COMPONENTS = self.components.ALL_UI_COMPONENTS
443
+ # Prepare a default output list. `gr.skip()` means "do not change this component".
444
+ output_updates = [gr.skip()] * len(ALL_UI_COMPONENTS)
445
+ if not preset_name or not preset_name.strip():
446
+ raise gr.Error("No preset selected to load.")
447
+
448
+ try:
449
+ # Load the preset data from the JSON file
450
+ preset_data = self.state.uidata.load_preset(preset_name.strip())
451
+ if not preset_data:
452
+ raise gr.Error(f"Preset '{preset_name}' is empty or could not be found.")
453
+
454
+ # Create a mapping of components to their indices in the output list for easy access
455
+ component_to_index = {id(comp): i for i, comp in enumerate(ALL_UI_COMPONENTS.values())}
456
+
457
+ for elem_id, component in ALL_UI_COMPONENTS.items():
458
+ # Check if there is a value for this component in the preset
459
+ if elem_id in preset_data:
460
+ value_from_preset = preset_data[elem_id]
461
+ output_index = component_to_index.get(id(component))
462
+ if output_index is None:
463
+ continue
464
+
465
+ # If the component is a PropertySheet, reconstruct the dataclass instance
466
+ if isinstance(component, PropertySheet):
467
+ dc_type = type(getattr(component, "_dataclass_value", None))
468
+
469
+ # If the component is a PropertySheet, reconstruct the dataclass instance
470
+ if dc_type and is_dataclass(dc_type) and isinstance(value_from_preset, dict):
471
+ if component.elem_id == "restorer_settings" and output_updates[0]["value"] == RestorerEngine.SUPIR.value:
472
+ dc_type = type(RESTORER_CONFIG_MAPPING[RestorerEngine.SUPIR.value])
473
+ instance = dataclass_from_dict(dc_type, value_from_preset)
474
+ instance = apply_dynamic_changes(instance, RESTORER_SHEET_DEPENDENCY_RULES)
475
+ RESTORER_CONFIG_MAPPING[RestorerEngine.SUPIR.value] = instance
476
+ elif component.elem_id == "restorer_supir_advanced_settings" and output_updates[0]["value"] == RestorerEngine.SUPIR.value:
477
+ dc_type = type(RESTORER_CONFIG_MAPPING["SUPIRAdvanced"])
478
+ instance = dataclass_from_dict(dc_type, value_from_preset)
479
+ instance = apply_dynamic_changes(instance, SUPIR_ADVANCED_RULES)
480
+ RESTORER_CONFIG_MAPPING["SUPIRAdvanced"] = instance
481
+ elif component.elem_id == "restorer_settings" and output_updates[0]["value"] == RestorerEngine.FaithDiff.value:
482
+ dc_type = type(RESTORER_CONFIG_MAPPING[RestorerEngine.FaithDiff.value])
483
+ instance = dataclass_from_dict(dc_type, value_from_preset)
484
+ instance = apply_dynamic_changes(instance, RESTORER_SHEET_DEPENDENCY_RULES)
485
+ RESTORER_CONFIG_MAPPING[RestorerEngine.FaithDiff.value] = instance
486
+
487
+ if component.elem_id == "upscaler_settings" and output_updates[1]["value"] == UpscalerEngine.SUPIR.value:
488
+ dc_type = type(UPSCALER_CONFIG_MAPPING[UpscalerEngine.SUPIR.value])
489
+ instance = dataclass_from_dict(dc_type, value_from_preset)
490
+ UPSCALER_CONFIG_MAPPING[UpscalerEngine.SUPIR.value] = instance
491
+ elif component.elem_id == "upscaler_supir_advanced_settings" and output_updates[1]["value"] == UpscalerEngine.SUPIR.value:
492
+ dc_type = type(UPSCALER_CONFIG_MAPPING["SUPIRAdvanced"])
493
+ instance = dataclass_from_dict(dc_type, value_from_preset)
494
+ instance = apply_dynamic_changes(instance, SUPIR_ADVANCED_RULES)
495
+ UPSCALER_CONFIG_MAPPING["SUPIRAdvanced"] = instance
496
+ elif component.elem_id == "upscaler_settings" and output_updates[1]["value"] == UpscalerEngine.FaithDiff.value:
497
+ dc_type = type(UPSCALER_CONFIG_MAPPING[UpscalerEngine.FaithDiff.value])
498
+ instance = dataclass_from_dict(dc_type, value_from_preset)
499
+ UPSCALER_CONFIG_MAPPING[UpscalerEngine.FaithDiff.value] = instance
500
+ elif component.elem_id == "upscaler_settings" and output_updates[1]["value"] == UpscalerEngine.ControlNetTile.value:
501
+ dc_type = type(UPSCALER_CONFIG_MAPPING[UpscalerEngine.ControlNetTile.value])
502
+ instance = dataclass_from_dict(dc_type, value_from_preset)
503
+ UPSCALER_CONFIG_MAPPING[UpscalerEngine.ControlNetTile.value] = instance
504
+
505
+ if instance is None:
506
+ instance = dataclass_from_dict(dc_type, value_from_preset)
507
+
508
+ output_updates[output_index] = gr.update(value=instance)
509
+ # For all other standard Gradio components
510
+ else:
511
+ output_updates[output_index] = gr.update(value=value_from_preset)
512
+
513
+ # Update the backend state (SAMPLER_MAPPING)
514
+ # Iterate over the special keys you saved for the samplers
515
+ for sampler_key in [
516
+ "restorer_sampler_settings",
517
+ "upscaler_sampler_settings",
518
+ ]:
519
+ if sampler_key in preset_data:
520
+ sampler_data = preset_data[sampler_key]
521
+ # The key in SAMPLER_MAPPING is the elem_id (e.g., "restorer_sampler_settings")
522
+ target_instance = SAMPLER_MAPPING.get(sampler_key.rpartition("_")[0])
523
+
524
+ if target_instance and is_dataclass(target_instance) and isinstance(sampler_data, dict):
525
+ # Populate the existing instance in SAMPLER_MAPPING with the preset data
526
+ # This is similar to how on_flyout_change works
527
+ for field_name, value in sampler_data.items():
528
+ if hasattr(target_instance, field_name):
529
+ setattr(target_instance, field_name, value)
530
+
531
+ gr.Info(f"Preset '{preset_name}' loaded successfully.")
532
+ return output_updates
533
+ except Exception as e:
534
+ raise gr.Error(f"Failed to load or apply preset '{preset_name}': {e}")
535
+
536
+ def restart(self):
537
+ """Triggers a restart of the Python script."""
538
+ print("Please wait. The UI is being restarted...")
539
+ os.execv(sys.executable, [os.path.basename(sys.executable)] + sys.argv)
540
+
541
+ # region Flyout Event Function Logic
542
+ def on_handle_flyout_toggle(self, is_vis, current_anchor, *, clicked_elem_id, target_elem_id):
543
+ """
544
+ Manages the visibility and content of a flyout panel based on user interaction.
545
+
546
+ This function determines whether to show, hide, or update a flyout panel.
547
+ - If the clicked element is already the active anchor, it hides the flyout.
548
+ - Otherwise, it shows the flyout, positioning it relative to the clicked element,
549
+ and populates it with the correct settings from `SAMPLER_MAPPING`.
550
+
551
+ Args:
552
+ is_vis (bool): The current visibility state of the flyout.
553
+ current_anchor (str): The elem_id of the current element the flyout is anchored to.
554
+ clicked_elem_id (str): The elem_id of the element that was just clicked.
555
+ target_elem_id (str): The elem_id of the flyout panel to control.
556
+
557
+ Returns:
558
+ Tuple[bool, Optional[str], gr.update, gr.update]: A tuple of updates for:
559
+ - flyout_visible (gr.State)
560
+ - active_anchor_id (gr.State)
561
+ - flyout_sheet (PropertySheet content)
562
+ - js_data_bridge (JSON data for the frontend)
563
+ """
564
+ settings_obj = SAMPLER_MAPPING.get(clicked_elem_id)
565
+
566
+ if settings_obj is None: # not a propertysheet
567
+ # Command JS to show and position
568
+ js_data = json.dumps(
569
+ {
570
+ "isVisible": True,
571
+ "anchorId": clicked_elem_id,
572
+ "targetId": target_elem_id,
573
+ }
574
+ )
575
+ return True, clicked_elem_id, gr.skip(), gr.update(value=js_data)
576
+
577
+ if is_vis and current_anchor == clicked_elem_id:
578
+ # Command JS to hide
579
+ js_data = json.dumps({"isVisible": False, "anchorId": None, "targetId": target_elem_id})
580
+ return False, None, gr.update(), gr.update(value=js_data)
581
+ else:
582
+ # Command JS to show and position
583
+ js_data = json.dumps(
584
+ {
585
+ "isVisible": True,
586
+ "anchorId": clicked_elem_id,
587
+ "targetId": target_elem_id,
588
+ }
589
+ )
590
+ return (
591
+ True,
592
+ clicked_elem_id,
593
+ gr.update(value=settings_obj),
594
+ gr.update(value=js_data),
595
+ )
596
+
597
+ def on_update_ear_visibility(self, elem_id: str):
598
+ """
599
+ Controls the visibility of an 'ear' button next to a sampler dropdown.
600
+
601
+ The button is made visible only if the selected sampler has advanced settings
602
+ defined in the global `SAMPLER_MAPPING`.
603
+
604
+ Args:
605
+ elem_id (str): The elem_id of the sampler dropdown component.
606
+
607
+ Returns:
608
+ gr.update: A Gradio update object to set the visibility of the ear button.
609
+ """
610
+ has_settings = elem_id in SAMPLER_MAPPING
611
+ return gr.update(visible=has_settings)
612
+
613
+ def on_flyout_change(self, updated_settings, active_id):
614
+ """
615
+ Callback for when the flyout PropertySheet's value changes.
616
+
617
+ It updates the corresponding sampler settings object in the global
618
+ `SAMPLER_MAPPING` dictionary with the new values from the flyout.
619
+
620
+ Args:
621
+ updated_settings (dataclass): The new settings object from the PropertySheet.
622
+ active_id (str): The elem_id of the component that triggered the flyout,
623
+ used as a key in `SAMPLER_MAPPING`.
624
+ """
625
+ if updated_settings is None or active_id is None:
626
+ return
627
+
628
+ if active_id in SAMPLER_MAPPING:
629
+ original_settings_obj = SAMPLER_MAPPING[active_id]
630
+ for f in dataclasses.fields(original_settings_obj):
631
+ if hasattr(updated_settings, f.name):
632
+ setattr(original_settings_obj, f.name, getattr(updated_settings, f.name))
633
+
634
+ def on_close_the_flyout(self, target_elem_id):
635
+ """
636
+ Closes the flyout panel.
637
+
638
+ This function prepares the necessary state and JS data to command the frontend
639
+ to hide the flyout panel.
640
+
641
+ Args:
642
+ target_elem_id (str): The elem_id of the flyout panel to close.
643
+
644
+ Returns:
645
+ Tuple[bool, None, gr.update]: Updates for flyout visibility state,
646
+ active anchor ID, and the JS data bridge.
647
+ """
648
+ js_data = json.dumps({"isVisible": False, "anchorId": None, "targetId": target_elem_id})
649
+ return False, None, gr.update(value=js_data)
650
+
651
+ def initial_flyout_setup(self):
652
+ """
653
+ Sets the initial visibility for all ear buttons on application load.
654
+
655
+ Returns:
656
+ Dict[gr.Button, gr.update]: A dictionary mapping each ear button
657
+ component to its visibility update.
658
+ """
659
+ return {
660
+ "restorer_sampler_ear_btn": self.on_update_ear_visibility("restorer_sampler"),
661
+ "upscaler_sampler_ear_btn": self.on_update_ear_visibility("upscaler_sampler"),
662
+ }
663
+
664
+ # endregion
665
+
666
+ # region Tokenizer Event Function Logic
667
+ def update_positive_tokenizer(self, p1, p2):
668
+ """
669
+ Combines two positive prompt textboxes into a single string for the tokenizer.
670
+
671
+ Args:
672
+ p1 (str): Content of the first prompt textbox.
673
+ p2 (str): Content of the second prompt textbox.
674
+
675
+ Returns:
676
+ gr.update: An update for the TokenizerTextBox with the combined prompt.
677
+ """
678
+ return gr.update(value=f"{p1}\n{p2}".strip())
679
+
680
+ def update_positive_prompt_helper(self, evt: gr.EventData):
681
+ """
682
+ Updates the target textbox for the positive TagGroupHelper.
683
+
684
+ This is triggered on focus for a prompt textbox, ensuring that when a tag
685
+ is clicked in the helper, it's inserted into the currently active prompt box.
686
+
687
+ Args:
688
+ evt (gr.EventData): Event data from Gradio, containing the target component.
689
+
690
+ Returns:
691
+ gr.update: An update for the TagGroupHelper to set its target textbox ID.
692
+ """
693
+ return gr.update(target_textbox_id=evt.target.elem_id)
694
+
695
+ def update_prompt_helper_from_tab(self, evt: gr.EventData):
696
+ """
697
+ Updates the target textboxes for both TagGroupHelpers when a tab is selected.
698
+
699
+ This ensures the helpers target the correct prompt and negative prompt
700
+ textboxes based on whether the 'Restoration' or 'Upscaling' tab is active.
701
+
702
+ Args:
703
+ evt (gr.EventData): Event data from Gradio, containing the selected tab.
704
+
705
+ Returns:
706
+ Dict[TagGroupHelper, gr.update]: A dictionary of updates for both the
707
+ positive and negative tag helpers.
708
+ """
709
+ if evt.target.elem_id == "res-tab":
710
+ return (
711
+ gr.update(target_textbox_id="restorer_prompt_1"),
712
+ gr.update(target_textbox_id="restorer_negative_prompt"),
713
+ )
714
+ else:
715
+ return (
716
+ gr.update(target_textbox_id="upscaler_prompt_1"),
717
+ gr.update(target_textbox_id="upscaler_negative_prompt"),
718
+ )
719
+
720
+ # endregion
721
+
722
+ # region Others UI Event Function Logic
723
+ def on_reset_settings(self):
724
+ """
725
+ Handles the 'reset settings' button click. Loads default
726
+ settings via `uidata` and displays an info message to the user before
727
+ the UI is restarted.
728
+ """
729
+ self.state.uidata.load_defaults()
730
+ gr.Info("Defaults loaded! UI will be restarted!")
731
+
732
+ def on_save_settings(self, settings_dict):
733
+ """
734
+ Handles the 'save settings' button click. Converts the settings
735
+ dataclass to a dictionary and saves it using `uidata`. Displays a
736
+ confirmation message before the UI is restarted.
737
+
738
+ Args:
739
+ settings_dict (AppSettings): The settings dataclass instance from the PropertySheet.
740
+ """
741
+ values_dict = asdict(settings_dict)
742
+ self.state.uidata.save_settings(values_dict)
743
+ gr.Info("Settings saved! UI will be restarted!")
744
+
745
+ def on_settings_sheet_change(self, updated_settings: Any):
746
+ """
747
+ Handles changes in the AppSettings PropertySheet.
748
+
749
+ It applies dynamic visibility rules to the settings sheet based on the
750
+ current values (e.g., hiding `quantization_mode` if `quantization_method`
751
+ is 'None').
752
+
753
+ Args:
754
+ updated_settings (Any): The updated AppSettings dataclass instance
755
+ from the PropertySheet.
756
+
757
+ Returns:
758
+ AppSettings: The modified AppSettings instance with dynamic rules applied.
759
+ """
760
+ if updated_settings is None:
761
+ return updated_settings
762
+
763
+ rules_to_apply = []
764
+
765
+ if updated_settings.quantization_method == "None":
766
+ rules_to_apply.append({"quantization_mode": False})
767
+ else:
768
+ rules_to_apply.append({"quantization_mode": True})
769
+
770
+ self.state.uidata.add_visibility_rules(APPSETTINGS_SHEET_DEPENDENCY_RULES, rules_to_apply)
771
+ return apply_dynamic_changes(updated_settings, APPSETTINGS_SHEET_DEPENDENCY_RULES)
772
+
773
+ def on_set_default_prompts(
774
+ self,
775
+ res_engine_name,
776
+ ups_engine_name,
777
+ res_prompt_value,
778
+ res_prompt_2_value,
779
+ res_negative_prompt_value,
780
+ ups_prompt_value,
781
+ ups_prompt_2_value,
782
+ ups_negative_prompt_value,
783
+ ):
784
+ """
785
+ Sets default prompts in the UI when an engine is selected or changed.
786
+
787
+ This function checks if the current prompt fields are empty or if the engine
788
+ has changed. If so, it populates the respective prompt fields with
789
+ pre-defined default values for the newly selected engine.
790
+
791
+ Args:
792
+ res_engine_name (str): The selected restorer engine name.
793
+ ups_engine_name (str): The selected upscaler engine name.
794
+ res_prompt_value (str): Current value of the restorer's prompt 1.
795
+ res_prompt_2_value (str): Current value of the restorer's prompt 2.
796
+ res_negative_prompt_value (str): Current value of the restorer's negative prompt.
797
+ ups_prompt_value (str): Current value of the upscaler's prompt 1.
798
+ ups_prompt_2_value (str): Current value of the upscaler's prompt 2.
799
+ ups_negative_prompt_value (str): Current value of the upscaler's negative prompt.
800
+
801
+ Returns:
802
+ Tuple[str, str, str, str, str, str]: A tuple containing the new values
803
+ for all six prompt textboxes.
804
+ """
805
+
806
+ restorer_engine_selected = self.state.restorer_engine_selected
807
+ upscaler_engine_selected = self.state.upscaler_engine_selected
808
+
809
+ # Set defaults for restorer prompts only if input is empty/None
810
+ if res_engine_name == "None":
811
+ res_prompt = ""
812
+ elif not res_prompt_value or (restorer_engine_selected != res_engine_name):
813
+ res_prompt = DEFAULT_PROMPTS["Restorer"][res_engine_name]["prompt"]
814
+ else:
815
+ res_prompt = res_prompt_value
816
+
817
+ if res_engine_name == "None":
818
+ res_prompt_2 = ""
819
+ elif not res_prompt_2_value or (restorer_engine_selected != res_engine_name):
820
+ res_prompt_2 = DEFAULT_PROMPTS["Restorer"][res_engine_name]["prompt_2"]
821
+ else:
822
+ res_prompt_2 = res_prompt_2_value
823
+
824
+ if res_engine_name == "None":
825
+ res_negative = ""
826
+ elif not res_negative_prompt_value or (restorer_engine_selected != res_engine_name):
827
+ res_negative = DEFAULT_PROMPTS["Restorer"][res_engine_name]["negative_prompt"]
828
+ else:
829
+ res_negative = res_negative_prompt_value
830
+
831
+ # Set defaults for upscaler prompts only if input is empty/None
832
+ if ups_engine_name == "None":
833
+ ups_prompt = ""
834
+ elif not ups_prompt_value or (upscaler_engine_selected != ups_engine_name):
835
+ ups_prompt = DEFAULT_PROMPTS["Upscaler"][ups_engine_name]["prompt"]
836
+ else:
837
+ ups_prompt = ups_prompt_value
838
+
839
+ if ups_engine_name == "None":
840
+ ups_prompt_2 = ""
841
+ elif not ups_prompt_2_value or (upscaler_engine_selected != ups_engine_name):
842
+ ups_prompt_2 = DEFAULT_PROMPTS["Upscaler"][ups_engine_name]["prompt_2"]
843
+ else:
844
+ ups_prompt_2 = ups_prompt_2_value
845
+
846
+ if ups_engine_name == "None":
847
+ ups_negative = ""
848
+ elif not ups_negative_prompt_value or (upscaler_engine_selected != ups_engine_name):
849
+ ups_negative = DEFAULT_PROMPTS["Upscaler"][ups_engine_name]["negative_prompt"]
850
+ else:
851
+ ups_negative = ups_negative_prompt_value
852
+
853
+ self.state.restorer_engine_selected = res_engine_name
854
+ self.state.upscaler_engine_selected = ups_engine_name
855
+
856
+ return (
857
+ res_prompt,
858
+ res_prompt_2,
859
+ res_negative,
860
+ ups_prompt,
861
+ ups_prompt_2,
862
+ ups_negative,
863
+ )
864
+
865
+ def _update_sheet_changes(self, updated_config, mode="Restorer"):
866
+ """
867
+ Internal helper to apply dynamic changes to a PropertySheet's dataclass.
868
+
869
+ This function applies visibility rules and handles seed randomization logic
870
+ common to both the restorer and upscaler PropertySheets.
871
+
872
+ Args:
873
+ updated_config (dataclass): The configuration dataclass instance to update.
874
+ mode (str): Either "Restorer" or "Upscaler" to apply the correct rules.
875
+
876
+ Returns:
877
+ dataclass: The modified configuration dataclass.
878
+ """
879
+
880
+ if mode == "Restorer":
881
+ updated_config = apply_dynamic_changes(updated_config, RESTORER_SHEET_DEPENDENCY_RULES)
882
+ previous_randomize_state = get_nested_attr(self.state.restorer_config_class, "general.randomize_seed")
883
+ else:
884
+ rules_to_apply = []
885
+ if updated_config.general.upscaling_mode == "Direct":
886
+ rules_to_apply.extend(
887
+ [
888
+ {"general.cfg_decay_rate": False},
889
+ {"general.strength_decay_rate": False},
890
+ ]
891
+ )
892
+ else:
893
+ rules_to_apply.extend(
894
+ [
895
+ {"general.cfg_decay_rate": True},
896
+ {"general.strength_decay_rate": True},
897
+ ]
898
+ )
899
+ self.state.uidata.add_visibility_rules(UPSCALER_SHEET_DEPENDENCY_RULES, rules_to_apply)
900
+ updated_config = apply_dynamic_changes(updated_config, UPSCALER_SHEET_DEPENDENCY_RULES)
901
+ previous_randomize_state = get_nested_attr(self.state.upscaler_config_class, "general.randomize_seed")
902
+
903
+ should_generate_new_seed = (updated_config.general.randomize_seed and not previous_randomize_state) or (
904
+ updated_config.general.randomize_seed and updated_config.general.seed == -1
905
+ )
906
+ if should_generate_new_seed:
907
+ updated_config.general.seed = random.randint(0, self.state.uidata.MAX_SEED)
908
+
909
+ return updated_config
910
+
911
+ def on_restore_engine_change(self, restorer_engine_name: str, upscaler_engine_name: str):
912
+ """
913
+ Handles UI changes when the restorer engine dropdown is modified.
914
+
915
+ This updates the visibility of the restoration tab, loads the correct
916
+ configuration object into the `restorer_sheet` PropertySheet, and shows/hides
917
+ the advanced SUPIR settings sheet accordingly.
918
+
919
+ Args:
920
+ restorer_engine_name (str): The newly selected restorer engine.
921
+ upscaler_engine_name (str): The current upscaler engine.
922
+
923
+ Returns:
924
+ Tuple[gr.update, gr.update, gr.update, gr.update, gr.update]: A tuple of
925
+ updates for the restoration tab, advanced settings, main settings sheet,
926
+ engine configuration accordion, and the config tabs selector.
927
+ """
928
+ is_restorer_active = restorer_engine_name != "None" and restorer_engine_name in RESTORER_CONFIG_MAPPING
929
+ is_upscaler_active = upscaler_engine_name != "None" and upscaler_engine_name in UPSCALER_CONFIG_MAPPING
930
+ is_supir = restorer_engine_name == "SUPIR"
931
+
932
+ if is_restorer_active:
933
+ config_class = RESTORER_CONFIG_MAPPING.get(restorer_engine_name, SUPIR_Config)
934
+ if is_supir:
935
+ self.state.restorer_supir_advanced_config_class = self.get_supir_advanced_values()
936
+ self.state.restorer_config_class = self._update_sheet_changes(config_class, "Restorer")
937
+ else:
938
+ self.state.restorer_config_class = None
939
+ if is_supir:
940
+ self.state.restorer_supir_advanced_config_class = None
941
+
942
+ ec_visible = is_restorer_active or is_upscaler_active
943
+ selected_tab = 1 if is_upscaler_active and not is_restorer_active else 0
944
+ return (
945
+ gr.update(visible=is_restorer_active),
946
+ gr.update(value=self.state.restorer_supir_advanced_config_class, visible=is_supir),
947
+ gr.update(
948
+ value=self.state.restorer_config_class,
949
+ label=f"{restorer_engine_name} Settings",
950
+ ),
951
+ gr.update(visible=ec_visible),
952
+ gr.update(selected=selected_tab),
953
+ )
954
+
955
+ def on_upscaler_engine_change(self, restorer_engine_name: str, upscaler_engine_name: str):
956
+ """
957
+ Handles UI changes when the upscaler engine dropdown is modified.
958
+
959
+ This updates the visibility of the upscaling tab, loads the correct
960
+ configuration object into the `upscaler_sheet` PropertySheet, and manages
961
+ the visibility of related UI elements like advanced SUPIR settings.
962
+
963
+ Args:
964
+ restorer_engine_name (str): The current restorer engine.
965
+ upscaler_engine_name (str): The newly selected upscaler engine.
966
+
967
+ Returns:
968
+ Tuple[gr.update, ...]: A tuple of updates for the upscaling tab,
969
+ advanced settings, main settings sheet, engine accordion, config tabs,
970
+ and the prompt method dropdown.
971
+ """
972
+
973
+ is_restorer_active = restorer_engine_name != "None" and restorer_engine_name in RESTORER_CONFIG_MAPPING
974
+ is_upscaler_active = upscaler_engine_name != "None" and upscaler_engine_name in UPSCALER_CONFIG_MAPPING
975
+ is_supir = upscaler_engine_name == "SUPIR"
976
+ is_controlnettile = upscaler_engine_name == "ControlNetTile"
977
+
978
+ if is_upscaler_active:
979
+ config_class = UPSCALER_CONFIG_MAPPING.get(upscaler_engine_name, ControlNetTile_Config)
980
+ if is_supir:
981
+ self.state.upscaler_supir_advanced_config_class = self.get_supir_advanced_values()
982
+ self.state.upscaler_config_class = self._update_sheet_changes(config_class, "Upscaler")
983
+ else:
984
+ self.state.upscaler_config_class = None
985
+ if is_supir:
986
+ self.state.upscaler_supir_advanced_config_class = None
987
+
988
+ ec_visible = is_restorer_active or is_upscaler_active
989
+ selected_tab = 1 if is_upscaler_active and not is_restorer_active else 0
990
+ return (
991
+ gr.update(visible=is_upscaler_active),
992
+ gr.update(value=self.state.upscaler_supir_advanced_config_class, visible=is_supir),
993
+ gr.update(
994
+ value=self.state.upscaler_config_class,
995
+ label=f"{upscaler_engine_name} Settings",
996
+ ),
997
+ gr.update(visible=ec_visible),
998
+ gr.update(selected=selected_tab),
999
+ gr.update(visible=not is_controlnettile),
1000
+ )
1001
+
1002
+ def on_restorer_sheet_change(self, updated_config: Union[SUPIR_Config, FaithDiff_Config, None]):
1003
+ """
1004
+ Handles changes from the restorer configuration PropertySheet.
1005
+
1006
+ It calls the internal `_update_sheet_changes` helper to apply dynamic rules
1007
+ and manage seed randomization, then updates the global state.
1008
+
1009
+ Args:
1010
+ updated_config (dataclass | None): The new configuration from the sheet.
1011
+
1012
+ Returns:
1013
+ dataclass: The updated and processed configuration dataclass.
1014
+ """
1015
+ if updated_config is None:
1016
+ return self.state.restorer_config_class
1017
+
1018
+ self.state.restorer_config_class = self._update_sheet_changes(updated_config, "Restorer")
1019
+
1020
+ return self.state.restorer_config_class
1021
+
1022
+ def on_restorer_supir_advanced_sheet_change(self, updated_config: SUPIRAdvanced_Config | None):
1023
+ """
1024
+ Handles changes from the restorer supir advanced configuration PropertySheet.
1025
+
1026
+ It calls the internal `apply_dynamic_changes` helper to apply dynamic rules
1027
+ and manage seed randomization, then updates the global state.
1028
+
1029
+ Args:
1030
+ updated_config (dataclass | None): The new configuration from the sheet.
1031
+
1032
+ Returns:
1033
+ dataclass: The updated and processed configuration dataclass.
1034
+ """
1035
+ if updated_config is None:
1036
+ return self.state.restorer_supir_advanced_config_class
1037
+
1038
+ self.state.restorer_supir_advanced_config_class = apply_dynamic_changes(updated_config, SUPIR_ADVANCED_RULES)
1039
+
1040
+ return self.state.restorer_supir_advanced_config_class
1041
+
1042
+ def on_upscaler_sheet_change(
1043
+ self,
1044
+ updated_config: Union[SUPIR_Config, ControlNetTile_Config, FaithDiff_Config, None],
1045
+ ):
1046
+ """
1047
+ Handles changes from the upscaler configuration PropertySheet.
1048
+
1049
+ It calls the internal `_update_sheet_changes` helper to apply dynamic rules
1050
+ (like for progressive upscaling) and manage seed randomization, then
1051
+ updates the global state.
1052
+
1053
+ Args:
1054
+ updated_config (dataclass | None): The new configuration from the sheet.
1055
+
1056
+ Returns:
1057
+ dataclass: The updated and processed configuration dataclass.
1058
+ """
1059
+ if updated_config is None:
1060
+ return self.state.upscaler_config_class
1061
+
1062
+ self.state.upscaler_config_class = self._update_sheet_changes(updated_config, "Upscaler")
1063
+
1064
+ return self.state.upscaler_config_class
1065
+
1066
+ def on_upscaler_supir_advanced_sheet_change(self, updated_config: SUPIRAdvanced_Config | None):
1067
+ """
1068
+ Handles changes from the upscaler supir advanced configuration PropertySheet.
1069
+
1070
+ It calls the internal `apply_dynamic_changes` helper to apply dynamic rules
1071
+ and manage seed randomization, then updates the global state.
1072
+
1073
+ Args:
1074
+ updated_config (dataclass | None): The new configuration from the sheet.
1075
+
1076
+ Returns:
1077
+ dataclass: The updated and processed configuration dataclass.
1078
+ """
1079
+ if updated_config is None:
1080
+ return self.state.upscaler_supir_advanced_config_class
1081
+
1082
+ self.state.upscaler_supir_advanced_config_class = apply_dynamic_changes(updated_config, SUPIR_ADVANCED_RULES)
1083
+
1084
+ return self.state.upscaler_supir_advanced_config_class
1085
+
1086
+ def on_settings_tab_select(self):
1087
+ """
1088
+ Callback triggered when the 'Settings' tab is selected.
1089
+
1090
+ Ensures the settings sheet is correctly rendered with any dynamic rules applied.
1091
+
1092
+ Returns:
1093
+ AppSettings: The processed AppSettings dataclass to be rendered.
1094
+ """
1095
+ return self.on_settings_sheet_change(self.state.uidata.settings)
1096
+
1097
+ def on_cancel_click(self):
1098
+ """
1099
+ Handles the 'Cancel' button click by setting a global threading event.
1100
+ The running pipeline periodically checks this event and will stop execution
1101
+ if it is set.
1102
+ """
1103
+ self.state.cancel_event.set()
1104
+
1105
+ def load_metadata(self, metadata: dict):
1106
+ """
1107
+ Loads settings from an image's metadata dictionary into the UI components.
1108
+
1109
+ This function acts as a bridge between the raw metadata and the UI. It defines
1110
+ mappings for complex components like PropertySheets and standard Gradio
1111
+ components, then uses a helper (`transfer_metadata`) to apply the values.
1112
+
1113
+ Args:
1114
+ metadata (dict): A dictionary of key-value pairs extracted from image metadata.
1115
+
1116
+ Returns:
1117
+ List[Any]: A list of values in the correct order to update the UI
1118
+ output components.
1119
+
1120
+ Raises:
1121
+ gr.Error: If metadata conversion for a sampler setting fails.
1122
+ """
1123
+ # Get UI components
1124
+ ui_inputs, output_fields = self.components._get_ui_inputs_and_outputs()
1125
+ ui_inputs_for_metadata = ui_inputs.copy()
1126
+ restorer_sheet = self.components.restorer_sheet
1127
+ upscaler_sheet = self.components.upscaler_sheet
1128
+ restorer_sheet_supir_advanced = self.components.restorer_sheet_supir_advanced
1129
+ upscaler_sheet_supir_advanced = self.components.upscaler_sheet_supir_advanced
1130
+
1131
+ # Define the map that tells the helper how to process each PropertySheet.
1132
+ # This is the "glue" between your generic helper and your specific app.
1133
+ source_restorer_engine = RestorerEngine.from_str(metadata["Image Restore Engine"])
1134
+ source_upscaler_engine = UpscalerEngine.from_str(metadata["Image Upscale Engine"])
1135
+ source_restorer_class = RESTORER_CONFIG_MAPPING.get(source_restorer_engine.value)
1136
+ source_upscaler_class = UPSCALER_CONFIG_MAPPING.get(source_upscaler_engine.value)
1137
+ sheet_map = {}
1138
+ if source_restorer_class:
1139
+ sheet_map[id(restorer_sheet)] = {
1140
+ "type": source_restorer_class.__class__,
1141
+ "prefixes": ["Restorer", "Image Restore Engine"],
1142
+ }
1143
+
1144
+ if source_restorer_engine == RestorerEngine.SUPIR:
1145
+ sheet_map[id(restorer_sheet_supir_advanced)] = {
1146
+ "type": restorer_sheet_supir_advanced._dataclass_type,
1147
+ "prefixes": ["Restorer"],
1148
+ }
1149
+
1150
+ if source_upscaler_class:
1151
+ sheet_map[id(upscaler_sheet)] = {
1152
+ "type": source_upscaler_class.__class__,
1153
+ "prefixes": ["Upscaler", "Image Upscale Engine"],
1154
+ }
1155
+ if source_upscaler_engine == UpscalerEngine.SUPIR:
1156
+ sheet_map[id(upscaler_sheet_supir_advanced)] = {
1157
+ "type": upscaler_sheet_supir_advanced._dataclass_type,
1158
+ "prefixes": ["Upscaler"],
1159
+ }
1160
+
1161
+ gradio_map = {id(component): label for label, component in ui_inputs_for_metadata.items()}
1162
+
1163
+ output_values = transfer_metadata(
1164
+ output_fields=output_fields,
1165
+ metadata=metadata,
1166
+ propertysheet_map=sheet_map,
1167
+ gradio_component_map=gradio_map,
1168
+ )
1169
+
1170
+ sampler_map_data = {
1171
+ "restorer_sampler_settings": {
1172
+ "prefix": "Restorer - Sampler",
1173
+ "instance": SAMPLER_MAPPING.get("restorer_sampler"),
1174
+ },
1175
+ "upscaler_sampler_settings": {
1176
+ "prefix": "Upscaler - Sampler",
1177
+ "instance": SAMPLER_MAPPING.get("upscaler_sampler"),
1178
+ },
1179
+ }
1180
+
1181
+ for _, data in sampler_map_data.items():
1182
+ sampler_instance, prefix = data["instance"], data["prefix"]
1183
+ if not (sampler_instance and is_dataclass(sampler_instance)):
1184
+ continue
1185
+
1186
+ for field in fields(sampler_instance):
1187
+ label = field.metadata.get("label", field.name.replace("_", " ").title())
1188
+ metadata_key = f"{prefix} - {label}"
1189
+
1190
+ if metadata_key in metadata:
1191
+ try:
1192
+ setattr(
1193
+ sampler_instance,
1194
+ field.name,
1195
+ infer_type(metadata[metadata_key]),
1196
+ )
1197
+ except (ValueError, TypeError):
1198
+ print(f"Warning: Could not convert metadata value '{metadata[metadata_key]}' for sampler field '{field.name}'.")
1199
+ raise gr.Error("Error loading Image metadata, see console log.")
1200
+
1201
+ return output_values
1202
+
1203
+ def on_load_metadata_from_gallery(self, folder_explorer, image_data: gr.EventData):
1204
+ """
1205
+ Callback to load metadata from an image selected in the 'Generated' gallery.
1206
+
1207
+ It extracts metadata from the selected image, calls the `load_metadata`
1208
+ helper to process it, and applies the final settings to the UI components.
1209
+ It also handles path corrections for example images.
1210
+
1211
+ Args:
1212
+ folder_explorer (Any): The current value of the folder explorer component.
1213
+ image_data (gr.EventData): Event data for the selected image, containing metadata.
1214
+
1215
+ Returns:
1216
+ List[Any]: A list of values to update the UI components with the loaded settings.
1217
+ """
1218
+ # Get output_fields
1219
+ _, output_fields = self.components._get_ui_inputs_and_outputs()
1220
+
1221
+ gallery_path = Path(folder_explorer)
1222
+ is_example = all(part in gallery_path.parts for part in ["outputs", "examples"])
1223
+
1224
+ # Initial checks for valid input
1225
+ if not image_data or not hasattr(image_data, "_data"):
1226
+ return [gr.skip()] * len(output_fields)
1227
+
1228
+ metadata = image_data._data
1229
+ output_values = self.load_metadata(metadata)
1230
+ if output_values[0]:
1231
+ self.state.restorer_config_class = self._update_sheet_changes(output_values[0], "Restorer")
1232
+ RESTORER_CONFIG_MAPPING[metadata["Image Restore Engine"]] = self.state.restorer_config_class
1233
+ if output_values[2]:
1234
+ self.state.upscaler_config_class = self._update_sheet_changes(output_values[2], "Upscaler")
1235
+ UPSCALER_CONFIG_MAPPING[metadata["Image Upscale Engine"]] = self.state.upscaler_config_class
1236
+
1237
+ output_values[0], output_values[2] = (
1238
+ self.state.restorer_config_class,
1239
+ self.state.upscaler_config_class,
1240
+ )
1241
+ input_image_name = output_values[11]
1242
+ output_values[11] = os.path.join("assets/samples", input_image_name) if is_example and input_image_name else None
1243
+
1244
+ gr.Info("Image metadata loaded.")
1245
+ return output_values
1246
+
1247
+ def on_input_image_change(self, input_image):
1248
+ self.state.input_image_path = getattr(input_image, "path", None)
1249
+
1250
+ def on_load_metadata_from_single_image(self, image_data):
1251
+ """
1252
+ Callback to load metadata from the main input image component.
1253
+
1254
+ Triggered when an image with embedded metadata is uploaded. It extracts the
1255
+ metadata and calls the `load_metadata` helper to apply it to the UI.
1256
+
1257
+ Args:
1258
+ image_data (Image.Image | None): The image object from the ImageMeta component.
1259
+
1260
+ Returns:
1261
+ List[Any]: A list of values/updates for the UI components.
1262
+ """
1263
+ # Get output_fields
1264
+ _, output_fields = self.components._get_ui_inputs_and_outputs()
1265
+
1266
+ # Initial checks for valid input
1267
+ if not image_data or not hasattr(image_data, "path"):
1268
+ return [gr.skip()] * len(output_fields)
1269
+
1270
+ # Extract the flat metadata dictionary from the image
1271
+ metadata = extract_metadata(image_data, only_custom_metadata=True)
1272
+ if not metadata:
1273
+ return [gr.skip()] * len(output_fields)
1274
+
1275
+ output_values = self.load_metadata(metadata)
1276
+ if output_values[0]:
1277
+ self.state.restorer_config_class = self._update_sheet_changes(output_values[0], "Restorer")
1278
+ RESTORER_CONFIG_MAPPING[metadata["Image Restore Engine"]] = self.state.restorer_config_class
1279
+ if output_values[2]:
1280
+ self.state.upscaler_config_class = self._update_sheet_changes(output_values[2], "Upscaler")
1281
+ UPSCALER_CONFIG_MAPPING[metadata["Image Upscale Engine"]] = self.state.upscaler_config_class
1282
+
1283
+ output_values[0], output_values[2] = (
1284
+ self.state.restorer_config_class,
1285
+ self.state.upscaler_config_class,
1286
+ )
1287
+ output_values[11] = None
1288
+
1289
+ gr.Info("Image metadata loaded.")
1290
+ return output_values
1291
+
1292
+ def on_clear_log_output(self):
1293
+ """
1294
+ Clears the content of the LiveLog viewer component.
1295
+
1296
+ Returns:
1297
+ None: Sets the value of the LiveLog component to None, clearing it.
1298
+ """
1299
+ return None
1300
+
1301
+ def on_check_inputs(
1302
+ self,
1303
+ restorer_engine,
1304
+ upscaler_engine,
1305
+ restorer_model_name,
1306
+ upscaler_model_name,
1307
+ action="generation_process",
1308
+ ):
1309
+ """
1310
+ Validates the core inputs before starting a process like generation or masking.
1311
+
1312
+ Raises a gr.Error with a user-friendly message if validation fails.
1313
+ Also configures the bottom bar and logger for the upcoming process.
1314
+
1315
+ Args:
1316
+ restorer_engine (str): The selected restorer engine.
1317
+ upscaler_engine (str): The selected upscaler engine.
1318
+ restorer_model_name (str): The selected restorer model.
1319
+ upscaler_model_name (str): The selected upscaler model.
1320
+ action (str): The type of action being initiated, used to tailor checks.
1321
+
1322
+ Returns:
1323
+ Tuple[gr.update, gr.update]: Updates to open the bottom bar and configure
1324
+ the LiveLog display mode.
1325
+ """
1326
+ if self.state.input_image_path is None:
1327
+ raise gr.Error("Input image is required. Please upload an image to proceed.")
1328
+
1329
+ if action == "generation_process":
1330
+ if restorer_engine == "None" and upscaler_engine == "None":
1331
+ raise gr.Error("Please select at least a Restorer or an Upscaler engine.")
1332
+ if restorer_engine != "None" and restorer_model_name == "None":
1333
+ raise gr.Error("Please select a Restorer model when using the Restore engine.")
1334
+ if upscaler_engine != "None" and upscaler_model_name == "None":
1335
+ raise gr.Error("Please select an Upscaler model when using the Upscale engine.")
1336
+
1337
+ return gr.update(open=True), gr.update(display_mode="full" if action == "generation_process" else "log")
1338
+
1339
+ def on_refresh_restoration_mask(self, mask_prompt, **kwargs):
1340
+ """
1341
+ Generates a face restoration mask based on a text prompt.
1342
+
1343
+ This function is decorated with `@livelog` to stream log outputs to the UI.
1344
+ It initializes the pipeline and calls the `generate_prompt_mask` method.
1345
+
1346
+ Args:
1347
+ mask_prompt (str): The prompt used to identify the area to mask (e.g., "head").
1348
+ lq_image_path (Any): The file object for the low-quality input image.
1349
+ **kwargs: Injected by the `@livelog` decorator (e.g., `log_callback`).
1350
+
1351
+ Returns:
1352
+ PIL.Image: The generated mask image.
1353
+ """
1354
+ log_callback = kwargs.get("log_callback")
1355
+ logger = logging.getLogger(kwargs.get("log_name", "suptoolbox_app"))
1356
+ log_callback(log_content="Starting masking generation...")
1357
+
1358
+ if self.state.input_image_path is None:
1359
+ raise gr.Error("Input image must be provided!")
1360
+
1361
+ lq_image = Image.open(self.state.input_image_path).convert("RGB")
1362
+ self.state.cancel_event.clear()
1363
+ self.update_pipeline(log_callback=log_callback)
1364
+
1365
+ try:
1366
+ mask, _ = sup_toolbox_pipe.generate_prompt_mask(lq_image, mask_prompt)
1367
+ if mask is None:
1368
+ gr.Warning("Mask couldn't be generated!")
1369
+ return mask
1370
+ except Exception as e:
1371
+ logger.error(f"Error in generation object mask: {e}, process aborted!", exc_info=True)
1372
+ raise e
1373
+
1374
+ def on_generate_caption(self, **kwargs):
1375
+ """
1376
+ Generates a descriptive caption for the input image.
1377
+
1378
+ Decorated with `@livelog` to stream logs. It initializes the pipeline
1379
+ and uses the `generate_caption` method.
1380
+
1381
+ Args:
1382
+ lq_image_path (Any): The file object for the low-quality input image.
1383
+ **kwargs: Injected by the `@livelog` decorator.
1384
+
1385
+ Returns:
1386
+ str: The generated image caption.
1387
+ """
1388
+ log_callback = kwargs.get("log_callback")
1389
+ logger = logging.getLogger(kwargs.get("log_name", "suptoolbox_app"))
1390
+ log_callback(log_content="Starting caption generation...")
1391
+
1392
+ if self.state.input_image_path is None:
1393
+ raise gr.Error("Input image must be provided!")
1394
+
1395
+ lq_image = Image.open(self.state.input_image_path).convert("RGB")
1396
+ self.state.cancel_event.clear()
1397
+ self.update_pipeline(log_callback=log_callback)
1398
+
1399
+ try:
1400
+ caption = sup_toolbox_pipe.generate_caption(lq_image)
1401
+ if caption is None:
1402
+ gr.Warning("Caption couldn't be generated!")
1403
+ return caption
1404
+ except Exception as e:
1405
+ logger.error(f"Error in caption generation: {e}, process aborted!", exc_info=True)
1406
+ raise e
1407
+
1408
+ def on_generate(self, *args):
1409
+ """
1410
+ Starts the image processing thread and yields updates from a queue.
1411
+ This method is the entry point for the Gradio event chain.
1412
+ """
1413
+
1414
+ # 1. Send initial UI state update (disable buttons, show progress).
1415
+ yield None, None, gr.update(interactive=False), gr.update(visible=True), gr.update(open=True)
1416
+ update_queue = queue.Queue()
1417
+
1418
+ # 2. Package arguments for the worker thread.
1419
+ thread_args = (update_queue, *args)
1420
+
1421
+ # 3. Start the image processing worker thread.
1422
+ diffusion_thread = threading.Thread(target=self._process_image, args=thread_args)
1423
+ diffusion_thread.start()
1424
+
1425
+ # 4. Loop to read from the queue and yield updates to the UI.
1426
+ final_images, log_update = None, None
1427
+ while True:
1428
+ update = update_queue.get()
1429
+ if update is None: # Sentinel value indicating the thread has finished.
1430
+ break
1431
+
1432
+ images, log_update = update
1433
+
1434
+ if images:
1435
+ final_images = images
1436
+
1437
+ # Yield the update to the Gradio outputs.
1438
+ yield final_images, log_update, gr.skip(), gr.skip(), gr.skip()
1439
+
1440
+ # 5. Send final UI state update (re-enable buttons).
1441
+ yield final_images, log_update, gr.update(interactive=True), gr.update(visible=False), gr.skip()
1442
+
1443
+ def _process_image(self, update_queue: queue.Queue, total_steps: int, *input_params: Any, **kwargs):
1444
+ """
1445
+ Executes the main image processing pipeline in a worker thread.
1446
+
1447
+ This method receives simple, serializable data types from the UI event.
1448
+ It retrieves complex configuration objects (like PropertySheet values)
1449
+ directly from the shared application state (`self.state`), which are kept
1450
+ up-to-date by their respective `.change()` events.
1451
+
1452
+ Args:
1453
+ update_queue (queue.Queue): The queue for sending status updates back to the UI.
1454
+ total_steps (int): The pre-calculated total number of diffusion steps.
1455
+ *input_params (Any): A tuple of the remaining simple UI input values
1456
+ (e.g., engine selections, prompts), passed positionally.
1457
+ **kwargs: Catches any extra keyword arguments.
1458
+
1459
+ Side Effects:
1460
+ - Puts multiple update dictionaries and a final `None` sentinel onto the `update_queue`.
1461
+ - Modifies and uses the shared `sup_toolbox_pipe` instance via `self.update_pipeline`.
1462
+ - Catches all exceptions and reports them via the queue.
1463
+ """
1464
+ from sup_toolbox.sup_toolbox_pipeline import PipelineCancelationRequested
1465
+
1466
+ # 1. Prepare input params
1467
+ restorer_config = self.state.restorer_config_class
1468
+ upscaler_config = self.state.upscaler_config_class
1469
+ restorer_supir_advanced_config = self.state.restorer_supir_advanced_config_class
1470
+ upscaler_supir_advanced_config = self.state.upscaler_supir_advanced_config_class
1471
+ ui_inputs, _ = self.components._get_ui_inputs_and_outputs()
1472
+ ui_inputs_keys_sliced = list(ui_inputs.keys())[4:]
1473
+ input_param_with_values = dict(zip(ui_inputs_keys_sliced, input_params))
1474
+ input_image, res_engine, ups_engine = (
1475
+ self.state.input_image_path,
1476
+ input_param_with_values.get("Image Restore Engine"),
1477
+ input_param_with_values.get("Image Upscale Engine"),
1478
+ )
1479
+
1480
+ if hasattr(input_image, "orig_name"):
1481
+ input_param_with_values["Input Image"] = input_image.orig_name
1482
+ else:
1483
+ input_param_with_values.pop("Input Image", None)
1484
+
1485
+ if input_image is None:
1486
+ update_queue.put((None, {"logs": [{"type": "log", "level": "ERROR", "content": "Error: Input image is required."}]}))
1487
+ update_queue.put(None)
1488
+ return
1489
+
1490
+ # 2. Generate image metadata
1491
+ image_metadata = self.generate_image_metadata(
1492
+ restorer_config,
1493
+ upscaler_config,
1494
+ (restorer_supir_advanced_config if res_engine == RestorerEngine.SUPIR.value else None),
1495
+ (upscaler_supir_advanced_config if ups_engine == UpscalerEngine.SUPIR.value else None),
1496
+ input_param_with_values,
1497
+ SAMPLER_MAPPING["restorer_sampler"],
1498
+ SAMPLER_MAPPING["upscaler_sampler"],
1499
+ )
1500
+
1501
+ tracker = None
1502
+ self.state.cancel_event.clear()
1503
+
1504
+ with capture_logs(log_level=logging.INFO, log_name=["suptoolbox_app", "suptoolbox"]) as get_logs:
1505
+ try:
1506
+ rate_queue = queue.Queue()
1507
+ tqdm_writer = TqdmToQueueWriter(rate_queue)
1508
+ progress_bar_handler = Tee(sys.stderr, tqdm_writer)
1509
+ all_logs, last_known_rate_data = [], None
1510
+
1511
+ # 2. Prepare selected engines
1512
+ config = self.state.uidata.config
1513
+
1514
+ _restorer_config, _upscaler_config = self.prepare_engine_configs(res_engine, restorer_config, ups_engine, upscaler_config)
1515
+
1516
+ # 3. Define update callback
1517
+ def process_and_send_updates(status="running", advance=0, final_image_payload=None):
1518
+ nonlocal all_logs, last_known_rate_data
1519
+ new_rate_data = None
1520
+ while not rate_queue.empty():
1521
+ try:
1522
+ new_rate_data = rate_queue.get_nowait()
1523
+ except queue.Empty:
1524
+ break
1525
+
1526
+ if new_rate_data:
1527
+ last_known_rate_data = new_rate_data
1528
+
1529
+ new_records = get_logs()
1530
+ if new_records:
1531
+ new_logs = [
1532
+ {"type": "log", "level": "SUCCESS" if r.levelno == logging.INFO + 5 else r.levelname, "content": r.getMessage()}
1533
+ for r in new_records
1534
+ ]
1535
+ all_logs.extend(new_logs)
1536
+
1537
+ update_dict = (
1538
+ tracker.update(advance=advance, status=status, logs=all_logs, rate_data=last_known_rate_data)
1539
+ if tracker
1540
+ else {"type": "progress", "logs": all_logs, "current": 0, "total": total_steps, "desc": "Diffusion Steps"}
1541
+ )
1542
+ update_queue.put((final_image_payload, update_dict))
1543
+
1544
+ logger = logging.getLogger("suptoolbox_app")
1545
+ logger.info("Starting diffusion process...")
1546
+ process_and_send_updates()
1547
+
1548
+ # 4. Create tracker
1549
+ tracker = ProgressTracker(total=total_steps, description="Diffusion Steps", rate_unit="s/it")
1550
+
1551
+ # 5. Define pipeline progress callback
1552
+ def progress_callback(_, __, ___, callback_kwargs):
1553
+ process_and_send_updates(advance=callback_kwargs["advance"])
1554
+ return callback_kwargs
1555
+
1556
+ # 6. Map all pipeline configuration from UI to SUP-Toolbox Pipeline
1557
+ config = self.state.uidata.config
1558
+ config.restorer_engine, config.upscaler_engine = (
1559
+ RestorerEngine.from_str(res_engine),
1560
+ UpscalerEngine.from_str(ups_engine),
1561
+ )
1562
+ config.selected_vae_model = input_param_with_values["VAE Model"]
1563
+ if res_engine == RestorerEngine.SUPIR.value:
1564
+ _restorer_config = cast(SUPIR_Config, _restorer_config)
1565
+ config.selected_restorer_checkpoint_model = input_param_with_values["Image Restore Model"]
1566
+ config.restorer_engine = RestorerEngine.SUPIR
1567
+ config.selected_restorer_sampler = Sampler.from_str(input_param_with_values["Image Restore Sampler"])
1568
+ config.restorer_pipeline_params.supir_model = SUPIRModel.from_str(_restorer_config.supir_model)
1569
+ config.restorer_pipeline_params.seed = _restorer_config.general.seed
1570
+ config.restorer_pipeline_params.upscale_factor = _restorer_config.general.upscale_factor
1571
+ config.restorer_pipeline_params.prompt = input_param_with_values["Image Restore - Prompt 1"]
1572
+ config.restorer_pipeline_params.prompt_2 = input_param_with_values["Image Restore - Prompt 2"]
1573
+ config.restorer_pipeline_params.negative_prompt = input_param_with_values["Image Restore - Negative prompt"]
1574
+ config.restore_face = input_param_with_values["Enable Face Restoration"]
1575
+ config.mask_prompt = input_param_with_values["Mask Prompt"]
1576
+ config.restorer_pipeline_params.num_images = _restorer_config.general.num_images
1577
+ config.restorer_pipeline_params.num_steps = _restorer_config.general.num_steps
1578
+ config.restorer_pipeline_params.use_lpw_prompt = (
1579
+ True if input_param_with_values["Image Restore - Prompt method"] == PromptMethod.Weighted.value else False
1580
+ )
1581
+ config.restorer_pipeline_params.tile_size = _restorer_config.general.tile_size
1582
+ config.restorer_pipeline_params.restoration_scale = float(_restorer_config.restoration_scale)
1583
+ config.restorer_pipeline_params.s_churn = float(_restorer_config.s_churn)
1584
+ config.restorer_pipeline_params.s_noise = float(_restorer_config.s_noise)
1585
+ config.restorer_pipeline_params.strength = float(_restorer_config.strength)
1586
+ config.restorer_pipeline_params.use_linear_CFG = _restorer_config.cfg_settings.use_linear_CFG
1587
+ config.restorer_pipeline_params.guidance_scale = float(_restorer_config.general.guidance_scale)
1588
+ config.restorer_pipeline_params.guidance_rescale = float(_restorer_config.general.guidance_rescale)
1589
+ config.restorer_pipeline_params.reverse_linear_CFG = float(_restorer_config.cfg_settings.reverse_linear_CFG)
1590
+ config.restorer_pipeline_params.guidance_scale_start = float(_restorer_config.cfg_settings.guidance_scale_start)
1591
+ config.restorer_pipeline_params.use_linear_control_scale = _restorer_config.controlnet_settings.use_linear_control_scale
1592
+ config.restorer_pipeline_params.reverse_linear_control_scale = _restorer_config.controlnet_settings.reverse_linear_control_scale
1593
+ config.restorer_pipeline_params.controlnet_conditioning_scale = float(_restorer_config.controlnet_settings.controlnet_conditioning_scale)
1594
+ config.restorer_pipeline_params.control_scale_start = float(_restorer_config.controlnet_settings.control_scale_start)
1595
+ config.restorer_pipeline_params.enable_PAG = _restorer_config.pag_settings.enable_PAG and len(_restorer_config.pag_settings.pag_layers) > 0
1596
+ config.restorer_pipeline_params.use_linear_PAG = _restorer_config.pag_settings.use_linear_PAG
1597
+ config.restorer_pipeline_params.reverse_linear_PAG = _restorer_config.pag_settings.reverse_linear_PAG
1598
+ config.restorer_pipeline_params.pag_scale = float(_restorer_config.pag_settings.pag_scale)
1599
+ config.restorer_pipeline_params.pag_scale_start = float(_restorer_config.pag_settings.pag_scale_start)
1600
+ config.restorer_pipeline_params.pag_layers = _restorer_config.pag_settings.pag_layers
1601
+ config.restorer_pipeline_params.start_point = StartPoint.from_str(_restorer_config.start_point)
1602
+ config.restorer_pipeline_params.image_size_fix_mode = ImageSizeFixMode.from_str(_restorer_config.general.image_size_fix_mode)
1603
+ config.restorer_pipeline_params.color_fix_mode = ColorFix.from_str(_restorer_config.post_processsing_settings.color_fix_mode)
1604
+ (
1605
+ config.restorer_pipeline_params.zero_sft_injection_configs,
1606
+ config.restorer_pipeline_params.zero_sft_injection_flags,
1607
+ ) = self.state.uidata.map_ui_supir_injection_to_pipeline_params(restorer_supir_advanced_config)
1608
+ config.restorer_pipeline_params.callback_on_step_end = progress_callback
1609
+ config.restorer_sampler_config = self.state.uidata.map_scheduler_settings_to_config(SAMPLER_MAPPING["restorer_sampler"])
1610
+ elif res_engine == RestorerEngine.FaithDiff.value:
1611
+ _restorer_config = cast(FaithDiff_Config, _restorer_config)
1612
+ config.selected_restorer_checkpoint_model = input_param_with_values["Image Restore Model"]
1613
+ config.restorer_engine = RestorerEngine.FaithDiff
1614
+ config.selected_restorer_sampler = Sampler.from_str(input_param_with_values["Image Restore Sampler"])
1615
+ config.restorer_pipeline_params.seed = _restorer_config.general.seed
1616
+ config.restorer_pipeline_params.upscale_factor = _restorer_config.general.upscale_factor
1617
+ config.restorer_pipeline_params.prompt = input_param_with_values["Image Restore - Prompt 1"]
1618
+ config.restorer_pipeline_params.prompt_2 = input_param_with_values["Image Restore - Prompt 2"]
1619
+ config.restorer_pipeline_params.negative_prompt = input_param_with_values["Image Restore - Negative prompt"]
1620
+ config.restore_face = input_param_with_values["Enable Face Restoration"]
1621
+ config.mask_prompt = input_param_with_values["Mask Prompt"]
1622
+ config.restorer_pipeline_params.num_images = _restorer_config.general.num_images
1623
+ config.restorer_pipeline_params.num_steps = _restorer_config.general.num_steps
1624
+ config.restorer_pipeline_params.use_lpw_prompt = (
1625
+ True if input_param_with_values["Image Restore - Prompt method"] == PromptMethod.Weighted.value else False
1626
+ )
1627
+ config.restorer_pipeline_params.tile_size = _restorer_config.general.tile_size
1628
+ config.restorer_pipeline_params.s_churn = float(_restorer_config.s_churn)
1629
+ config.restorer_pipeline_params.s_noise = float(_restorer_config.s_noise)
1630
+ config.restorer_pipeline_params.strength = float(_restorer_config.strength)
1631
+ config.restorer_pipeline_params.guidance_scale = float(_restorer_config.general.guidance_scale)
1632
+ config.restorer_pipeline_params.guidance_rescale = float(_restorer_config.general.guidance_rescale)
1633
+ config.restorer_pipeline_params.use_linear_control_scale = _restorer_config.controlnet_settings.use_linear_control_scale
1634
+ config.restorer_pipeline_params.reverse_linear_control_scale = _restorer_config.controlnet_settings.reverse_linear_control_scale
1635
+ config.restorer_pipeline_params.controlnet_conditioning_scale = float(_restorer_config.controlnet_settings.controlnet_conditioning_scale)
1636
+ config.restorer_pipeline_params.control_scale_start = float(_restorer_config.controlnet_settings.control_scale_start)
1637
+ config.restorer_pipeline_params.enable_PAG = _restorer_config.pag_settings.enable_PAG and len(_restorer_config.pag_settings.pag_layers) > 0
1638
+ config.restorer_pipeline_params.use_linear_PAG = _restorer_config.pag_settings.use_linear_PAG
1639
+ config.restorer_pipeline_params.reverse_linear_PAG = _restorer_config.pag_settings.reverse_linear_PAG
1640
+ config.restorer_pipeline_params.pag_scale = float(_restorer_config.pag_settings.pag_scale)
1641
+ config.restorer_pipeline_params.pag_scale_start = float(_restorer_config.pag_settings.pag_scale_start)
1642
+ config.restorer_pipeline_params.pag_layers = _restorer_config.pag_settings.pag_layers
1643
+ config.restorer_pipeline_params.start_point = StartPoint.from_str(_restorer_config.start_point)
1644
+ config.restorer_pipeline_params.image_size_fix_mode = ImageSizeFixMode.from_str(_restorer_config.general.image_size_fix_mode)
1645
+ config.restorer_pipeline_params.color_fix_mode = ColorFix.from_str(_restorer_config.post_processsing_settings.color_fix_mode)
1646
+ config.restorer_pipeline_params.invert_prompts = _restorer_config.invert_prompts
1647
+ config.restorer_pipeline_params.apply_ipa_embeds = _restorer_config.apply_ipa_embeds
1648
+ config.restorer_pipeline_params.callback_on_step_end = progress_callback
1649
+ config.restorer_sampler_config = self.state.uidata.map_scheduler_settings_to_config(SAMPLER_MAPPING["restorer_sampler"])
1650
+ if ups_engine == UpscalerEngine.SUPIR.value:
1651
+ _upscaler_config = cast(SUPIR_Config, _upscaler_config)
1652
+ config.selected_upscaler_checkpoint_model = input_param_with_values["Image Upscale Model"]
1653
+ config.upscaler_engine = UpscalerEngine.SUPIR
1654
+ config.selected_upscaler_sampler = Sampler.from_str(input_param_with_values["Image Upscale Sampler"])
1655
+ config.upscaler_pipeline_params.supir_model = SUPIRModel.from_str(_upscaler_config.supir_model)
1656
+ config.upscaler_pipeline_params.seed = _upscaler_config.general.seed
1657
+ config.upscaler_pipeline_params.upscale_factor = _upscaler_config.general.upscale_factor
1658
+ config.upscaler_pipeline_params.prompt = input_param_with_values["Image Upscale - Prompt 1"]
1659
+ config.upscaler_pipeline_params.prompt_2 = input_param_with_values["Image Upscale - Prompt 2"]
1660
+ config.upscaler_pipeline_params.negative_prompt = input_param_with_values["Image Upscale - Negative prompt"]
1661
+ config.upscaler_pipeline_params.num_images = _upscaler_config.general.num_images
1662
+ config.upscaler_pipeline_params.num_steps = _upscaler_config.general.num_steps
1663
+ config.upscaler_pipeline_params.use_lpw_prompt = (
1664
+ True if input_param_with_values["Image Upscale - Prompt method"] == PromptMethod.Weighted.value else False
1665
+ )
1666
+ config.upscaler_pipeline_params.tile_size = _upscaler_config.general.tile_size
1667
+ config.upscaler_pipeline_params.restoration_scale = float(_upscaler_config.restoration_scale)
1668
+ config.upscaler_pipeline_params.s_churn = float(_upscaler_config.s_churn)
1669
+ config.upscaler_pipeline_params.s_noise = float(_upscaler_config.s_noise)
1670
+ config.upscaler_pipeline_params.strength = float(_upscaler_config.strength)
1671
+ config.upscaler_pipeline_params.use_linear_CFG = _upscaler_config.cfg_settings.use_linear_CFG
1672
+ config.upscaler_pipeline_params.guidance_scale = float(_upscaler_config.general.guidance_scale)
1673
+ config.upscaler_pipeline_params.guidance_rescale = float(_upscaler_config.general.guidance_rescale)
1674
+ config.upscaler_pipeline_params.reverse_linear_CFG = float(_upscaler_config.cfg_settings.reverse_linear_CFG)
1675
+ config.upscaler_pipeline_params.guidance_scale_start = float(_upscaler_config.cfg_settings.guidance_scale_start)
1676
+ config.upscaler_pipeline_params.use_linear_control_scale = _upscaler_config.controlnet_settings.use_linear_control_scale
1677
+ config.upscaler_pipeline_params.reverse_linear_control_scale = _upscaler_config.controlnet_settings.reverse_linear_control_scale
1678
+ config.upscaler_pipeline_params.controlnet_conditioning_scale = float(_upscaler_config.controlnet_settings.controlnet_conditioning_scale)
1679
+ config.upscaler_pipeline_params.control_scale_start = float(_upscaler_config.controlnet_settings.control_scale_start)
1680
+ config.upscaler_pipeline_params.enable_PAG = _upscaler_config.pag_settings.enable_PAG
1681
+ config.upscaler_pipeline_params.use_linear_PAG = _upscaler_config.pag_settings.use_linear_PAG
1682
+ config.upscaler_pipeline_params.reverse_linear_PAG = (
1683
+ _upscaler_config.pag_settings.reverse_linear_PAG and len(_upscaler_config.pag_settings.pag_layers) > 0
1684
+ )
1685
+ config.upscaler_pipeline_params.pag_scale = float(_upscaler_config.pag_settings.pag_scale)
1686
+ config.upscaler_pipeline_params.pag_scale_start = float(_upscaler_config.pag_settings.pag_scale_start)
1687
+ config.upscaler_pipeline_params.pag_layers = _upscaler_config.pag_settings.pag_layers
1688
+ config.upscaler_pipeline_params.start_point = StartPoint.from_str(_upscaler_config.start_point)
1689
+ config.upscaler_pipeline_params.image_size_fix_mode = ImageSizeFixMode.from_str(_upscaler_config.general.image_size_fix_mode)
1690
+ config.upscaler_pipeline_params.upscaling_mode = UpscalingMode.from_str(_upscaler_config.general.upscaling_mode)
1691
+ (
1692
+ config.upscaler_pipeline_params.zero_sft_injection_configs,
1693
+ config.upscaler_pipeline_params.zero_sft_injection_flags,
1694
+ ) = self.state.uidata.map_ui_supir_injection_to_pipeline_params(upscaler_supir_advanced_config)
1695
+ config.upscaler_pipeline_params.cfg_decay_rate = _upscaler_config.general.cfg_decay_rate
1696
+ config.upscaler_pipeline_params.strength_decay_rate = _upscaler_config.general.strength_decay_rate
1697
+ config.upscaler_pipeline_params.color_fix_mode = ColorFix.from_str(_upscaler_config.post_processsing_settings.color_fix_mode)
1698
+ config.upscaler_pipeline_params.callback_on_step_end = progress_callback
1699
+ config.upscaler_sampler_config = self.state.uidata.map_scheduler_settings_to_config(SAMPLER_MAPPING["upscaler_sampler"])
1700
+ elif ups_engine == UpscalerEngine.FaithDiff.value:
1701
+ _upscaler_config = cast(FaithDiff_Config, _upscaler_config)
1702
+ config.selected_upscaler_checkpoint_model = input_param_with_values["Image Upscale Model"]
1703
+ config.upscaler_engine = UpscalerEngine.FaithDiff
1704
+ config.selected_upscaler_sampler = Sampler.from_str(input_param_with_values["Image Upscale Sampler"])
1705
+ config.upscaler_pipeline_params.seed = _upscaler_config.general.seed
1706
+ config.upscaler_pipeline_params.upscale_factor = _upscaler_config.general.upscale_factor
1707
+ config.upscaler_pipeline_params.prompt = input_param_with_values["Image Upscale - Prompt 1"]
1708
+ config.upscaler_pipeline_params.prompt_2 = input_param_with_values["Image Upscale - Prompt 2"]
1709
+ config.upscaler_pipeline_params.negative_prompt = input_param_with_values["Image Upscale - Negative prompt"]
1710
+ config.upscaler_pipeline_params.num_images = _upscaler_config.general.num_images
1711
+ config.upscaler_pipeline_params.num_steps = _upscaler_config.general.num_steps
1712
+ config.upscaler_pipeline_params.use_lpw_prompt = (
1713
+ True if input_param_with_values["Image Upscale - Prompt method"] == PromptMethod.Weighted.value else False
1714
+ )
1715
+ config.upscaler_pipeline_params.tile_size = _upscaler_config.general.tile_size
1716
+ config.upscaler_pipeline_params.s_churn = float(_upscaler_config.s_churn)
1717
+ config.upscaler_pipeline_params.s_noise = float(_upscaler_config.s_noise)
1718
+ config.upscaler_pipeline_params.strength = float(_upscaler_config.strength)
1719
+ config.upscaler_pipeline_params.guidance_scale = float(_upscaler_config.general.guidance_scale)
1720
+ config.upscaler_pipeline_params.guidance_rescale = float(_upscaler_config.general.guidance_rescale)
1721
+ config.upscaler_pipeline_params.use_linear_control_scale = _upscaler_config.controlnet_settings.use_linear_control_scale
1722
+ config.upscaler_pipeline_params.reverse_linear_control_scale = _upscaler_config.controlnet_settings.reverse_linear_control_scale
1723
+ config.upscaler_pipeline_params.controlnet_conditioning_scale = float(_upscaler_config.controlnet_settings.controlnet_conditioning_scale)
1724
+ config.upscaler_pipeline_params.control_scale_start = float(_upscaler_config.controlnet_settings.control_scale_start)
1725
+ config.upscaler_pipeline_params.enable_PAG = _upscaler_config.pag_settings.enable_PAG and len(_upscaler_config.pag_settings.pag_layers) > 0
1726
+ config.upscaler_pipeline_params.use_linear_PAG = _upscaler_config.pag_settings.use_linear_PAG
1727
+ config.upscaler_pipeline_params.reverse_linear_PAG = _upscaler_config.pag_settings.reverse_linear_PAG
1728
+ config.upscaler_pipeline_params.pag_scale = float(_upscaler_config.pag_settings.pag_scale)
1729
+ config.upscaler_pipeline_params.pag_scale_start = float(_upscaler_config.pag_settings.pag_scale_start)
1730
+ config.upscaler_pipeline_params.pag_layers = _upscaler_config.pag_settings.pag_layers
1731
+ config.upscaler_pipeline_params.start_point = StartPoint.from_str(_upscaler_config.start_point)
1732
+ config.upscaler_pipeline_params.image_size_fix_mode = ImageSizeFixMode.from_str(_upscaler_config.general.image_size_fix_mode)
1733
+ config.upscaler_pipeline_params.upscaling_mode = UpscalingMode.from_str(_upscaler_config.general.upscaling_mode)
1734
+ config.upscaler_pipeline_params.invert_prompts = _upscaler_config.invert_prompts
1735
+ config.upscaler_pipeline_params.apply_ipa_embeds = _upscaler_config.apply_ipa_embeds
1736
+ config.upscaler_pipeline_params.cfg_decay_rate = _upscaler_config.general.cfg_decay_rate
1737
+ config.upscaler_pipeline_params.strength_decay_rate = _upscaler_config.general.strength_decay_rate
1738
+ config.upscaler_pipeline_params.color_fix_mode = ColorFix.from_str(_upscaler_config.post_processsing_settings.color_fix_mode)
1739
+ config.upscaler_pipeline_params.callback_on_step_end = progress_callback
1740
+ config.upscaler_sampler_config = self.state.uidata.map_scheduler_settings_to_config(SAMPLER_MAPPING["upscaler_sampler"])
1741
+ elif ups_engine == UpscalerEngine.ControlNetTile.value:
1742
+ _upscaler_config = cast(ControlNetTile_Config, _upscaler_config)
1743
+ config.selected_upscaler_checkpoint_model = input_param_with_values["Image Upscale Model"]
1744
+ config.upscaler_engine = UpscalerEngine.ControlNetTile
1745
+ config.selected_upscaler_sampler = Sampler.from_str(input_param_with_values["Image Upscale Sampler"])
1746
+ config.upscaler_pipeline_params.seed = _upscaler_config.general.seed
1747
+ config.upscaler_pipeline_params.upscale_factor = _upscaler_config.general.upscale_factor
1748
+ config.upscaler_pipeline_params.prompt = input_param_with_values["Image Upscale - Prompt 1"]
1749
+ config.upscaler_pipeline_params.prompt_2 = input_param_with_values["Image Upscale - Prompt 2"]
1750
+ config.upscaler_pipeline_params.negative_prompt = input_param_with_values["Image Upscale - Negative prompt"]
1751
+ config.upscaler_pipeline_params.num_images = _upscaler_config.general.num_images
1752
+ config.upscaler_pipeline_params.num_steps = _upscaler_config.general.num_steps
1753
+ config.upscaler_pipeline_params.tile_size = _upscaler_config.general.tile_size
1754
+ config.upscaler_pipeline_params.tile_overlap = _upscaler_config.tile_overlap
1755
+ config.upscaler_pipeline_params.tile_weighting_method = WeightingMethod.from_str(_upscaler_config.tile_weighting_method)
1756
+ config.upscaler_pipeline_params.tile_gaussian_sigma = _upscaler_config.tile_gaussian_sigma
1757
+ config.upscaler_pipeline_params.strength = float(_upscaler_config.strength)
1758
+ config.upscaler_pipeline_params.guidance_scale = float(_upscaler_config.general.guidance_scale)
1759
+ config.upscaler_pipeline_params.guidance_rescale = float(_upscaler_config.general.guidance_rescale)
1760
+ config.upscaler_pipeline_params.image_size_fix_mode = ImageSizeFixMode.from_str(_upscaler_config.general.image_size_fix_mode)
1761
+ config.upscaler_pipeline_params.upscaling_mode = UpscalingMode.from_str(_upscaler_config.general.upscaling_mode)
1762
+ config.upscaler_pipeline_params.cfg_decay_rate = _upscaler_config.general.cfg_decay_rate
1763
+ config.upscaler_pipeline_params.strength_decay_rate = _upscaler_config.general.strength_decay_rate
1764
+ config.upscaler_pipeline_params.color_fix_mode = ColorFix.from_str(_upscaler_config.post_processsing_settings.color_fix_mode)
1765
+ config.upscaler_pipeline_params.callback_on_step_end = progress_callback
1766
+ config.upscaler_sampler_config = self.state.uidata.map_scheduler_settings_to_config(SAMPLER_MAPPING["upscaler_sampler"])
1767
+
1768
+ config.image_path = input_image
1769
+ self.update_pipeline(log_callback=process_and_send_updates, progress_bar_handler=progress_bar_handler)
1770
+
1771
+ initialize_status = sup_toolbox_pipe.initialize()
1772
+ if initialize_status:
1773
+ result, process_status = sup_toolbox_pipe.predict(metadata=image_metadata)
1774
+ if process_status:
1775
+ logger.log(logging.INFO + 5, "Image generated successfully!")
1776
+ process_and_send_updates(status="success", final_image_payload=(input_image, result))
1777
+ else:
1778
+ raise RuntimeError("Pipeline prediction returned a failure status.")
1779
+ else:
1780
+ raise RuntimeError("Pipeline initialization failed.")
1781
+
1782
+ except PipelineCancelationRequested as ce:
1783
+ logger.warning(str(ce))
1784
+ process_and_send_updates(status="error")
1785
+ except Exception as e:
1786
+ logger.error(f"Error in diffusion thread: {e}, process aborted!", exc_info=True)
1787
+ process_and_send_updates(status="error")
1788
+ finally:
1789
+ update_queue.put(None)
ui/ui_layout.py ADDED
@@ -0,0 +1,684 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The DEVAIEXP Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import Any, Dict
17
+
18
+ import gradio as gr
19
+
20
+ # Custom Component Imports
21
+ from gradio_bottombar import BottomBar
22
+ from gradio_buttonplus import ButtonPlus
23
+ from gradio_creditspanel import CreditsPanel
24
+ from gradio_dropdownplus import DropdownPlus
25
+ from gradio_folderexplorer import FolderExplorer
26
+ from gradio_htmlinjector import HTMLInjector
27
+ from gradio_imagemeta import ImageMeta
28
+ from gradio_livelog import LiveLog
29
+ from gradio_mediagallery import MediaGallery
30
+ from gradio_propertysheet import PropertySheet
31
+ from gradio_taggrouphelper import TagGroupHelper
32
+ from gradio_textboxplus import TextboxPlus
33
+ from gradio_tokenizertextbox import TokenizerTextBox
34
+ from gradio_topbar import TopBar
35
+
36
+ from ui.ui_config import (
37
+ CREDIT_LIST,
38
+ LICENSE_PATHS,
39
+ TAG_DATA_NEGATIVE,
40
+ TAG_DATA_POSITIVE,
41
+ AppSettings,
42
+ ControlNetTile_Config,
43
+ SUPIR_Config,
44
+ SUPIRAdvanced_Config,
45
+ )
46
+ from ui.ui_data import UIData
47
+
48
+
49
+ @dataclass
50
+ class UIComponents:
51
+ """A dataclass to hold all interactive UI components for type safety and Intellisense."""
52
+
53
+ html_injector: HTMLInjector
54
+ flyout_visible: gr.State
55
+ active_anchor_id: gr.State
56
+ js_data_bridge: gr.Textbox
57
+ total_inference_steps: gr.State
58
+ run_btn: gr.Button
59
+ cancel_btn: gr.Button
60
+ preset_name: TextboxPlus
61
+ presets: DropdownPlus
62
+ save_preset_btn: gr.Button
63
+ load_preset_btn: gr.Button
64
+ restorer_engine: DropdownPlus
65
+ upscaler_engine: DropdownPlus
66
+ restorer_model: DropdownPlus
67
+ upscaler_model: DropdownPlus
68
+ vae_model: gr.Dropdown
69
+ restorer_sampler: DropdownPlus
70
+ restorer_sampler_ear_btn: ButtonPlus
71
+ upscaler_sampler: DropdownPlus
72
+ upscaler_sampler_ear_btn: ButtonPlus
73
+ flyout_property_sheet_close_btn: gr.Button
74
+ flyout_sheet: PropertySheet
75
+ main_tabs: gr.Tabs
76
+ input_image: ImageMeta
77
+ result_slider: gr.ImageSlider
78
+ ec_accordion: gr.Accordion
79
+ config_tabs: gr.Tabs
80
+ restorer_tab: gr.Tab
81
+ res_prompt_method: gr.Dropdown
82
+ res_prompt: gr.Textbox
83
+ res_prompt_generate_btn: ButtonPlus
84
+ res_prompt_2: gr.Textbox
85
+ res_tokenizer_pos: TokenizerTextBox
86
+ res_negative_prompt: gr.Textbox
87
+ res_tokenizer_neg: TokenizerTextBox
88
+ preview_restoration_mask_chk: gr.Checkbox
89
+ restoration_mask_prompt: gr.Textbox
90
+ preview_restoration_mask_btn: ButtonPlus
91
+ flyout_restoration_image_close_btn: gr.Button
92
+ restoration_mask: gr.Image
93
+ restorer_sheet: PropertySheet
94
+ restorer_sheet_supir_advanced: PropertySheet
95
+ upscaler_tab: gr.Tab
96
+ ups_prompt_method: gr.Dropdown
97
+ ups_prompt: gr.Textbox
98
+ ups_prompt_generate_btn: ButtonPlus
99
+ ups_prompt_2: gr.Textbox
100
+ ups_tokenizer_pos: TokenizerTextBox
101
+ ups_negative_prompt: gr.Textbox
102
+ ups_tokenizer_neg: TokenizerTextBox
103
+ upscaler_sheet: PropertySheet
104
+ upscaler_sheet_supir_advanced: PropertySheet
105
+ generated_tab: gr.Tab
106
+ folder_explorer: FolderExplorer
107
+ generated_image_viewer: MediaGallery
108
+ settings_tab: gr.Tab
109
+ settings_sheet: PropertySheet
110
+ reset_settings_btn: gr.Button
111
+ save_settings_btn: gr.Button
112
+ about_tab: gr.Tab
113
+ tag_helper_pos: TagGroupHelper
114
+ tag_helper_neg: TagGroupHelper
115
+ bottom_bar: BottomBar
116
+ livelog_viewer: LiveLog
117
+ ALL_UI_COMPONENTS: Dict[str, Any] = field(default_factory=dict)
118
+
119
+ def _get_ui_inputs_and_outputs(self):
120
+ """Internal helper to reconstruct UI input/output lists when needed."""
121
+
122
+ ui_inputs = {
123
+ "Restorer Sheet": self.restorer_sheet,
124
+ "Restorer Sheet SUPIR Advanced": self.restorer_sheet_supir_advanced,
125
+ "Upscaler Sheet": self.upscaler_sheet,
126
+ "Upscaler Sheet SUPIR Advanced": self.upscaler_sheet_supir_advanced,
127
+ "Image Restore Engine": self.restorer_engine,
128
+ "Image Upscale Engine": self.upscaler_engine,
129
+ "Image Restore Model": self.restorer_model,
130
+ "Image Upscale Model": self.upscaler_model,
131
+ "VAE Model": self.vae_model,
132
+ "Image Restore Sampler": self.restorer_sampler,
133
+ "Image Upscale Sampler": self.upscaler_sampler,
134
+ "Input Image": self.input_image,
135
+ "Image Restore - Prompt method": self.res_prompt_method,
136
+ "Image Restore - Prompt 1": self.res_prompt,
137
+ "Image Restore - Prompt 2": self.res_prompt_2,
138
+ "Image Restore - Negative prompt": self.res_negative_prompt,
139
+ "Enable Face Restoration": self.preview_restoration_mask_chk,
140
+ "Mask Prompt": self.restoration_mask_prompt,
141
+ "Image Upscale - Prompt method": self.ups_prompt_method,
142
+ "Image Upscale - Prompt 1": self.ups_prompt,
143
+ "Image Upscale - Prompt 2": self.ups_prompt_2,
144
+ "Image Upscale - Negative prompt": self.ups_negative_prompt,
145
+ }
146
+ output_fields = [*ui_inputs.copy().values()]
147
+ ui_inputs.pop("Input Image", None)
148
+ return ui_inputs, output_fields
149
+
150
+
151
+ def get_initial_supir_advanced_values():
152
+ """Helper to provide initial default values to the UI component."""
153
+ initial_supir_advanced_settings = SUPIRAdvanced_Config()
154
+ initial_supir_advanced_settings.cross_up_block_0_stage1.sft_active = False
155
+ initial_supir_advanced_settings.cross_up_block_1_stage1.sft_active = False
156
+ return initial_supir_advanced_settings
157
+
158
+
159
+ def create_ui_components(uidata: UIData) -> UIComponents:
160
+ """
161
+ Builds and returns a dictionary of UI components.
162
+ This function MUST be called from within a `gr.Blocks()` context.
163
+ """
164
+
165
+ html_injector = HTMLInjector()
166
+ flyout_visible = gr.State(False)
167
+ active_anchor_id = gr.State(None)
168
+ js_data_bridge = gr.Textbox(visible=False, elem_id="js_data_bridge")
169
+ total_inference_steps = gr.State(0)
170
+ gr.Markdown(
171
+ """
172
+ <div align="center">
173
+
174
+ # SUP Toolbox App Demo
175
+
176
+ > **Note:** This is a demonstration version with limited features.
177
+ > The **Face Restoration** and **Auto Caption Generation** features are disabled in this online demo
178
+ > to ensure stability and reduce resource usage. For access to all features, please run the
179
+ > [full application locally](https://github.com/DEVAIEXP/sup-toolbox-app).
180
+
181
+ </div>
182
+ """
183
+ )
184
+ with gr.Row():
185
+ with TopBar(width="30%", height=80, bring_to_front=True, rounded_borders=True):
186
+ with gr.Row():
187
+ run_btn = gr.Button(f"{uidata.process_symbol} Run Process", variant="primary", elem_id="run-button")
188
+ cancel_btn = gr.Button(
189
+ f"{uidata.stop_symbol} Cancel",
190
+ variant="stop",
191
+ elem_id="cancel-button",
192
+ visible=False,
193
+ )
194
+
195
+ with gr.Sidebar(width=360):
196
+ with gr.Column(elem_classes=["flyout-context-area"], min_width=250):
197
+ gr.Markdown("### Preset Selection")
198
+ with gr.Row():
199
+ preset_name = TextboxPlus(
200
+ elem_id="preset_name",
201
+ label="Preset name",
202
+ help="Name of the preset to be saved or loaded",
203
+ )
204
+ presets = DropdownPlus(
205
+ elem_id="presets",
206
+ label="Presets",
207
+ interactive=True,
208
+ choices=uidata.PRESETS_LIST,
209
+ value=uidata.config.latest_preset,
210
+ help="Select a preset to load",
211
+ )
212
+ with gr.Row():
213
+ save_preset_btn = gr.Button(
214
+ f"{uidata.save_symbol} Save preset",
215
+ variant="primary",
216
+ elem_id="save_preset",
217
+ )
218
+ load_preset_btn = gr.Button(f"{uidata.download_symbol} Load preset", elem_id="load_preset")
219
+ with gr.Row():
220
+ gr.Markdown("### Engine Selection")
221
+ restorer_engine = DropdownPlus(
222
+ elem_id="restorer_engine",
223
+ label="Image Restore Engine",
224
+ choices=uidata.RESTORER_ENGINE_CHOICES,
225
+ value="SUPIR",
226
+ help="Select the image restoration engine to be used",
227
+ )
228
+ upscaler_engine = DropdownPlus(
229
+ elem_id="upscaler_engine",
230
+ label="Image Upscale Engine",
231
+ choices=uidata.UPSCALER_ENGINE_CHOICES,
232
+ value="None",
233
+ help="Select the image upscaling engine to be used",
234
+ )
235
+ gr.Markdown("### Model Selection")
236
+ restorer_model = DropdownPlus(
237
+ elem_id="restorer_model",
238
+ label="Image Restore Model",
239
+ choices=uidata.MODEL_CHOICES,
240
+ value=uidata.MODEL_CHOICES[0] if uidata.MODEL_CHOICES else None,
241
+ elem_classes=["custom-dropdown"],
242
+ help="Select the model to be used for the restoration engine",
243
+ )
244
+ upscaler_model = DropdownPlus(
245
+ elem_id="upscaler_model",
246
+ label="Image Upscale Model",
247
+ choices=uidata.MODEL_CHOICES,
248
+ value=uidata.MODEL_CHOICES[0] if uidata.MODEL_CHOICES else None,
249
+ elem_classes=["custom-dropdown"],
250
+ help="Select the model to be used for the upscaling engine",
251
+ )
252
+ vae_model = gr.Dropdown(
253
+ elem_id="vae_model",
254
+ label="VAE Model (Optional)",
255
+ choices=uidata.VAE_CHOICES,
256
+ value="Default",
257
+ )
258
+ gr.Markdown("### Sampler Selection")
259
+ with gr.Accordion(open=False, label="Create/Choose Sampler", elem_id="sampler-accordion"):
260
+ with gr.Group(elem_classes=["group"]):
261
+ with gr.Row(elem_classes=["fake-input-container", "no-border-dropdown"]):
262
+ restorer_sampler = DropdownPlus(
263
+ elem_id="restorer_sampler",
264
+ label="Image Restore Sampler",
265
+ choices=uidata.SAMPLER_SUPIR_LIST,
266
+ value="Euler",
267
+ help="Select the sampler to be used for the restoration engine",
268
+ )
269
+ restorer_sampler_ear_btn = ButtonPlus(
270
+ f"{uidata.engine_symbol}",
271
+ elem_id="restorer_sampler_ear_btn",
272
+ scale=1,
273
+ elem_classes=["integrated-ear-btn"],
274
+ help="Click to open advanced settings...",
275
+ )
276
+ with gr.Row(elem_classes=["fake-input-container", "no-border-dropdown"]):
277
+ upscaler_sampler = DropdownPlus(
278
+ elem_id="upscaler_sampler",
279
+ label="Image Upscale Sampler",
280
+ choices=uidata.SAMPLER_OTHERS_LIST,
281
+ value="Euler",
282
+ help="Select the sampler to be used for the upscaling engine",
283
+ )
284
+ upscaler_sampler_ear_btn = ButtonPlus(
285
+ f"{uidata.engine_symbol}",
286
+ elem_id="upscaler_sampler_ear_btn",
287
+ scale=1,
288
+ elem_classes=["integrated-ear-btn"],
289
+ help="Click to open advanced settings...",
290
+ )
291
+ with gr.Column(elem_id="flyout_property_sheet_panel", elem_classes=["flyout-source-hidden"]):
292
+ flyout_property_sheet_close_btn = gr.Button("×", elem_classes=["flyout-close-btn"])
293
+ flyout_sheet = PropertySheet(
294
+ elem_id="flyout_sheet",
295
+ visible=True,
296
+ container=False,
297
+ label="Settings",
298
+ show_group_name_only_one=False,
299
+ disable_accordion=True,
300
+ )
301
+
302
+ with gr.Tabs() as main_tabs:
303
+ with gr.TabItem(f"{uidata.process_symbol} Process", id="process-tab"):
304
+ with gr.Row():
305
+ with gr.Column(scale=3):
306
+ with gr.Row():
307
+ input_image = ImageMeta(
308
+ elem_id="input_image",
309
+ type="filepath",
310
+ label="Input Image",
311
+ height=500,
312
+ width=600,
313
+ popup_metadata_width=600,
314
+ popup_metadata_height=500,
315
+ only_custom_metadata=False,
316
+ )
317
+ result_slider = gr.ImageSlider(
318
+ elem_id="result_slider",
319
+ label="Result Comparison",
320
+ show_label=True,
321
+ height=500,
322
+ )
323
+
324
+ with gr.Accordion("Engine Configurations", open=True) as ec_accordion:
325
+ with gr.Tabs() as config_tabs:
326
+ with gr.TabItem("Restoration", id=0, visible=False, elem_id="res-tab") as restorer_tab:
327
+ res_prompt_method = gr.Dropdown(
328
+ elem_id="restorer_prompt_method",
329
+ label="Prompt Method",
330
+ choices=uidata.PROMPT_METHODS,
331
+ value=uidata.PROMPT_METHODS[1],
332
+ info="Method used to generate prompt embedding",
333
+ )
334
+ with gr.Group(elem_classes=["group"]):
335
+ with gr.Row(
336
+ elem_classes=[
337
+ "fake-input-container",
338
+ "no-border-dropdown",
339
+ "row-form-size-2",
340
+ ]
341
+ ):
342
+ res_prompt = gr.Textbox(
343
+ elem_id="restorer_prompt_1",
344
+ label="Prompt",
345
+ lines=3,
346
+ info="Your positive prompt",
347
+ value="Direct flash photography. Three (30-year-old men:1.1), (all black hair:1.2). Left man: (black t-shirt:1.1) with white text 'Road Kill Cafe' and in his right forearm has distinct (dark tribal tattoo:1.2).Their hands has clearly defined fingers and distinct outlines. A (plaster interior wall: 1.1) on the left.",
348
+ )
349
+ res_prompt_generate_btn = ButtonPlus(
350
+ value=f"{uidata.caption_symbol}",
351
+ elem_id="res_prompt_generate_btn",
352
+ elem_classes=["integrated-ear-btn"],
353
+ help="Click to auto generate the initial caption...",
354
+ visible=False
355
+ )
356
+ res_prompt_2 = gr.Textbox(
357
+ elem_id="restorer_prompt_2",
358
+ label="Prompt 2",
359
+ lines=2,
360
+ info="Your positive prompt complement",
361
+ value="Extremely detailed faces, flawless natural skin texture, skin pore detailing, 4k, 8k, clean image, no noise, shot on Fujifilm Superia 400, sharp focus, faithful colors.",
362
+ )
363
+ res_tokenizer_pos = TokenizerTextBox(
364
+ elem_id="restorer_tokenizer_prompt",
365
+ label="Positive Tokenizer",
366
+ hide_input=True,
367
+ model="Xenova/clip-vit-large-patch14",
368
+ )
369
+ res_negative_prompt = gr.Textbox(
370
+ elem_id="restorer_negative_prompt",
371
+ label="Negative Prompt",
372
+ lines=3,
373
+ info="Specify what you doesn't want to see",
374
+ value="low-res, disfigured, analog artifacts, smudged, animate, (out of focus:1.2), catchlights, over-smooth, extra eyes, worst quality, unreal engine, art, aberrations, surreal, pastel drawing, (tattoo patterns on walls:1.4), tatto patterns on skin, text on walls, green wall, grainy wall texture, harsh lighting, (tribal patterns on clothing text:1.3), tattoo on chest, dead eyes, deformed fingers, undistinct fingers outlines",
375
+ )
376
+ res_tokenizer_neg = TokenizerTextBox(
377
+ elem_id="restorer_tokenizer_negative_prompt",
378
+ label="Negative Tokenizer",
379
+ hide_input=True,
380
+ model="Xenova/clip-vit-large-patch14",
381
+ )
382
+ with gr.Accordion("Face Restoration", open=False, visible=False):
383
+ preview_restoration_mask_chk = gr.Checkbox(
384
+ elem_id="use_restoration_mask",
385
+ label="Enable Face Restoration",
386
+ value=False,
387
+ )
388
+ with gr.Row(
389
+ elem_classes=[
390
+ "fake-input-container",
391
+ "no-border-dropdown",
392
+ "row-form-size",
393
+ ]
394
+ ):
395
+ restoration_mask_prompt = gr.Textbox(
396
+ elem_id="restoration_mask_prompt",
397
+ label="Mask prompt",
398
+ placeholder="head",
399
+ value="head",
400
+ interactive=False,
401
+ )
402
+ preview_restoration_mask_btn = ButtonPlus(
403
+ value=f"{uidata.preview_symbol}",
404
+ elem_id="preview_restoration_mask_btn",
405
+ elem_classes=["integrated-ear-btn"],
406
+ interactive=False,
407
+ help="Click to open preview restoration mask...",
408
+ )
409
+ with gr.Column(
410
+ elem_id="flyout_restoration_mask_panel",
411
+ elem_classes=["flyout-source-hidden"],
412
+ ):
413
+ flyout_restoration_image_close_btn = gr.Button("×", elem_classes=["flyout-close-btn"])
414
+ restoration_mask = gr.Image(
415
+ elem_id="flyout_restoration_mask",
416
+ label="Masking preview",
417
+ type="pil",
418
+ interactive=False,
419
+ show_download_button=False,
420
+ show_share_button=False,
421
+ height=300,
422
+ )
423
+ restorer_sheet = PropertySheet(
424
+ elem_id="restorer_settings",
425
+ label="Restoration Settings",
426
+ value=SUPIR_Config(),
427
+ )
428
+ restorer_sheet_supir_advanced = PropertySheet(
429
+ elem_id="restorer_supir_advanced_settings",
430
+ label="SFT Injection Settings (Experimental parameters for advanced users)",
431
+ open=False,
432
+ value=get_initial_supir_advanced_values(),
433
+ )
434
+
435
+ with gr.TabItem("Upscaling", id=1, visible=False, elem_id="ups-tab") as upscaler_tab:
436
+ ups_prompt_method = gr.Dropdown(
437
+ elem_id="upscaler_prompt_method",
438
+ label="Prompt Method",
439
+ choices=uidata.PROMPT_METHODS,
440
+ value=uidata.PROMPT_METHODS[1],
441
+ info="Method used to generate prompt embedding",
442
+ )
443
+ with gr.Group(elem_classes=["group"]):
444
+ with gr.Row(
445
+ elem_classes=[
446
+ "fake-input-container",
447
+ "no-border-dropdown",
448
+ "row-form-size-2",
449
+ ]
450
+ ):
451
+ ups_prompt = gr.Textbox(
452
+ elem_id="upscaler_prompt_1",
453
+ label="Prompt",
454
+ lines=3,
455
+ info="Your positive prompt",
456
+ )
457
+ ups_prompt_generate_btn = ButtonPlus(
458
+ value=f"{uidata.caption_symbol}",
459
+ elem_id="ups_prompt_generate_btn",
460
+ elem_classes=["integrated-ear-btn"],
461
+ help="Click to auto generate the initial caption...",
462
+ visible=False
463
+ )
464
+ ups_prompt_2 = gr.Textbox(
465
+ elem_id="upscaler_prompt_2",
466
+ label="Prompt 2",
467
+ lines=2,
468
+ info="Your positive prompt complement",
469
+ )
470
+ ups_tokenizer_pos = TokenizerTextBox(
471
+ elem_id="upscaler_tokenizer_prompt",
472
+ label="Positive Tokenizer",
473
+ hide_input=True,
474
+ model="Xenova/clip-vit-large-patch14",
475
+ )
476
+ ups_negative_prompt = gr.Textbox(
477
+ elem_id="upscaler_negative_prompt",
478
+ label="Negative Prompt",
479
+ lines=3,
480
+ info="Specify what you doesn't want to see",
481
+ )
482
+ ups_tokenizer_neg = TokenizerTextBox(
483
+ elem_id="upscaler_tokenizer_negative_prompt",
484
+ label="Negative Tokenizer",
485
+ hide_input=True,
486
+ model="Xenova/clip-vit-large-patch14",
487
+ )
488
+ upscaler_sheet = PropertySheet(
489
+ elem_id="upscaler_settings",
490
+ label="Upscaling Settings",
491
+ value=ControlNetTile_Config(),
492
+ )
493
+ upscaler_sheet_supir_advanced = PropertySheet(
494
+ elem_id="upscaler_supir_advanced_settings",
495
+ label="SFT Injection Settings (Experimental parameters for advanced users)",
496
+ open=False,
497
+ value=get_initial_supir_advanced_values(),
498
+ )
499
+
500
+ with gr.TabItem(f"{uidata.folder_symbol} Generated") as generated_tab:
501
+ with gr.Row(equal_height=True, elem_classes="media-gallery-row"):
502
+ with gr.Column(scale=0, min_width=300):
503
+ folder_explorer = FolderExplorer(
504
+ label="Select a Folder",
505
+ root_dir=uidata.config.output_dir,
506
+ value=uidata.config.output_dir,
507
+ )
508
+ with gr.Column(scale=2):
509
+ generated_image_viewer = MediaGallery(
510
+ label="Media in Folder",
511
+ columns=4,
512
+ height="auto",
513
+ preview=False,
514
+ show_download_button=False,
515
+ only_custom_metadata=False,
516
+ popup_metadata_width="40%",
517
+ )
518
+
519
+ with gr.Tab(f"{uidata.settings_symbol} Settings", interactive=False) as settings_tab:
520
+ settings_sheet = PropertySheet(elem_id="settings_sheet", label="General Setting", value=AppSettings())
521
+ with gr.Row(elem_id="settings-buttons-row"):
522
+ reset_settings_btn = gr.Button(
523
+ value=f"{uidata.refresh_symbol} Load defaults and restart",
524
+ elem_id="reset_settings_button",
525
+ )
526
+ save_settings_btn = gr.Button(
527
+ value=f"{uidata.save_symbol} Save and restart",
528
+ elem_id="save_settings_button",
529
+ )
530
+
531
+ with gr.Tab(f"{uidata.about_symbol} About") as about_tab:
532
+ CreditsPanel(
533
+ elem_classes="credit-panel",
534
+ height="auto",
535
+ credits=CREDIT_LIST,
536
+ licenses=LICENSE_PATHS,
537
+ effect="scroll",
538
+ speed=60.0,
539
+ base_font_size=1.5,
540
+ intro_title="SUP Toolbox",
541
+ intro_subtitle="Scaling-UP Application",
542
+ sidebar_position="right",
543
+ logo_path=None,
544
+ show_logo=True,
545
+ show_licenses=True,
546
+ show_credits=True,
547
+ logo_position="center",
548
+ logo_sizing="resize",
549
+ logo_width="200px",
550
+ logo_height="100px",
551
+ scroll_background_color="#000000",
552
+ scroll_title_color="#FFFFFF",
553
+ scroll_name_color="#FFFFFF",
554
+ scroll_section_title_color="#FFFFFF",
555
+ layout_style="two-column",
556
+ title_uppercase=True,
557
+ name_uppercase=True,
558
+ section_title_uppercase=True,
559
+ swap_font_sizes_on_two_column=True,
560
+ scroll_logo_path="assets/devaixp_logo-white.png",
561
+ scroll_logo_height="200px",
562
+ )
563
+
564
+ with gr.Sidebar(position="right", width=360):
565
+ gr.Markdown("### Helpers")
566
+ tag_helper_pos = TagGroupHelper(
567
+ elem_id="tag_helper_pos",
568
+ label="Positive Keywords",
569
+ value=dict(TAG_DATA_POSITIVE),
570
+ target_textbox_id="upscaler_prompt_1",
571
+ separator=", ",
572
+ width=290,
573
+ font_size_scale=90,
574
+ interactive=True,
575
+ open=False,
576
+ )
577
+ tag_helper_neg = TagGroupHelper(
578
+ elem_id="tag_helper_neg",
579
+ label="Negative Keywords",
580
+ value=dict(TAG_DATA_NEGATIVE),
581
+ target_textbox_id="upscaler_negative_prompt",
582
+ separator=", ",
583
+ width=290,
584
+ font_size_scale=90,
585
+ interactive=True,
586
+ open=False,
587
+ )
588
+
589
+ with BottomBar("Status", bring_to_front=False, height=340, open=False, rounded_borders=True) as bottom_bar:
590
+ livelog_viewer = LiveLog(label="Process output", height=250, autoscroll=True, line_numbers=False)
591
+
592
+ all_ui_components_for_presets = {
593
+ restorer_engine.elem_id: restorer_engine,
594
+ upscaler_engine.elem_id: upscaler_engine,
595
+ restorer_model.elem_id: restorer_model,
596
+ upscaler_model.elem_id: upscaler_model,
597
+ vae_model.elem_id: vae_model,
598
+ restorer_sampler.elem_id: restorer_sampler,
599
+ upscaler_sampler.elem_id: upscaler_sampler,
600
+ res_prompt_method.elem_id: res_prompt_method,
601
+ res_prompt.elem_id: res_prompt,
602
+ res_prompt_2.elem_id: res_prompt_2,
603
+ res_negative_prompt.elem_id: res_negative_prompt,
604
+ preview_restoration_mask_chk.elem_id: preview_restoration_mask_chk,
605
+ restoration_mask_prompt.elem_id: restoration_mask_prompt,
606
+ restorer_sheet.elem_id: restorer_sheet,
607
+ restorer_sheet_supir_advanced.elem_id: restorer_sheet_supir_advanced,
608
+ ups_prompt_method.elem_id: ups_prompt_method,
609
+ ups_prompt.elem_id: ups_prompt,
610
+ ups_prompt_2.elem_id: ups_prompt_2,
611
+ ups_negative_prompt.elem_id: ups_negative_prompt,
612
+ upscaler_sheet.elem_id: upscaler_sheet,
613
+ upscaler_sheet_supir_advanced.elem_id: upscaler_sheet_supir_advanced,
614
+ }
615
+
616
+ components_dict = {
617
+ "html_injector": html_injector,
618
+ "flyout_visible": flyout_visible,
619
+ "active_anchor_id": active_anchor_id,
620
+ "js_data_bridge": js_data_bridge,
621
+ "total_inference_steps": total_inference_steps,
622
+ "run_btn": run_btn,
623
+ "cancel_btn": cancel_btn,
624
+ "preset_name": preset_name,
625
+ "presets": presets,
626
+ "save_preset_btn": save_preset_btn,
627
+ "load_preset_btn": load_preset_btn,
628
+ "restorer_engine": restorer_engine,
629
+ "upscaler_engine": upscaler_engine,
630
+ "restorer_model": restorer_model,
631
+ "upscaler_model": upscaler_model,
632
+ "vae_model": vae_model,
633
+ "restorer_sampler": restorer_sampler,
634
+ "restorer_sampler_ear_btn": restorer_sampler_ear_btn,
635
+ "upscaler_sampler": upscaler_sampler,
636
+ "upscaler_sampler_ear_btn": upscaler_sampler_ear_btn,
637
+ "flyout_property_sheet_close_btn": flyout_property_sheet_close_btn,
638
+ "flyout_sheet": flyout_sheet,
639
+ "main_tabs": main_tabs,
640
+ "input_image": input_image,
641
+ "result_slider": result_slider,
642
+ "ec_accordion": ec_accordion,
643
+ "config_tabs": config_tabs,
644
+ "restorer_tab": restorer_tab,
645
+ "res_prompt_method": res_prompt_method,
646
+ "res_prompt": res_prompt,
647
+ "res_prompt_generate_btn": res_prompt_generate_btn,
648
+ "res_prompt_2": res_prompt_2,
649
+ "res_tokenizer_pos": res_tokenizer_pos,
650
+ "res_negative_prompt": res_negative_prompt,
651
+ "res_tokenizer_neg": res_tokenizer_neg,
652
+ "preview_restoration_mask_chk": preview_restoration_mask_chk,
653
+ "restoration_mask_prompt": restoration_mask_prompt,
654
+ "preview_restoration_mask_btn": preview_restoration_mask_btn,
655
+ "flyout_restoration_image_close_btn": flyout_restoration_image_close_btn,
656
+ "restoration_mask": restoration_mask,
657
+ "restorer_sheet": restorer_sheet,
658
+ "restorer_sheet_supir_advanced": restorer_sheet_supir_advanced,
659
+ "upscaler_tab": upscaler_tab,
660
+ "ups_prompt_method": ups_prompt_method,
661
+ "ups_prompt": ups_prompt,
662
+ "ups_prompt_generate_btn": ups_prompt_generate_btn,
663
+ "ups_prompt_2": ups_prompt_2,
664
+ "ups_tokenizer_pos": ups_tokenizer_pos,
665
+ "ups_negative_prompt": ups_negative_prompt,
666
+ "ups_tokenizer_neg": ups_tokenizer_neg,
667
+ "upscaler_sheet": upscaler_sheet,
668
+ "upscaler_sheet_supir_advanced": upscaler_sheet_supir_advanced,
669
+ "generated_tab": generated_tab,
670
+ "folder_explorer": folder_explorer,
671
+ "generated_image_viewer": generated_image_viewer,
672
+ "settings_tab": settings_tab,
673
+ "settings_sheet": settings_sheet,
674
+ "reset_settings_btn": reset_settings_btn,
675
+ "save_settings_btn": save_settings_btn,
676
+ "about_tab": about_tab,
677
+ "tag_helper_pos": tag_helper_pos,
678
+ "tag_helper_neg": tag_helper_neg,
679
+ "bottom_bar": bottom_bar,
680
+ "livelog_viewer": livelog_viewer,
681
+ "ALL_UI_COMPONENTS": all_ui_components_for_presets,
682
+ }
683
+
684
+ return components_dict
ui/ui_state.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The DEVAIEXP Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from dataclasses import dataclass, field
16
+ from threading import Event
17
+
18
+ # Forward reference for type hint to avoid circular import
19
+ from typing import Any
20
+
21
+ from ui.ui_data import UIData
22
+
23
+
24
+ @dataclass
25
+ class AppState:
26
+ """A container for the shared state of the Gradio application."""
27
+
28
+ uidata: UIData
29
+ cancel_event: Event = field(default_factory=Event)
30
+
31
+ # All other state variables that were previously global
32
+ input_image_path: str = None
33
+ restorer_config_class: Any = None
34
+ restorer_supir_advanced_config_class: Any = None
35
+ upscaler_config_class: Any = None
36
+ upscaler_supir_advanced_config_class: Any = None
37
+ restorer_engine_selected: str = "SUPIR" # Initial value
38
+ upscaler_engine_selected: str = "None" # Initial value
ui/util/dataclass_helpers.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The DEVAIEXP Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import dataclasses
16
+ import functools
17
+ from dataclasses import Field
18
+ from typing import Any, Dict, List, Tuple, Type
19
+
20
+ from typing_extensions import Literal
21
+
22
+
23
+ # General Helper Functions
24
+ def get_nested_attr(obj: Any, path: str, default: Any = None) -> Any:
25
+ """
26
+ Accesses a nested attribute from an object using 'dot notation'.
27
+
28
+ Returns a default value if any attribute in the chain is missing or if
29
+ the object itself is None.
30
+
31
+ Args:
32
+ obj:
33
+ The root object.
34
+ path:
35
+ A string path like "parent.child.attribute".
36
+ default:
37
+ The value to return if the path is not found.
38
+
39
+ Returns:
40
+ The value of the nested attribute or the default value.
41
+ """
42
+ try:
43
+ return functools.reduce(getattr, path.split("."), obj)
44
+ except AttributeError:
45
+ return default
46
+
47
+
48
+ def _set_nested_dict_value(d: dict, path: str, value: Any):
49
+ """
50
+ Sets a value in a nested dictionary using 'dot notation'.
51
+
52
+ Args:
53
+ d:
54
+ The dictionary to modify.
55
+ path:
56
+ A string path like "parent.child.key".
57
+ value:
58
+ The new value to set.
59
+ """
60
+ keys = path.split(".")
61
+ for key in keys[:-1]:
62
+ d = d.setdefault(key, {})
63
+ d[keys[-1]] = value
64
+
65
+
66
+ def get_nested_field(dc_type: Type, path: str) -> Field:
67
+ """
68
+ Retrieves the dataclasses.Field object for a nested attribute.
69
+
70
+ Args:
71
+ dc_type:
72
+ The top-level dataclass type.
73
+ path:
74
+ A string path like "parent.child.field_name".
75
+
76
+ Returns:
77
+ The dataclasses.Field definition object.
78
+ """
79
+ parts = path.split(".")
80
+ current_type = dc_type
81
+ for i, part in enumerate(parts):
82
+ field_info = current_type.__dataclass_fields__[part]
83
+ if i == len(parts) - 1:
84
+ return field_info
85
+ current_type = field_info.type
86
+ raise KeyError(f"Field path '{path}' could not be fully resolved in {dc_type}.")
87
+
88
+
89
+ def get_nested_parent_and_field(obj: object, path: str) -> Tuple[object, str]:
90
+ """
91
+ Navigates a nested object path and returns the parent object and the
92
+ final field name.
93
+
94
+ Example: for path "general.sampler.seed", returns (sampler_object, "seed").
95
+
96
+ Args:
97
+ obj:
98
+ The root object to navigate.
99
+ path:
100
+ A dot-separated string path.
101
+
102
+ Returns:
103
+ A tuple containing the direct parent object and the final field name.
104
+ """
105
+ parts = path.split(".")
106
+ parent = obj
107
+ for part in parts[:-1]:
108
+ parent = getattr(parent, part)
109
+ return parent, parts[-1]
110
+
111
+
112
+ # Dataclass Instantiation Helper
113
+ def dataclass_from_dict(data_class: Type, data: dict) -> Any:
114
+ """
115
+ Recursively constructs a dataclass instance from a dictionary.
116
+
117
+ This helper is essential for creating a dataclass instance from dictionary
118
+ data, especially when dealing with nested dataclasses. It correctly
119
+ handles nested structures by recursively calling itself.
120
+
121
+ Args:
122
+ data_class:
123
+ The dataclass type to instantiate (e.g., `AppSettings`).
124
+ data:
125
+ A dictionary containing the data to populate the instance.
126
+ Keys should correspond to field names in the dataclass.
127
+
128
+ Returns:
129
+ An instance of `data_class` populated with data from the dictionary.
130
+ """
131
+ field_names = {f.name for f in dataclasses.fields(data_class)}
132
+ filtered_data = {k: v for k, v in data.items() if k in field_names}
133
+
134
+ for f in dataclasses.fields(data_class):
135
+ field_name = f.name
136
+ field_type = f.type
137
+
138
+ if field_name in filtered_data and dataclasses.is_dataclass(field_type):
139
+ if isinstance(filtered_data[field_name], dict):
140
+ nested_instance = dataclass_from_dict(field_type, filtered_data[field_name])
141
+ filtered_data[field_name] = nested_instance
142
+
143
+ return data_class(**filtered_data)
144
+
145
+
146
+ # The Core Engine for Dynamic Dataclass Reconstruction
147
+ def _rebuild_dataclass_recursively(dc_type: Type, path: str, new_field: Field) -> Type:
148
+ """
149
+ Internal function to recursively rebuild a nested dataclass structure.
150
+
151
+ It replaces an entire target field definition, allowing for changes to
152
+ any part of the field (type, metadata, default value, etc.).
153
+
154
+ Args:
155
+ dc_type:
156
+ The current dataclass type to rebuild.
157
+ path:
158
+ The remaining dot-separated path to the target field.
159
+ new_field:
160
+ The complete new dataclasses.Field object to insert.
161
+
162
+ Returns:
163
+ A new, dynamically created dataclass type with the updated structure.
164
+ """
165
+ parts = path.split(".")
166
+ field_to_change = parts[0]
167
+
168
+ if len(parts) == 1:
169
+ new_fields_spec = []
170
+ for f in dataclasses.fields(dc_type):
171
+ if f.name == field_to_change:
172
+ new_spec = (f.name, new_field.type, new_field)
173
+ new_fields_spec.append(new_spec)
174
+ else:
175
+ new_fields_spec.append((f.name, f.type, f))
176
+ class_name = f"Updated_{dc_type.__name__.rpartition('_')[-1]}"
177
+ return dataclasses.make_dataclass(
178
+ class_name,
179
+ new_fields_spec,
180
+ bases=dc_type.__bases__,
181
+ )
182
+ else:
183
+ nested_dc_type = dc_type.__dataclass_fields__[field_to_change].type
184
+ remaining_path = ".".join(parts[1:])
185
+ rebuilt_nested_type = _rebuild_dataclass_recursively(nested_dc_type, remaining_path, new_field)
186
+
187
+ new_parent_fields_spec = []
188
+ for f in dataclasses.fields(dc_type):
189
+ if f.name == field_to_change:
190
+ new_parent_nested_field = dataclasses.field(default_factory=f.default_factory, metadata=f.metadata, init=f.init)
191
+ new_spec = (f.name, rebuilt_nested_type, new_parent_nested_field)
192
+ new_parent_fields_spec.append(new_spec)
193
+ else:
194
+ new_parent_fields_spec.append((f.name, f.type, f))
195
+ class_name = f"Updated_{dc_type.__name__.rpartition('_')[-1]}"
196
+ return dataclasses.make_dataclass(class_name, new_parent_fields_spec, bases=dc_type.__bases__)
197
+
198
+
199
+ def apply_dynamic_changes(dc_instance: object, rules: dict) -> object:
200
+ """
201
+ Processes a dataclass instance against a set of UI rules.
202
+
203
+ This version ensures consistency by always rebuilding the dataclass
204
+ instance if any rule that modifies the class structure (like changing
205
+ options or visibility) is present. This prevents state inconsistencies
206
+ within the Gradio UI lifecycle.
207
+
208
+ Args:
209
+ dc_instance:
210
+ The source dataclass instance.
211
+ rules:
212
+ A dictionary containing the UI rules.
213
+
214
+ Returns:
215
+ A new dataclass instance of a dynamically created type, with all
216
+ structural rules applied.
217
+ """
218
+ current_dc = dc_instance
219
+ actions_to_process: List[Dict] = []
220
+
221
+ # Part 1: Gather dynamic actions based on current field values
222
+ if "dynamic_dependencies" in rules:
223
+ for modifier_path, rule_group in rules["dynamic_dependencies"].items():
224
+ modifier_value = get_nested_attr(current_dc, modifier_path)
225
+ for action in rule_group.get("actions", []):
226
+ mapping = action.get("mapping", {})
227
+ if modifier_value in mapping:
228
+ outcome = mapping[modifier_value]
229
+ actions_to_process.append({**action, "outcome": outcome})
230
+
231
+ # Part 2: Gather unconditional "on_load" actions
232
+ if "on_load_actions" in rules:
233
+ actions_to_process.extend(rules["on_load_actions"])
234
+
235
+ # Part 3: Process all gathered actions
236
+ for action in actions_to_process:
237
+ action_type = action.get("type")
238
+ target_path = action.get("target_field_path")
239
+ outcome = action.get("outcome") # Will be None for on_load_actions
240
+
241
+ if not all([action_type, target_path]):
242
+ continue
243
+
244
+ target_field_info = get_nested_field(type(current_dc), target_path)
245
+
246
+ # Handle actions that require rebuilding the dataclass
247
+ if action_type in ["update_options", "update_visibility"]:
248
+ new_field_type = target_field_info.type
249
+ new_metadata = dict(target_field_info.metadata)
250
+
251
+ if action_type == "update_options":
252
+ new_options = outcome.get("options", [])
253
+ new_field_type = Literal[tuple(new_options)] if new_options else str
254
+
255
+ elif action_type == "update_visibility":
256
+ is_visible = action.get("visible")
257
+ if is_visible is not None:
258
+ new_metadata["visible"] = is_visible
259
+
260
+ # Create the new field definition based on the changes
261
+ new_field = dataclasses.field(
262
+ default=target_field_info.default,
263
+ default_factory=target_field_info.default_factory,
264
+ init=target_field_info.init,
265
+ repr=target_field_info.repr,
266
+ hash=target_field_info.hash,
267
+ compare=target_field_info.compare,
268
+ metadata=new_metadata,
269
+ )
270
+ new_field.type = new_field_type
271
+ new_field.name = target_field_info.name
272
+
273
+ # Rebuild the dataclass and replace the current instance
274
+ NewDcClass = _rebuild_dataclass_recursively(type(current_dc), target_path, new_field)
275
+ values_dict = dataclasses.asdict(current_dc)
276
+
277
+ # Correct the value if it became invalid after an `update_options` action
278
+ if action_type == "update_options":
279
+ current_value = get_nested_attr(current_dc, target_path)
280
+ new_options = outcome.get("options", [])
281
+ new_default = outcome.get("new_default")
282
+ should_update = current_value not in new_options or (new_default is not None and current_value != new_default)
283
+ if should_update:
284
+ value_to_set = new_default if new_default is not None else (new_options[0] if new_options else None)
285
+ if value_to_set is not None:
286
+ _set_nested_dict_value(values_dict, target_path, value_to_set)
287
+
288
+ # The `current_dc` instance is replaced. The next loop iteration
289
+ # will operate on this newly created instance.
290
+ current_dc = dataclass_from_dict(NewDcClass, values_dict)
291
+
292
+ # Handle actions that modify the instance directly
293
+ elif action_type == "set_value":
294
+ new_value = outcome
295
+ current_value = get_nested_attr(current_dc, target_path)
296
+ if new_value != current_value:
297
+ parent_obj, field_name = get_nested_parent_and_field(current_dc, target_path)
298
+ setattr(parent_obj, field_name, new_value)
299
+
300
+ return current_dc