├── .gitignore ├── README.md ├── install.py ├── javascript └── image_browser.js ├── preload.py ├── scripts ├── image_browser.py └── wib │ └── wib_db.py └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | path_recorder.txt 3 | __pycache__ 4 | *.json 5 | *.sqlite3 6 | *.log 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## stable-diffusion-webui-images-browser 2 | 3 | A custom extension for [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui). 4 | 5 | This is an image browser for browsing past generated pictures, view their generated informations, send that information to txt2img, img2img and others, collect images to your "favorites" folder and delete the images you no longer need. 6 | 7 | ## Installation 8 | 9 | The extension can be installed directly from within the **Extensions** tab within the Webui. 10 | 11 | You can also install it manually by running the following command from within the webui directory: 12 | 13 | git clone https://github.com/AlUlkesh/stable-diffusion-webui-images-browser/ extensions/stable-diffusion-webui-images-browser 14 | 15 | and restart your stable-diffusion-webui, then you can see the new tab "Image Browser". 16 | 17 | Please be aware that when scanning a directory for the first time, the png-cache will be built. This can take several minutes, depending on the amount of images. 18 | 19 | ## Recent updates 20 | - Improved Search 21 | - Formatted EXIF display 22 | - Initial support for videos 23 | - --image-browser-tmp-db command line parameter to offload database operations to a different location 24 | - "All"-tab showing all the images from all tabs combined 25 | - Size tooltip for thumbnails 26 | - Optimized images in the thumbnail interface 27 | - Send to ControlNet 28 | - Hidable UI components 29 | - Send to openOutpaint 30 | - Regex search 31 | - Maximum aesthetic_score filter 32 | - Save ranking to EXIF option 33 | - Maintenance tab 34 | - Custom tabs 35 | - Copy/Move to directory 36 | - Keybindings 37 | - Additional sorting and filtering by EXIF data including .txt file information 38 | - Recyle bin option 39 | - Add/Remove from saved directories, via buttons 40 | - New dropdown with subdirs 41 | - Option to not show the images from subdirs 42 | - Refresh button 43 | - Sort order 44 | - View and save favorites with individual folder depth 45 | - Now also supports jpg 46 | 47 | Please also check the [discussions](https://github.com/AlUlkesh/stable-diffusion-webui-images-browser/discussions) for major update information. 48 | 49 | ## Keybindings 50 | | Key | Explanation | 51 | |---------|-------------| 52 | | `0-5` | Ranks the current image, with 0 being the last option (None) | 53 | | `F` | Adds the current image to Favorites | 54 | | `R` | Refreshes the image gallery | 55 | | `Delete` | Deletes the current image | 56 | | `Ctrl + Arrow Left` | Goes to the previous page of images | 57 | | `Ctrl + Arrow Right` | Goes to the next page of images | 58 | 59 | (Ctrl can be changed in settings) 60 | 61 | ## Credit 62 | 63 | Credit goes to the original maintainer of this extension: https://github.com/yfszzx and to major contributors https://github.com/Klace and https://github.com/EllangoK 64 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import launch 2 | 3 | if not launch.is_installed("send2trash"): 4 | launch.run_pip("install Send2Trash", "Send2Trash requirement for image browser") 5 | -------------------------------------------------------------------------------- /javascript/image_browser.js: -------------------------------------------------------------------------------- 1 | let image_browser_state = "free" 2 | let image_browser_webui_ready = false 3 | let image_browser_started = false 4 | let image_browser_console_log = "" 5 | let image_browser_debug = false 6 | let image_browser_img_show_in_progress = false 7 | 8 | function image_browser_delay(ms){return new Promise(resolve => setTimeout(resolve, ms))} 9 | 10 | onUiLoaded(image_browser_start_it_up) 11 | 12 | async function image_browser_wait_for_webui() { 13 | if (image_browser_debug) console.log("image_browser_wait_for_webui:start") 14 | await image_browser_delay(500) 15 | let sd_model = gradioApp().getElementById("setting_sd_model_checkpoint") 16 | if (!sd_model) { 17 | // Forge workaround 18 | sd_model = gradioApp().getElementsByClassName("model_selection")[0] 19 | } 20 | if (!sd_model.querySelector(".eta-bar")) { 21 | image_browser_webui_ready = true 22 | image_browser_start() 23 | } else { 24 | // Set timeout for MutationObserver 25 | const startTime = Date.now() 26 | // 40 seconds in milliseconds 27 | const timeout = 40000 28 | const webuiObserver = new MutationObserver(function(mutationsList) { 29 | if (image_browser_debug) console.log("webuiObserver:start") 30 | let found = false 31 | outerLoop: for (let i = 0; i < mutationsList.length; i++) { 32 | let mutation = mutationsList[i]; 33 | if (mutation.type === "childList") { 34 | for (let j = 0; j < mutation.removedNodes.length; j++) { 35 | let node = mutation.removedNodes[j]; 36 | if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains("eta-bar")) { 37 | found = true 38 | break outerLoop; 39 | } 40 | } 41 | } 42 | } 43 | if (found || (Date.now() - startTime > timeout)) { 44 | image_browser_webui_ready = true 45 | webuiObserver.disconnect() 46 | if (image_browser_debug) console.log("webuiObserver:end") 47 | image_browser_start() 48 | } 49 | }) 50 | webuiObserver.observe(gradioApp(), { childList:true, subtree:true }) 51 | } 52 | if (image_browser_debug) console.log("image_browser_wait_for_webui:end") 53 | } 54 | 55 | async function image_browser_start_it_up() { 56 | if (image_browser_debug) console.log("image_browser_start_it_up:start") 57 | container = gradioApp().getElementById("image_browser_tabs_container") 58 | let controls = container.querySelectorAll('[id*="_control_"]') 59 | controls.forEach(function(control) { 60 | control.style.pointerEvents = "none" 61 | control.style.cursor = "not-allowed" 62 | control.style.opacity = "0.65" 63 | }) 64 | /* 65 | let warnings = container.querySelectorAll('[id*="_warning_box"]') 66 | warnings.forEach(function(warning) { 67 | warning.innerHTML = '
Waiting for webui...' 68 | }) 69 | */ 70 | 71 | image_browser_wait_for_webui() 72 | if (image_browser_debug) console.log("image_browser_start_it_up:end") 73 | } 74 | 75 | async function image_browser_lock(reason) { 76 | if (image_browser_debug) console.log("image_browser_lock:start") 77 | // Wait until lock removed 78 | let i = 0 79 | while (image_browser_state != "free") { 80 | await image_browser_delay(200) 81 | i = i + 1 82 | if (i === 150) { 83 | throw new Error("Still locked after 30 seconds. Please Reload UI.") 84 | } 85 | } 86 | // Lock 87 | image_browser_state = reason 88 | if (image_browser_debug) console.log("image_browser_lock:end") 89 | } 90 | 91 | async function image_browser_unlock() { 92 | if (image_browser_debug) console.log("image_browser_unlock:start") 93 | image_browser_state = "free" 94 | if (image_browser_debug) console.log("image_browser_unlock:end") 95 | } 96 | 97 | const image_browser_click_image = async function() { 98 | if (image_browser_debug) console.log("image_browser_click_image:start") 99 | await image_browser_lock("image_browser_click_image") 100 | const tab_base_tag = image_browser_current_tab() 101 | const container = gradioApp().getElementById(tab_base_tag + "_image_browser_container") 102 | let child = this 103 | let index = 0 104 | while ((child = child.previousSibling) != null) { 105 | if (child.nodeType === Node.ELEMENT_NODE) { 106 | index = index + 1 107 | } 108 | } 109 | const set_btn = container.querySelector(".image_browser_set_index") 110 | let curr_idx 111 | try { 112 | curr_idx = set_btn.getAttribute("img_index") 113 | } catch (e) { 114 | curr_idx = -1 115 | } 116 | if (curr_idx != index) { 117 | set_btn.setAttribute("img_index", index) 118 | } 119 | await image_browser_unlock() 120 | set_btn.click() 121 | if (image_browser_debug) console.log("image_browser_click_image:end") 122 | } 123 | 124 | async function image_browser_get_current_img(tab_base_tag, img_index, page_index, filenames, turn_page_switch, image_gallery) { 125 | if (image_browser_debug) console.log("image_browser_get_current_img:start") 126 | await image_browser_lock("image_browser_get_current_img") 127 | img_index = gradioApp().getElementById(tab_base_tag + '_image_browser_set_index').getAttribute("img_index") 128 | await image_browser_unlock() 129 | if (image_browser_debug) console.log("image_browser_get_current_img:end") 130 | return [ 131 | tab_base_tag, 132 | img_index, 133 | page_index, 134 | filenames, 135 | turn_page_switch, 136 | image_gallery 137 | ] 138 | } 139 | 140 | async function image_browser_refresh_current_page_preview() { 141 | if (image_browser_debug) console.log("image_browser_refresh_current_page_preview:start") 142 | await image_browser_delay(200) 143 | const preview_div = gradioApp().querySelector('.preview') 144 | if (preview_div === null) { 145 | if (image_browser_debug) console.log("image_browser_refresh_current_page_preview:end") 146 | return 147 | } 148 | const tab_base_tag = image_browser_current_tab() 149 | const gallery = gradioApp().querySelector(`#${tab_base_tag}_image_browser`) 150 | const set_btn = gallery.querySelector(".image_browser_set_index") 151 | const curr_idx = parseInt(set_btn.getAttribute("img_index")) 152 | // no loading animation, so click immediately 153 | const gallery_items = gallery.querySelectorAll(".thumbnail-item") 154 | const curr_image = gallery_items[curr_idx] 155 | curr_image.click() 156 | if (image_browser_debug) console.log("image_browser_refresh_current_page_preview:end") 157 | } 158 | 159 | async function image_browser_turnpage(tab_base_tag) { 160 | if (image_browser_debug) console.log("image_browser_turnpage:start") 161 | while (!image_browser_started) { 162 | await image_browser_delay(200) 163 | } 164 | const gallery = gradioApp().querySelector(`#${tab_base_tag}_image_browser`) 165 | let clear 166 | try { 167 | clear = gallery.querySelector("button[aria-label='Clear']") 168 | if (clear) { 169 | clear.click() 170 | } 171 | } catch (e) { 172 | console.error(e) 173 | } 174 | 175 | // Scroll to top of grid 176 | try { 177 | const gridWrapElement = document.getElementById(tab_base_tag + "_image_browser_gallery").querySelector("div.grid-wrap") 178 | gridWrapElement.scrollTop = 0 179 | gridWrapElement.scrollLeft= 0 180 | } catch (e) { } 181 | 182 | if (image_browser_debug) console.log("image_browser_turnpage:end") 183 | } 184 | 185 | async function image_browser_select_image(tab_base_tag, img_index, select_image) { 186 | if (image_browser_debug) console.log("image_browser_select_image:start") 187 | if (select_image) { 188 | await image_browser_lock("image_browser_select_image") 189 | const del_img_btn = gradioApp().getElementById(tab_base_tag + "_image_browser_del_img_btn") 190 | // Prevent delete button spam 191 | del_img_btn.style.pointerEvents = "none" 192 | del_img_btn.style.cursor = "not-allowed" 193 | del_img_btn.style.opacity = "0.65" 194 | 195 | const gallery = gradioApp().getElementById(tab_base_tag + "_image_browser_gallery") 196 | const gallery_items = gallery.querySelectorAll(".thumbnail-item") 197 | if (img_index >= gallery_items.length || gallery_items.length == 0) { 198 | const refreshBtn = gradioApp().getElementById(tab_base_tag + "_image_browser_renew_page") 199 | refreshBtn.dispatchEvent(new Event("click")) 200 | } else { 201 | const curr_image = gallery_items[img_index] 202 | curr_image.click() 203 | } 204 | await image_browser_unlock() 205 | } 206 | if (image_browser_debug) console.log("image_browser_select_image:end") 207 | } 208 | 209 | async function image_browser_gototab(tabname) { 210 | if (image_browser_debug) console.log("image_browser_gototab:start") 211 | await image_browser_lock("image_browser_gototab") 212 | 213 | tabNav = gradioApp().querySelector(".tab-nav") 214 | const tabNavChildren = tabNav.children 215 | let tabNavButtonNum 216 | if (typeof tabname === "number") { 217 | let buttonCnt = 0 218 | for (let i = 0; i < tabNavChildren.length; i++) { 219 | if (tabNavChildren[i].tagName === "BUTTON") { 220 | if (buttonCnt === tabname) { 221 | tabNavButtonNum = i 222 | break 223 | } 224 | buttonCnt++ 225 | } 226 | } 227 | } else { 228 | for (let i = 0; i < tabNavChildren.length; i++) { 229 | if (tabNavChildren[i].tagName === "BUTTON" && tabNavChildren[i].textContent.trim() === tabname) { 230 | tabNavButtonNum = i 231 | break 232 | } 233 | } 234 | } 235 | let tabNavButton = tabNavChildren[tabNavButtonNum] 236 | tabNavButton.click() 237 | 238 | // Wait for click-action to complete 239 | const startTime = Date.now() 240 | // 60 seconds in milliseconds 241 | const timeout = 60000 242 | 243 | await image_browser_delay(100) 244 | while (!tabNavButton.classList.contains("selected")) { 245 | tabNavButton = tabNavChildren[tabNavButtonNum] 246 | if (Date.now() - startTime > timeout) { 247 | throw new Error("image_browser_gototab: 60 seconds have passed") 248 | } 249 | await image_browser_delay(200) 250 | } 251 | 252 | await image_browser_unlock() 253 | if (image_browser_debug) console.log("image_browser_gototab:end") 254 | } 255 | 256 | async function image_browser_get_image_for_ext(tab_base_tag, image_index) { 257 | if (image_browser_debug) console.log("image_browser_get_image_for_ext:start") 258 | const image_browser_image = gradioApp().querySelectorAll(`#${tab_base_tag}_image_browser_gallery .thumbnail-item`)[image_index] 259 | 260 | const canvas = document.createElement("canvas") 261 | const image = document.createElement("img") 262 | image.src = image_browser_image.querySelector("img").src 263 | 264 | await image.decode() 265 | 266 | canvas.width = image.width 267 | canvas.height = image.height 268 | 269 | canvas.getContext("2d").drawImage(image, 0, 0) 270 | 271 | if (image_browser_debug) console.log("image_browser_get_image_for_ext:end") 272 | return canvas.toDataURL() 273 | } 274 | 275 | function image_browser_openoutpaint_send(tab_base_tag, image_index, image_browser_prompt, image_browser_neg_prompt, name = "WebUI Resource") { 276 | if (image_browser_debug) console.log("image_browser_openoutpaint_send:start") 277 | image_browser_get_image_for_ext(tab_base_tag, image_index) 278 | .then((dataURL) => { 279 | // Send to openOutpaint 280 | openoutpaint_send_image(dataURL, name) 281 | 282 | // Send prompt to openOutpaint 283 | const tab = get_uiCurrentTabContent().id 284 | 285 | const prompt = image_browser_prompt 286 | const negPrompt = image_browser_neg_prompt 287 | openoutpaint.frame.contentWindow.postMessage({ 288 | key: openoutpaint.key, 289 | type: "openoutpaint/set-prompt", 290 | prompt, 291 | negPrompt, 292 | }) 293 | 294 | // Change Tab 295 | image_browser_gototab("openOutpaint") 296 | }) 297 | if (image_browser_debug) console.log("image_browser_openoutpaint_send:end") 298 | } 299 | 300 | async function image_browser_controlnet_send(toTabNum, tab_base_tag, image_index, controlnetNum, controlnetType) { 301 | if (image_browser_debug) console.log("image_browser_controlnet_send:start") 302 | // Logic originally based on github.com/fkunn1326/openpose-editor 303 | const dataURL = await image_browser_get_image_for_ext(tab_base_tag, image_index) 304 | const blob = await (await fetch(dataURL)).blob() 305 | const dt = new DataTransfer() 306 | dt.items.add(new File([blob], "ImageBrowser.png", { type: blob.type })) 307 | const list = dt.files 308 | 309 | await image_browser_gototab(toTabNum) 310 | const current_tabid = image_browser_webui_current_tab() 311 | const current_tab = current_tabid.replace("tab_", "") 312 | const tab_controlnet = gradioApp().getElementById(current_tab + "_controlnet") 313 | let accordion = tab_controlnet.querySelector("#controlnet > .label-wrap > .icon") 314 | if (accordion.style.transform.includes("rotate(90deg)")) { 315 | accordion.click() 316 | // Wait for click-action to complete 317 | const startTime = Date.now() 318 | // 60 seconds in milliseconds 319 | const timeout = 60000 320 | 321 | await image_browser_delay(100) 322 | while (accordion.style.transform.includes("rotate(90deg)")) { 323 | accordion = tab_controlnet.querySelector("#controlnet > .label-wrap > .icon") 324 | if (Date.now() - startTime > timeout) { 325 | throw new Error("image_browser_controlnet_send/accordion: 60 seconds have passed") 326 | } 327 | await image_browser_delay(200) 328 | } 329 | } 330 | 331 | let inputImage 332 | let inputContainer 333 | if (controlnetType == "single") { 334 | inputImage = gradioApp().getElementById(current_tab + "_controlnet_ControlNet_input_image") 335 | } else { 336 | const tabs = gradioApp().getElementById(current_tab + "_controlnet_tabs") 337 | const tab_num = (parseInt(controlnetNum) + 1).toString() 338 | tab_button = tabs.querySelector(".tab-nav > button:nth-child(" + tab_num + ")") 339 | tab_button.click() 340 | // Wait for click-action to complete 341 | const startTime = Date.now() 342 | // 60 seconds in milliseconds 343 | const timeout = 60000 344 | 345 | await image_browser_delay(100) 346 | while (!tab_button.classList.contains("selected")) { 347 | tab_button = tabs.querySelector(".tab-nav > button:nth-child(" + tab_num + ")") 348 | if (Date.now() - startTime > timeout) { 349 | throw new Error("image_browser_controlnet_send/tabs: 60 seconds have passed") 350 | } 351 | await image_browser_delay(200) 352 | } 353 | inputImage = gradioApp().getElementById(current_tab + "_controlnet_ControlNet-" + controlnetNum.toString() + "_input_image") 354 | } 355 | try { 356 | inputContainer = inputImage.querySelector('div[data-testid="image"]') 357 | } catch (e) {} 358 | 359 | const input = inputContainer.querySelector("input[type='file']") 360 | 361 | let clear 362 | try { 363 | clear = inputContainer.querySelector("button[aria-label='Remove Image']") 364 | if (clear) { 365 | clear.click() 366 | } 367 | } catch (e) { 368 | console.error(e) 369 | } 370 | 371 | try { 372 | // Wait for click-action to complete 373 | const startTime = Date.now() 374 | // 60 seconds in milliseconds 375 | const timeout = 60000 376 | while (clear) { 377 | clear = inputContainer.querySelector("button[aria-label='Remove Image']") 378 | if (Date.now() - startTime > timeout) { 379 | throw new Error("image_browser_controlnet_send/clear: 60 seconds have passed") 380 | } 381 | await image_browser_delay(200) 382 | } 383 | } catch (e) { 384 | console.error(e) 385 | } 386 | 387 | input.value = "" 388 | input.files = list 389 | const event = new Event("change", { "bubbles": true, "composed": true }) 390 | input.dispatchEvent(event) 391 | if (image_browser_debug) console.log("image_browser_controlnet_send:end") 392 | } 393 | 394 | function image_browser_controlnet_send_txt2img(tab_base_tag, image_index, controlnetNum, controlnetType) { 395 | image_browser_controlnet_send(0, tab_base_tag, image_index, controlnetNum, controlnetType) 396 | } 397 | 398 | function image_browser_controlnet_send_img2img(tab_base_tag, image_index, controlnetNum, controlnetType) { 399 | image_browser_controlnet_send(1, tab_base_tag, image_index, controlnetNum, controlnetType) 400 | } 401 | 402 | function image_browser_class_add(tab_base_tag) { 403 | gradioApp().getElementById(tab_base_tag + '_image_browser').classList.add("image_browser_container") 404 | gradioApp().getElementById(tab_base_tag + '_image_browser_set_index').classList.add("image_browser_set_index") 405 | gradioApp().getElementById(tab_base_tag + '_image_browser_del_img_btn').classList.add("image_browser_del_img_btn") 406 | gradioApp().getElementById(tab_base_tag + '_image_browser_gallery').classList.add("image_browser_gallery") 407 | } 408 | 409 | function btnClickHandler(tab_base_tag, btn) { 410 | if (image_browser_debug) console.log("btnClickHandler:start") 411 | const tabs_box = gradioApp().getElementById("image_browser_tabs_container") 412 | if (!tabs_box.classList.contains(tab_base_tag)) { 413 | gradioApp().getElementById(tab_base_tag + "_image_browser_renew_page").click() 414 | tabs_box.classList.add(tab_base_tag) 415 | } 416 | if (image_browser_debug) console.log("btnClickHandler:end") 417 | } 418 | 419 | function image_browser_init() { 420 | if (image_browser_debug) console.log("image_browser_init:start") 421 | const tab_base_tags = gradioApp().getElementById("image_browser_tab_base_tags_list") 422 | if (tab_base_tags) { 423 | const image_browser_tab_base_tags_list = tab_base_tags.querySelector("textarea").value.split(",") 424 | image_browser_tab_base_tags_list.forEach(function(tab_base_tag) { 425 | image_browser_class_add(tab_base_tag) 426 | }) 427 | 428 | const tab_btns = gradioApp().getElementById("image_browser_tabs_container").querySelector("div").querySelectorAll("button") 429 | tab_btns.forEach(function(btn, i) { 430 | const tab_base_tag = image_browser_tab_base_tags_list[i] 431 | btn.setAttribute("tab_base_tag", tab_base_tag) 432 | btn.removeEventListener('click', () => btnClickHandler(tab_base_tag, btn)) 433 | btn.addEventListener('click', () => btnClickHandler(tab_base_tag, btn)) 434 | }) 435 | //preload 436 | if (gradioApp().getElementById("image_browser_preload").querySelector("input").checked) { 437 | setTimeout(function(){tab_btns[0].click()}, 100) 438 | } 439 | } 440 | image_browser_keydown() 441 | 442 | const image_browser_swipe = gradioApp().getElementById("image_browser_swipe").getElementsByTagName("input")[0] 443 | if (image_browser_swipe.checked) { 444 | image_browser_touch() 445 | } 446 | if (image_browser_debug) console.log("image_browser_init:end") 447 | } 448 | 449 | async function image_browser_wait_for_gallery_btn(tab_base_tag){ 450 | if (image_browser_debug) console.log("image_browser_wait_for_gallery_btn:start") 451 | await image_browser_delay(100) 452 | while (!gradioApp().getElementById(image_browser_current_tab() + "_image_browser_gallery").getElementsByClassName("thumbnail-item")) { 453 | await image_browser_delay(200) 454 | } 455 | if (image_browser_debug) console.log("image_browser_wait_for_gallery_btn:end") 456 | } 457 | 458 | function image_browser_hijack_console_log() { 459 | (function () { 460 | const oldLog = console.log 461 | console.log = function (message) { 462 | const formattedTime = new Date().toISOString().slice(0, -5).replace(/[TZ]/g, ' ').trim().replace(/\s+/g, '-').replace(/:/g, '-') 463 | image_browser_console_log = image_browser_console_log + formattedTime + " " + "image_browser.js: " + message + "\n" 464 | oldLog.apply(console, arguments) 465 | } 466 | })() 467 | image_browser_debug = true 468 | } 469 | 470 | function get_js_logs() { 471 | log_to_py = image_browser_console_log 472 | image_browser_console_log = "" 473 | return log_to_py 474 | } 475 | 476 | function isNumeric(str) { 477 | if (typeof str != "string") return false 478 | return !isNaN(str) && !isNaN(parseFloat(str)) 479 | } 480 | 481 | function image_browser_start() { 482 | if (image_browser_debug) console.log("image_browser_start:start") 483 | image_browser_init() 484 | const mutationObserver = new MutationObserver(function(mutationsList) { 485 | const tab_base_tags = gradioApp().getElementById("image_browser_tab_base_tags_list") 486 | if (tab_base_tags) { 487 | const image_browser_tab_base_tags_list = tab_base_tags.querySelector("textarea").value.split(",") 488 | image_browser_tab_base_tags_list.forEach(function(tab_base_tag) { 489 | image_browser_class_add(tab_base_tag) 490 | const tab_gallery_items = gradioApp().querySelectorAll('#' + tab_base_tag + '_image_browser .thumbnail-item') 491 | 492 | const image_browser_img_info_json = gradioApp().getElementById(tab_base_tag + "_image_browser_img_info").querySelector('[data-testid="textbox"]').value 493 | const image_browser_img_info = JSON.parse(image_browser_img_info_json) 494 | const filenames = Object.keys(image_browser_img_info) 495 | 496 | tab_gallery_items.forEach(function(gallery_item, i) { 497 | gallery_item.removeEventListener('click', image_browser_click_image, true) 498 | gallery_item.addEventListener('click', image_browser_click_image, true) 499 | 500 | const filename = filenames[i] 501 | try { 502 | let x = image_browser_img_info[filename].x 503 | let y = image_browser_img_info[filename].y 504 | if (isNumeric(x) && isNumeric(y)) { 505 | gallery_item.title = x + "x" + y 506 | } 507 | } catch (e) {} 508 | 509 | document.onkeyup = async function(e) { 510 | if (!image_browser_active()) { 511 | if (image_browser_debug) console.log("image_browser_start:end") 512 | return 513 | } 514 | const current_tab = image_browser_current_tab() 515 | image_browser_wait_for_gallery_btn(current_tab).then(() => { 516 | let gallery_btn 517 | gallery_btn = gradioApp().getElementById(current_tab + "_image_browser_gallery").querySelector(".thumbnail-item .selected") 518 | gallery_btn = gallery_btn && gallery_btn.length > 0 ? gallery_btn[0] : null 519 | if (gallery_btn) { 520 | image_browser_click_image.call(gallery_btn) 521 | } 522 | }) 523 | } 524 | }) 525 | 526 | const cls_btn = gradioApp().getElementById(tab_base_tag + '_image_browser_gallery').querySelector("svg") 527 | if (cls_btn) { 528 | cls_btn.removeEventListener('click', () => image_browser_renew_page(tab_base_tag), false) 529 | cls_btn.addEventListener('click', () => image_browser_renew_page(tab_base_tag), false) 530 | } 531 | }) 532 | const debug_level_option = gradioApp().getElementById("image_browser_debug_level_option").querySelector("textarea").value 533 | if ((debug_level_option == 'javascript' || debug_level_option == 'capture') && !image_browser_debug) { 534 | image_browser_hijack_console_log() 535 | } 536 | } 537 | }) 538 | mutationObserver.observe(gradioApp(), { childList:true, subtree:true }) 539 | image_browser_started = true 540 | image_browser_activate_controls() 541 | if (image_browser_debug) console.log("image_browser_start:end") 542 | } 543 | 544 | async function image_browser_activate_controls() { 545 | if (image_browser_debug) console.log("image_browser_activate_controls:start") 546 | await image_browser_delay(500) 547 | container = gradioApp().getElementById("image_browser_tabs_container") 548 | let controls = container.querySelectorAll('[id*="_control_"]') 549 | controls.forEach(function(control) { 550 | control.style.pointerEvents = "auto" 551 | control.style.cursor = "default" 552 | control.style.opacity = "1" 553 | }) 554 | /* 555 | let warnings = container.querySelectorAll('[id*="_warning_box"]') 556 | warnings.forEach(function(warning) { 557 | warning.innerHTML = "" 558 | }) 559 | */ 560 | if (image_browser_debug) console.log("image_browser_activate_controls:end") 561 | } 562 | 563 | function image_browser_img_show_progress_update(tab_base_tag) { 564 | if (image_browser_debug) console.log("image_browser_img_show_progress_update:start") 565 | image_browser_img_show_in_progress = false 566 | // Prevent delete button spam 567 | const del_img_btn = gradioApp().getElementById(tab_base_tag + "_image_browser_del_img_btn") 568 | del_img_btn.style.pointerEvents = "auto" 569 | del_img_btn.style.cursor = "default" 570 | del_img_btn.style.opacity = "1" 571 | if (image_browser_debug) console.log("image_browser_img_show_progress_update:end") 572 | } 573 | 574 | function image_browser_renew_page(tab_base_tag) { 575 | if (image_browser_debug) console.log("image_browser_renew_page:start") 576 | gradioApp().getElementById(tab_base_tag + '_image_browser_renew_page').click() 577 | if (image_browser_debug) console.log("image_browser_renew_page:end") 578 | } 579 | 580 | function image_browser_current_tab() { 581 | if (image_browser_debug) console.log("image_browser_current_tab:start") 582 | const tabs = gradioApp().getElementById("image_browser_tabs_container").querySelectorAll('[id$="_image_browser_container"]') 583 | const tab_base_tags = gradioApp().getElementById("image_browser_tab_base_tags_list") 584 | const image_browser_tab_base_tags_list = tab_base_tags.querySelector("textarea").value.split(",").sort((a, b) => b.length - a.length) 585 | for (const element of tabs) { 586 | if (element.style.display === "block") { 587 | const id = element.id 588 | const tab_base_tag = image_browser_tab_base_tags_list.find(element => id.startsWith(element)) || null 589 | if (image_browser_debug) console.log("image_browser_current_tab:end") 590 | return tab_base_tag 591 | } 592 | } 593 | if (image_browser_debug) console.log("image_browser_current_tab:end") 594 | } 595 | 596 | function image_browser_webui_current_tab() { 597 | if (image_browser_debug) console.log("image_browser_webui_current_tab:start") 598 | const tabs = gradioApp().querySelectorAll("#tabs > [id^='tab_']") 599 | let id 600 | for (const element of tabs) { 601 | if (element.style.display === "block") { 602 | id = element.id 603 | break 604 | } 605 | } 606 | if (image_browser_debug) console.log("image_browser_webui_current_tab:end") 607 | return id 608 | } 609 | 610 | function image_browser_active() { 611 | if (image_browser_debug) console.log("image_browser_active:start") 612 | const ext_active = gradioApp().getElementById("tab_image_browser") 613 | if (image_browser_debug) console.log("image_browser_active:end") 614 | return ext_active && ext_active.style.display !== "none" 615 | } 616 | 617 | async function image_browser_delete_key(tab_base_tag) { 618 | const deleteBtn = gradioApp().getElementById(tab_base_tag + "_image_browser_del_img_btn") 619 | 620 | // Wait for img_show to end and deleteBtn to be active again 621 | const startTime = Date.now() 622 | // 60 seconds in milliseconds 623 | const timeout = 60000 624 | 625 | await image_browser_delay(100) 626 | while (image_browser_img_show_in_progress || deleteBtn.style.pointerEvents == "none") { 627 | if (Date.now() - startTime > timeout) { 628 | throw new Error("image_browser_delete_key: 60 seconds have passed / " + image_browser_img_show_in_progress + " / " + deleteBtn.style.pointerEvents) 629 | } 630 | await image_browser_delay(200) 631 | } 632 | 633 | deleteBtn.dispatchEvent(new Event("click")) 634 | } 635 | 636 | function image_browser_keydown() { 637 | if (image_browser_debug) console.log("image_browser_keydown:start") 638 | gradioApp().addEventListener("keydown", function(event) { 639 | // If we are not on the Image Browser Extension, dont listen for keypresses 640 | if (!image_browser_active()) { 641 | if (image_browser_debug) console.log("image_browser_keydown:end") 642 | return 643 | } 644 | 645 | // If the user is typing in an input field, dont listen for keypresses 646 | let target 647 | if (!event.composed) { // We shouldn't get here as the Shadow DOM is always active, but just in case 648 | target = event.target 649 | } else { 650 | target = event.composedPath()[0] 651 | } 652 | if (!target || target.nodeName === "INPUT" || target.nodeName === "TEXTAREA") { 653 | if (image_browser_debug) console.log("image_browser_keydown:end") 654 | return 655 | } 656 | 657 | const tab_base_tag = image_browser_current_tab() 658 | 659 | // Listens for keypresses 0-5 and updates the corresponding ranking (0 is the last option, None) 660 | if (event.code >= "Digit0" && event.code <= "Digit5") { 661 | const selectedValue = event.code.charAt(event.code.length - 1) 662 | const radioInputs = gradioApp().getElementById(tab_base_tag + "_control_image_browser_ranking").getElementsByTagName("input") 663 | for (const input of radioInputs) { 664 | if (input.value === selectedValue || (selectedValue === '0' && input === radioInputs[radioInputs.length - 1])) { 665 | input.checked = true 666 | input.dispatchEvent(new Event("change")) 667 | break 668 | } 669 | } 670 | } 671 | 672 | const mod_keys = gradioApp().querySelector(`#${tab_base_tag}_image_browser_mod_keys textarea`).value 673 | let modifiers_pressed = false 674 | if (mod_keys.indexOf("C") !== -1 && mod_keys.indexOf("S") !== -1) { 675 | if (event.ctrlKey && event.shiftKey) { 676 | modifiers_pressed = true 677 | } 678 | } else if (mod_keys.indexOf("S") !== -1) { 679 | if (!event.ctrlKey && event.shiftKey) { 680 | modifiers_pressed = true 681 | } 682 | } else { 683 | if (event.ctrlKey && !event.shiftKey) { 684 | modifiers_pressed = true 685 | } 686 | } 687 | 688 | let modifiers_none = false 689 | if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) { 690 | modifiers_none = true 691 | } 692 | 693 | if (event.code == "KeyF" && modifiers_none) { 694 | if (tab_base_tag == "image_browser_tab_favorites") { 695 | if (image_browser_debug) console.log("image_browser_keydown:end") 696 | return 697 | } 698 | const favoriteBtn = gradioApp().getElementById(tab_base_tag + "_image_browser_favorites_btn") 699 | favoriteBtn.dispatchEvent(new Event("click")) 700 | } 701 | 702 | if (event.code == "KeyR" && modifiers_none) { 703 | const refreshBtn = gradioApp().getElementById(tab_base_tag + "_image_browser_renew_page") 704 | refreshBtn.dispatchEvent(new Event("click")) 705 | } 706 | 707 | if (event.code == "Delete" && modifiers_none) { 708 | image_browser_delete_key(tab_base_tag) 709 | } 710 | 711 | if (event.code == "ArrowLeft" && modifiers_pressed) { 712 | const prevBtn = gradioApp().getElementById(tab_base_tag + "_control_image_browser_prev_page") 713 | prevBtn.dispatchEvent(new Event("click")) 714 | } 715 | 716 | if (event.code == "ArrowLeft" && modifiers_none) { 717 | image_browser_img_show_in_progress = true 718 | const tab_base_tag = image_browser_current_tab() 719 | const gallery = gradioApp().querySelector(`#${tab_base_tag}_image_browser`) 720 | const gallery_items = gallery.querySelectorAll(".thumbnail-item") 721 | const thumbnails = gallery_items.length / 2 722 | const set_btn = gradioApp().querySelector(`#${tab_base_tag}_image_browser .image_browser_set_index`) 723 | const curr_idx = parseInt(set_btn.getAttribute("img_index")) 724 | let new_idx = curr_idx - 1 725 | if (new_idx < 0) { 726 | new_idx = thumbnails - 1 727 | } 728 | set_btn.setAttribute("img_index", new_idx) 729 | image_browser_refresh_current_page_preview() 730 | } 731 | 732 | if (event.code == "ArrowRight" && modifiers_pressed) { 733 | const nextBtn = gradioApp().getElementById(tab_base_tag + "_control_image_browser_next_page") 734 | nextBtn.dispatchEvent(new Event("click")) 735 | } 736 | 737 | if (event.code == "ArrowRight" && modifiers_none) { 738 | image_browser_img_show_in_progress = true 739 | const tab_base_tag = image_browser_current_tab() 740 | const gallery = gradioApp().querySelector(`#${tab_base_tag}_image_browser`) 741 | const gallery_items = gallery.querySelectorAll(".thumbnail-item") 742 | const thumbnails = gallery_items.length / 2 743 | const set_btn = gradioApp().querySelector(`#${tab_base_tag}_image_browser .image_browser_set_index`) 744 | const curr_idx = parseInt(set_btn.getAttribute("img_index")) 745 | let new_idx = curr_idx + 1 746 | if (new_idx > thumbnails - 1) { 747 | new_idx = 0 748 | } 749 | set_btn.setAttribute("img_index", new_idx) 750 | image_browser_refresh_current_page_preview() 751 | } 752 | }) 753 | if (image_browser_debug) console.log("image_browser_keydown:end") 754 | } 755 | 756 | function image_browser_touch() { 757 | if (image_browser_debug) console.log("image_browser_touch:start") 758 | let touchStartX = 0 759 | let touchEndX = 0 760 | gradioApp().addEventListener("touchstart", function(event) { 761 | if (!image_browser_active()) { 762 | if (image_browser_debug) console.log("image_browser_touch:end") 763 | return 764 | } 765 | touchStartX = event.touches[0].clientX; 766 | }) 767 | gradioApp().addEventListener("touchend", function(event) { 768 | if (!image_browser_active()) { 769 | if (image_browser_debug) console.log("image_browser_touch:end") 770 | return 771 | } 772 | touchEndX = event.changedTouches[0].clientX 773 | const touchDiffX = touchStartX - touchEndX 774 | if (touchDiffX > 50) { 775 | const tab_base_tag = image_browser_current_tab() 776 | const set_btn = gradioApp().querySelector(`#${tab_base_tag}_image_browser .image_browser_set_index`) 777 | const curr_idx = parseInt(set_btn.getAttribute("img_index")) 778 | if (curr_idx >= 1) { 779 | set_btn.setAttribute("img_index", curr_idx - 1) 780 | image_browser_refresh_current_page_preview() 781 | } 782 | } else if (touchDiffX < -50) { 783 | const tab_base_tag = image_browser_current_tab() 784 | const gallery = gradioApp().querySelector(`#${tab_base_tag}_image_browser`) 785 | const gallery_items = gallery.querySelectorAll(".thumbnail-item") 786 | const thumbnails = gallery_items.length / 2 787 | const set_btn = gradioApp().querySelector(`#${tab_base_tag}_image_browser .image_browser_set_index`) 788 | const curr_idx = parseInt(set_btn.getAttribute("img_index")) 789 | if (curr_idx + 1 < thumbnails) { 790 | set_btn.setAttribute("img_index", curr_idx + 1) 791 | image_browser_refresh_current_page_preview() 792 | } 793 | } 794 | }) 795 | if (image_browser_debug) console.log("image_browser_touch:end") 796 | } 797 | -------------------------------------------------------------------------------- /preload.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def preload(parser): 4 | parser.add_argument( 5 | "--image-browser-tmp-db", 6 | action="store_true", 7 | help="Copy database file to and from /tmp when transacting (workaround for filesystems sqlite does not support)" 8 | ) -------------------------------------------------------------------------------- /scripts/image_browser.py: -------------------------------------------------------------------------------- 1 | import gradio as gr 2 | import codecs 3 | import csv 4 | import importlib 5 | import json 6 | import logging 7 | import math 8 | import os 9 | import platform 10 | import random 11 | import re 12 | import shutil 13 | import stat 14 | import subprocess as sp 15 | import sys 16 | import tempfile 17 | import time 18 | import torch 19 | import traceback 20 | import hashlib 21 | import modules.extras 22 | import modules.images 23 | import modules.ui 24 | from datetime import datetime 25 | from modules import paths, shared, script_callbacks, scripts, images 26 | from modules.shared import opts, cmd_opts 27 | from modules.ui_common import plaintext_to_html 28 | from modules.ui_components import ToolButton, DropdownMulti 29 | from PIL import Image, ImageOps, UnidentifiedImageError, ImageDraw 30 | from packaging import version 31 | from pathlib import Path 32 | from typing import List, Tuple 33 | from itertools import chain 34 | from io import StringIO 35 | 36 | try: 37 | from scripts.wib import wib_db 38 | except ModuleNotFoundError: 39 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "scripts"))) 40 | from wib import wib_db 41 | 42 | try: 43 | from send2trash import send2trash 44 | send2trash_installed = True 45 | except ImportError: 46 | print("Image Browser: send2trash is not installed. Recycle bin cannot be used.") 47 | send2trash_installed = False 48 | 49 | try: 50 | import cv2 51 | opencv_installed = True 52 | except ImportError: 53 | print("Image Browser: opencv is not installed. Video related actions cannot be performed.") 54 | opencv_installed = False 55 | 56 | try: 57 | from modules import generation_parameters_copypaste as sendto 58 | except ImportError: 59 | from modules import infotext_utils as sendto 60 | 61 | try: 62 | from modules_forge import forge_version 63 | forge = True 64 | except ImportError: 65 | forge = False 66 | 67 | # Force reload wib_db, as it doesn't get reloaded otherwise, if an extension update is started from webui 68 | importlib.reload(wib_db) 69 | 70 | yappi_do = False 71 | 72 | components_list = ["Sort by", "Filename keyword search", "EXIF keyword search", "Ranking Filter", "Aesthestic Score", "Generation Info", "File Name", "File Time", "Open Folder", "Send to buttons", "Copy to directory", "Gallery Controls Bar", "Ranking Bar", "Delete Bar", "Additional Generation Info"] 73 | 74 | num_of_imgs_per_page = 0 75 | loads_files_num = 0 76 | image_ext_list = [".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp", ".svg"] 77 | video_ext_list = [".mp4", ".mov", ".avi", ".wmv", ".flv", ".mkv", ".webm", ".mpeg", ".mpg", ".3gp", ".ogv", ".m4v"] 78 | if not opencv_installed: 79 | video_ext_list = [] 80 | exif_cache = {} 81 | aes_cache = {} 82 | none_select = "Nothing selected" 83 | refresh_symbol = '\U0001f504' # 🔄 84 | up_symbol = '\U000025b2' # ▲ 85 | down_symbol = '\U000025bc' # ▼ 86 | caution_symbol = '\U000026a0' # ⚠ 87 | folder_symbol = '\U0001f4c2' # 📂 88 | play_symbol = '\U000023f5' # ⏵ 89 | current_depth = 0 90 | init = True 91 | copy_move = ["Move", "Copy"] 92 | copied_moved = ["Moved", "Copied"] 93 | np = "negative_prompt: " 94 | openoutpaint = False 95 | controlnet = False 96 | js_dummy_return = None 97 | log_file = os.path.join(scripts.basedir(), "image_browser.log") 98 | optimized_cache = None 99 | show_progress_setting = True 100 | gradio_min = "3.23.0" 101 | gradio3_new_gallery_syntax = "3.39.0" 102 | gradio4 = "4.0.0" 103 | gradio4_new_gallery_syntax = "4.16.0" 104 | temp_temp = os.path.join(scripts.basedir(), "temp") 105 | 106 | db_version = wib_db.check() 107 | 108 | favorite_tab_name = "Favorites" 109 | default_tab_options = ["txt2img", "img2img", "txt2img-grids", "img2img-grids", "Extras", favorite_tab_name, "Others", "All", "Maintenance"] 110 | 111 | def check_image_browser_active_tabs(): 112 | last_default_tab = wib_db.get_last_default_tab() 113 | if last_default_tab[0] == "Others": 114 | # New tabs don't exist yet in image_browser_active_tabs, add them 115 | with wib_db.transaction() as cursor: 116 | wib_db.update_db_data(cursor, "last_default_tab", "Maintenance") 117 | 118 | if hasattr(opts, "image_browser_active_tabs"): 119 | active_and_new_tabs = f"{opts.image_browser_active_tabs}, All, Maintenance" 120 | shared.opts.__setattr__("image_browser_active_tabs", active_and_new_tabs) 121 | shared.opts.save(shared.config_filename) 122 | 123 | check_image_browser_active_tabs() 124 | tabs_list = [tab.strip() for tab in chain.from_iterable(csv.reader(StringIO(opts.image_browser_active_tabs))) if tab] if hasattr(opts, "image_browser_active_tabs") and opts.image_browser_active_tabs != "" else default_tab_options 125 | 126 | path_maps = { 127 | "txt2img": opts.outdir_samples or opts.outdir_txt2img_samples, 128 | "img2img": opts.outdir_samples or opts.outdir_img2img_samples, 129 | "txt2img-grids": opts.outdir_grids or opts.outdir_txt2img_grids, 130 | "img2img-grids": opts.outdir_grids or opts.outdir_img2img_grids, 131 | "Extras": opts.outdir_samples or opts.outdir_extras_samples, 132 | favorite_tab_name: opts.outdir_save 133 | } 134 | 135 | class ImageBrowserTab(): 136 | 137 | seen_base_tags = set() 138 | 139 | def __init__(self, name: str): 140 | self.name: str = os.path.basename(name) if os.path.isdir(name) else name 141 | self.path: str = os.path.realpath(path_maps.get(name, name)) 142 | self.base_tag: str = f"image_browser_tab_{self.get_unique_base_tag(self.remove_invalid_html_tag_chars(self.name).lower())}" 143 | 144 | def remove_invalid_html_tag_chars(self, tag: str) -> str: 145 | # Removes any character that is not a letter, a digit, a hyphen, or an underscore 146 | removed = re.sub(r'[^a-zA-Z0-9\-_]', '', tag) 147 | return removed 148 | 149 | def get_unique_base_tag(self, base_tag: str) -> str: 150 | counter = 1 151 | while base_tag in self.seen_base_tags: 152 | match = re.search(r'_(\d+)$', base_tag) 153 | if match: 154 | counter = int(match.group(1)) + 1 155 | base_tag = re.sub(r'_(\d+)$', f"_{counter}", base_tag) 156 | else: 157 | base_tag = f"{base_tag}_{counter}" 158 | counter += 1 159 | self.seen_base_tags.add(base_tag) 160 | return base_tag 161 | 162 | def __str__(self): 163 | return f"Name: {self.name} / Path: {self.path} / Base tag: {self.base_tag} / Seen base tags: {self.seen_base_tags}" 164 | 165 | tabs_list = [ImageBrowserTab(tab) for tab in tabs_list] 166 | 167 | # Set temp_dir from webui settings, so gradio uses it 168 | if shared.opts.temp_dir != "": 169 | tempfile.tempdir = shared.opts.temp_dir 170 | if forge: 171 | # Workaround for forge: Its ui_tempdir seems to not use the temp_dir configuration 172 | # The following makes sure, that onchange triggers after setting temp_dir 173 | shared.opts.temp_dir = temp_temp 174 | shared.opts.temp_dir = tempfile.tempdir 175 | 176 | debug_level_types = ["none", "warning log", "debug log", "javascript log", "capture logs to file"] 177 | 178 | debug_levels_list = [] 179 | for i in range(len(debug_level_types)): 180 | level = debug_level_types[i].split(" ")[0] 181 | text = str(i) + " - " + debug_level_types[i] 182 | debug_levels_list.append((level, text)) 183 | 184 | def debug_levels(arg_value=None, arg_level=None, arg_text=None): 185 | if arg_value is not None: 186 | return arg_value, debug_levels_list[arg_value] 187 | elif arg_level is not None: 188 | for i, (level, text) in enumerate(debug_levels_list): 189 | if level == arg_level: 190 | return i, debug_levels_list[i] 191 | elif arg_text is not None: 192 | for i, (level, text) in enumerate(debug_levels_list): 193 | if text == arg_text: 194 | return i, debug_levels_list[i] 195 | 196 | # Logging 197 | logger = None 198 | logger_mode = None 199 | 200 | def setup_file_handler(): 201 | handler_active = False 202 | handlers = logger.handlers 203 | for handler in handlers: 204 | if isinstance(handler, logging.FileHandler): 205 | if os.path.basename(handler.baseFilename) == os.path.basename(log_file): 206 | handler_active = True 207 | break 208 | if not handler_active: 209 | file_handler = logging.FileHandler(log_file) 210 | file_handler.setLevel(logger_mode) 211 | formatter = logging.Formatter(f'%(asctime)s image_browser.py: %(message)s', datefmt='%Y-%m-%d-%H:%M:%S') 212 | file_handler.setFormatter(formatter) 213 | logger.addHandler(file_handler) 214 | 215 | def setup_debug(): 216 | global logger, logger_mode 217 | try: 218 | common_logger = True 219 | logger = shared.log 220 | except AttributeError: 221 | common_logger = False 222 | logger = logging.getLogger(__name__) 223 | logger.disabled = False 224 | logger_mode = logging.ERROR 225 | level_value = 0 226 | capture_level_value = 99 227 | if hasattr(opts, "image_browser_debug_level"): 228 | warning_level_value, (warning_level, warning_level_text) = debug_levels(arg_level="warning") 229 | debug_level_value, (debug_level, debug_level_text) = debug_levels(arg_level="debug") 230 | capture_level_value, (capture_level, capture_level_text) = debug_levels(arg_level="capture") 231 | level_value, (level, level_text) = debug_levels(arg_text=opts.image_browser_debug_level) 232 | if level_value >= debug_level_value: 233 | logger_mode = logging.DEBUG 234 | elif level_value >= warning_level_value: 235 | logger_mode = logging.WARNING 236 | logger.setLevel(logger_mode) 237 | if not common_logger: 238 | console_handler = logging.StreamHandler() 239 | console_handler.setLevel(logger_mode) 240 | formatter = logging.Formatter(f'%(asctime)s image_browser.py: %(message)s', datefmt='%Y-%m-%d-%H:%M:%S') 241 | console_handler.setFormatter(formatter) 242 | logger.addHandler(console_handler) 243 | if level_value >= capture_level_value: 244 | try: 245 | os.unlink(log_file) 246 | except FileNotFoundError: 247 | pass 248 | setup_file_handler() 249 | logger.warning(f"debug_level: {level_value}") 250 | # Debug logging 251 | if logger.getEffectiveLevel() == logging.DEBUG: 252 | logger.debug(f"{sys.executable} {sys.version}") 253 | logger.debug(f"{platform.system()} {platform.version()}") 254 | try: 255 | git = os.environ.get('GIT', "git") 256 | webui_commit_hash = os.popen(f"{git} rev-parse HEAD").read().strip() 257 | sm_hashes = os.popen(f"{git} submodule").read() 258 | sm_hashes_lines = sm_hashes.splitlines() 259 | image_browser_commit_hash = f"image_browser_commit_hash not found: {sm_hashes}" 260 | for sm_hashes_line in sm_hashes_lines: 261 | if "images-browser" in sm_hashes_line.lower(): 262 | image_browser_commit_hash = sm_hashes_line[1:41] 263 | break 264 | except Exception as e: 265 | webui_commit_hash = e 266 | image_browser_commit_hash = e 267 | logger.debug(f"Webui {webui_commit_hash}") 268 | logger.debug(f"Image Browser {image_browser_commit_hash}") 269 | logger.debug(f"Gradio {gr.__version__}") 270 | logger.debug(f"{paths.script_path}") 271 | with open(cmd_opts.ui_config_file, "r", encoding="utf-8") as f: 272 | logger.debug(f.read()) 273 | with open(cmd_opts.ui_settings_file, "r", encoding="utf-8") as f: 274 | logger.debug(f.read()) 275 | logger.debug(os.path.realpath(__file__)) 276 | logger.debug([str(tab) for tab in tabs_list]) 277 | logger.debug(f"db_version: {db_version}") 278 | 279 | setup_debug() 280 | 281 | def delete_recycle(filename): 282 | if opts.image_browser_delete_recycle and send2trash_installed: 283 | send2trash(filename) 284 | else: 285 | file = Path(filename) 286 | file.unlink() 287 | return 288 | 289 | def img_path_subdirs_get(img_path): 290 | subdirs = [] 291 | subdirs.append(none_select) 292 | for item in os.listdir(img_path): 293 | item_path = os.path.join(img_path, item) 294 | if os.path.isdir(item_path): 295 | subdirs.append(item_path) 296 | return gr.update(choices=subdirs) 297 | 298 | def img_path_add_remove(img_dir, path_recorder, add_remove, img_path_depth): 299 | img_dir = os.path.realpath(img_dir) 300 | if add_remove == "add" or (add_remove == "remove" and img_dir in path_recorder): 301 | if add_remove == "add": 302 | path_recorder[img_dir] = { 303 | "depth": int(img_path_depth), 304 | "path_display": f"{img_dir} [{int(img_path_depth)}]" 305 | } 306 | wib_db.update_path_recorder(img_dir, path_recorder[img_dir]["depth"], path_recorder[img_dir]["path_display"]) 307 | else: 308 | del path_recorder[img_dir] 309 | wib_db.delete_path_recorder(img_dir) 310 | path_recorder_formatted = [value.get("path_display") for key, value in path_recorder.items()] 311 | path_recorder_formatted = sorted(path_recorder_formatted, key=lambda x: natural_keys(x.lower())) 312 | 313 | if add_remove == "remove": 314 | selected = path_recorder[list(path_recorder.keys())[0]]["path_display"] 315 | else: 316 | selected = path_recorder[img_dir]["path_display"] 317 | return path_recorder, gr.update(choices=path_recorder_formatted, value=selected) 318 | 319 | def sort_order_flip(turn_page_switch, sort_order): 320 | if sort_order == up_symbol: 321 | sort_order = down_symbol 322 | else: 323 | sort_order = up_symbol 324 | return 1, turn_page_switch + 1, sort_order 325 | 326 | def read_path_recorder(): 327 | path_recorder = wib_db.load_path_recorder() 328 | path_recorder_formatted = [value.get("path_display") for key, value in path_recorder.items()] 329 | path_recorder_formatted = sorted(path_recorder_formatted, key=lambda x: natural_keys(x.lower())) 330 | path_recorder_unformatted = list(path_recorder.keys()) 331 | path_recorder_unformatted = sorted(path_recorder_unformatted, key=lambda x: natural_keys(x.lower())) 332 | 333 | return path_recorder, path_recorder_formatted, path_recorder_unformatted 334 | 335 | def pure_path(path): 336 | if path == []: 337 | return path, 0 338 | match = re.search(r" \[(\d+)\]$", path) 339 | if match: 340 | path = path[:match.start()] 341 | depth = int(match.group(1)) 342 | else: 343 | depth = 0 344 | path = os.path.realpath(path) 345 | return path, depth 346 | 347 | def browser2path(img_path_browser): 348 | img_path, _ = pure_path(img_path_browser) 349 | return img_path 350 | 351 | def totxt(file): 352 | base, _ = os.path.splitext(file) 353 | file_txt = base + '.txt' 354 | 355 | return file_txt 356 | 357 | def tab_select(): 358 | path_recorder, path_recorder_formatted, path_recorder_unformatted = read_path_recorder() 359 | return path_recorder, gr.update(choices=path_recorder_unformatted) 360 | 361 | def js_logs_output(js_log): 362 | logger.debug(f"js_log: {js_log}") 363 | return js_log 364 | 365 | def ranking_filter_settings(page_index, turn_page_switch, ranking_filter): 366 | if ranking_filter == "Min-max": 367 | interactive = True 368 | else: 369 | interactive = False 370 | page_index = 1 371 | return page_index, turn_page_switch + 1, gr.update(interactive=interactive), gr.update(interactive=interactive) 372 | 373 | def reduplicative_file_move(src, dst): 374 | def same_name_file(basename, path): 375 | name, ext = os.path.splitext(basename) 376 | f_list = os.listdir(path) 377 | max_num = 0 378 | for f in f_list: 379 | if len(f) <= len(basename): 380 | continue 381 | f_ext = f[-len(ext):] if len(ext) > 0 else "" 382 | if f[:len(name)] == name and f_ext == ext: 383 | if f[len(name)] == "(" and f[-len(ext)-1] == ")": 384 | number = f[len(name)+1:-len(ext)-1] 385 | if number.isdigit(): 386 | if int(number) > max_num: 387 | max_num = int(number) 388 | return f"{name}({max_num + 1}){ext}" 389 | name = os.path.basename(src) 390 | save_name = os.path.join(dst, name) 391 | src_txt_exists = False 392 | if opts.image_browser_txt_files: 393 | src_txt = totxt(src) 394 | if os.path.exists(src_txt): 395 | src_txt_exists = True 396 | if not os.path.exists(save_name): 397 | if opts.image_browser_copy_image: 398 | shutil.copy2(src, dst) 399 | if opts.image_browser_txt_files and src_txt_exists: 400 | shutil.copy2(src_txt, dst) 401 | else: 402 | shutil.move(src, dst) 403 | if opts.image_browser_txt_files and src_txt_exists: 404 | shutil.move(src_txt, dst) 405 | else: 406 | name = same_name_file(name, dst) 407 | if opts.image_browser_copy_image: 408 | shutil.copy2(src, os.path.join(dst, name)) 409 | if opts.image_browser_txt_files and src_txt_exists: 410 | shutil.copy2(src_txt, totxt(os.path.join(dst, name))) 411 | else: 412 | shutil.move(src, os.path.join(dst, name)) 413 | if opts.image_browser_txt_files and src_txt_exists: 414 | shutil.move(src_txt, totxt(os.path.join(dst, name))) 415 | 416 | def save_image(file_name, filenames, page_index, turn_page_switch, dest_path): 417 | if file_name is not None and os.path.exists(file_name): 418 | reduplicative_file_move(file_name, dest_path) 419 | message = f"
{plaintext_to_html(str(key))}
1059 |{plaintext_to_html(str(text))}
1060 |Favorites path from settings: {opts.outdir_save}") 1288 | 1289 | with gr.Row(visible=others_dir): 1290 | with gr.Column(scale=10): 1291 | img_path = gr.Textbox(dir_name, label="Images directory", placeholder="Input images directory", interactive=others_dir) 1292 | with gr.Column(scale=1): 1293 | img_path_depth = gr.Number(value="0", label="Sub directory depth") 1294 | with gr.Column(scale=1): 1295 | img_path_save_button = gr.Button(value="Add to / replace in saved directories") 1296 | 1297 | with gr.Row(visible=others_dir): 1298 | with gr.Column(scale=10): 1299 | img_path_browser = gr.Dropdown(choices=path_recorder_formatted, label="Saved directories") 1300 | with gr.Column(scale=1): 1301 | img_path_remove_button = gr.Button(value="Remove from saved directories") 1302 | 1303 | with gr.Row(visible=others_dir): 1304 | with gr.Column(scale=10): 1305 | img_path_subdirs = gr.Dropdown(choices=[none_select], value=none_select, label="Sub directories", interactive=True, elem_id=f"{tab.base_tag}_img_path_subdirs") 1306 | with gr.Column(scale=1): 1307 | img_path_subdirs_button = gr.Button(value="Get sub directories") 1308 | 1309 | with gr.Row(visible=standard_ui, elem_id=f"{tab.base_tag}_image_browser") as main_panel: 1310 | with gr.Column(): 1311 | with gr.Row(): 1312 | with gr.Column(scale=2): 1313 | with gr.Row(elem_id=f"{tab.base_tag}_image_browser_gallery_controls") as gallery_controls_panel: 1314 | with gr.Column(scale=4, min_width=20): 1315 | first_page = gr.Button("First Page", elem_id=f"{tab.base_tag}_control_image_browser_first_page") 1316 | with gr.Column(scale=4, min_width=20): 1317 | prev_page = gr.Button("Prev Page", elem_id=f"{tab.base_tag}_control_image_browser_prev_page") 1318 | with gr.Row(elem_classes="page-index-panel"): 1319 | page_index = gr.Number(value=1, label="Page Index", elem_id=f"{tab.base_tag}_control_image_browser_page_index") 1320 | with gr.Column(scale=1, min_width=20, elem_classes="refresh-index-panel"): 1321 | refresh_index_button = ToolButton(value=refresh_symbol, elem_id=f"{tab.base_tag}_control_image_browser_refresh_index") 1322 | with gr.Column(scale=4, min_width=20): 1323 | next_page = gr.Button("Next Page", elem_id=f"{tab.base_tag}_control_image_browser_next_page") 1324 | with gr.Column(scale=4, min_width=20): 1325 | end_page = gr.Button("End Page", elem_id=f"{tab.base_tag}_control_image_browser_end_page") 1326 | with gr.Row(): 1327 | with gr.Column(scale=5, min_width=40, visible=False) as ranking_panel: 1328 | with gr.Row(): 1329 | with gr.Column(scale=1, min_width=20): 1330 | ranking_current = gr.Textbox(value="None", label="Current ranking", interactive=False) 1331 | with gr.Column(scale=4, min_width=20): 1332 | ranking = gr.Radio(choices=["1", "2", "3", "4", "5", "None"], label="Set ranking to", elem_id=f"{tab.base_tag}_control_image_browser_ranking", interactive=True) 1333 | with gr.Column(scale=1, min_width=20, visible=False) as video_checkbox_panel: 1334 | video_checkbox = gr.Checkbox(value=False, label="Show video frame", elem_id=f"{tab.base_tag}_image_browser_video_checkbox") 1335 | with gr.Column(scale=5, min_width=40): 1336 | gr.HTML(" ") 1337 | if opts.image_browser_video_pos == "Above": 1338 | with gr.Row(): 1339 | video_element = gr.Video(visible=False, width=opts.image_browser_video_x, height=opts.image_browser_video_y) 1340 | with gr.Row(): 1341 | if version.parse(gr.__version__) < version.parse(gradio3_new_gallery_syntax): 1342 | # Version for Gradio 3 1343 | image_gallery = gr.Gallery(show_label=False, elem_id=f"{tab.base_tag}_image_browser_gallery").style(columns=opts.image_browser_page_columns, height=("max-content" if opts.image_browser_height_auto else None)) 1344 | elif version.parse(gr.__version__) >= version.parse(gradio4_new_gallery_syntax): 1345 | # Most up-to-date version 1346 | image_gallery = gr.Gallery(show_label=False, elem_id=f"{tab.base_tag}_image_browser_gallery", interactive=False, columns=opts.image_browser_page_columns, height=("max-content" if opts.image_browser_height_auto else None)) 1347 | else: 1348 | # Version for Gradio 4.0 - 4.15 1349 | image_gallery = gr.Gallery(show_label=False, elem_id=f"{tab.base_tag}_image_browser_gallery", columns=opts.image_browser_page_columns, height=("max-content" if opts.image_browser_height_auto else None)) 1350 | if opts.image_browser_video_pos == "Below": 1351 | with gr.Row(): 1352 | video_element = gr.Video(visible=False, width=opts.image_browser_video_x, height=opts.image_browser_video_y) 1353 | with gr.Row() as delete_panel: 1354 | with gr.Column(scale=1): 1355 | delete_num = gr.Number(value=1, interactive=True, label="delete next", elem_id=f"{tab.base_tag}_image_browser_del_num") 1356 | delete_confirm = gr.Checkbox(value=False, label="also delete off-screen images") 1357 | with gr.Column(scale=3): 1358 | delete = gr.Button('Delete', elem_id=f"{tab.base_tag}_image_browser_del_img_btn") 1359 | with gr.Row() as info_add_panel: 1360 | with gr.Box(visible=opts.image_browser_info_add): 1361 | gr.HTML("
Choose Min-max to activate these controls
") 1387 | with gr.Box() as aesthetic_score_filter_panel: 1388 | with gr.Row(): 1389 | aes_filter_min = gr.Textbox(value="", label="Minimum score") 1390 | aes_filter_max = gr.Textbox(value="", label="Maximum score") 1391 | with gr.Box() as generation_info_panel: 1392 | with gr.Row(): 1393 | img_file_info_format = gr.Checkbox(value=opts.image_browser_info_format, label="Formatted display") 1394 | with gr.Row(): 1395 | img_file_info = gr.Textbox(label="Generation Info", interactive=False, lines=6, visible=not opts.image_browser_info_format) 1396 | img_file_info_formatted = gr.DataFrame(label="Generation Info", headers=["Key", "Value"], wrap=True, interactive=False, visible=opts.image_browser_info_format, elem_classes="image_browser_file_info_formatted") 1397 | with gr.Row() as filename_panel: 1398 | img_file_name = gr.Textbox(value="", label="File Name", interactive=False) 1399 | with gr.Row() as filetime_panel: 1400 | img_file_time= gr.HTML() 1401 | with gr.Row() as open_folder_panel: 1402 | if tab.name == "All": 1403 | open_folder_button = gr.Button(folder_symbol, visible=False) 1404 | else: 1405 | open_folder_button = gr.Button(folder_symbol, visible=standard_ui or others_dir) 1406 | gr.HTML(" ") 1407 | gr.HTML(" ") 1408 | gr.HTML(" ") 1409 | with gr.Row(elem_id=f"{tab.base_tag}_image_browser_button_panel", visible=False) as button_panel: 1410 | with gr.Column(): 1411 | with gr.Row(): 1412 | if tab.name == favorite_tab_name: 1413 | favorites_btn_show = False 1414 | else: 1415 | favorites_btn_show = True 1416 | favorites_btn = gr.Button(f'{copy_move[opts.image_browser_copy_image]} to favorites', elem_id=f"{tab.base_tag}_image_browser_favorites_btn", visible=favorites_btn_show) 1417 | try: 1418 | send_to_buttons = sendto.create_buttons(["txt2img", "img2img", "inpaint", "extras"]) 1419 | except: 1420 | pass 1421 | sendto_openoutpaint = gr.Button("Send to openOutpaint", elem_id=f"{tab.base_tag}_image_browser_openoutpaint_btn", visible=openoutpaint) 1422 | with gr.Row(visible=controlnet): 1423 | sendto_controlnet_txt2img = gr.Button("Send to txt2img ControlNet", visible=controlnet) 1424 | sendto_controlnet_img2img = gr.Button("Send to img2img ControlNet", visible=controlnet) 1425 | with gr.Row(visible=controlnet): 1426 | if "control_net_unit_count" in opts.data: 1427 | controlnet_max = opts.data.get("control_net_unit_count", 3) 1428 | else: 1429 | controlnet_max = opts.data.get("control_net_max_models_num", 3) 1430 | sendto_controlnet_num = gr.Dropdown([str(i) for i in range(controlnet_max)], label="ControlNet number", value="0", interactive=True, visible=(controlnet and controlnet_max > 1)) 1431 | if controlnet_max is None: 1432 | sendto_controlnet_type = gr.Textbox(value="none", visible=False) 1433 | elif controlnet_max == 1: 1434 | sendto_controlnet_type = gr.Textbox(value="single", visible=False) 1435 | else: 1436 | sendto_controlnet_type = gr.Textbox(value="multiple", visible=False) 1437 | with gr.Row(elem_id=f"{tab.base_tag}_image_browser_to_dir_panel", visible=False) as to_dir_panel: 1438 | with gr.Column(): 1439 | with gr.Row(): 1440 | to_dir_path = gr.Textbox(label="Directory path") 1441 | with gr.Row(): 1442 | to_dir_saved = gr.Dropdown(choices=path_recorder_unformatted, label="Saved directories") 1443 | with gr.Row(): 1444 | to_dir_btn = gr.Button(f'{copy_move[opts.image_browser_copy_image]} to directory', elem_id=f"{tab.base_tag}_image_browser_to_dir_btn") 1445 | 1446 | with gr.Row(): 1447 | collected_warning = gr.HTML() 1448 | 1449 | with gr.Row(visible=False): 1450 | renew_page = gr.Button("Renew Page", elem_id=f"{tab.base_tag}_image_browser_renew_page") 1451 | visible_img_num = gr.Number() 1452 | tab_base_tag_box = gr.Textbox(tab.base_tag) 1453 | image_index = gr.Textbox(value=-1, elem_id=f"{tab.base_tag}_image_browser_image_index") 1454 | set_index = gr.Button('set_index', elem_id=f"{tab.base_tag}_image_browser_set_index") 1455 | filenames = gr.State([]) 1456 | hidden = gr.Image(type="pil", elem_id=f"{tab.base_tag}_image_browser_hidden_image") 1457 | image_page_list = gr.Textbox(elem_id=f"{tab.base_tag}_image_browser_image_page_list") 1458 | info1 = gr.Textbox() 1459 | info2 = gr.Textbox() 1460 | # turn_page_switch: reload page at thumbnail overview, requests add 1 to trigger change-event 1461 | turn_page_switch = gr.Number(value=1, label="turn_page_switch") 1462 | load_switch = gr.Number(value=1, label="load_switch") 1463 | to_dir_load_switch = gr.Number(value=1, label="to_dir_load_switch") 1464 | to_dir_load_switch_panel = gr.Checkbox(value=True, label="to_dir_load_switch_panel") 1465 | select_image = gr.Number(value=1) 1466 | img_path_add = gr.Textbox(value="add") 1467 | img_path_remove = gr.Textbox(value="remove") 1468 | favorites_path = gr.Textbox(value=opts.outdir_save) 1469 | mod_keys = "" 1470 | if opts.image_browser_mod_ctrl_shift: 1471 | mod_keys = f"{mod_keys}CS" 1472 | elif opts.image_browser_mod_shift: 1473 | mod_keys = f"{mod_keys}S" 1474 | image_browser_mod_keys = gr.Textbox(value=mod_keys, elem_id=f"{tab.base_tag}_image_browser_mod_keys") 1475 | image_browser_prompt = gr.Textbox(elem_id=f"{tab.base_tag}_image_browser_prompt") 1476 | image_browser_neg_prompt = gr.Textbox(elem_id=f"{tab.base_tag}_image_browser_neg_prompt") 1477 | js_logs = gr.Textbox() 1478 | image_browser_img_info = gr.Textbox(value="[]", elem_id=f"{tab.base_tag}_image_browser_img_info") 1479 | 1480 | # Maintenance tab 1481 | with gr.Row(visible=maint): 1482 | with gr.Column(scale=4): 1483 | gr.HTML(f"{caution_symbol} Caution: You should only use these options if you know what you are doing. {caution_symbol}") 1484 | with gr.Column(scale=3): 1485 | maint_wait = gr.HTML("Status:") 1486 | with gr.Column(scale=7): 1487 | gr.HTML(" ") 1488 | with gr.Row(visible=maint): 1489 | maint_last_msg = gr.Textbox(label="Last message", interactive=False) 1490 | with gr.Row(visible=maint): 1491 | with gr.Column(scale=1): 1492 | maint_exif_rebuild = gr.Button(value="Rebuild exif cache") 1493 | with gr.Column(scale=1): 1494 | maint_exif_delete_0 = gr.Button(value="Delete 0-entries from exif cache") 1495 | with gr.Column(scale=10): 1496 | gr.HTML(visible=False) 1497 | with gr.Row(visible=maint): 1498 | with gr.Column(scale=2): 1499 | maint_update_dirs = gr.Button(value="Update directory names in database") 1500 | with gr.Column(scale=2, min_width=40): 1501 | maint_update_dirs_path_recorder = gr.Checkbox(value=True, label="path_recorder") 1502 | with gr.Column(scale=2, min_width=40): 1503 | maint_update_dirs_exif_data = gr.Checkbox(value=True, label="exif_data") 1504 | with gr.Column(scale=2, min_width=40): 1505 | maint_update_dirs_ranking = gr.Checkbox(value=True, label="ranking") 1506 | with gr.Column(scale=10): 1507 | maint_update_dirs_from = gr.Textbox(label="From (full path)") 1508 | with gr.Column(scale=10): 1509 | maint_update_dirs_to = gr.Textbox(label="to (full path)") 1510 | with gr.Row(visible=maint): 1511 | with gr.Column(scale=1): 1512 | maint_recreate_hash = gr.Button(value="Recreate hash for existing files") 1513 | with gr.Column(scale=10): 1514 | gr.HTML(visible=False) 1515 | with gr.Row(visible=maint): 1516 | with gr.Column(scale=1): 1517 | maint_reapply_ranking = gr.Button(value="Reapply ranking after moving files") 1518 | with gr.Column(scale=10): 1519 | gr.HTML(visible=False) 1520 | with gr.Row(visible=maint): 1521 | with gr.Column(scale=1): 1522 | maint_restart_debug = gr.Button(value="Restart debug") 1523 | with gr.Column(scale=10): 1524 | gr.HTML(visible=False) 1525 | with gr.Row(visible=maint): 1526 | with gr.Column(scale=1): 1527 | maint_get_js_logs = gr.Button(value="Get javascript logs") 1528 | with gr.Column(scale=10): 1529 | maint_show_logs = gr.Textbox(label="Javascript logs", lines=10, interactive=False) 1530 | with gr.Row(visible=False): 1531 | with gr.Column(scale=1): 1532 | maint_rebuild_ranking = gr.Button(value="Rebuild ranking from exif info") 1533 | with gr.Column(scale=10): 1534 | gr.HTML(visible=False) 1535 | 1536 | # Hide components based on opts.image_browser_hidden_components 1537 | hidden_component_map = { 1538 | "Sort by": sort_panel, 1539 | "Filename keyword search": filename_search_panel, 1540 | "EXIF keyword search": exif_search_panel, 1541 | "Ranking Filter": ranking_filter_panel, 1542 | "Aesthestic Score": aesthetic_score_filter_panel, 1543 | "Generation Info": generation_info_panel, 1544 | "File Name": filename_panel, 1545 | "File Time": filetime_panel, 1546 | "Open Folder": open_folder_panel, 1547 | "Send to buttons": button_panel, 1548 | "Copy to directory": to_dir_panel, 1549 | "Gallery Controls Bar": gallery_controls_panel, 1550 | "Ranking Bar": ranking_panel, 1551 | "Delete Bar": delete_panel, 1552 | "Additional Generation Info": info_add_panel 1553 | } 1554 | 1555 | if set(hidden_component_map.keys()) != set(components_list): 1556 | logger.warning(f"Invalid items present in either hidden_component_map or components_list. Make sure when adding new components they are added to both.") 1557 | 1558 | override_hidden = set() 1559 | if hasattr(opts, "image_browser_hidden_components"): 1560 | for item in opts.image_browser_hidden_components: 1561 | hidden_component_map[item].visible = False 1562 | override_hidden.add(hidden_component_map[item]) 1563 | 1564 | change_dir_outputs = [warning_box, main_panel, img_path_browser, path_recorder, load_switch, img_path, img_path_depth] 1565 | img_path.submit(change_dir_textbox, inputs=[img_path, path_recorder, load_switch, img_path_browser, img_path_depth], outputs=change_dir_outputs, show_progress=show_progress_setting) 1566 | img_path_browser.change(change_dir_dropdown, inputs=[img_path_browser, path_recorder, load_switch, img_path_browser, img_path_depth], outputs=change_dir_outputs, show_progress=show_progress_setting) 1567 | to_dir_saved.change(change_dir_dropdown, inputs=[to_dir_saved, path_recorder, to_dir_load_switch, to_dir_saved, img_path_depth], outputs=[warning_box, to_dir_load_switch_panel, to_dir_saved, path_recorder, to_dir_load_switch, to_dir_path, img_path_depth], show_progress=show_progress_setting) 1568 | 1569 | #delete 1570 | delete.click( 1571 | fn=delete_image, 1572 | inputs=[tab_base_tag_box, delete_num, img_file_name, filenames, image_index, visible_img_num, delete_confirm, turn_page_switch, image_page_list], 1573 | outputs=[filenames, delete_num, turn_page_switch, visible_img_num, image_gallery, select_image, image_page_list], 1574 | show_progress=show_progress_setting 1575 | ).then( 1576 | fn=None, 1577 | _js="image_browser_select_image", 1578 | inputs=[tab_base_tag_box, image_index, select_image], 1579 | outputs=[js_dummy_return], 1580 | show_progress=show_progress_setting 1581 | ) 1582 | 1583 | to_dir_btn.click(save_image, inputs=[img_file_name, filenames, page_index, turn_page_switch, to_dir_path], outputs=[collected_warning, filenames, page_index, turn_page_switch], show_progress=show_progress_setting) 1584 | #turn page 1585 | first_page.click(lambda s:(1, s + 1) , inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1586 | next_page.click(lambda p, s: (p + 1, s + 1), inputs=[page_index, turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1587 | prev_page.click(lambda p, s: (p - 1, s + 1), inputs=[page_index, turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1588 | end_page.click(lambda s: (-1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1589 | load_switch.change(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1590 | filename_keyword_search.submit(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1591 | exif_keyword_search.submit(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1592 | ranking_filter_min.submit(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1593 | ranking_filter_max.submit(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1594 | aes_filter_min.submit(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1595 | aes_filter_max.submit(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1596 | sort_by.change(lambda s:(1, s + 1), inputs=[turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1597 | page_index.submit(lambda s: s + 1, inputs=[turn_page_switch], outputs=[turn_page_switch], show_progress=show_progress_setting) 1598 | renew_page.click(lambda s: s + 1, inputs=[turn_page_switch], outputs=[turn_page_switch], show_progress=show_progress_setting) 1599 | refresh_index_button.click(lambda p, s:(p, s + 1), inputs=[page_index, turn_page_switch], outputs=[page_index, turn_page_switch], show_progress=show_progress_setting) 1600 | img_path_depth.change(lambda s: s + 1, inputs=[turn_page_switch], outputs=[turn_page_switch], show_progress=show_progress_setting) 1601 | 1602 | hide_on_thumbnail_view = [delete_panel, button_panel, ranking_panel, to_dir_panel, info_add_panel] 1603 | 1604 | sort_order.click( 1605 | fn=sort_order_flip, 1606 | inputs=[turn_page_switch, sort_order], 1607 | outputs=[page_index, turn_page_switch, sort_order], 1608 | show_progress=show_progress_setting 1609 | ) 1610 | ranking_filter.change( 1611 | fn=ranking_filter_settings, 1612 | inputs=[page_index, turn_page_switch, ranking_filter], 1613 | outputs=[page_index, turn_page_switch, ranking_filter_min, ranking_filter_max], 1614 | show_progress=show_progress_setting 1615 | ) 1616 | img_file_info_format.change( 1617 | fn=img_file_info_format_visibility, 1618 | inputs=[img_file_info_format], 1619 | outputs=[img_file_info, img_file_info_formatted], 1620 | show_progress=show_progress_setting 1621 | ) 1622 | img_file_info.change( 1623 | fn=img_file_info_do_format, 1624 | inputs=[img_file_info], 1625 | outputs=[img_file_info_formatted], 1626 | show_progress=show_progress_setting 1627 | ) 1628 | 1629 | # Others 1630 | img_path_subdirs_button.click( 1631 | fn=img_path_subdirs_get, 1632 | inputs=[img_path], 1633 | outputs=[img_path_subdirs], 1634 | show_progress=show_progress_setting 1635 | ) 1636 | img_path_subdirs.change( 1637 | fn=change_dir_dropdown, 1638 | inputs=[img_path_subdirs, path_recorder, load_switch, img_path_browser, img_path_depth], 1639 | outputs=change_dir_outputs, 1640 | show_progress=show_progress_setting 1641 | ) 1642 | img_path_save_button.click( 1643 | fn=img_path_add_remove, 1644 | inputs=[img_path, path_recorder, img_path_add, img_path_depth], 1645 | outputs=[path_recorder, img_path_browser], 1646 | show_progress=show_progress_setting 1647 | ) 1648 | img_path_remove_button.click( 1649 | fn=img_path_add_remove, 1650 | inputs=[img_path, path_recorder, img_path_remove, img_path_depth], 1651 | outputs=[path_recorder, img_path_browser], 1652 | show_progress=show_progress_setting 1653 | ) 1654 | maint_exif_rebuild.click( 1655 | fn=exif_rebuild, 1656 | inputs=[maint_wait], 1657 | outputs=[maint_wait, maint_last_msg], 1658 | show_progress=True 1659 | ) 1660 | maint_exif_delete_0.click( 1661 | fn=exif_delete_0, 1662 | inputs=[maint_wait], 1663 | outputs=[maint_wait, maint_last_msg], 1664 | show_progress=True 1665 | ) 1666 | maint_update_dirs.click( 1667 | fn=exif_update_dirs, 1668 | inputs=[maint_update_dirs_path_recorder, maint_update_dirs_exif_data, maint_update_dirs_ranking, maint_update_dirs_from, maint_update_dirs_to, maint_wait], 1669 | outputs=[maint_wait, maint_last_msg], 1670 | show_progress=True 1671 | ) 1672 | maint_recreate_hash.click( 1673 | fn=recreate_hash, 1674 | inputs=[maint_wait], 1675 | outputs=[maint_wait, maint_last_msg], 1676 | show_progress=True 1677 | ) 1678 | maint_reapply_ranking.click( 1679 | fn=reapply_ranking, 1680 | inputs=[path_recorder, maint_wait], 1681 | outputs=[maint_wait, maint_last_msg], 1682 | show_progress=True 1683 | ) 1684 | maint_restart_debug.click( 1685 | fn=restart_debug, 1686 | inputs=[maint_wait], 1687 | outputs=[maint_wait, maint_last_msg], 1688 | show_progress=True 1689 | ) 1690 | maint_get_js_logs.click( 1691 | fn=js_logs_output, 1692 | _js="get_js_logs", 1693 | inputs=[js_logs], 1694 | outputs=[maint_show_logs], 1695 | show_progress=True 1696 | ) 1697 | 1698 | # other functions 1699 | set_index.click( 1700 | fn=show_image_info, 1701 | _js="image_browser_get_current_img", 1702 | inputs=[tab_base_tag_box, image_index, page_index, filenames, turn_page_switch, image_gallery], 1703 | outputs=[img_file_name, img_file_time, image_index, hidden, turn_page_switch, img_file_info_add, image_gallery, video_checkbox, video_checkbox_panel], 1704 | show_progress=show_progress_setting 1705 | ).then( 1706 | fn=None, 1707 | _js="image_browser_img_show_progress_update", 1708 | inputs=[tab_base_tag_box], 1709 | outputs=[js_dummy_return], 1710 | show_progress=show_progress_setting 1711 | ) 1712 | 1713 | set_index.click(fn=lambda:(gr.update(visible=delete_panel not in override_hidden), gr.update(visible=button_panel not in override_hidden), gr.update(visible=ranking_panel not in override_hidden), gr.update(visible=to_dir_panel not in override_hidden), gr.update(visible=info_add_panel not in override_hidden)), inputs=None, outputs=hide_on_thumbnail_view, show_progress=show_progress_setting) 1714 | 1715 | favorites_btn.click(save_image, inputs=[img_file_name, filenames, page_index, turn_page_switch, favorites_path], outputs=[collected_warning, filenames, page_index, turn_page_switch], show_progress=show_progress_setting) 1716 | img_file_name.change(img_file_name_changed, inputs=[img_file_name, favorites_btn, to_dir_btn], outputs=[ranking_current, ranking, collected_warning, favorites_btn, to_dir_btn], show_progress=show_progress_setting) 1717 | 1718 | hidden.change(fn=run_pnginfo, inputs=[hidden, img_path, img_file_name], outputs=[info1, img_file_info, info2, image_browser_prompt, image_browser_neg_prompt], show_progress=show_progress_setting) 1719 | 1720 | #ranking 1721 | ranking.change(update_ranking, inputs=[img_file_name, ranking_current, ranking, img_file_info], outputs=[ranking_current, ranking, img_file_info], show_progress=show_progress_setting) 1722 | 1723 | try: 1724 | sendto.bind_buttons(send_to_buttons, hidden, img_file_info) 1725 | except: 1726 | pass 1727 | 1728 | if standard_ui: 1729 | current_gr_tab.select( 1730 | fn=tab_select, 1731 | inputs=[], 1732 | outputs=[path_recorder, to_dir_saved], 1733 | show_progress=show_progress_setting 1734 | ) 1735 | open_folder_button.click( 1736 | fn=lambda: open_folder(dir_name), 1737 | inputs=[], 1738 | outputs=[], 1739 | show_progress=show_progress_setting 1740 | ) 1741 | elif others_dir: 1742 | open_folder_button.click( 1743 | fn=open_folder, 1744 | inputs=[img_path], 1745 | outputs=[], 1746 | show_progress=show_progress_setting 1747 | ) 1748 | if standard_ui or others_dir: 1749 | turn_page_switch.change( 1750 | fn=get_image_page, 1751 | inputs=[img_path, page_index, filenames, filename_keyword_search, sort_by, sort_order, tab_base_tag_box, img_path_depth, ranking_filter, ranking_filter_min, ranking_filter_max, aes_filter_min, aes_filter_max, exif_keyword_search, negative_prompt_search, use_regex], 1752 | outputs=[filenames, page_index, image_gallery, img_file_name, img_file_time, img_file_info, visible_img_num, warning_box, hidden, image_page_list, image_browser_img_info, video_checkbox, video_checkbox_panel], 1753 | show_progress=show_progress_setting 1754 | ).then( 1755 | fn=None, 1756 | _js="image_browser_turnpage", 1757 | inputs=[tab_base_tag_box], 1758 | outputs=[js_dummy_return], 1759 | show_progress=show_progress_setting 1760 | ) 1761 | turn_page_switch.change(fn=lambda:(gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)), inputs=None, outputs=hide_on_thumbnail_view, show_progress=show_progress_setting) 1762 | sendto_openoutpaint.click( 1763 | fn=None, 1764 | inputs=[tab_base_tag_box, image_index, image_browser_prompt, image_browser_neg_prompt], 1765 | outputs=[js_dummy_return], 1766 | _js="image_browser_openoutpaint_send", 1767 | show_progress=show_progress_setting 1768 | ) 1769 | sendto_controlnet_txt2img.click( 1770 | fn=None, 1771 | inputs=[tab_base_tag_box, image_index, sendto_controlnet_num, sendto_controlnet_type], 1772 | outputs=[js_dummy_return], 1773 | _js="image_browser_controlnet_send_txt2img", 1774 | show_progress=show_progress_setting 1775 | ) 1776 | sendto_controlnet_img2img.click( 1777 | fn=None, 1778 | inputs=[tab_base_tag_box, image_index, sendto_controlnet_num, sendto_controlnet_type], 1779 | outputs=[js_dummy_return], 1780 | _js="image_browser_controlnet_send_img2img", 1781 | show_progress=show_progress_setting 1782 | ) 1783 | video_checkbox.change( 1784 | fn=show_video, 1785 | inputs=[video_checkbox, img_file_name], 1786 | outputs=[video_element], 1787 | show_progress=show_progress_setting 1788 | ) 1789 | 1790 | 1791 | def run_pnginfo(image, image_path, image_file_name): 1792 | if image is None or os.path.splitext(image_file_name)[1] not in image_ext_list: 1793 | return '', '', '', '', '' 1794 | try: 1795 | geninfo, items = images.read_info_from_image(image) 1796 | info = pnginfo2html(geninfo, items) 1797 | except UnidentifiedImageError as e: 1798 | geninfo = None 1799 | info = "" 1800 | 1801 | if geninfo is None: 1802 | try: 1803 | filename = os.path.splitext(image_file_name)[0] + ".txt" 1804 | geninfo = "" 1805 | with open(filename) as f: 1806 | for line in f: 1807 | geninfo += line 1808 | except Exception: 1809 | logger.warning(f"run_pnginfo: No EXIF in image or txt file") 1810 | 1811 | if openoutpaint: 1812 | prompt, neg_prompt = wib_db.select_prompts(image_file_name) 1813 | if prompt == "0": 1814 | prompt = "" 1815 | if neg_prompt == "0": 1816 | neg_prompt = "" 1817 | else: 1818 | prompt = "" 1819 | neg_prompt = "" 1820 | 1821 | return '', geninfo, info, prompt, neg_prompt 1822 | 1823 | 1824 | def on_ui_tabs(): 1825 | global num_of_imgs_per_page, loads_files_num, js_dummy_return 1826 | num_of_imgs_per_page = int(opts.image_browser_page_columns * opts.image_browser_page_rows) 1827 | loads_files_num = int(opts.image_browser_pages_perload * num_of_imgs_per_page) 1828 | with gr.Blocks(analytics_enabled=False) as image_browser: 1829 | if version.parse(gr.__version__) < version.parse(gradio_min): 1830 | gr.HTML(f'
You are running Gradio version {gr.__version__}. This version of the extension requires at least Gradio version {gradio_min}.
For more details see https://github.com/AlUlkesh/stable-diffusion-webui-images-browser/issues/116#issuecomment-1493259585
') 1831 | else: 1832 | with gr.Tabs(elem_id="image_browser_tabs_container") as tabs: 1833 | js_dummy_return = gr.Textbox(interactive=False, visible=False) 1834 | for i, tab in enumerate(tabs_list): 1835 | with gr.Tab(tab.name, elem_id=f"{tab.base_tag}_image_browser_container") as current_gr_tab: 1836 | with gr.Blocks(analytics_enabled=False): 1837 | create_tab(tab, current_gr_tab) 1838 | gr.Checkbox(value=opts.image_browser_preload, elem_id="image_browser_preload", visible=False) 1839 | gr.Textbox(",".join( [tab.base_tag for tab in tabs_list] ), elem_id="image_browser_tab_base_tags_list", visible=False) 1840 | gr.Checkbox(value=opts.image_browser_swipe, elem_id=f"image_browser_swipe", visible=False) 1841 | 1842 | javascript_level_value, (javascript_level, javascript_level_text) = debug_levels(arg_level="javascript") 1843 | level_value, (level, level_text) = debug_levels(arg_text=opts.image_browser_debug_level) 1844 | if level_value >= javascript_level_value: 1845 | debug_level_option = level 1846 | else: 1847 | debug_level_option = "" 1848 | gr.Textbox(value=debug_level_option, elem_id="image_browser_debug_level_option", visible=False) 1849 | 1850 | # Webui's ui_loadsave uses gradio labels as keys, this does not work with image browser, as the same labels are used on different tabs 1851 | # For this reason the do_not_save_to_config attribute is added to each gradio element 1852 | for key, value in image_browser.blocks.items(): 1853 | setattr(value, "do_not_save_to_config", True) 1854 | 1855 | return (image_browser, "Image Browser", "image_browser"), 1856 | 1857 | def move_setting(cur_setting_name, old_setting_name, option_info, section, added): 1858 | try: 1859 | old_value = shared.opts.__getattr__(old_setting_name) 1860 | except AttributeError: 1861 | old_value = None 1862 | try: 1863 | new_value = shared.opts.__getattr__(cur_setting_name) 1864 | except AttributeError: 1865 | new_value = None 1866 | if old_value is not None and new_value is None: 1867 | # Add new option 1868 | shared.opts.add_option(cur_setting_name, shared.OptionInfo(*option_info, section=section)) 1869 | shared.opts.__setattr__(cur_setting_name, old_value) 1870 | added = added + 1 1871 | # Remove old option 1872 | shared.opts.data.pop(old_setting_name, None) 1873 | 1874 | return added 1875 | 1876 | def on_ui_settings(): 1877 | # [current setting_name], [old setting_name], [default], [label], [component], [component_args] 1878 | active_tabs_description = f"List of active tabs (separated by commas). Available options are {', '.join(default_tab_options)}. Custom folders are also supported by specifying their path." 1879 | debug_level_choices = [] 1880 | for i in range(len(debug_level_types)): 1881 | level_value, (level, level_text) = debug_levels(arg_value=i) 1882 | debug_level_choices.append(level_text) 1883 | 1884 | image_browser_options = [ 1885 | ("image_browser_active_tabs", None, ", ".join(default_tab_options), active_tabs_description), 1886 | ("image_browser_hidden_components", None, [], "Select components to hide", DropdownMulti, lambda: {"choices": components_list}), 1887 | ("image_browser_with_subdirs", "images_history_with_subdirs", True, "Include images in sub directories"), 1888 | ("image_browser_preload", "images_history_preload", False, "Preload images at startup for first tab"), 1889 | ("image_browser_copy_image", "images_copy_image", False, "Move buttons copy instead of move"), 1890 | ("image_browser_delete_message", "images_delete_message", True, "Print image deletion messages to the console"), 1891 | ("image_browser_txt_files", "images_txt_files", True, "Move/Copy/Delete matching .txt files"), 1892 | ("image_browser_debug_level", None, debug_level_choices[0], "Debug level", gr.Dropdown, lambda: {"choices": debug_level_choices}), 1893 | ("image_browser_delete_recycle", "images_delete_recycle", True, "Use recycle bin when deleting images"), 1894 | ("image_browser_scan_exif", "images_scan_exif", True, "Scan Exif-/.txt-data (initially slower, but required for many features to work)"), 1895 | ("image_browser_mod_shift", None, False, "Change CTRL keybindings to SHIFT"), 1896 | ("image_browser_mod_ctrl_shift", None, False, "or to CTRL+SHIFT"), 1897 | ("image_browser_swipe", None, False, "Swipe left/right navigates to the next image"), 1898 | ("image_browser_ranking_pnginfo", None, False, "Save ranking in image's pnginfo"), 1899 | ("image_browser_page_columns", "images_history_page_columns", 6, "Number of columns on the page"), 1900 | ("image_browser_page_rows", "images_history_page_rows", 6, "Number of rows on the page"), 1901 | ("image_browser_pages_perload", "images_history_pages_perload", 20, "Minimum number of pages per load"), 1902 | ("image_browser_height_auto", None, False, "Use automatic height for gallery (requires Gradio >= 3.36.0)"), 1903 | ("image_browser_use_thumbnail", None, False, "Use optimized images in the thumbnail interface (significantly reduces the amount of data transferred)"), 1904 | ("image_browser_thumbnail_size", None, 200, "Size of the thumbnails (px)"), 1905 | ("image_browser_thumbnail_crop", None, False, "Crop thumbnail to square"), 1906 | ("image_browser_img_tooltips", None, True, "Enable thumbnail tooltips"), 1907 | ("image_browser_show_progress", None, True, "Show progress indicator"), 1908 | ("image_browser_info_format", None, True, "Initially display Generation Info as formatted"), 1909 | ("image_browser_info_order", None, "Prompt,Negative prompt,Model,VAE,Sampler,Schedule,Steps,CFG scale,Seed,Size", "Display order for Generation Info as formatted"), 1910 | ("image_browser_info_add", None, False, "Show Additional Generation Info"), 1911 | ("image_browser_video_pos", None, "Above", "Video above or below gallery", gr.Dropdown, lambda: {"choices": ["Above", "Below"]}), 1912 | ("image_browser_video_x", None, 640, "Video player width (px)"), 1913 | ("image_browser_video_y", None, 640, "Video player height (px)"), 1914 | ] 1915 | 1916 | section = ('image-browser', "Image Browser") 1917 | # Move historic setting names to current names 1918 | added = 0 1919 | for cur_setting_name, old_setting_name, *option_info in image_browser_options: 1920 | if old_setting_name is not None: 1921 | added = move_setting(cur_setting_name, old_setting_name, option_info, section, added) 1922 | if added > 0: 1923 | shared.opts.save(shared.config_filename) 1924 | 1925 | for cur_setting_name, _, *option_info in image_browser_options: 1926 | shared.opts.add_option(cur_setting_name, shared.OptionInfo(*option_info, section=section)) 1927 | 1928 | script_callbacks.on_ui_settings(on_ui_settings) 1929 | script_callbacks.on_ui_tabs(on_ui_tabs) 1930 | -------------------------------------------------------------------------------- /scripts/wib/wib_db.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import os 4 | import sqlite3 5 | import re 6 | from shutil import copy2 7 | from modules import scripts, shared 8 | from tempfile import gettempdir 9 | from PIL import Image 10 | from contextlib import contextmanager 11 | 12 | version = 7 13 | 14 | path_recorder_file = os.path.join(scripts.basedir(), "path_recorder.txt") 15 | aes_cache_file = os.path.join(scripts.basedir(), "aes_scores.json") 16 | exif_cache_file = os.path.join(scripts.basedir(), "exif_data.json") 17 | ranking_file = os.path.join(scripts.basedir(), "ranking.json") 18 | archive = os.path.join(scripts.basedir(), "archive") 19 | source_db_file = os.path.join(scripts.basedir(), "wib.sqlite3") 20 | tmp_db_file = os.path.join(gettempdir(), "sd-images-browser.sqlite3") 21 | 22 | db_file = source_db_file 23 | if getattr(shared.cmd_opts, "image_browser_tmp_db", False): 24 | db_file = tmp_db_file 25 | if os.path.exists(source_db_file): 26 | copy2(source_db_file, tmp_db_file) 27 | elif os.path.exists(tmp_db_file): 28 | os.remove(tmp_db_file) 29 | 30 | def backup_tmp_db(): 31 | if(db_file == tmp_db_file): 32 | copy2(tmp_db_file, source_db_file) 33 | 34 | np = "Negative prompt: " 35 | st = "Steps: " 36 | timeout = 30 37 | 38 | @contextmanager 39 | def transaction(db = db_file): 40 | conn = sqlite3.connect(db, timeout=timeout) 41 | try: 42 | conn.isolation_level = None 43 | cursor = conn.cursor() 44 | cursor.execute("BEGIN") 45 | yield cursor 46 | cursor.execute("COMMIT") 47 | finally: 48 | conn.close() 49 | backup_tmp_db() 50 | 51 | def create_filehash(cursor): 52 | cursor.execute(''' 53 | CREATE TABLE IF NOT EXISTS filehash ( 54 | file TEXT PRIMARY KEY, 55 | hash TEXT, 56 | created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 57 | updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP 58 | ) 59 | ''') 60 | 61 | cursor.execute(''' 62 | CREATE TRIGGER filehash_tr 63 | AFTER UPDATE ON filehash 64 | BEGIN 65 | UPDATE filehash SET updated = CURRENT_TIMESTAMP WHERE file = OLD.file; 66 | END; 67 | ''') 68 | 69 | return 70 | 71 | def create_work_files(cursor): 72 | cursor.execute(''' 73 | CREATE TABLE IF NOT EXISTS work_files ( 74 | file TEXT PRIMARY KEY 75 | ) 76 | ''') 77 | 78 | return 79 | 80 | def create_db(cursor): 81 | cursor.execute(''' 82 | CREATE TABLE IF NOT EXISTS db_data ( 83 | key TEXT PRIMARY KEY, 84 | value TEXT 85 | ) 86 | ''') 87 | 88 | cursor.execute(''' 89 | CREATE TABLE IF NOT EXISTS path_recorder ( 90 | path TEXT PRIMARY KEY, 91 | depth INT, 92 | path_display TEXT, 93 | created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 94 | updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP 95 | ) 96 | ''') 97 | 98 | cursor.execute(''' 99 | CREATE TRIGGER path_recorder_tr 100 | AFTER UPDATE ON path_recorder 101 | BEGIN 102 | UPDATE path_recorder SET updated = CURRENT_TIMESTAMP WHERE path = OLD.path; 103 | END; 104 | ''') 105 | 106 | cursor.execute(''' 107 | CREATE TABLE IF NOT EXISTS exif_data ( 108 | file TEXT, 109 | key TEXT, 110 | value TEXT, 111 | created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 112 | updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 113 | PRIMARY KEY (file, key) 114 | ) 115 | ''') 116 | 117 | cursor.execute(''' 118 | CREATE INDEX IF NOT EXISTS exif_data_key ON exif_data (key) 119 | ''') 120 | 121 | cursor.execute(''' 122 | CREATE TRIGGER exif_data_tr 123 | AFTER UPDATE ON exif_data 124 | BEGIN 125 | UPDATE exif_data SET updated = CURRENT_TIMESTAMP WHERE file = OLD.file AND key = OLD.key; 126 | END; 127 | ''') 128 | 129 | cursor.execute(''' 130 | CREATE TABLE IF NOT EXISTS ranking ( 131 | file TEXT PRIMARY KEY, 132 | name TEXT, 133 | ranking TEXT, 134 | created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 135 | updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP 136 | ) 137 | ''') 138 | 139 | cursor.execute(''' 140 | CREATE INDEX IF NOT EXISTS ranking_name ON ranking (name) 141 | ''') 142 | 143 | cursor.execute(''' 144 | CREATE TRIGGER ranking_tr 145 | AFTER UPDATE ON ranking 146 | BEGIN 147 | UPDATE ranking SET updated = CURRENT_TIMESTAMP WHERE file = OLD.file; 148 | END; 149 | ''') 150 | 151 | create_filehash(cursor) 152 | create_work_files(cursor) 153 | 154 | return 155 | 156 | def migrate_path_recorder(cursor): 157 | if os.path.exists(path_recorder_file): 158 | try: 159 | with open(path_recorder_file) as f: 160 | # json-version 161 | path_recorder = json.load(f) 162 | for path, values in path_recorder.items(): 163 | path = os.path.realpath(path) 164 | depth = values["depth"] 165 | path_display = f"{path} [{depth}]" 166 | cursor.execute(''' 167 | INSERT INTO path_recorder (path, depth, path_display) 168 | VALUES (?, ?, ?) 169 | ''', (path, depth, path_display)) 170 | except json.JSONDecodeError: 171 | with open(path_recorder_file) as f: 172 | # old txt-version 173 | path = f.readline().rstrip("\n") 174 | while len(path) > 0: 175 | path = os.path.realpath(path) 176 | cursor.execute(''' 177 | INSERT INTO path_recorder (path, depth, path_display) 178 | VALUES (?, ?, ?) 179 | ''', (path, 0, f"{path} [0]")) 180 | path = f.readline().rstrip("\n") 181 | 182 | return 183 | 184 | def split_exif_data(info): 185 | prompt = "0" 186 | negative_prompt = "0" 187 | key_values = "0: 0" 188 | key_value_pairs = [] 189 | 190 | def parse_value_pairs(kv_str, key_prefix=""): 191 | # Regular expression pattern to match key-value pairs, including multiline prompts 192 | pattern = r"((?:\w+ )?(?:Prompt|Negative Prompt)|[^:]+):\s*((?:[^,]+(?:,(?![^:]+:))?)+)" 193 | 194 | # Find all matches 195 | matches = re.findall(pattern, kv_str, re.IGNORECASE | re.DOTALL) 196 | result = {} 197 | current_prompt = None 198 | 199 | def process_prompt(key, value, current_prompt): 200 | if current_prompt is None: 201 | result[key] = value 202 | current_prompt = key 203 | else: 204 | pk_values = [v.strip() for v in key.split(",") if v.strip()] 205 | result[current_prompt] += f",{','.join(pk_values[:-1])}" 206 | current_prompt = pk_values[-1] 207 | result[current_prompt] = ",".join([v.strip() for v in value.split(",") if v.strip()]) 208 | 209 | return current_prompt 210 | 211 | def process_regular_key(key, value, current_prompt): 212 | values = [v.strip() for v in value.split(",") if v.strip()] 213 | if current_prompt is not None: 214 | pk_values = [v.strip() for v in key.split(",") if v.strip()] 215 | result[current_prompt] += f",{','.join(pk_values[:-1])}" 216 | current_prompt = None 217 | key = pk_values[-1] 218 | result[key] = values[0] if len(values) == 1 else ",".join(values) 219 | 220 | return current_prompt 221 | 222 | for key, value in matches: 223 | key = key.strip(" ,") 224 | value = value.strip() 225 | 226 | if "prompt" in key.lower() or "prompt" in value.lower(): 227 | current_prompt = process_prompt(key, value, current_prompt) 228 | else: 229 | current_prompt = process_regular_key(key, value, current_prompt) 230 | 231 | # Print the resulting key-value pairs 232 | for key, value in result.items(): 233 | value = value.strip(" ,") 234 | if value.startswith('"') and value.endswith('"'): 235 | value = value[1:-1] 236 | parse_value_pairs(value, f"{key_prefix} - {key}" if key_prefix != "" else key) 237 | 238 | key_value_pairs.append((f"{key_prefix} - {key}" if key_prefix != "" else key, value)) 239 | 240 | if info != "0": 241 | info_list = info.split("\n") 242 | prompt = "" 243 | negative_prompt = "" 244 | key_values = "" 245 | for info_item in info_list: 246 | if info_item.startswith(st): 247 | key_values = info_item 248 | elif info_item.startswith(np): 249 | negative_prompt = info_item.replace(np, "") 250 | else: 251 | if prompt == "": 252 | prompt = info_item 253 | else: 254 | # multiline prompts 255 | prompt = f"{prompt}\n{info_item}" 256 | 257 | if key_values != "": 258 | pattern = r'(\w+(?:\s+\w+)*?):\s*((?:"[^"]*"|[^,])+)(?:,\s*|$)' 259 | matches = re.findall(pattern, key_values) 260 | result = {key.strip(): value.strip() for key, value in matches} 261 | 262 | # Save resulting key-value pairs 263 | for key, value in result.items(): 264 | value = value.strip(" ,") 265 | if value.startswith('"') and value.endswith('"'): 266 | value = value[1:-1] 267 | parse_value_pairs(value, key) 268 | 269 | key_value_pairs.append((key, value)) 270 | 271 | return prompt, negative_prompt, key_value_pairs 272 | 273 | def update_exif_data(cursor, file, info): 274 | prompt, negative_prompt, key_value_pairs = split_exif_data(info) 275 | if key_value_pairs: 276 | try: 277 | cursor.execute(''' 278 | INSERT INTO exif_data (file, key, value) 279 | VALUES (?, ?, ?) 280 | ''', (file, "prompt", prompt)) 281 | except sqlite3.IntegrityError: 282 | # Duplicate, delete all "file" entries and try again 283 | cursor.execute(''' 284 | DELETE FROM exif_data 285 | WHERE file = ? 286 | ''', (file,)) 287 | 288 | cursor.execute(''' 289 | INSERT INTO exif_data (file, key, value) 290 | VALUES (?, ?, ?) 291 | ''', (file, "prompt", prompt)) 292 | 293 | cursor.execute(''' 294 | INSERT INTO exif_data (file, key, value) 295 | VALUES (?, ?, ?) 296 | ''', (file, "negative_prompt", negative_prompt)) 297 | 298 | for (key, value) in key_value_pairs: 299 | try: 300 | cursor.execute(''' 301 | INSERT INTO exif_data (file, key, value) 302 | VALUES (?, ?, ?) 303 | ''', (file, key, value)) 304 | except sqlite3.IntegrityError: 305 | pass 306 | 307 | return 308 | 309 | def migrate_exif_data(cursor): 310 | if os.path.exists(exif_cache_file): 311 | with open(exif_cache_file, 'r') as file: 312 | exif_cache = json.load(file) 313 | 314 | for file, info in exif_cache.items(): 315 | file = os.path.realpath(file) 316 | update_exif_data(cursor, file, info) 317 | 318 | return 319 | 320 | def migrate_ranking(cursor): 321 | if os.path.exists(ranking_file): 322 | with open(ranking_file, 'r') as file: 323 | ranking = json.load(file) 324 | for file, info in ranking.items(): 325 | if info != "None": 326 | file = os.path.realpath(file) 327 | name = os.path.basename(file) 328 | cursor.execute(''' 329 | INSERT INTO ranking (file, name, ranking) 330 | VALUES (?, ?, ?) 331 | ''', (file, name, info)) 332 | 333 | return 334 | 335 | def get_hash(file): 336 | # Get filehash without exif info 337 | try: 338 | image = Image.open(file) 339 | except Exception as e: 340 | print(e) 341 | 342 | hash = hashlib.sha512(image.tobytes()).hexdigest() 343 | image.close() 344 | 345 | return hash 346 | 347 | def migrate_filehash(cursor, version): 348 | if version <= "4": 349 | create_filehash(cursor) 350 | 351 | cursor.execute(''' 352 | SELECT file 353 | FROM ranking 354 | ''') 355 | for (file,) in cursor.fetchall(): 356 | if os.path.exists(file): 357 | hash = get_hash(file) 358 | cursor.execute(''' 359 | INSERT OR REPLACE 360 | INTO filehash (file, hash) 361 | VALUES (?, ?) 362 | ''', (file, hash)) 363 | 364 | return 365 | 366 | def migrate_work_files(cursor): 367 | create_work_files(cursor) 368 | 369 | return 370 | 371 | def update_db_data(cursor, key, value): 372 | cursor.execute(''' 373 | INSERT OR REPLACE 374 | INTO db_data (key, value) 375 | VALUES (?, ?) 376 | ''', (key, value)) 377 | 378 | return 379 | 380 | def get_version(): 381 | with transaction() as cursor: 382 | cursor.execute(''' 383 | SELECT value 384 | FROM db_data 385 | WHERE key = 'version' 386 | ''',) 387 | db_version = cursor.fetchone() 388 | 389 | return db_version 390 | 391 | def get_last_default_tab(): 392 | with transaction() as cursor: 393 | cursor.execute(''' 394 | SELECT value 395 | FROM db_data 396 | WHERE key = 'last_default_tab' 397 | ''',) 398 | last_default_tab = cursor.fetchone() 399 | 400 | return last_default_tab 401 | 402 | def migrate_path_recorder_dirs(cursor): 403 | cursor.execute(''' 404 | SELECT path, path_display 405 | FROM path_recorder 406 | ''') 407 | for (path, path_display) in cursor.fetchall(): 408 | real_path = os.path.realpath(path) 409 | if path != real_path: 410 | update_from = path 411 | update_to = real_path 412 | try: 413 | cursor.execute(''' 414 | UPDATE path_recorder 415 | SET path = ?, 416 | path_display = ? || SUBSTR(path_display, LENGTH(?) + 1) 417 | WHERE path = ? 418 | ''', (update_to, update_to, update_from, update_from)) 419 | except sqlite3.IntegrityError as e: 420 | # these are double keys, because the same file can be in the db with different path notations 421 | (e_msg,) = e.args 422 | if e_msg.startswith("UNIQUE constraint"): 423 | cursor.execute(''' 424 | DELETE FROM path_recorder 425 | WHERE path = ? 426 | ''', (update_from,)) 427 | else: 428 | raise 429 | 430 | return 431 | 432 | def migrate_exif_data_dirs(cursor): 433 | cursor.execute(''' 434 | SELECT file 435 | FROM exif_data 436 | ''') 437 | for (filepath,) in cursor.fetchall(): 438 | (path, file) = os.path.split(filepath) 439 | real_path = os.path.realpath(path) 440 | if path != real_path: 441 | update_from = filepath 442 | update_to = os.path.join(real_path, file) 443 | try: 444 | cursor.execute(''' 445 | UPDATE exif_data 446 | SET file = ? 447 | WHERE file = ? 448 | ''', (update_to, update_from)) 449 | except sqlite3.IntegrityError as e: 450 | # these are double keys, because the same file can be in the db with different path notations 451 | (e_msg,) = e.args 452 | if e_msg.startswith("UNIQUE constraint"): 453 | cursor.execute(''' 454 | DELETE FROM exif_data 455 | WHERE file = ? 456 | ''', (update_from,)) 457 | else: 458 | raise 459 | 460 | return 461 | 462 | def migrate_ranking_dirs(cursor, db_version): 463 | if db_version == "1": 464 | cursor.execute(''' 465 | ALTER TABLE ranking 466 | ADD COLUMN name TEXT 467 | ''') 468 | 469 | cursor.execute(''' 470 | CREATE INDEX IF NOT EXISTS ranking_name ON ranking (name) 471 | ''') 472 | 473 | cursor.execute(''' 474 | SELECT file, ranking 475 | FROM ranking 476 | ''') 477 | for (filepath, ranking) in cursor.fetchall(): 478 | if filepath == "" or ranking == "None": 479 | cursor.execute(''' 480 | DELETE FROM ranking 481 | WHERE file = ? 482 | ''', (filepath,)) 483 | else: 484 | (path, file) = os.path.split(filepath) 485 | real_path = os.path.realpath(path) 486 | name = file 487 | update_from = filepath 488 | update_to = os.path.join(real_path, file) 489 | try: 490 | cursor.execute(''' 491 | UPDATE ranking 492 | SET file = ?, 493 | name = ? 494 | WHERE file = ? 495 | ''', (update_to, name, update_from)) 496 | except sqlite3.IntegrityError as e: 497 | # these are double keys, because the same file can be in the db with different path notations 498 | (e_msg,) = e.args 499 | if e_msg.startswith("UNIQUE constraint"): 500 | cursor.execute(''' 501 | DELETE FROM ranking 502 | WHERE file = ? 503 | ''', (update_from,)) 504 | else: 505 | raise 506 | 507 | return 508 | 509 | def check(): 510 | if not os.path.exists(db_file): 511 | print("Image Browser: Creating database") 512 | with transaction() as cursor: 513 | create_db(cursor) 514 | update_db_data(cursor, "version", version) 515 | update_db_data(cursor, "last_default_tab", "Maintenance") 516 | migrate_path_recorder(cursor) 517 | migrate_exif_data(cursor) 518 | migrate_ranking(cursor) 519 | migrate_filehash(cursor, str(version)) 520 | print("Image Browser: Database created") 521 | db_version = get_version() 522 | 523 | with transaction() as cursor: 524 | if db_version[0] <= "2": 525 | # version 1 database had mixed path notations, changed them all to abspath 526 | # version 2 database still had mixed path notations, because of windows short name, changed them all to realpath 527 | print(f"Image Browser: Upgrading database from version {db_version[0]} to version {version}") 528 | migrate_path_recorder_dirs(cursor) 529 | migrate_exif_data_dirs(cursor) 530 | migrate_ranking_dirs(cursor, db_version[0]) 531 | if db_version[0] <= "4": 532 | migrate_filehash(cursor, db_version[0]) 533 | if db_version[0] <= "5": 534 | migrate_work_files(cursor) 535 | if db_version[0] <= "6": 536 | update_db_data(cursor, "last_default_tab", "Others") 537 | 538 | update_db_data(cursor, "version", version) 539 | print(f"Image Browser: Database upgraded from version {db_version[0]} to version {version}") 540 | 541 | return version 542 | 543 | def load_path_recorder(): 544 | with transaction() as cursor: 545 | cursor.execute(''' 546 | SELECT path, depth, path_display 547 | FROM path_recorder 548 | ''') 549 | path_recorder = {path: {"depth": depth, "path_display": path_display} for path, depth, path_display in cursor.fetchall()} 550 | 551 | return path_recorder 552 | 553 | def select_ranking(file): 554 | with transaction() as cursor: 555 | cursor.execute(''' 556 | SELECT ranking 557 | FROM ranking 558 | WHERE file = ? 559 | ''', (file,)) 560 | ranking_value = cursor.fetchone() 561 | 562 | if ranking_value is None: 563 | return_ranking = "None" 564 | else: 565 | (return_ranking,) = ranking_value 566 | 567 | return return_ranking 568 | 569 | def update_ranking(file, ranking): 570 | name = os.path.basename(file) 571 | with transaction() as cursor: 572 | if ranking == "None": 573 | cursor.execute(''' 574 | DELETE FROM ranking 575 | WHERE file = ? 576 | ''', (file,)) 577 | else: 578 | cursor.execute(''' 579 | INSERT OR REPLACE 580 | INTO ranking (file, name, ranking) 581 | VALUES (?, ?, ?) 582 | ''', (file, name, ranking)) 583 | 584 | hash = get_hash(file) 585 | cursor.execute(''' 586 | INSERT OR REPLACE 587 | INTO filehash (file, hash) 588 | VALUES (?, ?) 589 | ''', (file, hash)) 590 | 591 | return 592 | 593 | def update_path_recorder(path, depth, path_display): 594 | with transaction() as cursor: 595 | cursor.execute(''' 596 | INSERT OR REPLACE 597 | INTO path_recorder (path, depth, path_display) 598 | VALUES (?, ?, ?) 599 | ''', (path, depth, path_display)) 600 | 601 | return 602 | 603 | def update_path_recorder(path, depth, path_display): 604 | with transaction() as cursor: 605 | cursor.execute(''' 606 | INSERT OR REPLACE 607 | INTO path_recorder (path, depth, path_display) 608 | VALUES (?, ?, ?) 609 | ''', (path, depth, path_display)) 610 | 611 | return 612 | 613 | def delete_path_recorder(path): 614 | with transaction() as cursor: 615 | cursor.execute(''' 616 | DELETE FROM path_recorder 617 | WHERE path = ? 618 | ''', (path,)) 619 | 620 | return 621 | 622 | def update_path_recorder_mult(cursor, update_from, update_to): 623 | cursor.execute(''' 624 | UPDATE path_recorder 625 | SET path = ?, 626 | path_display = ? || SUBSTR(path_display, LENGTH(?) + 1) 627 | WHERE path = ? 628 | ''', (update_to, update_to, update_from, update_from)) 629 | 630 | return 631 | 632 | def update_exif_data_mult(cursor, update_from, update_to): 633 | update_from = update_from + os.path.sep 634 | update_to = update_to + os.path.sep 635 | cursor.execute(''' 636 | UPDATE exif_data 637 | SET file = ? || SUBSTR(file, LENGTH(?) + 1) 638 | WHERE file like ? || '%' 639 | ''', (update_to, update_from, update_from)) 640 | 641 | return 642 | 643 | def update_ranking_mult(cursor, update_from, update_to): 644 | update_from = update_from + os.path.sep 645 | update_to = update_to + os.path.sep 646 | cursor.execute(''' 647 | UPDATE ranking 648 | SET file = ? || SUBSTR(file, LENGTH(?) + 1) 649 | WHERE file like ? || '%' 650 | ''', (update_to, update_from, update_from)) 651 | 652 | return 653 | 654 | def delete_exif_0(cursor): 655 | cursor.execute(''' 656 | DELETE FROM exif_data 657 | WHERE file IN ( 658 | SELECT file FROM exif_data a 659 | WHERE value = '0' 660 | GROUP BY file 661 | HAVING COUNT(*) = (SELECT COUNT(*) FROM exif_data WHERE file = a.file) 662 | ) 663 | ''') 664 | 665 | return 666 | 667 | def get_ranking_by_file(cursor, file): 668 | cursor.execute(''' 669 | SELECT ranking 670 | FROM ranking 671 | WHERE file = ? 672 | ''', (file,)) 673 | ranking_value = cursor.fetchone() 674 | 675 | return ranking_value 676 | 677 | def get_ranking_by_name(cursor, name): 678 | cursor.execute(''' 679 | SELECT file, ranking 680 | FROM ranking 681 | WHERE name = ? 682 | ''', (name,)) 683 | ranking_value = cursor.fetchone() 684 | 685 | if ranking_value is not None: 686 | (file, _) = ranking_value 687 | cursor.execute(''' 688 | SELECT hash 689 | FROM filehash 690 | WHERE file = ? 691 | ''', (file,)) 692 | hash_value = cursor.fetchone() 693 | else: 694 | hash_value = None 695 | 696 | return ranking_value, hash_value 697 | 698 | def insert_ranking(cursor, file, ranking, hash): 699 | name = os.path.basename(file) 700 | cursor.execute(''' 701 | INSERT INTO ranking (file, name, ranking) 702 | VALUES (?, ?, ?) 703 | ''', (file, name, ranking)) 704 | 705 | cursor.execute(''' 706 | INSERT OR REPLACE 707 | INTO filehash (file, hash) 708 | VALUES (?, ?) 709 | ''', (file, hash)) 710 | 711 | return 712 | 713 | def replace_ranking(cursor, file, alternate_file, hash): 714 | name = os.path.basename(file) 715 | cursor.execute(''' 716 | UPDATE ranking 717 | SET file = ? 718 | WHERE file = ? 719 | ''', (file, alternate_file)) 720 | 721 | cursor.execute(''' 722 | INSERT OR REPLACE 723 | INTO filehash (file, hash) 724 | VALUES (?, ?) 725 | ''', (file, hash)) 726 | 727 | return 728 | 729 | def update_exif_data_by_key(cursor, file, key, value): 730 | cursor.execute(''' 731 | INSERT OR REPLACE 732 | INTO exif_data (file, key, value) 733 | VALUES (?, ?, ?) 734 | ''', (file, key, value)) 735 | 736 | return 737 | 738 | def select_prompts(file): 739 | with transaction() as cursor: 740 | cursor.execute(''' 741 | SELECT key, value 742 | FROM exif_data 743 | WHERE file = ? 744 | AND KEY in ('prompt', 'negative_prompt') 745 | ''', (file,)) 746 | 747 | rows = cursor.fetchall() 748 | prompt = "" 749 | neg_prompt = "" 750 | for row in rows: 751 | (key, value) = row 752 | if key == 'prompt': 753 | prompt = value 754 | elif key == 'negative_prompt': 755 | neg_prompt = value 756 | 757 | return prompt, neg_prompt 758 | 759 | def load_exif_data(exif_cache): 760 | with transaction() as cursor: 761 | cursor.execute(''' 762 | SELECT file, group_concat( 763 | case when key = 'prompt' or key = 'negative_prompt' then key || ': ' || value || '\n' 764 | else key || ': ' || value 765 | end, ', ') AS string 766 | FROM ( 767 | SELECT * 768 | FROM exif_data 769 | ORDER BY 770 | CASE WHEN key = 'prompt' THEN 0 771 | WHEN key = 'negative_prompt' THEN 1 772 | ELSE 2 END, 773 | key 774 | ) 775 | GROUP BY file 776 | ''') 777 | 778 | rows = cursor.fetchall() 779 | for row in rows: 780 | exif_cache[row[0]] = row[1] 781 | 782 | return exif_cache 783 | 784 | def load_exif_data_by_key(cache, key1, key2): 785 | with transaction() as cursor: 786 | cursor.execute(''' 787 | SELECT file, value 788 | FROM exif_data 789 | WHERE key IN (?, ?) 790 | ''', (key1, key2)) 791 | 792 | rows = cursor.fetchall() 793 | for row in rows: 794 | cache[row[0]] = row[1] 795 | 796 | return cache 797 | 798 | def get_exif_dirs(): 799 | with transaction() as cursor: 800 | cursor.execute(''' 801 | SELECT file 802 | FROM exif_data 803 | ''') 804 | 805 | rows = cursor.fetchall() 806 | 807 | dirs = {} 808 | for row in rows: 809 | dir = os.path.dirname(row[0]) 810 | dirs[dir] = dir 811 | 812 | return dirs 813 | 814 | def fill_work_files(cursor, fileinfos): 815 | filenames = [x[0] for x in fileinfos] 816 | 817 | cursor.execute(''' 818 | DELETE 819 | FROM work_files 820 | ''') 821 | 822 | sql = ''' 823 | INSERT INTO work_files (file) 824 | VALUES (?) 825 | ''' 826 | 827 | cursor.executemany(sql, [(x,) for x in filenames]) 828 | 829 | return 830 | 831 | def filter_aes(cursor, fileinfos, aes_filter_min_num, aes_filter_max_num): 832 | key = "aesthetic_score" 833 | 834 | cursor.execute(''' 835 | DELETE 836 | FROM work_files 837 | WHERE file not in ( 838 | SELECT file 839 | FROM exif_data b 840 | WHERE file = b.file 841 | AND b.key = ? 842 | AND CAST(b.value AS REAL) between ? and ? 843 | ) 844 | ''', (key, aes_filter_min_num, aes_filter_max_num)) 845 | 846 | cursor.execute(''' 847 | SELECT file 848 | FROM work_files 849 | ''') 850 | 851 | rows = cursor.fetchall() 852 | 853 | fileinfos_dict = {pair[0]: pair[1] for pair in fileinfos} 854 | fileinfos_new = [] 855 | for (file,) in rows: 856 | if fileinfos_dict.get(file) is not None: 857 | fileinfos_new.append((file, fileinfos_dict[file])) 858 | 859 | return fileinfos_new 860 | 861 | def filter_ranking(cursor, fileinfos, ranking_filter, ranking_filter_min_num, ranking_filter_max_num): 862 | if ranking_filter == "None": 863 | cursor.execute(''' 864 | DELETE 865 | FROM work_files 866 | WHERE file IN ( 867 | SELECT file 868 | FROM ranking b 869 | WHERE file = b.file 870 | ) 871 | ''') 872 | elif ranking_filter == "Min-max": 873 | cursor.execute(''' 874 | DELETE 875 | FROM work_files 876 | WHERE file NOT IN ( 877 | SELECT file 878 | FROM ranking b 879 | WHERE file = b.file 880 | AND b.ranking BETWEEN ? AND ? 881 | ) 882 | ''', (ranking_filter_min_num, ranking_filter_max_num)) 883 | else: 884 | cursor.execute(''' 885 | DELETE 886 | FROM work_files 887 | WHERE file NOT IN ( 888 | SELECT file 889 | FROM ranking b 890 | WHERE file = b.file 891 | AND b.ranking = ? 892 | ) 893 | ''', (ranking_filter,)) 894 | 895 | cursor.execute(''' 896 | SELECT file 897 | FROM work_files 898 | ''') 899 | 900 | rows = cursor.fetchall() 901 | 902 | fileinfos_dict = {pair[0]: pair[1] for pair in fileinfos} 903 | fileinfos_new = [] 904 | for (file,) in rows: 905 | if fileinfos_dict.get(file) is not None: 906 | fileinfos_new.append((file, fileinfos_dict[file])) 907 | 908 | return fileinfos_new 909 | 910 | def select_x_y(cursor, file): 911 | cursor.execute(''' 912 | SELECT value 913 | FROM exif_data 914 | WHERE file = ? 915 | AND key = 'Size' 916 | ''', (file,)) 917 | size_value = cursor.fetchone() 918 | 919 | if size_value is None: 920 | x = "?" 921 | y = "?" 922 | else: 923 | (size,) = size_value 924 | parts = size.split("x") 925 | x = parts[0] 926 | y = parts[1] 927 | 928 | return x, y 929 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .thumbnails.svelte-1tkea93.svelte-1tkea93 { 2 | justify-content: initial; 3 | } 4 | 5 | [id^='image_browser_tab_'] .thumbnails.scroll-hide { 6 | justify-content: initial; 7 | } 8 | 9 | div[id^="image_browser_tab"][id$="image_browser_gallery"].hide_loading > .svelte-gjihhp { 10 | display: none; 11 | } 12 | 13 | /* hack to fix the alignment of the page index, refresh, and delete buttons*/ 14 | div[id$='_control_image_browser_page_index'] { 15 | margin-top: -20px !important; 16 | display: grid; 17 | justify-content: end; 18 | } 19 | button[id$='_control_image_browser_refresh_index']{ 20 | align-self: center !important; 21 | height: 2em !important; 22 | cursor: pointer !important; 23 | } 24 | button[id$='_image_browser_del_img_btn'] { 25 | margin-top: calc(var(--body-text-size) * var(--line-sm)); 26 | } 27 | 28 | #tab_image_browser .no-gap-top { 29 | margin-top: calc(var(--layout-gap) * -1); 30 | } 31 | 32 | #tab_image_browser .mb-0 { 33 | margin-bottom: 0; 34 | } 35 | 36 | #tab_image_browser .text-subdued { 37 | color: var(--body-text-color-subdued); 38 | } 39 | 40 | #tab_image_browser button[id$='_control_image_browser_first_page'], 41 | #tab_image_browser button[id$='_control_image_browser_prev_page'], 42 | #tab_image_browser button[id$='_control_image_browser_next_page'], 43 | #tab_image_browser button[id$='_control_image_browser_end_page'] { 44 | cursor: pointer !important; 45 | } 46 | 47 | #tab_image_browser div[id$='_control_image_browser_page_index'], 48 | #tab_image_browser .right-column-panel { 49 | margin-top: calc(var(--body-text-size) * var(--line-sm) * -1) !important; 50 | } 51 | 52 | #tab_image_browser .sort-panel button { 53 | margin-top: calc(var(--body-text-size) * var(--line-sm)); 54 | align-self: center; 55 | } 56 | 57 | #tab_image_browser .ranking-filter-input input:disabled{ 58 | cursor: not-allowed; 59 | } 60 | 61 | /* Fit the image inside the button */ 62 | #tab_image_browser .thumbnail-item > img { 63 | object-fit: contain !important; 64 | } 65 | #tab_image_browser .thumbnail-lg > img { 66 | object-fit: contain !important; 67 | } 68 | /*Fit the button around the image 69 | #tab_image_browser .thumbnail-item { 70 | aspect-ratio: auto !important; 71 | }*/ 72 | 73 | #tab_image_browser .thumbnails .thumbnail-item:first-child{ 74 | margin-left: var(--spacing-lg); 75 | } 76 | 77 | #tab_image_browser .thumbnails .thumbnail-item:last-child{ 78 | margin-right: var(--spacing-lg); 79 | } 80 | 81 | #tab_image_browser .page-index-panel > div { 82 | flex-wrap: nowrap; 83 | align-self: flex-start; 84 | } 85 | 86 | #tab_image_browser .page-index-panel > div > div { 87 | min-width: auto; 88 | } 89 | 90 | #tab_image_browser .refresh-index-panel > div { 91 | margin-top: var(--spacing-sm); 92 | } 93 | 94 | #tab_image_browser .gradio-dropdown ul.options li.item { 95 | white-space: wrap; 96 | word-break: break-all; 97 | } 98 | 99 | #tab_image_browser .gradio-box { 100 | padding: 5px !important; 101 | 102 | } 103 | .image_browser_symbol_button { 104 | margin-top: var(--text-xxl) !important; 105 | padding: 1px !important; 106 | } 107 | .image_browser_file_info_formatted { 108 | width: 100%; 109 | } 110 | .image_browser_file_info_formatted th { 111 | display: none; 112 | } 113 | .image_browser_file_info_formatted td:first-child { 114 | width: 30%; 115 | } 116 | .image_browser_file_info_formatted td:not(:first-child) { 117 | width: 70%; 118 | } --------------------------------------------------------------------------------