diff --git a/README.md b/README.md index 69bcf0d..a55dd30 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,22 @@ https://github.com/BlafKing/sd-civitai-browser-plus/assets/9644716/44c5c7a0-4854 # Changelog 📋 +

v3.3.0

+ +* Feature: New txt2img and img2img model info overlay on CivitAI button press. +* Feature: Base Model as sub folder option. +* Feature: Ability to multi-download to selected folder. +* Feature: Use the same folder as older versions when updating using multi-download. +* Feature: txt2img and img2img CivitAI buttons can use local HTML file, toggle in settings. +* New setting: Save API info of model when saving model info. +* New setting: Automatically save all images after download. +* New setting: Use local HTML file for model info. +* Bug fix: better JSON decode, now forces UTF-8 +* Bug fix: Now uses the proper default file when using multi-download +* Bug fix: Hide early access models fix, now works when published_at does not exist in API. +* Bug fix: Fix attempt for queue clearing upon download fail. + +---

v3.2.5

* Bug fix: Removed default API Key since it gets blocked after many downloads. diff --git a/javascript/civitai-html.js b/javascript/civitai-html.js index 440ce6a..d406e08 100644 --- a/javascript/civitai-html.js +++ b/javascript/civitai-html.js @@ -432,7 +432,7 @@ function createCardButtons(event) { newDiv.classList.add('goto-civitbrowser', 'card-button'); newDiv.addEventListener('click', function (event) { event.stopPropagation(); - sendModelToBrowser(modelName, content_type); + modelInfoPopUp(modelName, content_type); }); const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); @@ -468,53 +468,125 @@ function createCardButtons(event) { document.addEventListener('click', createCardButtons); // Sends the selected model list to a python function -function sendModelToBrowser(modelName, content_type) { - const tabNav = document.querySelector('.tab-nav'); - const buttons = tabNav.querySelectorAll('button'); - for (const button of buttons) { - if (button.textContent.includes('Browser+')) { - button.click(); - - const firstButton = document.querySelector('#tab_civitai_interface > div > div > div > button'); - if (firstButton) { - firstButton.click(); - } - } - } +function modelInfoPopUp(modelName, content_type) { select_model(modelName, null, true, content_type); + + // Create the overlay + var overlay = document.createElement('div'); + overlay.classList.add('civitaiOverlayGlobal'); + overlay.style.position = 'fixed'; + overlay.style.top = '0'; + overlay.style.left = '0'; + overlay.style.width = '100%'; + overlay.style.height = '100%'; + overlay.style.backgroundColor = 'rgba(20, 20, 20, 0.95)'; + overlay.style.zIndex = '1001'; + overlay.style.overflowY = 'auto'; + + // Create the pop-up window + var popup = document.createElement('div'); + popup.classList.add('civitaiOverlay'); + popup.style.display = 'flex'; + popup.style.justifyContent = 'center'; + popup.style.position = 'absolute'; + popup.style.top = '50%'; + popup.style.left = '50%'; + popup.style.width = '56em'; + popup.style.transform = 'translate(-50%, -50%)'; + popup.style.background = 'var(--body-background-fill)'; + popup.style.padding = '2em'; + popup.style.borderRadius = 'var(--block-radius)'; + popup.style.borderStyle = 'solid'; + popup.style.borderWidth = 'var(--block-border-width)'; + popup.style.borderColor = 'var(--block-border-color)'; + popup.style.zIndex = '1001'; + + // Add content to the popup + var popupContent = document.createElement('div'); // Change from

to

+ popupContent.classList.add('civitaiLoadingText'); + popupContent.textContent = 'Loading model info, please wait!'; + popupContent.style.fontSize = '24px'; + popupContent.style.color = 'white'; + popupContent.style.fontFamily = 'var(--font)'; + popup.appendChild(popupContent); + + // Create the close button + var closeButton = document.createElement('div'); + closeButton.textContent = '×'; + closeButton.style.position = 'fixed'; + closeButton.style.right = '0.25em'; + closeButton.style.top = '0'; + closeButton.style.cursor = 'pointer'; + closeButton.style.color = 'white'; + closeButton.style.fontSize = '32pt'; + closeButton.addEventListener('click', hidePopup); + document.addEventListener('keydown', handleKeyPress); + + // Append the close button to the overlay + overlay.appendChild(closeButton); + + // Append the popup to the overlay + overlay.appendChild(popup); + + // Append the overlay to the body + document.body.style.overflow = 'hidden'; // Prevent scrolling on the main page + document.body.appendChild(overlay); + + overlay.addEventListener('click', function (event) { + if (event.target === overlay) { + hidePopup(); + } + }); +} + +// Function to hide the popup +function hidePopup() { + var overlay = document.querySelector('.civitaiOverlayGlobal'); + if (overlay) { + document.body.removeChild(overlay); + document.body.style.overflow = 'auto'; + } +} + +// Function to handle key presses +function handleKeyPress(event) { + if (event.key === 'Escape') { + hidePopup(); + } } // Creates a list of the selected models var selectedModels = []; -function multi_model_select(modelName, isChecked) { +var selectedTypes = []; +function multi_model_select(modelName, modelType, isChecked) { if (arguments.length === 0) { selectedModels = []; + selectedTypes = []; return; } if (isChecked) { if (!selectedModels.includes(modelName)) { selectedModels.push(modelName); } + selectedTypes.push(modelType) } else { - var index = selectedModels.indexOf(modelName); - if (index > -1) { - selectedModels.splice(index, 1); + var modelIndex = selectedModels.indexOf(modelName); + if (modelIndex > -1) { + selectedModels.splice(modelIndex, 1); + } + var typesIndex = selectedTypes.indexOf(modelType); + if (typesIndex > -1) { + selectedTypes.splice(typesIndex, 1); } } - const output = gradioApp().querySelector('#selected_list textarea'); - output.value = JSON.stringify(selectedModels); - updateInput(output); -} + const selected_model_list = gradioApp().querySelector('#selected_model_list textarea'); + selected_model_list.value = JSON.stringify(selectedModels); -// Clicks the first item in the browser cards list -function clickFirstFigureInColumn() { - const columnDiv = document.querySelector('.column.civmodellist'); - if (columnDiv) { - const firstFigure = columnDiv.querySelector('figure'); - if (firstFigure) { - firstFigure.click(); - } - } + const selected_type_list = gradioApp().querySelector('#selected_type_list textarea'); + selected_type_list.value = JSON.stringify(selectedTypes); + + updateInput(selected_model_list); + updateInput(selected_type_list); } // Metadata button click detector @@ -533,6 +605,37 @@ document.addEventListener('click', function(event) { } }, true); +function inputHTMLPreviewContent(html_input) { + var overlay = document.querySelector('.civitaiOverlay') + let startIndex = html_input.indexOf("'value': '"); + if (startIndex !== -1) { + startIndex += "'value': '".length; + const endIndex = html_input.indexOf("', 'type': None,", startIndex); + if (endIndex !== -1) { + let extractedText = html_input.substring(startIndex, endIndex); + var modelIdNotFound = extractedText.includes(">Model ID not found.
Maybe"); + + extractedText = extractedText.replace(/\\n\s* checkbox.checked); const allUnchecked = checkboxes.every(checkbox => !checkbox.checked); - if (allChecked || allUnchecked) { - checkboxes.forEach(clickCheckbox); + checkboxes.forEach(sendClick); } else { - checkboxes.filter(checkbox => !checkbox.checked).forEach(clickCheckbox); - } - - function clickCheckbox(checkbox) { - const clickEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - checkbox.dispatchEvent(clickEvent); + checkboxes.filter(checkbox => !checkbox.checked).forEach(sendClick); } } @@ -601,25 +703,20 @@ function selectAllModels() { function deselectAllModels() { setTimeout(() => { const checkboxes = Array.from(document.querySelectorAll('.model-checkbox')); - checkboxes.filter(checkbox => checkbox.checked).forEach(uncheckCheckbox); - function uncheckCheckbox(checkbox) { - const clickEvent = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - checkbox.dispatchEvent(clickEvent); - } + checkboxes.filter(checkbox => checkbox.checked).forEach(sendClick); }, 1000); } // Sends Image URL to Python to pull generation info function sendImgUrl(image_url) { const randomNumber = Math.floor(Math.random() * 1000); + const genButton = gradioApp().querySelector('#txt2img_extra_tabs > div > button') const paddedNumber = String(randomNumber).padStart(3, '0'); const input = gradioApp().querySelector('#civitai_text2img_input textarea'); input.value = paddedNumber + "." + image_url; updateInput(input); + hidePopup(); + sendClick(genButton); } // Sends txt2img info to txt2img tab diff --git a/scripts/civitai_api.py b/scripts/civitai_api.py index 57102c5..a18238b 100644 --- a/scripts/civitai_api.py +++ b/scripts/civitai_api.py @@ -228,9 +228,11 @@ def model_list_html(json_data): if hide_early_access: early_access_days = version['earlyAccessTimeFrame'] if early_access_days != 0: - published_at = datetime.datetime.strptime(version['publishedAt'], "%Y-%m-%dT%H:%M:%S.%fZ") - adjusted_date = published_at + datetime.timedelta(days=early_access_days) - if not current_time > adjusted_date: + published_at_str = version.get('publishedAt') + if published_at_str is not None: + published_at = datetime.datetime.strptime(version['publishedAt'], "%Y-%m-%dT%H:%M:%S.%fZ") + adjusted_date = published_at + datetime.timedelta(days=early_access_days) + if not current_time > adjusted_date or not published_at_str: continue versions_to_keep.append(version) @@ -256,7 +258,7 @@ def model_list_html(json_data): existing_files.add(file) if file.endswith('.json'): json_path = os.path.join(root, file) - with open(json_path, 'r') as f: + with open(json_path, 'r', encoding="utf-8") as f: try: json_file = json.load(f) if isinstance(json_file, dict): @@ -322,7 +324,7 @@ def model_list_html(json_data): model_string = escape(f"{model_name_js} ({model_id})") model_card = f'
' if installstatus != "civmodelcardinstalled": - model_card += f'' \ + model_card += f'' \ + f'' if len(item["name"]) > 40: display_name = item["name"][:40] + '...' @@ -553,62 +555,61 @@ def update_model_list(content_type=None, sort_type=None, period_type=None, use_s gr.Textbox.update(value=None) # Model Filename ) -def update_model_versions(model_id): - item_id_and_types = {item['id']: (item['type'], item['description']) for item in gl.json_data['items']} - if model_id is not None: - selected_content_type, desc = item_id_and_types.get(model_id, (None, None)) - if selected_content_type is None: - return - - versions_dict = defaultdict(list) - installed_versions = set() - - model_folder = os.path.join(contenttype_folder(selected_content_type, desc)) - gl.main_folder = model_folder - - item = next((item for item in gl.json_data['items'] if item['id'] == model_id), None) - if item is None: - return - versions = item['modelVersions'] - - version_files = set() - for version in versions: - versions_dict[version['name']].append(item["name"]) - for version_file in version['files']: - file_sha256 = version_file.get('hashes', {}).get('SHA256', "").upper() - version_filename = version_file['name'] - version_files.add((version['name'], version_filename, file_sha256)) - - for root, _, files in os.walk(model_folder): - for file in files: - if file.endswith('.json'): - try: - json_path = os.path.join(root, file) - with open(json_path, 'r') as f: - json_data = json.load(f) - if isinstance(json_data, dict): - if 'sha256' in json_data and json_data['sha256']: - sha256 = json_data.get('sha256', "").upper() - for version_name, _, file_sha256 in version_files: - if sha256 == file_sha256: - installed_versions.add(version_name) - break - except Exception as e: - print(f"failed to read: \"{file}\": {e}") - - for version_name, version_filename, _ in version_files: - if file == version_filename: - installed_versions.add(version_name) - break - - version_names = list(versions_dict.keys()) - display_version_names = [f"{v} [Installed]" if v in installed_versions else v for v in version_names] - default_installed = next((f"{v} [Installed]" for v in installed_versions), None) - default_value = default_installed or next(iter(version_names), None) - - return gr.Dropdown.update(choices=display_version_names, value=default_value, interactive=True) # Version List +def update_model_versions(model_id, json_input=None): + if json_input: + api_json = json_input else: - return gr.Dropdown.update(choices=[], value=None, interactive=False) # Version List + api_json = gl.json_data + for item in api_json['items']: + if int(item['id']) == int(model_id): + content_type = item['type'] + desc = item.get('description', "None") + + versions_dict = defaultdict(list) + installed_versions = set() + + model_folder = os.path.join(contenttype_folder(content_type, desc)) + gl.main_folder = model_folder + versions = item['modelVersions'] + + version_files = set() + for version in versions: + versions_dict[version['name']].append(item["name"]) + for version_file in version['files']: + file_sha256 = version_file.get('hashes', {}).get('SHA256', "").upper() + version_filename = version_file['name'] + version_files.add((version['name'], version_filename, file_sha256)) + + for root, _, files in os.walk(model_folder): + for file in files: + if file.endswith('.json'): + try: + json_path = os.path.join(root, file) + with open(json_path, 'r', encoding="utf-8") as f: + json_data = json.load(f) + if isinstance(json_data, dict): + if 'sha256' in json_data and json_data['sha256']: + sha256 = json_data.get('sha256', "").upper() + for version_name, _, file_sha256 in version_files: + if sha256 == file_sha256: + installed_versions.add(version_name) + break + except Exception as e: + print(f"failed to read: \"{file}\": {e}") + + for version_name, version_filename, _ in version_files: + if file == version_filename: + installed_versions.add(version_name) + break + + version_names = list(versions_dict.keys()) + display_version_names = [f"{v} [Installed]" if v in installed_versions else v for v in version_names] + default_installed = next((f"{v} [Installed]" for v in installed_versions), None) + default_value = default_installed or next(iter(version_names), None) + + return gr.Dropdown.update(choices=display_version_names, value=default_value, interactive=True) # Version List + + return gr.Dropdown.update(choices=[], value=None, interactive=False) # Version List def cleaned_name(file_name): if platform.system() == "Windows": @@ -638,22 +639,29 @@ def extract_model_info(input_string): return name, int(id_number) -def update_model_info(model_string=None, model_version=None): +def update_model_info(model_string=None, model_version=None, only_html=False, input_id=None, json_input=None, from_preview=False): video_playback = getattr(opts, "video_playback", True) playback = "" if video_playback: playback = "autoplay loop" + if json_input: + api_data = json_input + else: + api_data = gl.json_data + BtnDownInt = True BtnDel = False BtnImage = False model_id = None - model_name = None - model_name, model_id = extract_model_info(model_string) + if not input_id: + _, model_id = extract_model_info(model_string) + else: + model_id = input_id if model_version and "[Installed]" in model_version: model_version = model_version.replace(" [Installed]", "") - if model_id and model_version: + if model_id: output_html = "" output_training = "" output_basemodel = "" @@ -664,12 +672,13 @@ def update_model_info(model_string=None, model_version=None): default_file = None model_filename = None sha256_value = None - for item in gl.json_data['items']: - if item['id'] == model_id: + for item in api_data['items']: + if int(item['id']) == int(model_id): content_type = item['type'] if content_type == "LORA": is_LORA = True desc = item['description'] + model_name = item['name'] model_folder = os.path.join(contenttype_folder(content_type, desc)) model_uploader = item['creator']['username'] uploader_avatar = item['creator']['image'] @@ -679,173 +688,182 @@ def update_model_info(model_string=None, model_version=None): uploader_avatar = f'
' tags = item.get('tags', "") model_desc = item.get('description', "") - for model in item['modelVersions']: - if model['name'] == model_version: - if model['trainedWords']: - output_training = ",".join(model['trainedWords']) - output_training = re.sub(r'<[^>]*:[^>]*>', '', output_training) - output_training = re.sub(r', ?', ', ', output_training) - output_training = output_training.strip(', ') - if model['baseModel']: - output_basemodel = model['baseModel'] - for file in model['files']: - dl_dict[file['name']] = file['downloadUrl'] - - if not model_filename: - model_filename = file['name'] - dl_url = file['downloadUrl'] - gl.json_info = item - sha256_value = file['hashes'].get('SHA256', 'Unknown') - - size = file['metadata'].get('size', 'Unknown') - format = file['metadata'].get('format', 'Unknown') - fp = file['metadata'].get('fp', 'Unknown') - sizeKB = file.get('sizeKB', 0) * 1024 - filesize = _download.convert_size(sizeKB) - - unique_file_name = f"{size} {format} {fp} ({filesize})" - is_primary = file.get('primary', False) - file_list.append(unique_file_name) - if is_primary: - default_file = unique_file_name - model_filename = file['name'] - dl_url = file['downloadUrl'] - gl.json_info = item - sha256_value = file['hashes'].get('SHA256', 'Unknown') + if model_version is None: + selected_version = item['modelVersions'][0] + else: + for model in item['modelVersions']: + if model['name'] == model_version: + selected_version = model + break + + if selected_version['trainedWords']: + output_training = ",".join(selected_version['trainedWords']) + output_training = re.sub(r'<[^>]*:[^>]*>', '', output_training) + output_training = re.sub(r', ?', ', ', output_training) + output_training = output_training.strip(', ') + if selected_version['baseModel']: + output_basemodel = selected_version['baseModel'] + for file in selected_version['files']: + dl_dict[file['name']] = file['downloadUrl'] + + if not model_filename: + model_filename = file['name'] + dl_url = file['downloadUrl'] + gl.json_info = item + sha256_value = file['hashes'].get('SHA256', 'Unknown') - if is_LORA and file_list: - extracted_formats = [file.split(' ')[1] for file in file_list] + size = file['metadata'].get('size', 'Unknown') + format = file['metadata'].get('format', 'Unknown') + fp = file['metadata'].get('fp', 'Unknown') + sizeKB = file.get('sizeKB', 0) * 1024 + filesize = _download.convert_size(sizeKB) + + unique_file_name = f"{size} {format} {fp} ({filesize})" + is_primary = file.get('primary', False) + file_list.append(unique_file_name) + if is_primary: + default_file = unique_file_name + model_filename = file['name'] + dl_url = file['downloadUrl'] + gl.json_info = item + sha256_value = file['hashes'].get('SHA256', 'Unknown') + + if is_LORA and file_list: + extracted_formats = [file.split(' ')[1] for file in file_list] - if "SafeTensor" in extracted_formats and "PickleTensor" in extracted_formats: - if "PickleTensor" in file_list[0].split(' ')[1]: - if float(file_list[0].split(' ')[0]) <= 100: - model_folder = os.path.join(contenttype_folder("TextualInversion")) + if "SafeTensor" in extracted_formats and "PickleTensor" in extracted_formats: + if "PickleTensor" in file_list[0].split(' ')[1]: + if float(file_list[0].split(' ')[0]) <= 100: + model_folder = os.path.join(contenttype_folder("TextualInversion")) + + model_url = selected_version['downloadUrl'] + model_main_url = f"https://civitai.com/models/{item['id']}" + img_html = '
' + for index, pic in enumerate(selected_version['images']): + meta_button = False + meta = pic['meta'] + if meta and meta.get('prompt'): + meta_button = True + BtnImage = True + # Change width value in URL to original image width + image_url = re.sub(r'/width=\d+', f'/width={pic["width"]}', pic["url"]) + if pic['type'] == "video": + image_url = image_url.replace("width=", "transcode=true,width=") + nsfw = 'class="model-block"' + + if pic['nsfw'] not in ["None", "Soft"]: + nsfw = 'class="civnsfw model-block"' + + img_html += f''' +
+
+ + + + ''' + + if meta_button: + img_html += f''' +
+ +
+ ''' + else: + img_html += '
' - model_url = model['downloadUrl'] - model_main_url = f"https://civitai.com/models/{item['id']}" - img_html = '
' - for index, pic in enumerate(model['images']): - meta_button = False - meta = pic['meta'] - if meta and meta.get('prompt'): - meta_button = True - BtnImage = True - # Change width value in URL to original image width - image_url = re.sub(r'/width=\d+', f'/width={pic["width"]}', pic["url"]) - if pic['type'] == "video": - image_url = image_url.replace("width=", "transcode=true,width=") - nsfw = 'class="model-block"' - - if pic['nsfw'] not in ["None", "Soft"]: - nsfw = 'class="civnsfw model-block"' + if meta: + img_html += '
' + # Define the preferred order of keys and convert them to lowercase + preferred_order = ["prompt", "negativePrompt", "seed", "Size", "Model", "clipSkip", "sampler", "steps", "cfgScale"] + preferred_order_lower = [key.lower() for key in preferred_order] + # Loop through the keys in the preferred order and add them to the HTML + for key in preferred_order: + if key in meta: + value = meta[key] + img_html += f'
{escape(str(key).capitalize())}
{escape(str(value))}
' + # Check if there are remaining keys in meta + remaining_keys = [key for key in meta if key.lower() not in preferred_order_lower] - img_html += f''' -
-
- - - - ''' - - if meta_button: - img_html += f''' -
- -
- ''' - else: - img_html += '
' - - if meta: - img_html += '
' - # Define the preferred order of keys and convert them to lowercase - preferred_order = ["prompt", "negativePrompt", "seed", "Size", "Model", "clipSkip", "sampler", "steps", "cfgScale"] - preferred_order_lower = [key.lower() for key in preferred_order] - # Loop through the keys in the preferred order and add them to the HTML - for key in preferred_order: - if key in meta: - value = meta[key] - img_html += f'
{escape(str(key).capitalize())}
{escape(str(value))}
' - # Check if there are remaining keys in meta - remaining_keys = [key for key in meta if key.lower() not in preferred_order_lower] + img_html += '
' - # Add the rest - if remaining_keys: - img_html += f""" -
-
- - -
- """ - for key in remaining_keys: - value = meta[key] - img_html += f'
{escape(str(key).capitalize())}
{escape(str(value))}
' - img_html = img_html + '
' - - img_html += '
' - - img_html = img_html + '
' - img_html = img_html + '
' - tags_html = ''.join([f'{escape(str(tag))}' for tag in tags]) - def perms_svg(color): - return f''\ - f'' - allow_svg = f'{perms_svg("lime")}' - deny_svg = f'{perms_svg("red")}' - perms_html= '

'\ - f'{allow_svg if item.get("allowNoCredit") else deny_svg} Use the model without crediting the creator
'\ - f'{allow_svg if item.get("allowCommercialUse") in ["Image", "Rent", "RentCivit", "Sell"] else deny_svg} Sell images they generate
'\ - f'{allow_svg if item.get("allowCommercialUse") in ["Rent", "Sell"] else deny_svg} Run on services that generate images for money
'\ - f'{allow_svg if item.get("allowCommercialUse") in ["RentCivit", "Rent", "Sell"] else deny_svg} Run on Civitai
'\ - f'{allow_svg if item.get("allowDerivatives") else deny_svg} Share merges using this model
'\ - f'{allow_svg if item.get("allowCommercialUse") == "Sell" else deny_svg} Sell this model or merges using this model
'\ - f'{allow_svg if item.get("allowDifferentLicense") else deny_svg} Have different permissions when sharing merges'\ - '

' - output_html = f''' -
-

{escape(str(model_name))}

-

Uploaded by {escape(str(model_uploader))}{uploader_avatar}

-
-
-
Version
-
{escape(str(model_version))}
-
Base Model
-
{escape(str(output_basemodel))}
-
CivitAI Tags
-
-
- {tags_html} -
-
-
Download Link
-
{model_url}
-
-
-
- {perms_html} -
+ img_html = img_html + '
' + img_html = img_html + '
' + tags_html = ''.join([f'{escape(str(tag))}' for tag in tags]) + def perms_svg(color): + return f''\ + f'' + allow_svg = f'{perms_svg("lime")}' + deny_svg = f'{perms_svg("red")}' + perms_html= '

'\ + f'{allow_svg if item.get("allowNoCredit") else deny_svg} Use the model without crediting the creator
'\ + f'{allow_svg if item.get("allowCommercialUse") in ["Image", "Rent", "RentCivit", "Sell"] else deny_svg} Sell images they generate
'\ + f'{allow_svg if item.get("allowCommercialUse") in ["Rent", "Sell"] else deny_svg} Run on services that generate images for money
'\ + f'{allow_svg if item.get("allowCommercialUse") in ["RentCivit", "Rent", "Sell"] else deny_svg} Run on Civitai
'\ + f'{allow_svg if item.get("allowDerivatives") else deny_svg} Share merges using this model
'\ + f'{allow_svg if item.get("allowCommercialUse") == "Sell" else deny_svg} Sell this model or merges using this model
'\ + f'{allow_svg if item.get("allowDifferentLicense") else deny_svg} Have different permissions when sharing merges'\ + '

' + output_html = f''' +
+

{escape(str(model_name))}

+

Uploaded by {escape(str(model_uploader))}{uploader_avatar}

+
+
+
Version
+
{escape(str(model_version))}
+
Base Model
+
{escape(str(output_basemodel))}
+
CivitAI Tags
+
+
+ {tags_html}
-
-
-

Description

- {model_desc} + +
Download Link
+
{model_url}
+ +
+
+ {perms_html}
-
{img_html}
- ''' - +
+
+

Description

+ {model_desc} +
+
+
{img_html}
+ ''' + + if only_html: + return output_html + folder_location = "None" default_subfolder = "None" sub_folders = ["None"] @@ -854,7 +872,7 @@ def update_model_info(model_string=None, model_version=None): for filename in files: if filename.endswith('.json'): json_file_path = os.path.join(root, filename) - with open(json_file_path, 'r') as f: + with open(json_file_path, 'r', encoding="utf-8") as f: try: data = json.load(f) sha256 = data.get('sha256') @@ -896,13 +914,15 @@ def update_model_info(model_string=None, model_version=None): sub_folders.remove("None") sub_folders = sorted(sub_folders, key=lambda x: (x.lower(), x)) sub_folders.insert(0, "None") - sub_opt1 = os.path.join(os.sep, cleaned_name(model_uploader)) - sub_opt2 = os.path.join(os.sep, cleaned_name(model_name)) - sub_opt3 = os.path.join(os.sep, cleaned_name(model_name), cleaned_name(model_version)) + sub_opt1 = os.path.join(os.sep, cleaned_name(output_basemodel)) + sub_opt2 = os.path.join(os.sep, cleaned_name(model_uploader)) + sub_opt3 = os.path.join(os.sep, cleaned_name(model_name)) + sub_opt4 = os.path.join(os.sep, cleaned_name(model_name), cleaned_name(model_version)) if insert_sub: sub_folders.insert(1, sub_opt1) sub_folders.insert(2, sub_opt2) sub_folders.insert(3, sub_opt3) + sub_folders.insert(4, sub_opt4) list = set() sub_folders = [x for x in sub_folders if not (x in list or list.add(x))] @@ -910,12 +930,14 @@ def update_model_info(model_string=None, model_version=None): sub_folders = ["None"] default_sub = sub_folder_value(content_type, desc) - if default_sub == f"{os.sep}Author Name": + if default_sub == f"{os.sep}Base Model": default_sub = sub_opt1 - elif default_sub == f"{os.sep}Model Name": + elif default_sub == f"{os.sep}Author Name": default_sub = sub_opt2 - elif default_sub == f"{os.sep}Model Name{os.sep}Version Name": + elif default_sub == f"{os.sep}Model Name": default_sub = sub_opt3 + elif default_sub == f"{os.sep}Model Name{os.sep}Version Name": + default_sub = sub_opt4 if folder_location == "None": folder_location = model_folder @@ -929,7 +951,7 @@ def update_model_info(model_string=None, model_version=None): default_subfolder = f'{os.sep}{relative_path}' if relative_path != "." else default_sub if BtnDel == False else "None" if gl.isDownloading: item = gl.download_queue[0] - if model_id == item['model_id']: + if int(model_id) == int(item['model_id']): BtnDel = False BtnDownTxt = "Download model" if len(gl.download_queue) > 0: @@ -998,7 +1020,7 @@ def update_file_info(model_string, model_version, file_metadata): model_version = model_version.replace(" [Installed]", "") if model_id and model_version: for item in gl.json_data['items']: - if item['id'] == model_id: + if int(item['id']) == int(model_id): content_type = item['type'] if content_type == "LORA": is_LORA = True @@ -1048,7 +1070,7 @@ def update_file_info(model_string, model_version, file_metadata): for root, _, files in os.walk(model_folder): for filename in files: if filename.endswith('.json'): - with open(os.path.join(root, filename), 'r') as f: + with open(os.path.join(root, filename), 'r', encoding="utf-8") as f: try: data = json.load(f) sha256_value = data.get('sha256') diff --git a/scripts/civitai_download.py b/scripts/civitai_download.py index 51c0d11..4a82156 100644 --- a/scripts/civitai_download.py +++ b/scripts/civitai_download.py @@ -11,7 +11,6 @@ import stat import json import time from pathlib import Path -from urllib.parse import urlsplit from modules.shared import opts, cmd_opts from scripts.civitai_global import print import scripts.civitai_global as gl @@ -62,7 +61,7 @@ def start_aria2_rpc(): os.rename(start_file, running_file) return else: - with open(start_file, 'w'): + with open(start_file, 'w', encoding="utf-8"): pass try: @@ -98,7 +97,7 @@ elif os_type == 'Linux': class TimeOutFunction(Exception): pass -def create_model_item(dl_url, model_filename, install_path, model_name, version_name, model_sha256, model_id, create_json): +def create_model_item(dl_url, model_filename, install_path, model_name, version_name, model_sha256, model_id, create_json, from_batch=False): if model_id: model_id = int(model_id) if model_sha256: @@ -107,13 +106,13 @@ def create_model_item(dl_url, model_filename, install_path, model_name, version_ filtered_items = [] for item in gl.json_data['items']: - if item['id'] == model_id: + if int(item['id']) == int(model_id): filtered_items.append(item) break model_json = {"items": filtered_items} - model_versions = _api.update_model_versions(model_id) + (preview_html,_,_,_,_,_,_,_,_,_,_,existing_path,_) = _api.update_model_info(None, model_versions.get('value'), False, model_id) for item in gl.download_queue: if item['dl_url'] == dl_url: @@ -129,12 +128,15 @@ def create_model_item(dl_url, model_filename, install_path, model_name, version_ "model_id" : model_id, "create_json" : create_json, "model_json" : model_json, - "model_versions" : model_versions + "model_versions" : model_versions, + "preview_html" : preview_html['value'], + "existing_path": existing_path['value'], + "from_batch" : from_batch } return item -def selected_to_queue(model_list, download_start, create_json): +def selected_to_queue(model_list, subfolder, download_start, create_json): global total_count, current_count if gl.download_queue: number = download_start @@ -144,18 +146,24 @@ def selected_to_queue(model_list, download_start, create_json): current_count = 0 model_list = json.loads(model_list) - + for model_string in model_list: model_name, model_id = _api.extract_model_info(model_string) for item in gl.json_data['items']: - if item['id'] == model_id: + if int(item['id']) == int(model_id): model_id, desc, content_type = item['id'], item['description'], item['type'] version = item.get('modelVersions', [])[0] version_name = version.get('name') files = version.get('files', []) - model_filename = _api.cleaned_name(files[0].get('name')) - model_sha256 = files[0].get('hashes', {}).get('SHA256') - dl_url = files[0].get('downloadUrl') + primary_file = next((file for file in files if file.get('primary', False)), None) + if primary_file: + model_filename = _api.cleaned_name(primary_file.get('name')) + model_sha256 = primary_file.get('hashes', {}).get('SHA256') + dl_url = primary_file.get('downloadUrl') + else: + model_filename = _api.cleaned_name(files[0].get('name')) + model_sha256 = files[0].get('hashes', {}).get('SHA256') + dl_url = files[0].get('downloadUrl') break model_folder = _api.contenttype_folder(content_type, desc) @@ -168,13 +176,23 @@ def selected_to_queue(model_list, download_start, create_json): default_sub = sub_opt1 elif default_sub == f"{os.sep}Model Name{os.sep}Version Name": default_sub = sub_opt2 + + if subfolder and subfolder != "None": + from_batch = False + if platform.system() == "Windows": + subfolder = re.sub(r'[/:*?"<>|]', '', subfolder) - if default_sub != "None": - install_path = model_folder + default_sub + if not subfolder.startswith(os.sep): + subfolder = os.sep + subfolder + install_path = model_folder + subfolder else: - install_path = model_folder + from_batch = True + if default_sub != "None": + install_path = model_folder + default_sub + else: + install_path = model_folder - model_item = create_model_item(dl_url, model_filename, install_path, model_name, version_name, model_sha256, model_id, create_json) + model_item = create_model_item(dl_url, model_filename, install_path, model_name, version_name, model_sha256, model_id, create_json, from_batch) if model_item: gl.download_queue.append(model_item) total_count += 1 @@ -214,11 +232,11 @@ def download_start(download_start, dl_url, model_filename, install_path, model_s def download_finish(model_filename, version, model_id): if model_id: model_id = int(model_id) - gr_components = _api.update_model_versions(model_id) + model_versions = _api.update_model_versions(model_id) else: - gr_components = None - if gr_components: - version_choices = gr_components['choices'] + model_versions = None + if model_versions: + version_choices = model_versions.get('choices', []) else: version_choices = [] prev_version = gl.last_version + " [Installed]" @@ -427,7 +445,7 @@ def info_to_json(install_path, model_id, model_sha256, unpackList=None): json_file = os.path.splitext(install_path)[0] + ".json" if os.path.exists(json_file): try: - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: data = json.load(f) except Exception as e: print(f"Failed to open {json_file}: {e}") @@ -439,7 +457,7 @@ def info_to_json(install_path, model_id, model_sha256, unpackList=None): if unpackList: data['unpackList'] = unpackList - with open(json_file, 'w') as f: + with open(json_file, 'w', encoding="utf-8") as f: json.dump(data, f, indent=4) def download_file_old(url, file_path, progress=gr.Progress() if queue else None): @@ -479,7 +497,7 @@ def download_file_old(url, file_path, progress=gr.Progress() if queue else None) headers = {"Range": f"bytes={downloaded_size}-"} else: headers = {} - with open(file_path, "ab") as f: + with open(file_path, "ab", encoding="utf-8") as f: while gl.isDownloading: try: if gl.cancel_status: @@ -578,9 +596,13 @@ def download_create_thread(download_finish, queue_trigger, progress=gr.Progress( gl.cancel_status = False use_aria2 = getattr(opts, "use_aria2", True) unpack_zip = getattr(opts, "unpack_zip", False) + save_all_images = getattr(opts, "auto_save_all_img", False) gl.recent_model = item['model_name'] gl.last_version = item['version_name'] - + + if item['from_batch']: + item['install_path'] = item['existing_path'] + gl.isDownloading = True if not os.path.exists(item['install_path']): os.makedirs(item['install_path']) @@ -617,9 +639,12 @@ def download_create_thread(download_finish, queue_trigger, progress=gr.Progress( print(f"Failed to extract {item['model_filename']} with error: {e}") if not gl.cancel_status: if item['create_json']: - _file.save_model_info(item['install_path'], item['model_filename'], item['model_sha256'], api_response=item['model_json']) + _file.save_model_info(item['install_path'], item['model_filename'], item['model_sha256'], item['preview_html'], api_response=item['model_json']) info_to_json(path_to_new_file, item['model_id'], item['model_sha256'], unpackList) - _file.save_preview(path_to_new_file, item['model_json'], True, item['model_sha256']) + if save_all_images: + _file.save_images(item['preview_html'], item['model_filename'], item['install_path'], ) + else: + _file.save_preview(path_to_new_file, item['model_json'], True, item['model_sha256']) base_name = os.path.splitext(item['model_filename'])[0] base_name_preview = base_name + '.preview' diff --git a/scripts/civitai_file_manage.py b/scripts/civitai_file_manage.py index 9ddc260..81634fb 100644 --- a/scripts/civitai_file_manage.py +++ b/scripts/civitai_file_manage.py @@ -12,6 +12,8 @@ import requests import hashlib from pathlib import Path from urllib.parse import urlparse + +from sympy import preview from modules.shared import cmd_opts, opts from scripts.civitai_global import print import scripts.civitai_global as gl @@ -46,20 +48,20 @@ except: def delete_model(delete_finish=None, model_filename=None, model_string=None, list_versions=None, sha256=None, selected_list=None, model_ver=None, model_json=None): deleted = False model_id = None - + if model_string: _, model_id = _api.extract_model_info(model_string) if not model_ver: - gr_components = _api.update_model_versions(model_id) - else: gr_components = model_ver + model_versions = _api.update_model_versions(model_id) + else: model_versions = model_ver - (model_name, ver_value, ver_choices) = _file.card_update(gr_components, model_string, list_versions, False) + (model_name, ver_value, ver_choices) = _file.card_update(model_versions, model_string, list_versions, False) if not model_json: if model_id != None: selected_content_type = None for item in gl.json_data['items']: - if item['id'] == model_id: + if int(item['id']) == int(model_id): selected_content_type = item['type'] desc = item['description'] break @@ -82,9 +84,11 @@ def delete_model(delete_finish=None, model_filename=None, model_string=None, lis if file.endswith('.json'): file_path = os.path.join(root, file) try: - with open(file_path, 'r') as json_file: + with open(file_path, 'r', encoding="utf-8") as json_file: data = json.load(json_file) - file_sha256 = data.get('sha256', '').upper() + file_sha256 = data.get('sha256', '') + if file_sha256: + file_sha256 = file_sha256.upper() except Exception as e: print(f"Failed to open: {file_path}: {e}") file_sha256 = "0" @@ -170,7 +174,7 @@ def save_preview(file_path, api_response, overwrite_toggle=False, sha256=None): if not sha256: if os.path.exists(json_file): try: - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: data = json.load(f) if 'sha256' in data and data['sha256']: sha256 = data['sha256'].upper() @@ -199,26 +203,25 @@ def save_preview(file_path, api_response, overwrite_toggle=False, sha256=None): print(f"No preview images found for \"{name}\"") return -def save_images(preview_html, model_filename, model_string, install_path, sub_folder): - - if model_string: - model_name, model_id = _api.extract_model_info(model_string) - +def save_images(preview_html, model_filename, install_path, sub_folder=None): image_location = getattr(opts, "image_location", r"") sub_image_location = getattr(opts, "sub_image_location", True) + image_path = install_path if image_location: if sub_image_location: desc = gl.json_info['description'] content_type = gl.json_info['type'] - install_path = os.path.join(_api.contenttype_folder(content_type, desc, custom_folder=image_location)) - if sub_folder and sub_folder != "None": - install_path = os.path.join(install_path, sub_folder.lstrip("/").lstrip("\\")) - else: - install_path = Path(image_location) + image_path = os.path.join(_api.contenttype_folder(content_type, desc, custom_folder=image_location)) + if not sub_folder: + sub_folder = os.path.relpath(install_path, image_path) - - if not os.path.exists(install_path): - os.makedirs(install_path) + if sub_folder and sub_folder != "None": + image_path = os.path.join(image_path, sub_folder.lstrip("/").lstrip("\\")) + else: + image_path = Path(image_location) + + if not os.path.exists(image_path): + os.makedirs(image_path) img_urls = re.findall(r'data-sampleimg="true" src=[\'"]?([^\'" >]+)', preview_html) name = os.path.splitext(model_filename)[0] @@ -227,45 +230,20 @@ def save_images(preview_html, model_filename, model_string, install_path, sub_fo opener.addheaders = [('User-agent', 'Mozilla/5.0')] urllib.request.install_opener(opener) - HTML = preview_html - image_count = 0 for i, img_url in enumerate(img_urls): - image_count += 1 - filename = f'{name}_{i}.png' - filenamethumb = f'{name}.png' - if model_id is not None: - for item in gl.json_data['items']: - if item['id'] == model_id: - if item['type'] == "TextualInversion": - filename = f'{name}_{i}.preview.png' - filenamethumb = f'{name}.preview.png' - HTML = HTML.replace(img_url,f'{filename}') - img_url = urllib.parse.quote(img_url, safe=':/=') + if i == 0: + filename = f'{name}.preview.png' + else: + filename = f'{name}_{i}.png' + img_url = urllib.parse.quote(img_url, safe=':/=') try: with urllib.request.urlopen(img_url) as url: - with open(os.path.join(install_path, filename), 'wb') as f: + with open(os.path.join(install_path if i == 0 else image_path, filename), 'wb') as f: f.write(url.read()) - if i == 0 and not os.path.exists(os.path.join(install_path, filenamethumb)): - shutil.copy2(os.path.join(install_path, filename),os.path.join(install_path, filenamethumb)) - print(f"Downloaded image {image_count}") + print(f"Downloaded {filename}") except urllib.error.URLError as e: print(f'Error: {e.reason}') - match = re.search(r'(\s*)
', preview_html) - if match: - indentation = match.group(1) - else: - indentation = '' - css_link = f'' - head_section = f'{indentation}{indentation} {css_link}{indentation}' - - HTML = head_section + HTML - path_to_new_file = os.path.join(install_path, f'{name}.html') - with open(path_to_new_file, 'wb') as f: - f.write(HTML.encode('utf8')) - path_to_new_file = os.path.join(install_path, f'{name}.civitai.info') - with open(path_to_new_file, mode="w", encoding="utf-8") as f: - json.dump(gl.json_info, f, indent=4, ensure_ascii=False) def card_update(gr_components, model_name, list_versions, is_install): if gr_components: @@ -316,7 +294,7 @@ def gen_sha256(file_path): if os.path.exists(json_file): try: - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: data = json.load(f) if 'sha256' in data and data['sha256']: @@ -344,25 +322,27 @@ def gen_sha256(file_path): if os.path.exists(json_file): try: - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: data = json.load(f) if 'sha256' in data and data['sha256']: data['sha256'] = hash_value - with open(json_file, 'w') as f: + with open(json_file, 'w', encoding="utf-8") as f: json.dump(data, f, indent=4) except Exception as e: print(f"Failed to open {json_file}: {e}") else: data = {'sha256': hash_value} - with open(json_file, 'w') as f: + with open(json_file, 'w', encoding="utf-8") as f: json.dump(data, f, indent=4) return hash_value -def model_from_sent(model_name, content_type, click_first_item, tile_count): +def model_from_sent(model_name, content_type, tile_count): modelID_failed = False + output_html = None + use_local_html = getattr(opts, "use_local_html", False) model_name = re.sub(r'\.\d{3}$', '', model_name) content_type = re.sub(r'\.\d{3}$', '', content_type) content_mapping = { @@ -382,38 +362,68 @@ def model_from_sent(model_name, content_type, click_first_item, tile_count): if file.startswith(model_name) and file.endswith(tuple(extensions)): model_file = os.path.join(folder_path, file) - modelID = get_models(model_file, True) - if not modelID or modelID == "Model not found": - HTML = '
Model ID not found.
maybe the model doesn\'t exist on CivitAI?
' - modelID_failed = True - if modelID == "offline": - HTML = offlineHTML - modelID_failed = True - if not modelID_failed: - gl.json_data = _api.api_to_data(content_type, "Newest", "AllTime", "Model name", None, None, None, tile_count, f"civitai.com/models/{modelID}") - else: gl.json_data = None + if use_local_html: + html_file = os.path.splitext(model_file)[0] + ".html" + if os.path.exists(html_file): + with open(html_file, 'r', encoding='utf-8') as html: + output_html = html.read() + index = output_html.find("") + if index != -1: + output_html = output_html[index + len(""):] + + fail_html = '
Model ID not found.
Maybe the model doesn\'t exist on CivitAI?
' - if gl.json_data == "timeout": - HTML = offlineHTML - if gl.json_data != None and gl.json_data != "timeout": - HTML = _api.model_list_html(gl.json_data) - (hasPrev, hasNext, current_page, total_pages) = _api.pagecontrol(gl.json_data) - page_string = f"Page: {current_page}/{total_pages}" - number = _download.random_number(click_first_item) - else: - number = click_first_item - hasPrev = False - hasNext = False - page_string = "Page: 0/0" - current_page = 0 - total_pages = 0 + if not output_html: + modelID = get_models(model_file, True) + if not modelID or modelID == "Model not found": + output_html = fail_html + modelID_failed = True + if modelID == "offline": + output_html = fail_html + modelID_failed = True + if not modelID_failed: + json_data = _api.api_to_data(content_type, "Newest", "AllTime", "Model name", None, None, None, tile_count, f"civitai.com/models/{modelID}") + else: json_data = None + if json_data == "timeout": + output_html = fail_html + if json_data != None and json_data != "timeout": + model_versions = _api.update_model_versions(modelID, json_data) + output_html = _api.update_model_info(None, model_versions.get('value'), True, modelID, json_data) + + css_path = Path(__file__).resolve().parents[1] / "style_html.css" + with open(css_path, 'r', encoding='utf-8') as css_file: + css = css_file.read() + replacements = { + '#0b0f19': 'var(--body-background-fill)', + '#F3F4F6': 'var(--body-text-color)', + 'white': 'var(--body-text-color)', + '#80a6c8': 'var(--secondary-300)', + '#60A5FA': 'var(--link-text-color-hover)', + '#1F2937': 'var(--input-background-fill)', + '#374151': 'var(--input-border-color)', + 'top: 50%;': '', + 'padding-top: 0px;': 'padding-top: 475px;', + '.civitai_txt2img': '.civitai_placeholder' + } + + for old, new in replacements.items(): + css = css.replace(old, new) + + style_tag = f'' + head_section = f'{style_tag}' + + output_html = output_html.replace('display:flex;align-items:flex-start;', 'display:flex;align-items:flex-start;flex-wrap:wrap;justify-content:center;') + output_html = str(head_section + output_html) + output_html = output_html.replace('zoom-radio', 'zoom-preview-radio') + output_html = output_html.replace('zoomRadio', 'zoomPreviewRadio') + output_html = output_html.replace('zoom-overlay', 'zoom-preview-overlay') + output_html = output_html.replace('resetZoom', 'resetPreviewZoom') + + number = _download.random_number() + return ( - gr.HTML.update(HTML), # Card HTML - gr.Button.update(interactive=hasPrev), # Prev Button - gr.Button.update(interactive=hasNext), # Next Button - gr.Slider.update(value=current_page, maximum=total_pages, label=page_string), # Page Slider - gr.Textbox.update(number) # Click first card trigger + gr.Textbox.update(value=output_html, placeholder=number), # Preview HTML ) def is_image_url(url): @@ -436,28 +446,48 @@ def clean_description(desc): cleaned_text = desc return cleaned_text -def save_model_info(install_path, file_name, sha256=None, overwrite_toggle=False, api_response=None): - file_path = os.path.join(install_path, file_name) - json_file = os.path.splitext(file_path)[0] + ".json" +def save_model_info(install_path, file_name, sha256=None, preview_html=None, overwrite_toggle=False, api_response=None): + filename = os.path.splitext(file_name)[0] + json_file = os.path.join(install_path, f'{filename}.json') + if not os.path.exists(install_path): + os.makedirs(install_path) + save_api_info = getattr(opts, "save_api_info", False) + if not sha256: if os.path.exists(json_file): try: - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: data = json.load(f) if 'sha256' in data and data['sha256']: sha256 = data['sha256'].upper() except Exception as e: print(f"Failed to open {json_file}: {e}") - if not api_response: - api_response = gl.json_data - + result = find_and_save(api_response, sha256, file_name, json_file, False, overwrite_toggle) - if result == "found": - return - else: - result = find_and_save(api_response, sha256, file_name, json_file, True, overwrite_toggle) + if result != "found": + result = find_and_save(api_response, sha256, file_name, json_file, True, overwrite_toggle) + + if preview_html: + match = re.search(r'(\s*)
', preview_html) + if match: + indentation = match.group(1) + else: + indentation = '' + css_link = f'' + utf8_meta_tag = f'{indentation}' + head_section = f'{indentation}{indentation} {utf8_meta_tag}{indentation} {css_link}{indentation}' + HTML = head_section + preview_html + path_to_new_file = os.path.join(install_path, f'{filename}.html') + with open(path_to_new_file, 'wb') as f: + f.write(HTML.encode('utf8')) + + if save_api_info: + path_to_new_file = os.path.join(install_path, f'{filename}.api_info.json') + with open(path_to_new_file, mode="w", encoding="utf-8") as f: + json.dump(gl.json_info, f, indent=4, ensure_ascii=False) + def find_and_save(api_response, sha256=None, file_name=None, json_file=None, no_hash=None, overwrite_toggle=None): for item in api_response.get('items', []): @@ -467,6 +497,7 @@ def find_and_save(api_response, sha256=None, file_name=None, json_file=None, no_ sha256_api = file.get('hashes', {}).get('SHA256', '') if file_name == file_name_api if no_hash else sha256 == sha256_api: + gl.json_info = item trained_words = model_version.get('trainedWords', []) model_id = model_version.get('modelId', '') @@ -498,7 +529,7 @@ def find_and_save(api_response, sha256=None, file_name=None, json_file=None, no_ trained_tags = trained_words if os.path.exists(json_file): - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: try: content = json.load(f) except: @@ -522,7 +553,7 @@ def find_and_save(api_response, sha256=None, file_name=None, json_file=None, no_ content["sd version"] = base_model changed = True - with open(json_file, 'w') as f: + with open(json_file, 'w', encoding="utf-8") as f: json.dump(content, f, indent=4) if changed: print(f"Model info saved to \"{json_file}\"") @@ -536,7 +567,7 @@ def get_models(file_path, gen_hash=None): json_file = os.path.splitext(file_path)[0] + ".json" if os.path.exists(json_file): try: - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: data = json.load(f) if 'modelId' in data: @@ -575,13 +606,13 @@ def get_models(file_path, gen_hash=None): if os.path.exists(json_file): try: - with open(json_file, 'r') as f: + with open(json_file, 'r', encoding="utf-8") as f: data = json.load(f) data['modelId'] = modelId data['sha256'] = sha256.upper() - with open(json_file, 'w') as f: + with open(json_file, 'w', encoding="utf-8") as f: json.dump(data, f, indent=4) except Exception as e: print(f"Failed to open {json_file}: {e}") @@ -590,7 +621,7 @@ def get_models(file_path, gen_hash=None): 'modelId': modelId, 'sha256': sha256.upper() } - with open(json_file, 'w') as f: + with open(json_file, 'w', encoding="utf-8") as f: json.dump(data, f, indent=4) return modelId @@ -611,7 +642,7 @@ def version_match(file_paths, api_response): for file_path in file_paths: json_path = f"{os.path.splitext(file_path)[0]}.json" if os.path.exists(json_path): - with open(json_path, 'r') as f: + with open(json_path, 'r', encoding="utf-8") as f: try: json_data = json.load(f) sha256 = json_data.get('sha256') @@ -624,7 +655,8 @@ def version_match(file_paths, api_response): for file_path in file_paths: file_name = os.path.basename(file_path) file_name_without_ext = os.path.splitext(file_name)[0] - file_sha256 = sha256_hashes.get(file_name, "").upper() + file_sha256 = sha256_hashes.get(file_name, "") + if file_sha256: file_sha256 = file_sha256.upper() file_names_and_hashes.add((file_name_without_ext, file_sha256)) for item in api_response.get('items', []): @@ -638,7 +670,8 @@ def version_match(file_paths, api_response): match_found = False for file_entry in files: entry_name = os.path.splitext(file_entry.get('name', ''))[0] - entry_sha256 = file_entry.get('hashes', {}).get('SHA256', "").upper() + entry_sha256 = file_entry.get('hashes', {}).get('SHA256', "") + if entry_sha256: entry_sha256 = entry_sha256.upper() if (entry_name, entry_sha256) in file_names_and_hashes: match_found = True @@ -741,6 +774,7 @@ def file_scan(folders, ver_finish, tag_finish, installed_finish, preview_finish, outdated_models = [] all_model_ids = [] file_paths = [] + all_ids = [] for file_path in files: if gl.cancel_status: @@ -763,6 +797,7 @@ def file_scan(folders, ver_finish, tag_finish, installed_finish, preview_finish, print(f"model: \"{file_name}\" not found on CivitAI servers.") elif model_id != None: all_model_ids.append(f"&ids={model_id}") + all_ids.append(model_id) file_paths.append(file_path) elif not model_id and update_log: print(f"model ID not found for: \"{file_name}\"") @@ -915,9 +950,11 @@ def file_scan(folders, ver_finish, tag_finish, installed_finish, preview_finish, ) elif from_tag: - for file in file_paths: - install_path, file_name = os.path.split(file) - save_model_info(install_path, file_name, api_response=api_response, overwrite_toggle=overwrite_toggle) + for file_path, id_value in zip(file_paths, all_ids): + install_path, file_name = os.path.split(file_path) + model_versions = _api.update_model_versions(id_value, api_response) + preview_html = _api.update_model_info(None, model_versions.get('value'), True, id_value, api_response) + save_model_info(install_path, file_name, preview_html=preview_html, api_response=api_response, overwrite_toggle=overwrite_toggle) if progress != None: progress(1, desc=f"All tags succesfully saved!") gl.scan_files = False diff --git a/scripts/civitai_gui.py b/scripts/civitai_gui.py index 7634edc..1d214a8 100644 --- a/scripts/civitai_gui.py +++ b/scripts/civitai_gui.py @@ -5,7 +5,6 @@ import json import fnmatch import re import subprocess -from pathlib import Path from modules.shared import opts, cmd_opts from modules.paths import extensions_dir from scripts.civitai_global import print @@ -76,30 +75,56 @@ def saveSettings(ust, ct, pt, st, bf, cj, td, ol, hi, sn, ss, ts): data.update(settings_map) # Save the modified content back to the file - with open(config, 'w') as file: + with open(config, 'w', encoding="utf-8") as file: json.dump(data, file, indent=4) print(f"Updated settings to: {config}") def all_visible(html_check): return gr.Button.update(visible="model-checkbox" in html_check) -def show_multi_buttons(input_list, version_value, model_id): - input_list = json.loads(input_list) - BtnDwn = version_value and not version_value.endswith('[Installed]') and not input_list +def show_multi_buttons(model_list, type_list, version_value): + model_list = json.loads(model_list) + type_list = json.loads(type_list) + otherButtons = True + multi_file_subfolder = False + default_subfolder = "Only available if the selected files are of the same model type" + sub_folders = ["None"] + BtnDwn = version_value and not version_value.endswith('[Installed]') and not model_list BtnDel = version_value.endswith('[Installed]') - - multi = bool(input_list) and not len(gl.download_queue) > 0 - BtnDwnInt = BtnDwn - if len(gl.download_queue) > 0: - for item in gl.download_queue: - if int(model_id) == int(item['model_id']): - BtnDwnInt = False - break + dot_subfolders = getattr(opts, "dot_subfolders", True) + + multi = bool(model_list) and not len(gl.download_queue) > 0 + if model_list: + otherButtons = False + if type_list and all(x == type_list[0] for x in type_list): + multi_file_subfolder = True + model_folder = os.path.join(_api.contenttype_folder(type_list[0])) + default_subfolder = "None" + try: + for root, dirs, _ in os.walk(model_folder): + if dot_subfolders: + dirs = [d for d in dirs if not d.startswith('.')] + dirs = [d for d in dirs if not any(part.startswith('.') for part in os.path.join(root, d).split(os.sep))] + for d in dirs: + sub_folder = os.path.relpath(os.path.join(root, d), model_folder) + if sub_folder: + sub_folders.append(f'{os.sep}{sub_folder}') + sub_folders.remove("None") + sub_folders = sorted(sub_folders, key=lambda x: (x.lower(), x)) + sub_folders.insert(0, "None") + + list = set() + sub_folders = [x for x in sub_folders if not (x in list or list.add(x))] + except: + sub_folders = ["None"] return (gr.Button.update(visible=multi, interactive=multi), # Download Multi Button - gr.Button.update(visible=BtnDwn if multi else True if not version_value.endswith('[Installed]') else False, interactive=BtnDwnInt), # Download Button - gr.Button.update(visible=BtnDel) # Delete Button + gr.Button.update(visible=BtnDwn if multi else True if not version_value.endswith('[Installed]') else False), # Download Button + gr.Button.update(visible=BtnDel if not model_list else False), # Delete Button + gr.Button.update(visible=otherButtons), # Save model info Button + gr.Button.update(visible=otherButtons), # Save images Button + gr.Dropdown.update(visible=multi, interactive=multi_file_subfolder, choices=sub_folders, value=default_subfolder) # Selected type sub folder ) def txt2img_output(image_url): @@ -202,7 +227,8 @@ def on_ui_tabs(): save_images = gr.Button(value="Save images", interactive=False) delete_model = gr.Button(value="Delete model", interactive=False, visible=False) download_model = gr.Button(value="Download model", interactive=False) - download_selected = gr.Button(value="Download all selected", interactive=False, visible=False) + subfolder_selected = gr.Dropdown(label="Sub folder for selected files:", choices=[], interactive=False, visible=False, value=None, allow_custom_value=True) + download_selected = gr.Button(value="Download all selected", interactive=False, visible=False, elem_id="download_all_button") with gr.Row(): cancel_all_model = gr.Button(value="Cancel all downloads", interactive=False, visible=False) cancel_model = gr.Button(value="Cancel current download", interactive=False, visible=False) @@ -241,16 +267,17 @@ def on_ui_tabs(): installed_progress = gr.HTML(value='
') #Invisible triggers/variables + model_id = gr.Textbox(visible=False) queue_trigger = gr.Textbox(visible=False) dl_url = gr.Textbox(visible=False) - text2imgOutput = gr.Textbox(visible=False) - txt2imgInput = gr.Textbox(elem_id="civitai_text2img_input", visible=False) - selected_list = gr.Textbox(elem_id="selected_list", visible=False) + civitai_text2img_output = gr.Textbox(visible=False) + civitai_text2img_input = gr.Textbox(elem_id="civitai_text2img_input", visible=False) + selected_model_list = gr.Textbox(elem_id="selected_model_list", visible=False) + selected_type_list = gr.Textbox(elem_id="selected_type_list", visible=False) model_select = gr.Textbox(elem_id="model_select", visible=False) model_sent = gr.Textbox(elem_id="model_sent", visible=False) type_sent = gr.Textbox(elem_id="type_sent", visible=False) - click_first_item = gr.Textbox(visible=False) download_start = gr.Textbox(visible=False) download_finish = gr.Textbox(visible=False) tag_start = gr.Textbox(visible=False) @@ -264,6 +291,7 @@ def on_ui_tabs(): delete_finish = gr.Textbox(visible=False) current_model = gr.Textbox(visible=False) current_sha256 = gr.Textbox(visible=False) + model_preview_html = gr.Textbox(visible=False) def ToggleDate(toggle_date): gl.sortNewest = toggle_date @@ -280,14 +308,12 @@ def on_ui_tabs(): list_html.change(fn=None, inputs=hide_installed, _js="(toggleValue) => hideInstalled(toggleValue)") hide_installed.input(fn=None, inputs=hide_installed, _js="(toggleValue) => hideInstalled(toggleValue)") - text2imgOutput.change(fn=None, inputs=text2imgOutput, _js="(genInfo) => genInfo_to_txt2img(genInfo)") + civitai_text2img_output.change(fn=None, inputs=civitai_text2img_output, _js="(genInfo) => genInfo_to_txt2img(genInfo)") download_selected.click(fn=None, _js="() => deselectAllModels()") select_all.click(fn=None, _js="() => selectAllModels()") - click_first_item.change(fn=None, _js="() => clickFirstFigureInColumn()") - list_models.select(fn=None, inputs=list_models, _js="(list_models) => select_model(list_models)") preview_html.change(fn=None, _js="() => adjustFilterBoxAndButtons()") @@ -306,6 +332,8 @@ def on_ui_tabs(): list_html.change(fn=None, inputs=size_slider, _js="(size) => updateCardSize(size, size * 1.5)") size_slider.change(fn=None, inputs=size_slider, _js="(size) => updateCardSize(size, size * 1.5)") + model_preview_html.change(fn=None, inputs=model_preview_html, _js="(html_input) => inputHTMLPreviewContent(html_input)") + # Filter button Functions # save_settings.click( @@ -333,17 +361,17 @@ def on_ui_tabs(): # Model Button Functions # - txt2imgInput.change(fn=txt2img_output,inputs=txt2imgInput,outputs=text2imgOutput) + civitai_text2img_input.change(fn=txt2img_output,inputs=civitai_text2img_input,outputs=civitai_text2img_output) list_html.change(fn=all_visible,inputs=list_html,outputs=select_all) def update_models_dropdown(input): model_string = re.sub(r'\.\d{3}$', '', input) model_name, model_id = _api.extract_model_info(model_string) - ret_versions = _api.update_model_versions(model_id) - (html, tags, base_mdl, DwnButton, SaveImages, DelButton, filelist, filename, dl_url, id, current_sha256, install_path, sub_folder) = _api.update_model_info(model_string, ret_versions['value']) + model_versions = _api.update_model_versions(model_id) + (html, tags, base_mdl, DwnButton, SaveImages, DelButton, filelist, filename, dl_url, id, current_sha256, install_path, sub_folder) = _api.update_model_info(model_string, model_versions.get('value')) return (gr.Dropdown.update(value=model_string, interactive=True), - ret_versions,html,tags,base_mdl,filename,install_path,sub_folder,DwnButton,SaveImages,DelButton,filelist,dl_url,id,current_sha256, + model_versions,html,tags,base_mdl,filename,install_path,sub_folder,DwnButton,SaveImages,DelButton,filelist,dl_url,id,current_sha256, gr.Button.update(interactive=True)) model_select.change( @@ -371,8 +399,8 @@ def on_ui_tabs(): model_sent.change( fn=_file.model_from_sent, - inputs=[model_sent, type_sent, click_first_item, tile_count_slider], - outputs=[list_html, get_prev_page , get_next_page, page_slider, click_first_item] + inputs=[model_sent, type_sent, tile_count_slider], + outputs=[model_preview_html] ) sub_folder.select( @@ -425,13 +453,16 @@ def on_ui_tabs(): # Download/Save Model Button Functions # - selected_list.change( + selected_model_list.change( fn=show_multi_buttons, - inputs=[selected_list, list_versions, model_id], + inputs=[selected_model_list, selected_type_list, list_versions], outputs=[ download_selected, download_model, - delete_model + delete_model, + save_info, + save_images, + subfolder_selected ] ) @@ -459,7 +490,7 @@ def on_ui_tabs(): download_selected.click( fn=_download.selected_to_queue, - inputs=[selected_list, download_start, create_json], + inputs=[selected_model_list, subfolder_selected, download_start, create_json], outputs=[ download_model, cancel_model, @@ -510,7 +541,7 @@ def on_ui_tabs(): list_models, list_versions, current_sha256, - selected_list + selected_model_list ], outputs=[ download_model, @@ -527,7 +558,8 @@ def on_ui_tabs(): inputs=[ install_path, model_filename, - current_sha256 + current_sha256, + preview_html ], outputs=[] ) @@ -537,7 +569,6 @@ def on_ui_tabs(): inputs=[ preview_html, model_filename, - list_models, install_path, sub_folder ], @@ -807,9 +838,10 @@ def subfolder_list(folder, desc=None): sub_folders = sorted(sub_folders, key=lambda x: (x.lower(), x)) sub_folders.insert(0, "None") if insert_sub: + sub_folders.insert(1, f"{os.sep}Base Model") sub_folders.insert(2, f"{os.sep}Author Name") - sub_folders.insert(2, f"{os.sep}Model Name") - sub_folders.insert(3, f"{os.sep}Model Name{os.sep}Version Name") + sub_folders.insert(3, f"{os.sep}Model Name") + sub_folders.insert(4, f"{os.sep}Model Name{os.sep}Version Name") list = set() sub_folders = [x for x in sub_folders if not (x in list or list.add(x))] @@ -899,6 +931,26 @@ def on_ui_settings(): ) ) + shared.opts.add_option( + "save_api_info", + shared.OptionInfo( + False, + "Save API info of model when saving model info", + section=download, + **({'category_id': cat_id} if ver_bool else {}) + ).info("creates an api_info.json file when saving any model info with all the API data of the model") + ) + + shared.opts.add_option( + "auto_save_all_img", + shared.OptionInfo( + False, + "Automatically save all images", + section=download, + **({'category_id': cat_id} if ver_bool else {}) + ).info("Automatically saves all the images of a model after downloading") + ) + # Browser Options shared.opts.add_option( "custom_api_key", @@ -934,7 +986,7 @@ def on_ui_settings(): "insert_sub", shared.OptionInfo( True, - f"Insert [{os.sep}Author Name] & [{os.sep}Model Name] & [{os.sep}Model Name{os.sep}Version Name] as sub folder options", + f"Insert [{os.sep}Base model] &[{os.sep}Author Name] & [{os.sep}Model Name] & [{os.sep}Model Name{os.sep}Version Name] as sub folder options", section=browser, **({'category_id': cat_id} if ver_bool else {}) ) @@ -949,6 +1001,16 @@ def on_ui_settings(): **({'category_id': cat_id} if ver_bool else {}) ) ) + + shared.opts.add_option( + "use_local_html", + shared.OptionInfo( + False, + "Use local HTML file for model info", + section=browser, + **({'category_id': cat_id} if ver_bool else {}) + ).info("Uses the matching local HTML file when pressing CivitAI button on model cards in txt2img and img2img") + ) shared.opts.add_option( "page_header", diff --git a/style.css b/style.css index 061a59d..879ec73 100644 --- a/style.css +++ b/style.css @@ -143,6 +143,13 @@ min-height: 650px; } +#download_all_button { + max-height: 40px; + height: 40px; + align-self: end; + margin-bottom: 1px; +} + #searchBox > label > textarea { padding-top: 11px !important; } diff --git a/style_html.css b/style_html.css index 9d13f9e..a01debf 100644 --- a/style_html.css +++ b/style_html.css @@ -51,7 +51,7 @@ p { dt { font-size: medium; - color: #80a6c8!important; + color: #80a6c8 !important; font-size: 16px; } @@ -78,7 +78,7 @@ a:hover { /* Preview Image zoom */ .zoom-radio { - display: none!important; + display: none !important; } /* Style for when the image is clicked (radio button checked) */ @@ -186,6 +186,19 @@ a:hover { font-size: large; } +.civitai-tags-container { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.civitai-tag { + background-color: #1F2937; + border-radius: 8px; + padding: 4px 6px; + border: 1px solid #374151; +} + /* Icon */ .tab-label::before { content: "❯";