|
78 | 78 | </form> |
79 | 79 | </div> |
80 | 80 |
|
81 | | - <div class="d-flex flex-row gap-2 justify-content-center pt-5" autocomplete="off"> |
82 | | - <button type="button" class="btn-cancel btn btn-warning w-25" disabled>Cancel</button> |
83 | | - <button id="btn-txt2img-execute" type="button" class="btn btn-success w-25">Generate</button> |
| 81 | + <div class="d-flex flex-row gap-2 justify-content-between pt-5" autocomplete="off"> |
| 82 | + <div class="d-flex flex-column justify-content-end w-50"> |
| 83 | + <div class="d-flex flex-row gap-2"> |
| 84 | + <button type="button" class="btn-cancel btn btn-warning w-100" disabled>Cancel</button> |
| 85 | + <button id="btn-txt2img-execute" type="button" class="btn btn-success w-100">Generate</button> |
| 86 | + </div> |
| 87 | + </div> |
| 88 | + <div class="d-flex flex-row justify-content-end w-50"> |
| 89 | + <div> |
| 90 | + <small>Mode</small> |
| 91 | + <div class="d-flex flex-row gap-1"> |
| 92 | + <select id="result-layout" class="form-control form-select form-select-sm"> |
| 93 | + <option value="Single">Single</option> |
| 94 | + <option value="Multiple">Multiple</option> |
| 95 | + </select> |
| 96 | + <button id="btn-clear" class="btn btn-sm btn-danger"> |
| 97 | + <i class="fa fa-trash"></i> |
| 98 | + </button> |
| 99 | + </div> |
| 100 | + </div> |
| 101 | + </div> |
84 | 102 | </div> |
85 | 103 | </div> |
86 | 104 | </div> |
|
89 | 107 | <div class="d-flex w-100 p-2 border border-1"> |
90 | 108 | <div id="output-container" class="d-flex flex-fill flex-wrap justify-content-center align-content-center align-items-center"> |
91 | 109 | </div> |
| 110 | + <div id="output-container-grid" class="d-flex flex-fill flex-wrap justify-content-start align-content-start align-items-start gap-3" style="overflow-y:auto"> |
| 111 | + </div> |
92 | 112 | </div> |
93 | 113 | </div> |
94 | 114 | </div> |
95 | 115 |
|
96 | 116 | <script id="progressResultTemplate" type="text/html"> |
97 | | - <div class="d-flex flex-column border border-1 p-1" style="min-width:256px"> |
| 117 | + <div class="output-progress d-flex flex-column border border-1 p-1" style="min-width:256px;"> |
98 | 118 | <div style="overflow:hidden;"> |
99 | | - <img id="img-result" width="512" src="~/images/placeholder.jpg" /> |
| 119 | + <img width="{{width}}" height="{{height}}" src="~/images/placeholder.jpg" /> |
100 | 120 | </div> |
101 | 121 | <div class="d-flex flex-column pt-2"> |
102 | 122 | <div class="progress"> |
103 | | - <div id="progress-result" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> |
| 123 | + <div class="progress-result progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> |
104 | 124 | </div> |
105 | 125 | </div> |
106 | 126 | <div class="d-flex flex-row gap-2 pt-2"> |
|
111 | 131 | </script> |
112 | 132 |
|
113 | 133 | <script id="outputResultTemplate" type="text/html"> |
114 | | - <div class="d-flex flex-column border border-1 p-1" style="min-width:256px"> |
115 | | - <div style="overflow:hidden;"> |
116 | | - <img id="img-result" style="max-width:100%;max-height:100%;width:auto;height:auto" src="{{outputImageUrl}}" /> |
| 134 | + <div class="d-flex flex-column border border-1 p-1" style="min-width:256px;"> |
| 135 | + <div style="overflow:hidden;text-align:center"> |
| 136 | + <img id="img-result" width="{{width}}" height="{{height}}" src="{{outputImageUrl}}" /> |
117 | 137 | </div> |
118 | 138 | <div class="d-flex flex-column pt-2"> |
119 | 139 | <div class="progress"> |
120 | 140 | <div class="progress-bar" role="progressbar" style="width: 100%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">Completed</div> |
121 | 141 | </div> |
122 | 142 | </div> |
123 | 143 | <div class="d-flex flex-row gap-2 pt-2"> |
124 | | - <a class="btn btn-sm btn-success w-100" href="{{outputImageUrl}}" download="{{OutputImage}}">Download</a> |
| 144 | + <div class="btn-group dropend w-100"> |
| 145 | + <a class="btn btn-sm btn-success w-100" href="{{outputImageUrl}}" download="{{OutputImage}}">Download</a> |
| 146 | + <button type="button" class="btn btn-sm btn-success dropdown-toggle dropdown-toggle-split w-25" data-bs-toggle="dropdown" aria-expanded="false"> |
| 147 | + |
| 148 | + </button> |
| 149 | + <ul class="dropdown-menu"> |
| 150 | + <li><a class="dropdown-item" href="{{outputImageUrl}}" download="{{OutputImage}}">Download PNG</a></li> |
| 151 | + <li><a class="dropdown-item" href="{{outputOptionsUrl}}" download="{{OutputOptions}}">Download JSON</a></li> |
| 152 | + </ul> |
| 153 | + </div> |
125 | 154 | <button type="button" class="btn btn-sm btn-info w-100" disabled>Image To Image</button> |
126 | 155 | </div> |
127 | 156 | </div> |
|
135 | 164 |
|
136 | 165 | const stableDiffusionTextToImage = () => { |
137 | 166 |
|
| 167 | + let diffusionProcess; |
138 | 168 | const optionsForm = $("#TextToImageParameters") |
139 | 169 | const outputContainer = $("#output-container"); |
| 170 | + const outputContainerGrid = $("#output-container-grid"); |
140 | 171 | const progressResultTemplate = $("#progressResultTemplate").html(); |
141 | 172 | const outputResultTemplate = $("#outputResultTemplate").html(); |
142 | 173 | const connection = new signalR.HubConnectionBuilder().withUrl("/StableDiffusionHub").build(); |
143 | 174 |
|
144 | | - let diffusionProcess; |
145 | | -
|
146 | 175 | const onResponse = (response) => { |
147 | 176 | if (!response) |
148 | 177 | return; |
|
167 | 196 |
|
168 | 197 | const onCanceled = (response) => { |
169 | 198 | console.log(response); |
170 | | - outputContainer.html(''); |
| 199 | + updatePlaceholderImage(); |
171 | 200 | processEnd(); |
172 | 201 | } |
173 | 202 |
|
174 | 203 | const executeTextToImage = async () => { |
175 | | -
|
176 | 204 | const diffusionParams = serializeFormToJson(optionsForm); |
177 | 205 | if (!validateForm()) |
178 | 206 | return; |
179 | 207 |
|
180 | 208 | processBegin(); |
181 | | - outputContainer.html(Mustache.render(progressResultTemplate)); |
| 209 | + updatePlaceholderImage(); |
182 | 210 | diffusionProcess = await connection |
183 | 211 | .stream("ExecuteTextToImage", diffusionParams) |
184 | 212 | .subscribe({ |
|
193 | 221 | } |
194 | 222 |
|
195 | 223 | const updateResultImage = (response) => { |
196 | | - outputContainer.html(Mustache.render(outputResultTemplate, response)); |
| 224 | + const width = getWidth(); |
| 225 | + const height = getHeight(); |
| 226 | +
|
| 227 | + const size = getSafeSize(width, height, 512); |
| 228 | + outputContainer.html(Mustache.render(outputResultTemplate, { |
| 229 | + width: size.width, |
| 230 | + height: size.height, |
| 231 | + ...response |
| 232 | + })); |
| 233 | +
|
| 234 | + const gridSize = getSafeSize(width, height, 256); |
| 235 | + outputContainerGrid.find(".output-progress").remove(); |
| 236 | + outputContainerGrid.prepend(Mustache.render(outputResultTemplate, { |
| 237 | + width: gridSize.width, |
| 238 | + height: gridSize.height, |
| 239 | + ...response |
| 240 | + })); |
| 241 | + } |
| 242 | +
|
| 243 | + const updatePlaceholderImage = () => { |
| 244 | + const width = getWidth(); |
| 245 | + const height = getHeight(); |
| 246 | +
|
| 247 | + const size = getSafeSize(width, height, 512); |
| 248 | + outputContainer.html(Mustache.render(progressResultTemplate, { |
| 249 | + width: size.width, |
| 250 | + height: size.height, |
| 251 | + })); |
| 252 | +
|
| 253 | + const gridSize = getSafeSize(width, height, 256); |
| 254 | + outputContainerGrid.find(".output-progress").remove(); |
| 255 | + outputContainerGrid.prepend(Mustache.render(progressResultTemplate, { |
| 256 | + width: gridSize.width, |
| 257 | + height: gridSize.height, |
| 258 | + })); |
197 | 259 | } |
198 | 260 |
|
199 | 261 | const updateProgress = (response) => { |
200 | 262 | const increment = Math.max(100 / response.total, 1); |
201 | 263 | const progressPercent = Math.round(Math.min(increment * response.progress, 100), 0); |
202 | | - const progressBar = $("#progress-result"); |
| 264 | + const progressBar = $(".progress-result"); |
203 | 265 | progressBar.css("width", progressPercent + "%"); |
204 | 266 | progressBar.text(progressPercent + "%"); |
205 | 267 | } |
|
219 | 281 | return optionsForm.valid(); |
220 | 282 | } |
221 | 283 |
|
| 284 | + const clearHistory = () => { |
| 285 | + outputContainer.empty(); |
| 286 | + outputContainerGrid.empty(); |
| 287 | + updatePlaceholderImage(); |
| 288 | + } |
| 289 | +
|
| 290 | + const getLayout = () => { |
| 291 | + return $("option:selected", "#result-layout").val(); |
| 292 | + } |
| 293 | +
|
| 294 | + const getWidth = () => { |
| 295 | + return +$("option:selected", "#Options_Width").val(); |
| 296 | + } |
| 297 | +
|
| 298 | + const getHeight = () => { |
| 299 | + return +$("option:selected", "#Options_Height").val(); |
| 300 | + } |
| 301 | +
|
| 302 | + const updateLayout = () => { |
| 303 | + const layout = getLayout(); |
| 304 | + if (layout == "Single") { |
| 305 | + outputContainer.removeClass("d-none").addClass("d-flex"); |
| 306 | + outputContainerGrid.removeClass("d-flex").addClass("d-none"); |
| 307 | + updatePlaceholderImage(); |
| 308 | + } |
| 309 | + else if (layout == "Multiple") { |
| 310 | + outputContainer.removeClass("d-flex").addClass("d-none"); |
| 311 | + outputContainerGrid.removeClass("d-none").addClass("d-flex"); |
| 312 | + } |
| 313 | + } |
| 314 | +
|
| 315 | + const getSafeSize = (width, height, max) => { |
| 316 | + let largest = Math.max(width, height); |
| 317 | + if (largest > max) { |
| 318 | + while (largest > max) { |
| 319 | + largest -= 16; |
| 320 | + } |
| 321 | +
|
| 322 | + const delta = Math.max(width, height) / largest; |
| 323 | + return { |
| 324 | + width: width / delta, |
| 325 | + height: height / delta |
| 326 | + }; |
| 327 | + } |
| 328 | + else if (largest < max) { |
| 329 | + while (largest < max) { |
| 330 | + largest += 16; |
| 331 | + } |
| 332 | +
|
| 333 | + const delta = largest - Math.max(width, height); |
| 334 | + return { |
| 335 | + width: width + delta, |
| 336 | + height: height + delta |
| 337 | + }; |
| 338 | + } |
| 339 | + return { |
| 340 | + width: width, |
| 341 | + height: height |
| 342 | + }; |
| 343 | + } |
| 344 | +
|
222 | 345 | const serializeFormToJson = (form) => { |
223 | 346 | const formDataJson = {}; |
224 | 347 | const formData = new FormData(document.getElementById(form.attr("id"))); |
|
244 | 367 |
|
245 | 368 |
|
246 | 369 | $(".btn-cancel").on("click", cancelDiffusion); |
| 370 | + $("#btn-clear").on("click", clearHistory); |
247 | 371 | $("#btn-txt2img-execute").on("click", executeTextToImage); |
| 372 | + $("#result-layout").on("change", updateLayout).trigger("change"); |
248 | 373 | $(".slider").on("input", function (e) { |
249 | 374 | const slider = $(this); |
250 | 375 | slider.next().text(slider.val()); |
|
0 commit comments