From b85fe70488e036ddc11fa58e0d73b88b8266bd51 Mon Sep 17 00:00:00 2001 From: backflow Date: Sat, 8 Jul 2023 03:32:49 +0800 Subject: [PATCH] feat: LyCORIS (also called "locon") model supported. feat: Support model deletion function in "Extra Network" model viewer. fix: "Download all files" now working as expect. fix: Auto select newest version when load model info from url. fix: Auto select default '/' folder when load model info from url. feat: Press "`" key to toggle "Extra Network" model viewer under "img2img" and "txt2img" tab. feat: Press "x" key to fast paste Civitai model url and load model info under "Civitai Helper" tab,then click "Download Model". feat: Press "ctrl + x" keys trigger model generate. refact: Move scripted css to style.css and other style adjusts. --- README.md | 2 +- javascript/civitai_helper.js | 745 ++++++++++++------------- scripts/ch_lib/civitai.py | 53 +- scripts/ch_lib/downloader.py | 21 +- scripts/ch_lib/js_action_civitai.py | 14 +- scripts/ch_lib/model.py | 3 +- scripts/ch_lib/model_action_civitai.py | 76 ++- scripts/ch_lib/msg_handler.py | 2 +- scripts/ch_lib/util.py | 2 +- scripts/civitai_helper.py | 8 +- style.css | 42 +- 11 files changed, 498 insertions(+), 470 deletions(-) diff --git a/README.md b/README.md index 9d393fc..18a1f1d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Civitai: [Civitai Url](https://civitai.com/models/16768/civitai-helper-sd-webui- - 🖼️: Modified "replace preview" text into this icon - 🌐: Open this model's Civitai url in a new tab - 💡: Add this model's trigger words to prompt - - 🏷️: Use this model's preview image's prompt + - 🪞: Use this model's preview image's prompt * Above buttons support thumbnail mode of Extra Network * Option to always show additional buttons, to work with touchscreen. diff --git a/javascript/civitai_helper.js b/javascript/civitai_helper.js index 4454d0d..e698b6b 100644 --- a/javascript/civitai_helper.js +++ b/javascript/civitai_helper.js @@ -1,68 +1,58 @@ "use strict"; - -function ch_convert_file_path_to_url(path){ +function ch_convert_file_path_to_url(path) { let prefix = "file="; let path_to_url = path.replaceAll('\\', '/'); - return prefix+path_to_url; + return prefix + path_to_url; } -function ch_img_node_str(path){ +function ch_img_node_str(path) { return ``; } - -function ch_gradio_version(){ +function ch_gradio_version() { let foot = gradioApp().getElementById("footer"); - if (!foot){return null;} + if (!foot) { return null; } let versions = foot.querySelector(".versions"); - if (!versions){return null;} + if (!versions) { return null; } - if (versions.innerHTML.indexOf("gradio: 3.16.2")>0) { + if (versions.innerHTML.indexOf("gradio: 3.16.2") > 0) { return "3.16.2"; } else { return "3.23.0"; } - } - // send msg to python side by filling a hidden text box // then will click a button to trigger an action // msg is an object, not a string, will be stringify in this function -function send_ch_py_msg(msg){ - console.log("run send_ch_py_msg") +function send_ch_py_msg(msg) { + // console.log("run send_ch_py_msg") let js_msg_txtbox = gradioApp().querySelector("#ch_js_msg_txtbox textarea"); if (js_msg_txtbox && msg) { // fill to msg box js_msg_txtbox.value = JSON.stringify(msg); js_msg_txtbox.dispatchEvent(new Event("input")); } - } // get msg from python side from a hidden textbox // normally this is an old msg, need to wait for a new msg -function get_ch_py_msg(){ +function get_ch_py_msg() { console.log("run get_ch_py_msg") const py_msg_txtbox = gradioApp().querySelector("#ch_py_msg_txtbox textarea"); if (py_msg_txtbox && py_msg_txtbox.value) { - console.log("find py_msg_txtbox"); - console.log("py_msg_txtbox value: "); - console.log(py_msg_txtbox.value) + console.log("find py_msg_txtbox, value:", py_msg_txtbox.value) return py_msg_txtbox.value } else { return "" } } - // get msg from python side from a hidden textbox // it will try once in every sencond, until it reach the max try times -const get_new_ch_py_msg = (max_count=3) => new Promise((resolve, reject) => { - console.log("run get_new_ch_py_msg") - +const get_new_ch_py_msg = (max_count = 7) => new Promise((resolve, reject) => { let count = 0; let new_msg = ""; let find_msg = false; @@ -71,13 +61,10 @@ const get_new_ch_py_msg = (max_count=3) => new Promise((resolve, reject) => { count++; if (py_msg_txtbox && py_msg_txtbox.value) { - console.log("find py_msg_txtbox"); - console.log("py_msg_txtbox value: "); - console.log(py_msg_txtbox.value) - + console.log("find py_msg_txtbox, value: ", py_msg_txtbox.value) new_msg = py_msg_txtbox.value if (new_msg != "") { - find_msg=true + find_msg = true } } @@ -96,11 +83,9 @@ const get_new_ch_py_msg = (max_count=3) => new Promise((resolve, reject) => { reject(''); clearInterval(interval); } - - }, 1000); + }, 400); }) - function getActiveTabType() { const currentTab = get_uiCurrentTabContent(); switch (currentTab.id) { @@ -112,8 +97,6 @@ function getActiveTabType() { return null; } - - function getActivePrompt() { const currentTab = get_uiCurrentTabContent(); switch (currentTab.id) { @@ -136,9 +119,8 @@ function getActiveNegativePrompt() { return null; } - -//button's click function -async function open_model_url(event, model_type, search_term){ +// button's click function +async function open_model_url(event, model_type, search_term) { console.log("start open_model_url"); //get hidden components of extension @@ -147,7 +129,6 @@ async function open_model_url(event, model_type, search_term){ return } - //msg to python side let msg = { "action": "", @@ -157,7 +138,6 @@ async function open_model_url(event, model_type, search_term){ "neg_prompt": "", } - msg["action"] = "open_url"; msg["model_type"] = model_type; msg["search_term"] = search_term; @@ -176,8 +156,7 @@ async function open_model_url(event, model_type, search_term){ //check response msg from python let new_py_msg = await get_new_ch_py_msg(); - console.log("new_py_msg:"); - console.log(new_py_msg); + // console.log("new_py_msg:", new_py_msg); //check msg if (new_py_msg) { @@ -187,19 +166,12 @@ async function open_model_url(event, model_type, search_term){ if (py_msg_json.content.url) { window.open(py_msg_json.content.url, "_blank"); } - } - - } - - - console.log("end open_model_url"); - - + console.log("end open_model_url") } -function add_trigger_words(event, model_type, search_term){ +function add_trigger_words(event, model_type, search_term) { console.log("start add_trigger_words"); //get hidden components of extension @@ -208,7 +180,6 @@ function add_trigger_words(event, model_type, search_term){ return } - //msg to python side let msg = { "action": "", @@ -232,16 +203,13 @@ function add_trigger_words(event, model_type, search_term){ //click hidden button js_add_trigger_words_btn.click(); - console.log("end add_trigger_words"); event.stopPropagation() event.preventDefault() - - } -function use_preview_prompt(event, model_type, search_term){ +function use_preview_prompt(event, model_type, search_term) { console.log("start use_preview_prompt"); //get hidden components of extension @@ -281,13 +249,53 @@ function use_preview_prompt(event, model_type, search_term){ event.stopPropagation() event.preventDefault() - } +async function delete_model(event, model_type, search_term) { + if (!confirm('Comfirm delete model: "' + search_term + '"?')) { return } + //get hidden components of extension + let js_delete_model_btn = gradioApp().getElementById("ch_js_delete_model_btn"); + if (!js_delete_model_btn) { + return + } + + //msg to python side + let msg = { + "action": "delete_model", + "model_type": model_type, + "search_term": search_term + } + + // fill to msg box + send_ch_py_msg(msg) + + //click hidden button + js_delete_model_btn.click(); + + // stop parent event + event.stopPropagation() + event.preventDefault() + + //check response msg from python + let new_py_msg = await get_new_ch_py_msg(); + console.log("new_py_msg:", new_py_msg); + + //check msg + if (new_py_msg) { + let py_msg_json = JSON.parse(new_py_msg); + //check delete result + console.log(py_msg_json) + if (py_msg_json && py_msg_json.result) { + alert('Model delete successfully!!') + let card = event.target.closest('.card') + card.parentNode.removeChild(card) + } + } +} // download model's new version into SD at python side -function ch_dl_model_new_version(event, model_path, version_id, download_url){ +function ch_dl_model_new_version(event, model_path, version_id, download_url) { console.log("start ch_dl_model_new_version"); // must confirm before downloading @@ -325,10 +333,237 @@ function ch_dl_model_new_version(event, model_path, version_id, download_url){ event.stopPropagation() event.preventDefault() - - } +function createAdditionalButton(btnProps) { + let el = document.createElement("a"); + Object.assign(el, btnProps); + el.setAttribute('onclick', btnProps.onclick) + el.className = 'civitai-helper-action' + el.href = "#"; + return el +} + +function convert_to_py_model_type(js_model_type) { + //get model_type for python side + switch (js_model_type) { + case "textual_inversion": return "ti"; + case "hypernetworks": return "hyper"; + case "checkpoints": return "ckp"; + case "lora": return "lora"; + case "lycoris": return "lycoris"; + } +} + +// add just one model_type cards buttons +function update_card_for_one_tab(model_type, container) { + + let ch_btn_txts = ['🌐', '💡', '🪞', '🗑️']; + let replace_preview_text = getTranslation("replace preview"); + if (!replace_preview_text) { + replace_preview_text = "replace preview"; + } + + // get component + let ch_always_display_ckb = gradioApp().querySelector("#ch_always_display_ckb input"); + let ch_show_btn_on_thumb_ckb = gradioApp().querySelector("#ch_show_btn_on_thumb_ckb input"); + let ch_always_display = false; + let ch_show_btn_on_thumb = false; + if (ch_always_display_ckb) { + ch_always_display = ch_always_display_ckb.checked; + } + if (ch_show_btn_on_thumb_ckb) { + ch_show_btn_on_thumb = ch_show_btn_on_thumb_ckb.checked; + } + + //change all "replace preview" into an icon + let extra_network_id = ""; + let metadata_button = null; + let additional_node = null; + let replace_preview_btn = null; + let ul_node = null; + let search_term_node = null; + let search_term = ""; + let is_thumb_mode = false; + + // check if extr network is under thumbnail mode + is_thumb_mode = false; + if (container.className == "extra-network-thumbs") { + is_thumb_mode = true; + // if (!ch_show_btn_on_thumb) {continue;} + } + + model_type = convert_to_py_model_type(model_type); + + for (let card of container.children) { + //get ul node, which is the parent of all buttons + ul_node = card.querySelector(".actions .additional ul"); + + if (ul_node.childElementCount > 1) { + // buttons all ready added, just quit + console.log('buttons all ready added, just quit') + return + } + + //metadata_buttoncard + metadata_button = card.querySelector(".metadata-button"); + //additional node + additional_node = card.querySelector(".actions .additional"); + // replace preview text button + replace_preview_btn = card.querySelector(".actions .additional a"); + + // check thumb mode + if (is_thumb_mode) { + additional_node.style.display = null; + + if (ch_show_btn_on_thumb) { + ul_node.style.background = "rgba(0, 0, 0, .5)"; + } else { + //reset + ul_node.style.background = null; + // console.log("remove existed buttons"); + // remove existed buttons + if (ul_node) { + // find all .a child nodes + let atags = ul_node.querySelectorAll("a"); + + for (let atag of atags) { + //reset display + atag.style.display = null; + //remove extension's button + if (ch_btn_txts.indexOf(atag.innerHTML) >= 0) { + //need to remove + ul_node.removeChild(atag); + } else { + //do not remove, just reset + atag.innerHTML = replace_preview_text; + atag.style.display = null; + atag.style.fontSize = null; + atag.style.position = null; + atag.style.backgroundImage = null; + } + } + + //also remove br tag in ul + let brtag = ul_node.querySelector("br"); + if (brtag) { + ul_node.removeChild(brtag); + } + } + //just reset and remove nodes, do nothing else + continue; + } + + } else { + // full preview mode + additional_node.style.display = ch_always_display ? "block" : null; + + // remove br tag + let brtag = ul_node.querySelector("br"); + if (brtag) { + ul_node.removeChild(brtag); + } + } + + // change replace preview text button into icon + if (replace_preview_btn) { + replace_preview_btn.className = "civitai-helper-action"; + replace_preview_btn.innerHTML = "🖼️"; + } + + // search_term node + // search_term = subfolder path + model name + ext + search_term_node = card.querySelector(".actions .additional .search_term"); + if (!search_term_node) { + console.log("can not find search_term node for cards in " + extra_network_id); + continue; + } + + // get search_term + search_term = search_term_node.innerText; + if (!search_term) { + console.log("search_term is empty for cards in " + extra_network_id); + continue; + } + // remove webui added extra checkpoint's sha256 value, + // in file: stable-diffusion-webui/modules/ui_extra_networks_checkpoints.py + // line: 24: "search_term": self.search_terms_from_path(checkpoint.filename) + " " + (checkpoint.sha256 or ""), + search_term = search_term.split(" ")[0]; + + // if (is_thumb_mode) { + // ul_node.style.background = btn_thumb_background; + // } + + // then we need to add 4 buttons to each ul node: + let open_url_node = createAdditionalButton({ + innerHTML: "🌐", + title: "Open this model's civitai url", + onclick: "open_model_url(event, '" + model_type + "', '" + search_term + "')" + }) + + let add_trigger_words_node = createAdditionalButton({ + innerHTML: "💡", + title: "Add trigger words to prompt", + onclick: "add_trigger_words(event, '" + model_type + "', '" + search_term + "')" + }) + + let use_preview_prompt_node = createAdditionalButton({ + innerHTML: "🪞", + title: "Use prompt from preview image", + onclick: "use_preview_prompt(event, '" + model_type + "', '" + search_term + "')" + }) + + let delete_model_node = createAdditionalButton({ + innerHTML: "🗑️", + title: "Delete model", + onclick: "delete_model(event, '" + model_type + "', '" + search_term + "')", + }) + + //add to card + ul_node.appendChild(open_url_node); + //add br if metadata_button exists + if (is_thumb_mode && metadata_button) { + ul_node.appendChild(document.createElement("br")); + } + ul_node.appendChild(add_trigger_words_node); + ul_node.appendChild(use_preview_prompt_node); + ul_node.appendChild(delete_model_node); + } +} + +// fast pasete civitai model url and trigger model info loading +async function checkClipboard() { + let text = await navigator.clipboard.readText() + if (text.startsWith('https://civitai.com/models/')) { + let comp = document.querySelector('#model_download_url_txt') + let textarea = comp.querySelector('textarea') + textarea.value = text + textarea.dispatchEvent(new Event('input')) + comp.querySelector('button').click() + } +} + +// shotcut key event listener +window.addEventListener('keydown', e => { + let el = e.target + switch (e.key) { + case '`': + if (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { e.preventDefault() } + let type = getActiveTabType() + if (type.endsWith('2img')) { + document.querySelector('#' + type + '_extra_networks')?.click() + } + break + case 'x': + let txt = document.querySelector('#quicksettings + .tabs button.selected').innerText + if (txt == 'Civitai Helper') + checkClipboard() + else if (txt.endsWith('2img') && e.altKey) + document.querySelector(`button#${txt}_generate`).click() + break + } +}) + onUiLoaded(() => { @@ -336,12 +571,19 @@ onUiLoaded(() => { let gradio_ver = ch_gradio_version(); console.log("gradio_ver:" + gradio_ver); - // get all extra network tabs let tab_prefix_list = ["txt2img", "img2img"]; - let model_type_list = ["textual_inversion", "hypernetworks", "checkpoints", "lora"]; - let cardid_suffix = "cards"; + let model_type_list = ["textual_inversion", "hypernetworks", "checkpoints", "lora", "lycoris"]; - //get init py msg + // add all model_type cards buttons by tab_prefix + function update_card_form_all_tab(tab_prefix) { + for (let model_type of model_type_list) { + let container_id = [tab_prefix, model_type, 'cards'].join('_') + let container = document.getElementById(container_id) + update_card_for_one_tab(model_type, container); + } + } + + // get init py msg // let init_py_msg_str = get_ch_py_msg(); // let extension_path = ""; // if (!init_py_msg_str) { @@ -354,375 +596,80 @@ onUiLoaded(() => { // } // } - // //icon image node as string + // // icon image node as string // function icon(icon_name){ // let icon_path = extension_path+"/icon/"+icon_name; // return ch_img_node_str(icon_path); // } + // check cards number change, and re-craete buttons + function checkPeriodically(tab_prefix, model_type) { + let container_id = '#' + [tab_prefix, model_type, 'cards_html'].join('_') + let container = document.querySelector(container_id + ' ' + container_id) - // update extra network tab pages' cards - // * replace "replace preview" text button into an icon - // * add 3 button to each card: - // - open model url 🌐 - // - add trigger words 💡 - // - use preview image's prompt 🏷️ - // notice: javascript can not get response from python side - // so, these buttons just sent request to python - // then, python side gonna open url and update prompt text box, without telling js side. - function update_card_for_civitai(){ + // record current cards size + let len = container.querySelectorAll('.card').length - //css - let btn_margin = "0px 5px"; - let btn_fontSize = "200%"; - let btn_thumb_fontSize = "100%"; - let btn_thumb_display = "inline"; - let btn_thumb_pos = "static"; - let btn_thumb_backgroundImage = "none"; - let btn_thumb_background = "rgba(0, 0, 0, 0.8)"; + // we only wait 5s, after that we assumed that DOM will never changed + let millis = 1000 * 5 - let ch_btn_txts = ['🌐', '💡', '🏷️']; - let replace_preview_text = getTranslation("replace preview"); - if (!replace_preview_text) { - replace_preview_text = "replace preview"; - } - - - - // get component - let ch_always_display_ckb = gradioApp().querySelector("#ch_always_display_ckb input"); - let ch_show_btn_on_thumb_ckb = gradioApp().querySelector("#ch_show_btn_on_thumb_ckb input"); - let ch_always_display = false; - let ch_show_btn_on_thumb = false; - if (ch_always_display_ckb) { - ch_always_display = ch_always_display_ckb.checked; - } - if (ch_show_btn_on_thumb_ckb) { - ch_show_btn_on_thumb = ch_show_btn_on_thumb_ckb.checked; - } - - - //change all "replace preview" into an icon - let extra_network_id = ""; - let extra_network_node = null; - let metadata_button = null; - let additional_node = null; - let replace_preview_btn = null; - let ul_node = null; - let search_term_node = null; - let search_term = ""; - let model_type = ""; - let cards = null; - let need_to_add_buttons = false; - let is_thumb_mode = false; - - //get current tab - let active_tab_type = getActiveTabType(); - if (!active_tab_type){active_tab_type = "txt2img";} - - for (const tab_prefix of tab_prefix_list) { - if (tab_prefix != active_tab_type) {continue;} - - - //find out current selected model type tab - let active_extra_tab_type = ""; - let extra_tabs = gradioApp().getElementById(tab_prefix+"_extra_tabs"); - if (!extra_tabs) {console.log("can not find extra_tabs: " + tab_prefix+"_extra_tabs");} - - //get active extratab - const active_extra_tab = Array.from(get_uiCurrentTabContent().querySelectorAll('.extra-network-cards,.extra-network-thumbs')) - .find(el => el.closest('.tabitem').style.display === 'block') - ?.id.match(/^(txt2img|img2img)_(.+)_cards$/)[2] - - - console.log("found active tab: " + active_extra_tab); - - switch (active_extra_tab) { - case "textual_inversion": - active_extra_tab_type = "ti"; - break; - case "hypernetworks": - active_extra_tab_type = "hyper"; - break; - case "checkpoints": - active_extra_tab_type = "ckp"; - break; - case "lora": - active_extra_tab_type = "lora"; - break; + // wait for server response and DOM updates, if cards size changed, + // then our buttons is losted, put them back again + let timer = setInterval(() => { + let new_len = container.querySelectorAll('.card').length + if (len != new_len) { + update_card_for_one_tab(model_type, container.lastElementChild) + clearInterval(timer) } - - - for (const js_model_type of model_type_list) { - //get model_type for python side - switch (js_model_type) { - case "textual_inversion": - model_type = "ti"; - break; - case "hypernetworks": - model_type = "hyper"; - break; - case "checkpoints": - model_type = "ckp"; - break; - case "lora": - model_type = "lora"; - break; - } - - if (!model_type) { - console.log("can not get model_type from: " + js_model_type); - continue; - } - - - //only handle current sub-tab - if (model_type != active_extra_tab_type) { - continue; - } - - console.log("handle active extra tab"); - - - extra_network_id = tab_prefix+"_"+js_model_type+"_"+cardid_suffix; - // console.log("searching extra_network_node: " + extra_network_id); - extra_network_node = gradioApp().getElementById(extra_network_id); - // check if extr network is under thumbnail mode - is_thumb_mode = false - if (extra_network_node) { - if (extra_network_node.className == "extra-network-thumbs") { - console.log(extra_network_id + " is in thumbnail mode"); - is_thumb_mode = true; - // if (!ch_show_btn_on_thumb) {continue;} - } - } else { - console.log("can not find extra_network_node: " + extra_network_id); - continue; - } - // console.log("find extra_network_node: " + extra_network_id); - - // get all card nodes - cards = extra_network_node.querySelectorAll(".card"); - for (let card of cards) { - //metadata_buttoncard - metadata_button = card.querySelector(".metadata-button"); - //additional node - additional_node = card.querySelector(".actions .additional"); - //get ul node, which is the parent of all buttons - ul_node = card.querySelector(".actions .additional ul"); - // replace preview text button - replace_preview_btn = card.querySelector(".actions .additional a"); - - // check thumb mode - if (is_thumb_mode) { - additional_node.style.display = null; - - if (ch_show_btn_on_thumb) { - ul_node.style.background = btn_thumb_background; - } else { - //reset - ul_node.style.background = null; - // console.log("remove existed buttons"); - // remove existed buttons - if (ul_node) { - // find all .a child nodes - let atags = ul_node.querySelectorAll("a"); - - for (let atag of atags) { - //reset display - atag.style.display = null; - //remove extension's button - if (ch_btn_txts.indexOf(atag.innerHTML)>=0) { - //need to remove - ul_node.removeChild(atag); - } else { - //do not remove, just reset - atag.innerHTML = replace_preview_text; - atag.style.display = null; - atag.style.fontSize = null; - atag.style.position = null; - atag.style.backgroundImage = null; - } - } - - //also remove br tag in ul - let brtag = ul_node.querySelector("br"); - if (brtag) { - ul_node.removeChild(brtag); - } - - } - //just reset and remove nodes, do nothing else - continue; - - } - - } else { - // full preview mode - if (ch_always_display) { - additional_node.style.display = "block"; - } else { - additional_node.style.display = null; - } - - // remove br tag - let brtag = ul_node.querySelector("br"); - if (brtag) { - ul_node.removeChild(brtag); - } - - } - - // change replace preview text button into icon - if (replace_preview_btn) { - if (replace_preview_btn.innerHTML !== "🖼️") { - need_to_add_buttons = true; - replace_preview_btn.innerHTML = "🖼️"; - if (!is_thumb_mode) { - replace_preview_btn.style.fontSize = btn_fontSize; - replace_preview_btn.style.margin = btn_margin; - } else { - replace_preview_btn.style.display = btn_thumb_display; - replace_preview_btn.style.fontSize = btn_thumb_fontSize; - replace_preview_btn.style.position = btn_thumb_pos; - replace_preview_btn.style.backgroundImage = btn_thumb_backgroundImage; - } - - } - } - - if (!need_to_add_buttons) { - continue; - } - - - // search_term node - // search_term = subfolder path + model name + ext - search_term_node = card.querySelector(".actions .additional .search_term"); - if (!search_term_node){ - console.log("can not find search_term node for cards in " + extra_network_id); - continue; - } - - // get search_term - search_term = search_term_node.innerHTML; - if (!search_term) { - console.log("search_term is empty for cards in " + extra_network_id); - continue; - } - - - - // if (is_thumb_mode) { - // ul_node.style.background = btn_thumb_background; - // } - - // then we need to add 3 buttons to each ul node: - let open_url_node = document.createElement("a"); - open_url_node.href = "#"; - open_url_node.innerHTML = "🌐"; - if (!is_thumb_mode) { - open_url_node.style.fontSize = btn_fontSize; - open_url_node.style.margin = btn_margin; - } else { - open_url_node.style.display = btn_thumb_display; - open_url_node.style.fontSize = btn_thumb_fontSize; - open_url_node.style.position = btn_thumb_pos; - open_url_node.style.backgroundImage = btn_thumb_backgroundImage; - } - open_url_node.title = "Open this model's civitai url"; - open_url_node.setAttribute("onclick","open_model_url(event, '"+model_type+"', '"+search_term+"')"); - - let add_trigger_words_node = document.createElement("a"); - add_trigger_words_node.href = "#"; - add_trigger_words_node.innerHTML = "💡"; - if (!is_thumb_mode) { - add_trigger_words_node.style.fontSize = btn_fontSize; - add_trigger_words_node.style.margin = btn_margin; - } else { - add_trigger_words_node.style.display = btn_thumb_display; - add_trigger_words_node.style.fontSize = btn_thumb_fontSize; - add_trigger_words_node.style.position = btn_thumb_pos; - add_trigger_words_node.style.backgroundImage = btn_thumb_backgroundImage; - } - - add_trigger_words_node.title = "Add trigger words to prompt"; - add_trigger_words_node.setAttribute("onclick","add_trigger_words(event, '"+model_type+"', '"+search_term+"')"); - - let use_preview_prompt_node = document.createElement("a"); - use_preview_prompt_node.href = "#"; - use_preview_prompt_node.innerHTML = "🏷️"; - if (!is_thumb_mode) { - use_preview_prompt_node.style.fontSize = btn_fontSize; - use_preview_prompt_node.style.margin = btn_margin; - } else { - use_preview_prompt_node.style.display = btn_thumb_display; - use_preview_prompt_node.style.fontSize = btn_thumb_fontSize; - use_preview_prompt_node.style.position = btn_thumb_pos; - use_preview_prompt_node.style.backgroundImage = btn_thumb_backgroundImage; - } - use_preview_prompt_node.title = "Use prompt from preview image"; - use_preview_prompt_node.setAttribute("onclick","use_preview_prompt(event, '"+model_type+"', '"+search_term+"')"); - - //add to card - ul_node.appendChild(open_url_node); - //add br if metadata_button exists - if (is_thumb_mode && metadata_button) { - ul_node.appendChild(document.createElement("br")); - } - ul_node.appendChild(add_trigger_words_node); - ul_node.appendChild(use_preview_prompt_node); - - - - - } - - + millis -= 500 + if (millis <= 0) { + clearInterval(timer) } - } - - + }, 500) } - let tab_id = "" let extra_tab = null; let extra_toolbar = null; let extra_network_refresh_btn = null; - //add refresh button to extra network's toolbar - for (let prefix of tab_prefix_list) { - tab_id = prefix + "_extra_tabs"; + // add refresh button to extra network's toolbar + for (let tab_prefix of tab_prefix_list) { + tab_id = tab_prefix + "_extra_tabs"; extra_tab = gradioApp().getElementById(tab_id); //get toolbar //get Refresh button - extra_network_refresh_btn = gradioApp().getElementById(prefix+"_extra_refresh"); + extra_network_refresh_btn = gradioApp().getElementById(tab_prefix + "_extra_refresh"); - - if (!extra_network_refresh_btn){ + if (!extra_network_refresh_btn) { console.log("can not get extra network refresh button for " + tab_id); continue; } // add refresh button to toolbar let ch_refresh = document.createElement("button"); - ch_refresh.innerHTML = "🔁"; + ch_refresh.innerHTML = "🔄️"; ch_refresh.title = "Refresh Civitai Helper's additional buttons"; ch_refresh.className = "lg secondary gradio-button"; - ch_refresh.style.fontSize = "200%"; - ch_refresh.onclick = update_card_for_civitai; + ch_refresh.style.fontSize = "2em"; + ch_refresh.onclick = () => update_card_form_all_tab(tab_prefix) extra_network_refresh_btn.parentNode.appendChild(ch_refresh); + // listen to refresh buttons' click event + // check and re-add buttons back on + document.getElementById(tab_prefix + '_extra_refresh').addEventListener('click', e => { + let model_type = e.target.closest('.tab-nav').querySelector('button.selected').innerText.replaceAll(' ', '_').toLowerCase() + checkPeriodically(tab_prefix, model_type) + }) + + // listen to "Extra Networks" toggle button's click event, + // then initialiy add all buttons, only trigger once, + // after that all updates are trigger by refresh button click. + document.getElementById(tab_prefix + '_extra_networks').addEventListener('click', () => { + // wait UI updates + setTimeout(() => update_card_form_all_tab(tab_prefix), 1500); + }, { once: true }) } - - //run it once - update_card_for_civitai(); - - -}); - - - +}); \ No newline at end of file diff --git a/scripts/ch_lib/civitai.py b/scripts/ch_lib/civitai.py index 2a15ae7..374dbf0 100644 --- a/scripts/ch_lib/civitai.py +++ b/scripts/ch_lib/civitai.py @@ -23,7 +23,8 @@ model_type_dict = { "TextualInversion": "ti", "Hypernetwork": "hyper", "LORA": "lora", - "LoCon": "lora", + "LoCon": "lycoris", + "LyCORIS": "lycoris", } @@ -277,6 +278,7 @@ def get_model_names_by_type_and_filter(model_type:str, filter:dict) -> list: return model_names + def get_model_names_by_input(model_type, empty_info_only): return get_model_names_by_type_and_filter(model_type, {"empty_info_only":empty_info_only}) @@ -365,9 +367,7 @@ def get_preview_image_by_model_path(model_path:str, max_size_preview, skip_nsfw_ # search local model by version id in 1 folder, no subfolder # return - model_info def search_local_model_info_by_version_id(folder:str, version_id:int) -> dict: - util.printD("Searching local model by version id") - util.printD("folder: " + folder) - util.printD("version_id: " + str(version_id)) + util.printD("Searching local model by version id: " + str(version_id) + "in folder: " + folder) if not folder: util.printD("folder is none") @@ -414,9 +414,6 @@ def search_local_model_info_by_version_id(folder:str, version_id:int) -> dict: return - - - # check new version for a model by model path # return (model_path, model_id, model_name, new_verion_id, new_version_name, description, download_url, img_url) def check_model_new_version_by_path(model_path:str, delay:float=1) -> tuple: @@ -603,10 +600,46 @@ def check_models_new_version_by_model_types(model_types:list, delay:float=1) -> # add to list new_versions.append(r) - - - return new_versions +# delete model file by model type and search_term +# parameter: model_type, search_term +# return: delete result +def delete_model_by_search_term(model_type:str, search_term:str): + if model_type not in model.folders.keys(): + msg = f"unknow model type: {model_type}" + util.printD(msg) + return + + model_type_key = {k for k in model_type_dict if model_type_dict[k] == model_type} + util.printD(f"Find {model_type_key} model file: '{search_term}'") + + # search_term = subfolderpath + model name + ext. And it always start with a / even there is no sub folder + base, ext = os.path.splitext(search_term) + model_info_base = base + if base[:1] == "/": + model_info_base = base[1:] + # find 3 kinds of files: .model.info, .safetensor and .priview.jpeg + model_folder = model.folders[model_type] + model_info_filename = model_info_base + suffix + model.info_ext + model_info_filepath = os.path.join(model_folder, model_info_filename) + + model_image_filepath = os.path.join(model_folder, model_info_base + ".preview.png") + model_filename = model_folder + search_term + + if os.path.isfile(model_info_filepath): + os.remove(model_info_filepath) + + if os.path.isfile(model_image_filepath): + os.remove(model_image_filepath) + + result = False + if os.path.isfile(model_filename): + os.remove(model_filename) + result = True + else: + print("Error: Model file: %s not found" % model_filename) + + return result diff --git a/scripts/ch_lib/downloader.py b/scripts/ch_lib/downloader.py index 2b14ebc..771e21c 100644 --- a/scripts/ch_lib/downloader.py +++ b/scripts/ch_lib/downloader.py @@ -10,6 +10,13 @@ dl_ext = ".downloading" # disable ssl warning info requests.packages.urllib3.disable_warnings() +def human_readable_size(size, decimal_places=2): + for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']: + if size < 1024.0 or unit == 'PiB': + break + size /= 1024.0 + return f"{size:.{decimal_places}f} {unit}" + # output is downloaded file path def dl(url, folder, filename, filepath): util.printD("Start downloading from: " + url) @@ -35,7 +42,7 @@ def dl(url, folder, filename, filepath): # get file size total_size = 0 total_size = int(rh.headers['Content-Length']) - util.printD(f"File size: {total_size}") + util.printD(f"File size: {human_readable_size(total_size)}") # if file_path is empty, need to get file name from download url's header if not file_path: @@ -65,7 +72,7 @@ def dl(url, folder, filename, filepath): count = 2 new_base = base while os.path.isfile(file_path): - util.printD("Target file already exist.") + util.printD("Target file'" + file_path + "'already exist.") # re-name new_base = base + "_" + str(count) file_path = new_base + ext @@ -81,8 +88,8 @@ def dl(url, folder, filename, filepath): downloaded_size = 0 if os.path.exists(dl_file_path): downloaded_size = os.path.getsize(dl_file_path) - - util.printD(f"Downloaded size: {downloaded_size}") + if downloaded_size > 0: + util.printD(f"Downloaded size: {downloaded_size}") # create header range headers = {'Range': 'bytes=%d-' % downloaded_size} @@ -101,15 +108,15 @@ def dl(url, folder, filename, filepath): f.flush() # progress - progress = int(50 * downloaded_size / total_size) + progress = int(100 * downloaded_size / total_size) sys.stdout.reconfigure(encoding='utf-8') - sys.stdout.write("\r[%s%s] %d%%" % ('-' * progress, ' ' * (50 - progress), 100 * downloaded_size / total_size)) + sys.stdout.write("\r[%s%s] %d%%" % ('=' * progress, ' ' * (100 - progress), 100 * downloaded_size / total_size)) sys.stdout.flush() print() # rename file os.rename(dl_file_path, file_path) - util.printD(f"File Downloaded to: {file_path}") + util.printD(f"File save to: {file_path}") return file_path diff --git a/scripts/ch_lib/js_action_civitai.py b/scripts/ch_lib/js_action_civitai.py index 0c81853..5ed770e 100644 --- a/scripts/ch_lib/js_action_civitai.py +++ b/scripts/ch_lib/js_action_civitai.py @@ -175,6 +175,18 @@ def use_preview_image_prompt(msg): return [preview_prompt, preview_neg_prompt, preview_prompt, preview_neg_prompt] +def delete_model(msg): + util.printD("Start delete_model") + + result = msg_handler.parse_js_msg(msg) + if not result: + util.printD("Parsing js ms failed") + return + + model_type = result["model_type"] + search_term = result["search_term"] + result = civitai.delete_model_by_search_term(model_type, search_term) + return json.dumps({"result": result}) # download model's new verson by model path, version id and download url # output is a md log @@ -251,6 +263,6 @@ def dl_model_new_version(msg, max_size_preview, skip_nsfw_preview): # then, get preview image civitai.get_preview_image_by_model_path(new_model_path, max_size_preview, skip_nsfw_preview) - output = "Done. Model downloaded to: " + new_model_path + output = "Done, model save to: " + new_model_path util.printD(output) return output diff --git a/scripts/ch_lib/model.py b/scripts/ch_lib/model.py index 729012d..a0c3a92 100644 --- a/scripts/ch_lib/model.py +++ b/scripts/ch_lib/model.py @@ -17,6 +17,7 @@ folders = { "hyper": os.path.join(root_path, "models", "hypernetworks"), "ckp": os.path.join(root_path, "models", "Stable-diffusion"), "lora": os.path.join(root_path, "models", "Lora"), + "lycoris": os.path.join(root_path, "models", "LyCORIS"), } exts = (".bin", ".pt", ".safetensors", ".ckpt") @@ -48,7 +49,7 @@ def get_custom_model_folder(): # write model info to file def write_model_info(path, model_info): - util.printD("Write model info to file: " + path) + util.printD("Write model info to: " + path) with open(os.path.realpath(path), 'w') as f: f.write(json.dumps(model_info, indent=4)) diff --git a/scripts/ch_lib/model_action_civitai.py b/scripts/ch_lib/model_action_civitai.py index f09f075..6465761 100644 --- a/scripts/ch_lib/model_action_civitai.py +++ b/scripts/ch_lib/model_action_civitai.py @@ -264,7 +264,7 @@ def get_model_info_by_url(model_url_or_id:str) -> tuple: subfolders = [] # add default root folder - subfolders.append("/") + subfolders.append(os.sep) util.printD("Get following info for downloading:") util.printD(f"model_name:{model_name}") @@ -435,42 +435,30 @@ def dl_model_by_input(model_info:dict, model_type:str, subfolder_str:str, versio output = "Fail to get version info, check console log for detail" util.printD(output) return output - - version_id = ver_info["id"] - - if dl_all_bool: - # get all download url from files info - # some model versions have multiple files - download_urls = [] - if "files" in ver_info.keys(): - for file_info in ver_info["files"]: - if "downloadUrl" in file_info.keys(): - download_urls.append(file_info["downloadUrl"]) - - if not len(download_urls): - if "downloadUrl" in ver_info.keys(): - download_urls.append(ver_info["downloadUrl"]) - - - # check if this model is already existed - r = civitai.search_local_model_info_by_version_id(model_folder, version_id) - if r: - output = "This model version is already existed" - util.printD(output) - return output + modelVersions = model_info['modelVersions'] + if dl_all_bool: - # download filepath = "" - for url in download_urls: - model_filepath = downloader.dl(url, model_folder, None, None) - if not model_filepath: + idx = 0 + for modelVer in modelVersions: + # check if this model is already existed + r = civitai.search_local_model_info_by_version_id(model_folder, modelVer["id"]) + if r: + util.printD("Model file: '" + r["files"][0]["name"] + "' already existed, skiping it.") + continue + + filepath = downloader.dl(modelVer["downloadUrl"], model_folder, None, None) + if not filepath: output = "Downloading failed, check console log for detail" util.printD(output) - return output + continue + + idx = idx + 1 + save_info_and_preview_image(filepath, modelVer, max_size_preview, skip_nsfw_preview) + util.printD(str(idx) + " of " + str(len(modelVersions)) + " files downloaded, save to: " + filepath) - if url == ver_info["downloadUrl"]: - filepath = model_filepath + output = "Done, " + str(idx) + "/" + str(len(modelVersions)) + " files downloaded" else: # only download one file # get download url @@ -486,18 +474,19 @@ def dl_model_by_input(model_info:dict, model_type:str, subfolder_str:str, versio output = "Downloading failed, check console log for detail" util.printD(output) return output - - - if not filepath: - filepath = model_filepath - # get version info - version_info = civitai.get_version_info_by_version_id(version_id) - if not version_info: - output = "Model downloaded, but failed to get version info, check console log for detail. Model saved to: " + filepath - util.printD(output) - return output + # get version info + version_info = [mv for mv in modelVersions if mv["id"] == ver_info["id"]][0] + if not version_info: + output = "Model downloaded, but failed to get version info, check console log for detail. Model saved to: " + filepath + util.printD(output) + return output + + output = save_info_and_preview_image(filepath, version_info, max_size_preview, skip_nsfw_preview) + return output + +def save_info_and_preview_image(filepath:str, version_info:dict, max_size_preview:bool, skip_nsfw_preview:bool): # write version info to file base, ext = os.path.splitext(filepath) info_file = base + civitai.suffix + model.info_ext @@ -506,6 +495,7 @@ def dl_model_by_input(model_info:dict, model_type:str, subfolder_str:str, versio # then, get preview image civitai.get_preview_image_by_model_path(filepath, max_size_preview, skip_nsfw_preview) - output = "Done. Model downloaded to: " + filepath + output = "Done, model save to: " + filepath util.printD(output) - return output + + return output \ No newline at end of file diff --git a/scripts/ch_lib/msg_handler.py b/scripts/ch_lib/msg_handler.py index 1132d77..74964e1 100644 --- a/scripts/ch_lib/msg_handler.py +++ b/scripts/ch_lib/msg_handler.py @@ -4,7 +4,7 @@ import json from . import util # action list -js_actions = ("open_url", "add_trigger_words", "use_preview_prompt", "dl_model_new_version") +js_actions = ("open_url", "add_trigger_words", "use_preview_prompt", "delete_model", "dl_model_new_version") py_actions = ("open_url") diff --git a/scripts/ch_lib/util.py b/scripts/ch_lib/util.py index 3e43c98..3b98312 100644 --- a/scripts/ch_lib/util.py +++ b/scripts/ch_lib/util.py @@ -60,7 +60,7 @@ def download_file(url, path): r.raw.decode_content = True shutil.copyfileobj(r.raw, f) - printD("File downloaded to: " + path) + printD("File save to: " + path) # get subfolder list def get_subfolders(folder:str) -> list: diff --git a/scripts/civitai_helper.py b/scripts/civitai_helper.py index ad9e70a..5be1e3a 100644 --- a/scripts/civitai_helper.py +++ b/scripts/civitai_helper.py @@ -81,7 +81,9 @@ def on_ui_tabs(): if r: model_info, model_name, model_type, subfolders, version_strs = r - return [model_info, model_name, model_type, dl_subfolder_drop.update(choices=subfolders), dl_version_drop.update(choices=version_strs)] + value = os.sep if len(subfolders) == 1 else "" + + return [model_info, model_name, model_type, dl_subfolder_drop.update(choices=subfolders, value=value), dl_version_drop.update(choices=version_strs, value=version_strs[0])] # ====UI==== with gr.Blocks(analytics_enabled=False) as civitai_helper: @@ -133,7 +135,7 @@ def on_ui_tabs(): with gr.Box(elem_classes="ch_box"): with gr.Column(): gr.Markdown("### Download Model") - with gr.Row(): + with gr.Row(elem_id="model_download_url_txt"): dl_model_url_or_id_txtbox = gr.Textbox(label="Civitai URL", lines=1, value="") dl_model_info_btn = gr.Button(value="1. Get Model Info by Civitai Url", variant="primary") @@ -181,6 +183,7 @@ def on_ui_tabs(): js_open_url_btn = gr.Button(value="Open Model Url", visible=False, elem_id="ch_js_open_url_btn") js_add_trigger_words_btn = gr.Button(value="Add Trigger Words", visible=False, elem_id="ch_js_add_trigger_words_btn") js_use_preview_prompt_btn = gr.Button(value="Use Prompt from Preview Image", visible=False, elem_id="ch_js_use_preview_prompt_btn") + js_use_delete_model_btn = gr.Button(value="Delete Model", visible=False, elem_id="ch_js_delete_model_btn") js_dl_model_new_version_btn = gr.Button(value="Download Model's new version", visible=False, elem_id="ch_js_dl_model_new_version_btn") # ====events==== @@ -207,6 +210,7 @@ def on_ui_tabs(): js_open_url_btn.click(js_action_civitai.open_model_url, inputs=[js_msg_txtbox, open_url_with_js_ckb], outputs=py_msg_txtbox) js_add_trigger_words_btn.click(js_action_civitai.add_trigger_words, inputs=[js_msg_txtbox], outputs=[txt2img_prompt, img2img_prompt]) js_use_preview_prompt_btn.click(js_action_civitai.use_preview_image_prompt, inputs=[js_msg_txtbox], outputs=[txt2img_prompt, txt2img_neg_prompt, img2img_prompt, img2img_neg_prompt]) + js_use_delete_model_btn.click(js_action_civitai.delete_model, inputs=[js_msg_txtbox], outputs=[py_msg_txtbox]) js_dl_model_new_version_btn.click(js_action_civitai.dl_model_new_version, inputs=[js_msg_txtbox, max_size_preview_ckb, skip_nsfw_preview_ckb], outputs=dl_log_md) # the third parameter is the element id on html, with a "tab_" as prefix diff --git a/style.css b/style.css index 387156e..24ba023 100644 --- a/style.css +++ b/style.css @@ -2,17 +2,51 @@ blockquote ul { list-style:disc; margin:4px 40px; } - blockquote ol { list-style:decimal; margin:4px 40px; } - .block.padded.ch_box { padding: 10px !important; } - .block.padded.ch_vpadding { padding: 10px 0 !important; } - +.civitai-helper-action { + font-size: 1.8em; + line-height: 1; + vertical-align: middle; + opacity: .7; +} +.civitai-helper-action:hover { + filter: drop-shadow(2px 4px 6px black); + opacity: 1; +} +.extra-network-cards .civitai-helper-action:nth-child(2) { + margin: 0 .1em 0 .15em; + font-size: 3em; +} +.extra-network-thumbs .card { + height: 8rem !important; + width: 8rem !important; +} +.extra-network-thumbs .civitai-helper-action { + background-image: none !important; + display: inline-block !important; + position: static !important; + font-size: 1.25em !important; + line-height: 1.25em; +} +.extra-network-thumbs .civitai-helper-action:nth-child(2) { + font-size: 2em !important; + height: 1.35em !important; + margin-left: 0.25em; +} +.extra-network-thumbs .card ul { + position: absolute; + width: 100%; + height: 2em; + display: flex; + justify-content: space-between; + align-items: center; +} \ No newline at end of file