diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6dd4465..f790956f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log for SD.Next -## Update for 2025-08-02 +## Update for 2025-08-04 - **Models** - [FLUX.1-Krea-Dev](https://www.krea.ai/blog/flux-krea-open-source-release) @@ -14,15 +14,21 @@ - new embedded docs/wiki search! **Docs** search: fully-local and works in real-time on all document pages **Wiki** search: uses github api to search online wiki pages - - quicksettings reset button to restore all quicksettings to default values - because things do sometimes get wrong... - updated real-time hints, thanks @CalamitousFelicitousness - - updated *models -> current* tab + - rewritten **CivitAI downloader** + in *models -> civitai* + - updated *models -> current* tab - updated *models -> list models* tab - updated *models -> metadata* tab - - more css optimizations and styling + - quicksettings reset button to restore all quicksettings to default values + because things do sometimes get wrong... + - redesign *settings -> user interface* - gallery bypass browser cache for thumbnails - gallery safer delete operation + - more css optimizations and styling + - *hint*: card layout + card layout is used by networks, gallery, civitai search, etc. + you can change card size in *settings -> user interface* - **Offloading** - changed **default** values for offloading based on detected gpu memory see [offloading docs](https://vladmandic.github.io/sdnext-docs/Offload/) for details diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 540353613..89c232814 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 540353613400c419f8f0d92d6569e1e695e04f03 +Subproject commit 89c232814a3a65f00783af962975ffb31ce336db diff --git a/javascript/civitai.js b/javascript/civitai.js index e61980770..417c08584 100644 --- a/javascript/civitai.js +++ b/javascript/civitai.js @@ -1,31 +1,76 @@ -// hack to get pythons str.format in js -String.prototype.format = function (arguments) { // eslint-disable-line no-extend-native, func-names +String.prototype.format = function (args) { // eslint-disable-line no-extend-native, func-names let thisString = ''; for (let charPos = 0; charPos < this.length; charPos++) thisString += this[charPos]; - for (const key in arguments) { // eslint-disable-line guard-for-in - error(key, arguments[key]); + for (const key in args) { // eslint-disable-line guard-for-in const stringKey = `{${key}}`; - thisString = thisString.replace(new RegExp(stringKey, 'g'), arguments[key]); + thisString = thisString.replace(new RegExp(stringKey, 'g'), args[key]); } return thisString; }; +let selectedURL = ''; +let selectedName = ''; +let selectedType = ''; + +function clearModelDetails() { + const el = gradioApp().getElementById('model-details') || gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome'); + if (!el) return; + el.innerHTML = ''; +} + const modelDetailsHTML = ` -
-

{name}

-

Type: {type}

-

Tags: {tags}

-

NSFW: {nsfw}/{level}

-

Availability: {availability}

-

Downloads: {downloads}

-

Author: {creator}

-
{versions}
+
+ + + + + + + + + + + +
Name{name}
Type{type}
Tags
{tags}
NSFW{nsfw} | {level}
Availability{availability}
Downloads{downloads}
Author{creator}
Description
{desc}
+
+ + + + + + + + + + + + + + + + {versions} + +
VersionTypeBaseFileUpdatedSizeAvailabilityDescription
`; +const modelVersionsHTML = ` + + {url} + {name} + {type} + {base} + {file} + {mtime} + {size} + {availability} +
{desc}
+ +`; + async function modelCardClick(id) { log('modelCardClick id', id); - const el = gradioApp().getElementById('model-details'); + const el = gradioApp().getElementById('model-details') || gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome'); if (!el) return; const res = await fetch(`${window.api}/civitai?model_id=${encodeURI(id)}`); if (!res || res.status !== 200) { @@ -36,229 +81,49 @@ async function modelCardClick(id) { log('modelCardClick data', data); if (!data || data.length === 0) return; data = data[0]; // assuming the first item is the one we want - const obj = { - name: data.name || 'unknown', + + const versionsHTML = data.versions.map((v) => modelVersionsHTML.format({ + url: ``, + name: v.name || 'unknown', + type: v.files[0]?.type || 'unknown', + base: v.base || 'unknown', + mtime: (new Date(v.mtime)).toLocaleDateString(), + availability: v.availability || 'unknown', + size: v.files[0]?.size ? `${(v.files[0].size / 1024 / 1024).toFixed(2)} MB` : 'unknown', + file: `${v.files[0]?.name || 'unknown'}`, + desc: v.desc || 'no description available', + })).join(''); + const url = `${data.name || 'unknown'}`; + const creator = `${data.creator || 'unknown'}`; + const images = data.versions.map((v) => v.images).flat().map((i) => i.url); // TODO image gallery + const modelHTML = modelDetailsHTML.format({ + name: url, type: data.type || 'unknown', tags: data.tags?.join(', ') || '', nsfw: data.nsfw ? 'yes' : 'no', level: data.level?.toString() || '', availability: data.availability || 'unknown', downloads: data.downloads?.toString() || '', - creator: data.creator || 'unknown', - versions: JSON.stringify(data.versions) || '[]', - }; - log(obj); - el.innerHTML = modelDetailsHTML.format({ - name: data.name || 'unknown', - type: data.type || 'unknown', - tags: data.tags?.join(', ') || '', - nsfw: data.nsfw ? 'yes' : 'no', - level: data.level?.toString() || '', - availability: data.availability || 'unknown', - downloads: data.downloads?.toString() || '', - creator: data.creator || 'unknown', - versions: JSON.stringify(data.versions) || '[]', + creator, + desc: data.desc || 'no description available', + image: images.length > 0 ? images[0] : './sd_extra_networks/thumb?filename=html/card-no-preview.png', + versions: versionsHTML || '', }); + el.innerHTML = modelHTML; } -const example = { - id: 1157409, - url: 'https://civitai.com/models/1157409', - type: 'Checkpoint', - name: 'Tempest-by-Vlad', - html: '', - desc: 'Base versionFlexible SDXL model with custom encoder and finetuned for larger landscape resolutions with high details and high contrast.Recommended to use medium-low...', - tags: [ - 'base model', - ], - nsfw: false, - level: 15, - availability: 'Public', - downloads: 407, - creator: 'vmandic', - versions: [ - { - id: 1301775, - name: 'Base v0.1', - base: 'SDXL 1.0', - mtime: '2025-01-19T02:53:53.903Z', - downloads: 346, - availability: 'Public', - html: '', - desc: 'Initial release', - files: [ - { - id: 1206102, - size: 6938089790, - name: 'tempestByVlad_baseV01.safetensors', - type: 'Model', - hashes: [ - '79CB1E32', - '8BFAD17222', - '8BFAD1722243955B3F94103C69079C280D348B14729251E86824972C1063B616', - '43E5E3BB', - 'DE83D56256411853AB6595CC3D8E865D5310D4A58D49A839DDC104C7F3429D4A', - '4E933E1EBE61', - ], - url: 'https://civitai.com/api/download/models/1301775', - dct: {}, - }, - ], - images: [ - { - id: 52503951, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/18c749f2-42ec-4024-9d20-0b1202b6bacc/width=1024/52503951.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508539, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/634d0ea8-ecdb-4ca6-a4ff-145319bc3fd3/width=1024/52508539.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508563, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/be529820-a89e-458f-8a3d-86cb43b154ac/width=1024/52508563.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508588, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/b2eb456a-a664-4de8-8c3e-6ecd1c4acb38/width=1024/52508588.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508654, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f7b09e3f-4a48-459b-9b32-fa207904f74c/width=1024/52508654.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508659, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/fff89f18-628a-43b3-b9f5-44951dc078f7/width=1024/52508659.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52508671, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/fdd3f774-ce2e-4ea7-82a0-bdb523fb86f6/width=1024/52508671.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 52512251, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f26971c8-1123-45e3-a85b-2b97c6334b85/width=1024/52512251.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - ], - dct: {}, - }, - { - id: 1343512, - name: 'Hyper v0.1', - base: 'SDXL 1.0', - mtime: '2025-01-28T22:54:12.734Z', - downloads: 61, - availability: 'Public', - html: '', - desc: 'Time-distilled version', - files: [ - { - id: 1246991, - size: 6938085702, - name: 'tempestByVlad_hyperV01.safetensors', - type: 'Model', - hashes: [ - '15943FD9', - '4104FC6601', - '4104FC6601F71C4C7A770AD422483FD700C8ECF72D06FCD8C4E8CD4B2D1C7DBB', - '9F87BCEA', - 'CB52894625E9C13331285E4435799D707C4EAEF464974159C8B4B217EA32298E', - 'A0EE15E503DD', - ], - url: 'https://civitai.com/api/download/models/1343512', - dct: {}, - }, - ], - images: [ - { - id: 54462987, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/1dd020c8-8a9a-4eb3-afec-fe83613217c5/width=1024/54462987.jpeg', - width: 1024, - height: 768, - type: 'image', - dct: {}, - }, - { - id: 54462992, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/89edf233-939f-4b2e-97c8-175498704362/width=1536/54462992.jpeg', - width: 1536, - height: 640, - type: 'image', - dct: {}, - }, - { - id: 54463002, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/87aea8fb-7687-48d0-98e6-27555b2ff87f/width=768/54463002.jpeg', - width: 768, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463010, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/f1ee57cc-8920-4b8d-853a-ad1cfc7d9a5a/width=1024/54463010.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463011, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/dda6411b-fd36-484f-a9f0-0db847463128/width=1024/54463011.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463016, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/5f266a8c-f215-4e0f-8a64-aacc97f81d70/width=1024/54463016.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - { - id: 54463019, - url: 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/0ad04bdc-41bd-433c-9c25-f5365fbe082c/width=1024/54463019.jpeg', - width: 1024, - height: 1024, - type: 'image', - dct: {}, - }, - ], - dct: {}, - }, - ], - dct: {}, -}; +function startCivitDownload(url, name, type) { + log('startCivitDownload', { url, name, type }); + selectedURL = url; + selectedName = name; + selectedType = type; + const civitDownloadBtn = gradioApp().getElementById('civitai_download_btn'); + if (civitDownloadBtn) civitDownloadBtn.click(); +} + +function downloadCivitModel(modelUrl, modelName, modelType, modelPath, civitToken, innerHTML) { + log('downloadCivitModel', { modelUrl, modelName, modelType, modelPath, civitToken }); + const el = gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome'); + const currentHTML = el?.innerHTML || ''; + return [selectedURL, selectedName, selectedType, modelPath, civitToken, currentHTML]; +} diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 1a9ae4fe6..a93cff798 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -110,8 +110,8 @@ function readCardDescription(page, item) { function getCardsForActivePage() { const pagename = getENActivePage(); if (!pagename) return []; - const allCards = Array.from(gradioApp().querySelectorAll('.extra-network-cards > .card')); - const cards = allCards.filter((el) => el.dataset.page.toLowerCase().includes(pagename.toLowerCase())); + let allCards = Array.from(gradioApp().querySelectorAll('.extra-network-cards > .card')); + allCards = allCards.filter((el) => el.dataset.page?.toLowerCase().includes(pagename.toLowerCase())); // log('getCardsForActivePage', pagename, cards.length); return allCards; } diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 2013cabdb..7c3874a30 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -66,6 +66,10 @@ button { min-width: unset !important; } +h4 { + margin: 0.2em 0em 0.2em 0em; +} + input[type='color'] { height: 32px; width: 64px; @@ -122,6 +126,17 @@ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { overflow: auto; } +.link { + background-color: var(--background-fill-primary); + cursor: pointer; + border-radius: var(--input-radius); + width: 2em; +} + +.link:hover { + background-color: var(--button-primary-background-fill); +} + .gradio-dropdown, .block.gradio-slider, .block.gradio-checkbox, .block.gradio-textbox, .block.gradio-radio, .block.gradio-checkboxgroup, .block.gradio-number, .block.gradio-colorpicker { border-width: 0 !important; box-shadow: none !important; @@ -1226,11 +1241,13 @@ table.settings-value-table td { } .extra-network-cards .card { - height: fit-content; - margin: 0 0 0.5em 0.5em; - position: relative; - scroll-margin-top: 0; - scroll-snap-align: start; + margin: 0 0 0.5em 0.5em; + position: relative; + scroll-margin-top: 0; + scroll-snap-align: start; + height: var(--card-size); + width: var(--card-size); + contain: strict; } .extra-network-cards .card .overlay { @@ -1923,10 +1940,6 @@ div:has(>#tab-gallery-folders) { padding: 0.2em; } -.docs-results { - background-color: var(--sd-group-background-color); -} - .docs-card { margin: 1em 0; background-color: var(--background-fill-primary); @@ -1972,6 +1985,13 @@ div:has(>#tab-gallery-folders) { overflow: auto; } +.model-config { + font-size: 0.8em !important; + opacity: 0.8; + max-height: 6em; + overflow-y: auto; +} + .simple-table tr { vertical-align: baseline; } @@ -1980,13 +2000,36 @@ div:has(>#tab-gallery-folders) { padding: 0.2em !important; } -.model-config { - font-size: 0.8em !important; - opacity: 0.8; - max-height: 6em; +.simple-table tr { + vertical-align: baseline; +} + +.simple-table thead tr { + background-color: var(--button-primary-border-color) !important; +} + +.simple-table tr:nth-child(odd) { + background-color: var(--neutral-900); +} + +.simple-table td { + padding: 0.2em !important; + white-space: pre-wrap; +} + +.simple-table td div { + padding: 0.2em !important; + white-space: pre-wrap; + max-height: 7em; + overflow-x: hidden; overflow-y: auto; } +.simple-table td:nth-child(1) { + color: var(--button-primary-border-color); + font-weight: bold; +} + @keyframes move { from { background-position-x: 0, -40px; diff --git a/modules/civitai/download_civitai.py b/modules/civitai/download_civitai.py index e69de29bb..7e013454c 100644 --- a/modules/civitai/download_civitai.py +++ b/modules/civitai/download_civitai.py @@ -0,0 +1,191 @@ +import os +import json +import rich.progress as p +from PIL import Image +from modules import shared, errors, paths + + +pbar = None + + +def save_video_frame(filepath: str): + from modules import video + try: + frames, fps, duration, w, h, codec, frame = video.get_video_params(filepath, capture=True) + except Exception as e: + shared.log.error(f'Video: file={filepath} {e}') + return None + if frame is not None: + basename = os.path.splitext(filepath) + thumb = f'{basename[0]}.thumb.jpg' + shared.log.debug(f'Video: file={filepath} frames={frames} fps={fps} size={w}x{h} codec={codec} duration={duration} thumb={thumb}') + frame.save(thumb) + else: + shared.log.error(f'Video: file={filepath} no frames found') + return frame + + +def download_civit_meta(model_path: str, model_id): + fn = os.path.splitext(model_path)[0] + '.json' + url = f'https://civitai.com/api/v1/models/{model_id}' + r = shared.req(url) + if r.status_code == 200: + try: + data = r.json() + shared.writefile(data, filename=fn, mode='w', silent=True) + shared.log.info(f'CivitAI download: id={model_id} url={url} file="{fn}"') + return r.status_code, len(data), '' # code/size/note + except Exception as e: + errors.display(e, 'civitai meta') + shared.log.error(f'CivitAI meta: id={model_id} url={url} file="{fn}" {e}') + return r.status_code, '', str(e) + return r.status_code, '', '' + + +def download_civit_preview(model_path: str, preview_url: str): + global pbar # pylint: disable=global-statement + if model_path is None: + pbar = None + return 500, '', '' + ext = os.path.splitext(preview_url)[1] + preview_file = os.path.splitext(model_path)[0] + ext + is_video = preview_file.lower().endswith('.mp4') + is_json = preview_file.lower().endswith('.json') + if is_json: + shared.log.warning(f'CivitAI download: url="{preview_url}" skip json') + return 500, '', 'exepected preview image got json' + if os.path.exists(preview_file): + return 304, '', 'already exists' + # res = f'CivitAI download: url={preview_url} file="{preview_file}"' + r = shared.req(preview_url, stream=True) + total_size = int(r.headers.get('content-length', 0)) + block_size = 16384 # 16KB blocks + written = 0 + img = None + shared.state.begin('CivitAI') + if pbar is None: + pbar = p.Progress(p.TextColumn('[cyan]Download'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[yellow]{task.description}'), console=shared.console) + try: + with open(preview_file, 'wb') as f: + with pbar: + task = pbar.add_task(description=preview_file, total=total_size) + for data in r.iter_content(block_size): + written = written + len(data) + f.write(data) + pbar.update(task, advance=block_size) + if written < 1024: # min threshold + os.remove(preview_file) + return 400, '', 'removed invalid download' + if is_video: + img = save_video_frame(preview_file) + else: + img = Image.open(preview_file) + except Exception as e: + shared.log.error(f'CivitAI download error: url={preview_url} file="{preview_file}" written={written} {e}') + return 500, '', str(e) + shared.state.end() + if img is None: + return 500, '', 'image is none' + shared.log.info(f'CivitAI download: url={preview_url} file="{preview_file}" size={total_size} image={img.size}') + img.close() + return 200, str(total_size), '' # code/size/note + + +def download_civit_model_thread(model_name: str, model_url: str, model_path: str = "", model_type: str = "Model", token: str = None): + import hashlib + sha256 = hashlib.sha256() + sha256.update(model_url.encode('utf-8')) + temp_file = sha256.hexdigest()[:8] + '.tmp' + + headers = {} + starting_pos = 0 + if os.path.isfile(temp_file): + starting_pos = os.path.getsize(temp_file) + headers['Range'] = f'bytes={starting_pos}-' + if token is None or len(token) == 0: + token = shared.opts.civitai_token + if token is not None and len(token) > 0: + headers['Authorization'] = f'Bearer {token}' + + r = shared.req(model_url, headers=headers, stream=True) + total_size = int(r.headers.get('content-length', 0)) + if model_name is None or len(model_name) == 0: + cn = r.headers.get('content-disposition', '') + model_name = cn.split('filename=')[-1].strip('"') + + model_path = model_path.strip() + if len(model_path) > 0: + if os.path.isabs(model_path): + pass + else: + model_path = os.path.join(paths.models_path, model_path) + elif model_type.lower() == 'lora': + model_path = shared.opts.lora_dir + elif model_type.lower() == 'embedding': + model_path = shared.opts.embeddings_dir + elif model_type.lower() == 'vae': + model_path = shared.opts.vae_dir + else: + model_path = shared.opts.ckpt_dir + model_file = os.path.join(model_path, model_name) + temp_file = os.path.join(model_path, temp_file) + + res = f'Model download: name="{model_name}" url="{model_url}" path="{model_path}" temp="{temp_file}"' + if os.path.isfile(model_file): + res += ' already exists' + shared.log.warning(res) + return res + + res += f' size={round((starting_pos + total_size)/1024/1024, 2)}Mb' + shared.log.info(res) + shared.state.begin('CivitAI') + block_size = 16384 # 16KB blocks + written = starting_pos + global pbar # pylint: disable=global-statement + if pbar is None: + pbar = p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[cyan]{task.fields[name]}'), console=shared.console) + with pbar: + task = pbar.add_task(description="Download starting", total=starting_pos+total_size, name=model_name) + try: + with open(temp_file, 'ab') as f: + for data in r.iter_content(block_size): + if written == 0: + try: # check if response is JSON message instead of bytes + shared.log.error(f'Model download: response={json.loads(data.decode("utf-8"))}') + raise ValueError('response: type=json expected=bytes') + except Exception: # this is good + pass + written = written + len(data) + f.write(data) + pbar.update(task, description="Download", completed=written) + if written < 1024: # min threshold + os.remove(temp_file) + raise ValueError(f'removed invalid download: bytes={written}') + except Exception as e: + shared.log.error(f'{res} {e}') + finally: + pbar.stop_task(task) + pbar.remove_task(task) + if starting_pos+total_size != written: + shared.log.warning(f'{res} written={round(written/1024/1024)}Mb incomplete download') + elif os.path.exists(temp_file): + shared.log.debug(f'Model download complete: temp="{temp_file}" path="{model_file}"') + os.rename(temp_file, model_file) + shared.state.end() + if os.path.exists(model_file): + return model_file + else: + return None + + +def download_civit_model(model_url: str, model_name: str = '', model_path: str = '', model_type: str = '', token: str = None): + import threading + if model_url is None or len(model_url) == 0: + err = 'Model download: no url provided' + shared.log.error(err) + return err + thread = threading.Thread(target=download_civit_model_thread, args=(model_name, model_url, model_path, model_type, token)) + thread.start() + thread.join() + from modules.sd_models import list_models # pylint: disable=W0621 + list_models() diff --git a/modules/civitai/metadata_civitai.py b/modules/civitai/metadata_civitai.py index 8bc2d47b9..3d40296b5 100644 --- a/modules/civitai/metadata_civitai.py +++ b/modules/civitai/metadata_civitai.py @@ -1,7 +1,6 @@ import os import re import time -import json import gradio as gr from modules.shared import log, opts, req, readfile, max_workers @@ -58,7 +57,8 @@ def civit_update_metadata(): return html.format(tbody=tbody) log.debug('CivitAI update metadata: models') - from modules import ui_extra_networks, modelloader + from modules import ui_extra_networks + from modules.civitai.download_civitai import download_civit_meta pages = ui_extra_networks.get_pages('Model') if len(pages) == 0: return 'CivitAI update metadata: no models found' @@ -75,7 +75,7 @@ def civit_update_metadata(): if r.status_code == 200: d = r.json() model.id = d['modelId'] - modelloader.download_civit_meta(model.fn, model.id) + download_civit_meta(model.fn, model.id) fn = os.path.splitext(item['filename'])[0] + '.json' model.meta = readfile(fn, silent=True) model.name = model.meta.get('name', model.name) @@ -158,64 +158,8 @@ def civit_search_model(name, tag, model_type): return res, gr.update(visible=len(data1) > 0, value=data1 if len(data1) > 0 else []), gr.update(visible=False, value=None), gr.update(visible=False, value=None) -def civit_select1(evt: gr.SelectData, in_data): - model_id = in_data[evt.index[0]][0] - data2 = [] - preview_img = None - for model in data: - if model['id'] == model_id: - for d in model['modelVersions']: - try: - if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: - preview_img = d['images'][0]['url'] - data2.append([d.get('id', None), d.get('modelId', None) or model_id, d.get('name', None), d.get('baseModel', None), d.get('createdAt', None) or d.get('publishedAt', None)]) - except Exception as e: - log.error(f'CivitAI select: model="{in_data[evt.index[0]]}" {e}') - log.error(f'CivitAI version data={type(d)}: {d}') - log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" versions={len(data2)}') - return data2, None, preview_img - - -def civit_select2(evt: gr.SelectData, in_data): - variant_id = in_data[evt.index[0]][0] - model_id = in_data[evt.index[0]][1] - data3 = [] - for model in data: - if model['id'] == model_id: - for variant in model['modelVersions']: - if variant['id'] == variant_id: - for f in variant['files']: - try: - if os.path.splitext(f['name'])[1].lower() in ['.safetensors', '.ckpt', '.pt', '.pth', '.bin']: - data3.append([f['name'], round(f['sizeKB']), json.dumps(f['metadata']), f['downloadUrl']]) - except Exception: - pass - log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" files={len(data3)}') - return data3 - - -def civit_select3(evt: gr.SelectData, in_data): - log.debug(f'CivitAI select: variant={in_data[evt.index[0]]}') - return in_data[evt.index[0]][3], in_data[evt.index[0]][0], gr.update(interactive=True) - - -def civit_download_model(model_url: str, model_name: str, model_path: str, model_type: str, token: str = None): - if model_url is None or len(model_url) == 0: - return 'No model selected' - try: - from modules.modelloader import download_civit_model - res = download_civit_model(model_url, model_name, model_path, model_type, token=token) - except Exception as e: - res = f"CivitAI model downloaded error: model={model_url} {e}" - log.error(res) - return res - from modules.sd_models import list_models # pylint: disable=W0621 - list_models() - return res - - def atomic_civit_search_metadata(item, results): - from modules.modelloader import download_civit_preview, download_civit_meta + from modules.civitai.download_civitai import download_civit_preview, download_civit_meta if item is None: return try: @@ -344,9 +288,3 @@ def civit_search_metadata(title: str = None): t1 = time.time() log.debug(f'CivitAI search metadata: scanned={scanned} skipped={skipped} time={t1-t0:.2f}') return create_search_metadata_table(results) - - -def civitai_update_token(token): - log.debug('CivitAI update token') - opts.civitai_token = token - opts.save() diff --git a/modules/modelloader.py b/modules/modelloader.py index 9813b5ee5..c4a1d3e00 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -1,14 +1,11 @@ import io import os import time -import json import shutil import importlib import contextlib from typing import Dict from urllib.parse import urlparse -from PIL import Image -import rich.progress as p import huggingface_hub as hf from installer import install, log from modules import shared, errors, files_cache @@ -48,185 +45,6 @@ def hf_login(token=None): return True -def save_video_frame(filepath: str): - from modules import video - try: - frames, fps, duration, w, h, codec, frame = video.get_video_params(filepath, capture=True) - except Exception as e: - shared.log.error(f'Video: file={filepath} {e}') - return None - if frame is not None: - basename = os.path.splitext(filepath) - thumb = f'{basename[0]}.thumb.jpg' - shared.log.debug(f'Video: file={filepath} frames={frames} fps={fps} size={w}x{h} codec={codec} duration={duration} thumb={thumb}') - frame.save(thumb) - else: - shared.log.error(f'Video: file={filepath} no frames found') - return frame - - -def download_civit_meta(model_path: str, model_id): - fn = os.path.splitext(model_path)[0] + '.json' - url = f'https://civitai.com/api/v1/models/{model_id}' - r = shared.req(url) - if r.status_code == 200: - try: - data = r.json() - shared.writefile(data, filename=fn, mode='w', silent=True) - shared.log.info(f'CivitAI download: id={model_id} url={url} file="{fn}"') - return r.status_code, len(data), '' # code/size/note - except Exception as e: - errors.display(e, 'civitai meta') - shared.log.error(f'CivitAI meta: id={model_id} url={url} file="{fn}" {e}') - return r.status_code, '', str(e) - return r.status_code, '', '' - - -def download_civit_preview(model_path: str, preview_url: str): - global pbar # pylint: disable=global-statement - if model_path is None: - pbar = None - return 500, '', '' - ext = os.path.splitext(preview_url)[1] - preview_file = os.path.splitext(model_path)[0] + ext - is_video = preview_file.lower().endswith('.mp4') - is_json = preview_file.lower().endswith('.json') - if is_json: - shared.log.warning(f'CivitAI download: url="{preview_url}" skip json') - return 500, '', 'exepected preview image got json' - if os.path.exists(preview_file): - return 304, '', 'already exists' - # res = f'CivitAI download: url={preview_url} file="{preview_file}"' - r = shared.req(preview_url, stream=True) - total_size = int(r.headers.get('content-length', 0)) - block_size = 16384 # 16KB blocks - written = 0 - img = None - shared.state.begin('CivitAI') - if pbar is None: - pbar = p.Progress(p.TextColumn('[cyan]Download'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[yellow]{task.description}'), console=shared.console) - try: - with open(preview_file, 'wb') as f: - with pbar: - task = pbar.add_task(description=preview_file, total=total_size) - for data in r.iter_content(block_size): - written = written + len(data) - f.write(data) - pbar.update(task, advance=block_size) - if written < 1024: # min threshold - os.remove(preview_file) - return 400, '', 'removed invalid download' - if is_video: - img = save_video_frame(preview_file) - else: - img = Image.open(preview_file) - except Exception as e: - shared.log.error(f'CivitAI download error: url={preview_url} file="{preview_file}" written={written} {e}') - return 500, '', str(e) - shared.state.end() - if img is None: - return 500, '', 'image is none' - shared.log.info(f'CivitAI download: url={preview_url} file="{preview_file}" size={total_size} image={img.size}') - img.close() - return 200, str(total_size), '' # code/size/note - - -download_pbar = None - -def download_civit_model_thread(model_name: str, model_url: str, model_path: str = "", model_type: str = "Model", token: str = None): - import hashlib - sha256 = hashlib.sha256() - sha256.update(model_url.encode('utf-8')) - temp_file = sha256.hexdigest()[:8] + '.tmp' - - headers = {} - starting_pos = 0 - if os.path.isfile(temp_file): - starting_pos = os.path.getsize(temp_file) - headers['Range'] = f'bytes={starting_pos}-' - if token is None: - token = shared.opts.civitai_token - if token is not None and len(token) > 0: - headers['Authorization'] = f'Bearer {token}' - - r = shared.req(model_url, headers=headers, stream=True) - total_size = int(r.headers.get('content-length', 0)) - if model_name is None or len(model_name) == 0: - cn = r.headers.get('content-disposition', '') - model_name = cn.split('filename=')[-1].strip('"') - - if model_type == 'LoRA': - model_file = os.path.join(shared.opts.lora_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.lora_dir, model_path, temp_file) - elif model_type == 'Embedding': - model_file = os.path.join(shared.opts.embeddings_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.embeddings_dir, model_path, temp_file) - elif model_type == 'VAE': - model_file = os.path.join(shared.opts.vae_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.vae_dir, model_path, temp_file) - else: - model_file = os.path.join(shared.opts.ckpt_dir, model_path, model_name) - temp_file = os.path.join(shared.opts.ckpt_dir, model_path, temp_file) - - res = f'Model download: name="{model_name}" url="{model_url}" path="{model_path}" temp="{temp_file}"' - if os.path.isfile(model_file): - res += ' already exists' - shared.log.warning(res) - return res - - res += f' size={round((starting_pos + total_size)/1024/1024, 2)}Mb' - shared.log.info(res) - shared.state.begin('CivitAI') - block_size = 16384 # 16KB blocks - written = starting_pos - global download_pbar # pylint: disable=global-statement - if download_pbar is None: - download_pbar = p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), p.TextColumn('[cyan]{task.fields[name]}'), console=shared.console) - with download_pbar: - task = download_pbar.add_task(description="Download starting", total=starting_pos+total_size, name=model_name) - try: - with open(temp_file, 'ab') as f: - for data in r.iter_content(block_size): - if written == 0: - try: # check if response is JSON message instead of bytes - shared.log.error(f'Model download: response={json.loads(data.decode("utf-8"))}') - raise ValueError('response: type=json expected=bytes') - except Exception: # this is good - pass - written = written + len(data) - f.write(data) - download_pbar.update(task, description="Download", completed=written) - if written < 1024: # min threshold - os.remove(temp_file) - raise ValueError(f'removed invalid download: bytes={written}') - except Exception as e: - shared.log.error(f'{res} {e}') - finally: - download_pbar.stop_task(task) - download_pbar.remove_task(task) - if starting_pos+total_size != written: - shared.log.warning(f'{res} written={round(written/1024/1024)}Mb incomplete download') - elif os.path.exists(temp_file): - shared.log.debug(f'Model download complete: temp="{temp_file}" path="{model_file}"') - os.rename(temp_file, model_file) - shared.state.end() - if os.path.exists(model_file): - return model_file - else: - return None - - -def download_civit_model(model_url: str, model_name: str, model_path: str, model_type: str, token: str = None): - import threading - if model_name is None or len(model_name) == 0: - err = 'Model download: no target model name provided' - shared.log.error(err) - return err - thread = threading.Thread(target=download_civit_model_thread, args=(model_name, model_url, model_path, model_type, token)) - thread.start() - return f'Model download: name={model_name} url={model_url} path={model_path}' - - def download_diffusers_model(hub_id: str, cache_dir: str = None, download_config: Dict[str, str] = None, token = None, variant = None, revision = None, mirror = None, custom_pipeline = None): if hub_id is None or len(hub_id) == 0: return None @@ -430,6 +248,7 @@ def load_civitai(model: str, url: str): return name # already downloaded else: shared.log.debug(f'Reference download start: model="{name}"') + from modules.civitai.download_civitai import download_civit_model_thread download_civit_model_thread(model_name=model, model_url=url, model_path='', model_type='safetensors', token=shared.opts.civitai_token) shared.log.debug(f'Reference download complete: model="{name}"') sd_models.list_models() diff --git a/modules/sd_checkpoint.py b/modules/sd_checkpoint.py index 1f193f603..62f464946 100644 --- a/modules/sd_checkpoint.py +++ b/modules/sd_checkpoint.py @@ -244,7 +244,8 @@ def get_closet_checkpoint_match(s: str) -> CheckpointInfo: # civitai search if shared.opts.sd_checkpoint_autodownload and s.startswith("https://civitai.com/api/download/models"): - fn = modelloader.download_civit_model_thread(model_name=None, model_url=s, model_path='', model_type='Model', token=None) + from modules.civitai.download_civitai import download_civit_model_thread + fn = download_civit_model_thread(model_name=None, model_url=s, model_path='', model_type='Model', token=None) if fn is not None: checkpoint_info = CheckpointInfo(fn) return checkpoint_info diff --git a/modules/shared.py b/modules/shared.py index 6c971f4eb..7df525310 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -514,26 +514,43 @@ options_templates.update(options_section(('image-metadata', "Image Metadata"), { })) options_templates.update(options_section(('ui', "User Interface"), { + "themes_sep_ui": OptionInfo("

Theme options

", "", gr.HTML), "theme_type": OptionInfo("Standard", "Theme type", gr.Radio, {"choices": ["Modern", "Standard", "None"]}), "theme_style": OptionInfo("Auto", "Theme mode", gr.Radio, {"choices": ["Auto", "Dark", "Light"]}), "gradio_theme": OptionInfo("black-teal", "UI theme", gr.Dropdown, lambda: {"choices": theme.list_themes()}, refresh=theme.refresh_themes), - "ui_locale": OptionInfo("Auto", "UI locale", gr.Dropdown, lambda: {"choices": theme.list_locales()}), - "subpath": OptionInfo("", "Mount URL subpath"), + + "quicksetting_sep_images": OptionInfo("

Quicksettings

", "", gr.HTML), + "quicksettings_list": OptionInfo(["sd_model_checkpoint"], "Quicksettings list", gr.Dropdown, lambda: {"multiselect":True, "choices": opts.list()}), + + "server_sep_ui": OptionInfo("

Startup & Server Options

", "", gr.HTML), "autolaunch": OptionInfo(False, "Autolaunch browser upon startup"), + "motd": OptionInfo(False, "Show MOTD"), + "subpath": OptionInfo("", "Mount URL subpath"), + "ui_request_timeout": OptionInfo(30000, "UI request timeout", gr.Slider, {"minimum": 1000, "maximum": 120000, "step": 10}), + + "cards_sep_ui": OptionInfo("

Card options

", "", gr.HTML), + "extra_networks_card_size": OptionInfo(140, "UI card size (px)", gr.Slider, {"minimum": 20, "maximum": 2000, "step": 1}), + "extra_networks_card_cover": OptionInfo("sidebar", "UI position", gr.Radio, {"choices": ["cover", "inline", "sidebar"]}), + "extra_networks_card_square": OptionInfo(True, "UI disable variable aspect ratio"), + + "other_sep_ui": OptionInfo("

Other...

", "", gr.HTML), + "ui_locale": OptionInfo("Auto", "UI locale", gr.Dropdown, lambda: {"choices": theme.list_locales()}), "font_size": OptionInfo(14, "Font size", gr.Slider, {"minimum": 8, "maximum": 32, "step": 1}), "aspect_ratios": OptionInfo("1:1, 4:3, 3:2, 16:9, 16:10, 21:9, 2:3, 3:4, 9:16, 10:16, 9:21", "Allowed aspect ratios"), - "logmonitor_show": OptionInfo(True, "Show log view"), - "logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}), - "ui_request_timeout": OptionInfo(30000, "UI request timeout", gr.Slider, {"minimum": 1000, "maximum": 120000, "step": 10}), - "motd": OptionInfo(False, "Show MOTD"), "compact_view": OptionInfo(False, "Compact view"), "ui_columns": OptionInfo(4, "Gallery view columns", gr.Slider, {"minimum": 1, "maximum": 8, "step": 1}), + + "images_sep_log": OptionInfo("

Log Display

", "", gr.HTML), + "logmonitor_show": OptionInfo(True, "Show log view"), + "logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}), + + "images_sep_ui": OptionInfo("

Outputs & Images

", "", gr.HTML), "return_grid": OptionInfo(True, "Show grid in results"), "return_mask": OptionInfo(False, "Inpainting include greyscale mask in results"), "return_mask_composite": OptionInfo(False, "Inpainting include masked composite in results"), "send_seed": OptionInfo(True, "Send seed when sending prompt or image to other interface", gr.Checkbox, {"visible": False}), "send_size": OptionInfo(False, "Send size when sending prompt or image to another interface", gr.Checkbox, {"visible": False}), - "quicksettings_list": OptionInfo(["sd_model_checkpoint"], "Quicksettings list", gr.Dropdown, lambda: {"multiselect":True, "choices": opts.list()}), + })) options_templates.update(options_section(('live-preview', "Live Previews"), { @@ -642,11 +659,8 @@ options_templates.update(options_section(('extra_networks', "Networks"), { "extra_networks": OptionInfo(["All"], "Available networks", gr.Dropdown, lambda: {"multiselect":True, "choices": ['All'] + [en.title for en in extra_networks]}), "extra_networks_sort": OptionInfo("Default", "Sort order", gr.Dropdown, {"choices": ['Default', 'Name [A-Z]', 'Name [Z-A]', 'Date [Newest]', 'Date [Oldest]', 'Size [Largest]', 'Size [Smallest]']}), "extra_networks_view": OptionInfo("gallery", "UI view", gr.Radio, {"choices": ["gallery", "list"]}), - "extra_networks_card_cover": OptionInfo("sidebar", "UI position", gr.Radio, {"choices": ["cover", "inline", "sidebar"]}), - "extra_networks_height": OptionInfo(0, "UI height (%)", gr.Slider, {"minimum": 0, "maximum": 100, "step": 1}), # set in ui_javascript "extra_networks_sidebar_width": OptionInfo(35, "UI sidebar width (%)", gr.Slider, {"minimum": 10, "maximum": 80, "step": 1}), - "extra_networks_card_size": OptionInfo(140, "UI card size (px)", gr.Slider, {"minimum": 20, "maximum": 2000, "step": 1}), - "extra_networks_card_square": OptionInfo(True, "UI disable variable aspect ratio"), + "extra_networks_height": OptionInfo(0, "UI height (%)", gr.Slider, {"minimum": 0, "maximum": 100, "step": 1}), # set in ui_javascript "extra_networks_fetch": OptionInfo(True, "UI fetch network info on mouse-over"), "extra_network_skip_indexing": OptionInfo(False, "Build info on first access", gr.Checkbox), diff --git a/modules/ui_models.py b/modules/ui_models.py index d008d80fb..f6f2aa257 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -475,13 +475,28 @@ def create_ui(): html = create_model_cards(results) return html + def civitai_update_token(token): + log.debug('CivitAI update token') + opts.civitai_token = token + opts.save() + + def civitai_download(model_url, model_name, model_type, model_path, civit_token, model_output): + from modules.civitai.download_civitai import download_civit_model + msg = f"

Initiating download

{model_name} | {model_type} | {model_url}

" + yield msg + model_output + download_civit_model(model_url, model_name, model_path, model_type, civit_token) + yield model_output + with gr.Row(): gr.HTML('

Search & Download

') with gr.Row(elem_id='civitai_search_row'): civit_search_text = gr.Textbox(label='', placeholder='keyword', elem_id="civit_search_text") civit_search_tag = gr.Textbox(label='', placeholder='tag', elem_id="civit_search_text") civit_search_text_btn = ToolButton(value=ui_symbols.search, interactive=True) - with gr.Accordion(label='Search options', open=False, elem_id="civitai_search_options"): + with gr.Accordion(label='Advanced', open=False, elem_id="civitai_search_options"): + civit_download_btn = gr.Button(value="Download model", variant='primary', elem_id="civitai_download_btn", visible=False) + with gr.Row(): + civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') with gr.Row(): civit_nsfw = gr.Checkbox(label='NSFW allowed', value=True) with gr.Row(): @@ -489,71 +504,17 @@ def create_ui(): with gr.Row(): civit_base = gr.Textbox(label='Base model', placeholder='SDXL, ...') with gr.Row(): - civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') + civit_folder = gr.Textbox(label='Download folder', placeholder='optional folder for downloads') + with gr.Row(): + civitai_models_output = gr.HTML('', elem_id="civitai_models_output") # sort, period, limit + _dummy = gr.Label(visible=False) # dummy component to get argspec later civit_inputs = [civit_search_text, civit_search_tag, civit_nsfw, civit_type, civit_base, civit_token] - civit_search_text_btn.click(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) - civit_search_text.submit(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) - civit_search_tag.submit(fn=civitai_search, inputs=civit_inputs, outputs=[models_outcome]) - - """ - from modules.civitai.legacy_civitai import civitai_update_token, civit_search_model, civit_search_metadata, civit_select1, civit_select2, civit_select3, civit_download_model - - with gr.Row(): - gr.HTML('

Search for models

') - with gr.Row(): - with gr.Column(scale=1): - civit_model_type = gr.Dropdown(label='CivitAI model type', choices=['Model', 'LoRA', 'Embedding', 'VAE', 'Other'], value='Model') - with gr.Column(scale=15): - with gr.Row(): - civit_search_text = gr.Textbox('', label='Search models', placeholder='keyword') - civit_search_tag = gr.Textbox('', label='', placeholder='tags') - civit_search_btn = ToolButton(value=ui_symbols.search, interactive=True) - with gr.Row(): - civit_search_res = gr.HTML('') - with gr.Row(): - gr.HTML('

 CivitAI download model

') - with gr.Row(): - civit_download_model_btn = gr.Button(value="Download", variant='primary') - gr.HTML('Select a model, model version and and model variant from the search results to download or enter model URL manually
') - with gr.Row(): - civit_token = gr.Textbox(opts.civitai_token, label='CivitAI token', placeholder='optional access token for private or gated models') - civit_token.change(fn=civitai_update_token, inputs=[civit_token], outputs=[]) - with gr.Row(): - civit_name = gr.Textbox('', label='Model name', placeholder='select model from search results', visible=True) - civit_selected = gr.Textbox('', label='Model URL', placeholder='select model from search results', visible=True) - civit_path = gr.Textbox('', label='Download path', placeholder='optional subfolder path where to save model', visible=True) - with gr.Row(): - gr.HTML('

Search results

') - with gr.Row(): - civit_headers1 = ['ID', 'Name', 'Tags', 'Downloads', 'Rating'] - civit_types1 = ['number', 'str', 'str', 'number', 'number'] - civit_results1 = gr.DataFrame(value=None, label=None, show_label=False, interactive=False, wrap=True, headers=civit_headers1, datatype=civit_types1, type='array', visible=False) - with gr.Row(): - with gr.Column(): - civit_headers2 = ['ID', 'ModelID', 'Name', 'Base', 'Created', 'Preview'] - civit_types2 = ['number', 'number', 'str', 'str', 'date', 'str'] - civit_results2 = gr.DataFrame(value=None, label='Model versions', show_label=True, interactive=False, wrap=True, headers=civit_headers2, datatype=civit_types2, type='array', visible=False) - with gr.Column(): - civit_headers3 = ['Name', 'Size', 'Metadata', 'URL'] - civit_types3 = ['str', 'number', 'str', 'str'] - civit_results3 = gr.DataFrame(value=None, label='Model variants', show_label=True, interactive=False, wrap=True, headers=civit_headers3, datatype=civit_types3, type='array', visible=False) - - def is_visible(component): - visible = len(component) > 0 if component is not None else False - return gr.update(visible=visible) - - civit_search_text.submit(fn=civit_search_model, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_search_res, civit_results1, civit_results2, civit_results3]) - civit_search_tag.submit(fn=civit_search_model, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_search_res, civit_results1, civit_results2, civit_results3]) - civit_search_btn.click(fn=civit_search_model, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_search_res, civit_results1, civit_results2, civit_results3]) - civit_results1.select(fn=civit_select1, inputs=[civit_results1], outputs=[civit_results2, civit_results3, models_image]) - civit_results2.select(fn=civit_select2, inputs=[civit_results2], outputs=[civit_results3]) - civit_results3.select(fn=civit_select3, inputs=[civit_results3], outputs=[civit_selected, civit_name, civit_search_btn]) - civit_results1.change(fn=is_visible, inputs=[civit_results1], outputs=[civit_results1]) - civit_results2.change(fn=is_visible, inputs=[civit_results2], outputs=[civit_results2]) - civit_results3.change(fn=is_visible, inputs=[civit_results3], outputs=[civit_results3]) - civit_download_model_btn.click(fn=civit_download_model, inputs=[civit_selected, civit_name, civit_path, civit_model_type, civit_token], outputs=[models_outcome]) - """ + civit_search_text_btn.click(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) + civit_search_text.submit(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) + civit_search_tag.submit(fn=civitai_search, inputs=civit_inputs, outputs=[civitai_models_output]) + civit_token.change(fn=civitai_update_token, inputs=[civit_token], outputs=[]) + civit_download_btn.click(fn=civitai_download, _js="downloadCivitModel", inputs=[_dummy, _dummy, _dummy, civit_folder, civit_token, civitai_models_output], outputs=[civitai_models_output]) with gr.Tab(label="Huggingface", elem_id="models_huggingface_tab"): from modules.models_hf import hf_search, hf_select, hf_download_model, hf_update_token