1207 lines
55 KiB
Python
1207 lines
55 KiB
Python
import json
|
||
import inspect
|
||
import os
|
||
import os.path
|
||
import re
|
||
import urllib
|
||
import requests
|
||
import gradio as gr # type: ignore
|
||
from modules import script_callbacks, sd_models, shared, images, scripts # type: ignore
|
||
import modules.textual_inversion.textual_inversion # type: ignore
|
||
current_extension_directory = scripts.basedir()
|
||
from PIL import Image
|
||
import base64
|
||
import csv
|
||
from io import BytesIO
|
||
from lxml_html_clean.clean import Cleaner
|
||
|
||
import importlib.util
|
||
|
||
def import_lora_module():
|
||
# import/update the lora module if its available
|
||
try:
|
||
spec = importlib.util.find_spec('extensions.sd-webui-additional-networks.scripts.model_util')
|
||
if spec:
|
||
additional_networks = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(additional_networks)
|
||
else:
|
||
additional_networks = None
|
||
except:
|
||
additional_networks = None
|
||
return additional_networks
|
||
|
||
def import_lora_module_builtin():
|
||
# import/update the lora module if its available from the builtin extensions
|
||
|
||
possible_lora_modules = [
|
||
'extensions-builtin.Lora.lora',
|
||
'extensions-builtin.sd_forge_lora.lora'
|
||
]
|
||
for module in possible_lora_modules:
|
||
try:
|
||
spec = importlib.util.find_spec(module)
|
||
if spec:
|
||
additional_networks_builtin = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(additional_networks_builtin)
|
||
return additional_networks_builtin
|
||
except:
|
||
pass
|
||
return None
|
||
|
||
|
||
def import_lycoris_module():
|
||
# import/update the lycoris module if it's available
|
||
|
||
possible_lycoris_modules = [
|
||
'extensions-builtin.a1111-sd-webui-lycoris.lycoris',
|
||
'extensions.a1111-sd-webui-lycoris.lycoris'
|
||
]
|
||
for module in possible_lycoris_modules:
|
||
try:
|
||
spec = importlib.util.find_spec(module)
|
||
if spec:
|
||
loaded_lycoris_module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(loaded_lycoris_module)
|
||
return loaded_lycoris_module
|
||
except:
|
||
pass
|
||
|
||
return None
|
||
|
||
# define a global Cleaner instance with specific options for sanitization
|
||
cleaner = Cleaner(
|
||
safe_attrs_only=True, # Only allow safe attributes
|
||
host_whitelist=set(['www.youtube.com'])
|
||
)
|
||
|
||
def sanitize_html(html_content):
|
||
# check if the HTML content is empty, if so we dont have to do anything, return an empty string
|
||
if html_content is None or not html_content.strip():
|
||
return ""
|
||
|
||
# check if the entire HTML string is surrounded in comment tags (This is done by some civitai extensions)
|
||
if html_content.strip().startswith('<!--') and html_content.strip().endswith('-->'):
|
||
# Remove the comment tags
|
||
html_content = html_content.strip()[4:-3].strip()
|
||
|
||
try:
|
||
# clean the HTML content using the global Cleaner instance
|
||
cleaned_html = cleaner.clean_html(html_content)
|
||
except Exception as e:
|
||
# if there is an error cleaning the HTML, return "Unable to parse HTML"
|
||
return "Unable to parse HTML"
|
||
|
||
# return the cleaned HTML
|
||
return cleaned_html
|
||
|
||
embedding_db = None
|
||
|
||
# try and get the lora module
|
||
additional_networks = import_lora_module()
|
||
additional_networks_builtin = import_lora_module_builtin()
|
||
|
||
# try and get the lycoris module
|
||
lycoris_module = import_lycoris_module()
|
||
|
||
refresh_symbol = '🔄'
|
||
update_symbol = '↙️'
|
||
|
||
html_ext_pattern = r'html'
|
||
civitai_ext_pattern = r'civitai.info'
|
||
md_ext_pattern = r'md'
|
||
txt_ext_pattern = r'txt'
|
||
tags_ext_pattern = r'tags'
|
||
prompts_ext_pattern = r'(?:prompt|prompts)'
|
||
img_ext_pattern = r'(?:png|jpg|jpeg|webp|jxk|avif)'
|
||
all_ext_pattern = r'(?:' + html_ext_pattern\
|
||
+ r'|' + civitai_ext_pattern\
|
||
+ r'|' + md_ext_pattern\
|
||
+ r'|' + txt_ext_pattern\
|
||
+ r'|' + tags_ext_pattern\
|
||
+ r'|' + prompts_ext_pattern\
|
||
+ r'|' + img_ext_pattern\
|
||
+ r')'
|
||
|
||
def is_in_directory(parent_dir, child_path):
|
||
# get the directory of the child path
|
||
child_dir = os.path.dirname(child_path)
|
||
|
||
# get the absolute paths of both directories
|
||
parent_dir = os.path.abspath(os.path.realpath(parent_dir))
|
||
child_dir = os.path.abspath(os.path.realpath(child_dir))
|
||
|
||
# return false if either directory is not a valid directory
|
||
if not os.path.isdir(parent_dir) or not os.path.isdir(child_dir):
|
||
return False
|
||
|
||
# get the common prefix of the paths to see if the child dir is in the parent
|
||
common_prefix = os.path.commonprefix([parent_dir, child_dir])
|
||
return common_prefix == parent_dir and child_dir != parent_dir
|
||
|
||
def is_dir_in_list(dir_list, check_dir):
|
||
# Convert all directories in the list to absolute paths
|
||
dir_list = [os.path.abspath(d) for d in dir_list]
|
||
# Convert the specified directory to an absolute path
|
||
check_dir = os.path.abspath(check_dir)
|
||
# Check if the specified directory is in the list of directories
|
||
for dir_path in dir_list:
|
||
if os.path.samefile(check_dir, dir_path):
|
||
return True
|
||
return False
|
||
|
||
def natural_order_number(s):
|
||
# split a string into segments of strings and ints that will be used to sort naturally
|
||
return [int(x) if x.isdigit() else x.lower() for x in re.split('(\d+)', s)]
|
||
|
||
def clean_modelname(modelname):
|
||
# remove the extension and the hash if it exists at the end of the model name (this is added by a1111) and
|
||
# if the model name contains a path (which happens when a checkpoint is in a subdirectory) just return the model name portion
|
||
return re.sub(r"(?i)(\.pt|\.bin|\.ckpt|\.safetensors)?( \[[a-f0-9]{10,12}\]|\([a-f0-9]{10,12}\))?$", "", modelname).split("\\")[-1].split("/")[-1]
|
||
|
||
# keep a copy of the choices to give control to user when to refresh
|
||
checkpoint_choices = []
|
||
embedding_choices = []
|
||
hypernetwork_choices = []
|
||
lora_choices = []
|
||
lycoris_choices = []
|
||
tags = {
|
||
"checkpoints": {},
|
||
"embeddings": {},
|
||
"hypernetworks": {},
|
||
"loras": {},
|
||
"lycoris": {}
|
||
}
|
||
|
||
def search_for_tags(model_names, model_tags, paths):
|
||
model_tags.clear()
|
||
general_tag_pattern = re.compile(r'^.*(?i:\.tags)$')
|
||
|
||
# support the ability to check multiple paths
|
||
for path in paths:
|
||
# loop through all files in the path and any subdirectories
|
||
for dirpath, dirnames, filenames in os.walk(path, followlinks=True):
|
||
# get a list of all parent directories
|
||
directories = dirpath.split(os.path.sep)
|
||
|
||
index_models = []
|
||
if shared.opts.model_preview_xd_name_matching == "Index":
|
||
index_txt_filename = next((filename for filename in filenames if filename.lower() == "index.txt"), None)
|
||
if index_txt_filename is not None:
|
||
index_txt_path = os.path.join(dirpath, index_txt_filename)
|
||
output_text = ""
|
||
with open(index_txt_path, "r", encoding="utf8") as file:
|
||
output_text = file.read()
|
||
index_models = [model.strip() for model in output_text.replace(",", "\n").splitlines()]
|
||
|
||
# check each file to see if it is a preview file
|
||
for filename in filenames:
|
||
file_path = os.path.join(dirpath, filename)
|
||
if general_tag_pattern.match(filename):
|
||
for model_name in model_names:
|
||
clean_model_name = clean_modelname(model_name)
|
||
|
||
# if we are not using folder match mode look for files normally otherwise we are using folder match mode so make sure at least one parent directory is equal to the name of the model
|
||
if shared.opts.model_preview_xd_name_matching == "Folder" and clean_model_name not in directories:
|
||
continue
|
||
|
||
index_has_model = False
|
||
index_models_pattern = None
|
||
if shared.opts.model_preview_xd_name_matching == "Index":
|
||
index_has_model = clean_model_name in index_models
|
||
filtered_index_models = [re.escape(model) for model in index_models if model != clean_model_name]
|
||
if len(filtered_index_models) > 0:
|
||
index_models_pattern = re.compile(r'^(?:' + r'|'.join(filtered_index_models) + r')(?i:\.' + tags_ext_pattern + r')$')
|
||
if index_models_pattern.match(filename):
|
||
continue
|
||
|
||
if shared.opts.model_preview_xd_name_matching == "Strict" or (not index_has_model and shared.opts.model_preview_xd_name_matching == "Index"):
|
||
tag_pattern = re.compile(r'^' + re.escape(clean_model_name) + r'(?i:\.' + tags_ext_pattern + r')$')
|
||
elif shared.opts.model_preview_xd_name_matching == "Folder" or (index_has_model and shared.opts.model_preview_xd_name_matching == "Index"):
|
||
tag_pattern = re.compile(r'^.*(?i:\.' + tags_ext_pattern + r')$')
|
||
else:
|
||
tag_pattern = re.compile(r'^.*' + re.escape(clean_model_name) + r'.*(?i:\.' + tags_ext_pattern + r')$')
|
||
|
||
if tag_pattern.match(filename):
|
||
output_text = ""
|
||
with open(file_path, "r", encoding="utf8") as file:
|
||
output_text = file.read()
|
||
if output_text.strip() != "":
|
||
if model_name in model_tags:
|
||
model_tags[model_name] += f", {output_text}"
|
||
else:
|
||
model_tags[model_name] = output_text
|
||
|
||
def list_all_models():
|
||
global checkpoint_choices
|
||
# gets the list of checkpoints
|
||
model_list = sd_models.checkpoint_tiles()
|
||
checkpoint_choices = sorted(model_list, key=natural_order_number)
|
||
search_for_tags(checkpoint_choices, tags["checkpoints"], get_checkpoints_dirs())
|
||
return checkpoint_choices
|
||
|
||
def list_all_embeddings():
|
||
global embedding_choices, embedding_db
|
||
# Embeddings may not have been loaded yet. (Fixes empty embeddings list on startup) -n15g
|
||
if embedding_db is None:
|
||
embedding_db = modules.textual_inversion.textual_inversion.EmbeddingDatabase()
|
||
embedding_db.add_embedding_dir(shared.cmd_opts.embeddings_dir)
|
||
if "sync_with_sd_model" in inspect.signature(embedding_db.load_textual_inversion_embeddings).parameters:
|
||
embedding_db.load_textual_inversion_embeddings(sync_with_sd_model=False)
|
||
else:
|
||
embedding_db.load_textual_inversion_embeddings()
|
||
# get the list of embeddings
|
||
list = [x for x in embedding_db.word_embeddings.keys()]
|
||
list.extend([x for x in embedding_db.skipped_embeddings.keys()])
|
||
embedding_choices = sorted(list, key=natural_order_number)
|
||
search_for_tags(embedding_choices, tags["embeddings"], get_embedding_dirs())
|
||
return embedding_choices
|
||
|
||
def list_all_hypernetworks():
|
||
global hypernetwork_choices
|
||
# get the list of hyperlinks
|
||
list = [x for x in shared.hypernetworks.keys()]
|
||
hypernetwork_choices = sorted(list, key=natural_order_number)
|
||
search_for_tags(hypernetwork_choices, tags["hypernetworks"], get_hypernetwork_dirs())
|
||
return hypernetwork_choices
|
||
|
||
def list_all_loras():
|
||
global lora_choices, additional_networks, additional_networks_builtin
|
||
# create an empty set for lora models
|
||
loras = set()
|
||
|
||
# import/update the lora module
|
||
additional_networks = import_lora_module()
|
||
if additional_networks is not None:
|
||
# copy the list of models
|
||
loras_list = additional_networks.lora_models.copy()
|
||
# remove the None item from the list
|
||
loras_list.pop("None", None)
|
||
# remove hash from model
|
||
loras_list = [re.sub(r'\([a-fA-F0-9]{10,12}\)$', '', model) for model in loras_list.keys()]
|
||
loras.update(loras_list)
|
||
|
||
# import/update the builtin lora module
|
||
additional_networks_builtin = import_lora_module_builtin()
|
||
if additional_networks_builtin is not None:
|
||
# copy the list of models
|
||
loras_list = additional_networks_builtin.available_loras.copy()
|
||
# remove the None item from the list
|
||
loras_list.pop("None", None)
|
||
loras.update(loras_list.keys())
|
||
|
||
# return the list
|
||
lora_choices = sorted(loras, key=natural_order_number)
|
||
search_for_tags(lora_choices, tags["loras"], get_lora_dirs())
|
||
return lora_choices
|
||
|
||
def list_all_lycorii():
|
||
global lycoris_choices, lycoris_module
|
||
# create an empty set for lycoris models
|
||
lycorii = set()
|
||
|
||
# import/update the lycoris module
|
||
lycoris_module = import_lycoris_module()
|
||
if lycoris_module is not None:
|
||
# copy the list of models
|
||
lycorii_list = lycoris_module.available_lycos.copy()
|
||
# remove the None item from the list
|
||
lycorii_list.pop("None", None)
|
||
# remove hash from model
|
||
lycorii_list = [re.sub(r'\([a-fA-F0-9]{10,12}\)$', '', lyco) for lyco in lycorii_list.keys()]
|
||
lycorii.update(lycorii_list)
|
||
|
||
# return the list
|
||
lycoris_choices = sorted(lycorii, key=natural_order_number)
|
||
search_for_tags(lycoris_choices, tags["lycoris"], get_lycoris_dirs())
|
||
return lycoris_choices
|
||
|
||
def refresh_models(choice = None, filter = None):
|
||
global checkpoint_choices
|
||
# update the choices for the checkpoint list
|
||
checkpoint_choices = list_all_models()
|
||
return filter_models(filter), *show_model_preview(choice)
|
||
|
||
def refresh_embeddings(choice = None, filter = None):
|
||
global embedding_choices
|
||
# update the choices for the embeddings list
|
||
embedding_choices = list_all_embeddings()
|
||
return filter_embeddings(filter), *show_embedding_preview(choice)
|
||
|
||
def refresh_hypernetworks(choice = None, filter = None):
|
||
global hypernetwork_choices
|
||
# update the choices for the hypernetworks list
|
||
hypernetwork_choices = list_all_hypernetworks()
|
||
return filter_hypernetworks(filter), *show_hypernetwork_preview(choice)
|
||
|
||
def refresh_loras(choice = None, filter = None):
|
||
global lora_choices
|
||
# update the choices for the lora list
|
||
lora_choices = list_all_loras()
|
||
return filter_loras(filter), *show_lora_preview(choice)
|
||
|
||
def refresh_lycorii(choice = None, filter = None):
|
||
global lycoris_choices
|
||
# update the choices for the lycoris list
|
||
lycoris_choices = list_all_lycorii()
|
||
return filter_lycorii(filter), *show_lycoris_preview(choice)
|
||
|
||
def filter_choices(choices, filter, tags_obj):
|
||
filtered_choices = choices
|
||
if filter is not None and filter.strip() != "":
|
||
# filter the choices based on the provided filter string
|
||
filter_tags = [tag.strip().lower() for tag in filter.split(",")]
|
||
filtered_choices = [choice for choice in filtered_choices if
|
||
all(tag in tags_obj.get(choice, '').lower() for tag in filter_tags) or
|
||
all(tag in choice.lower() for tag in filter_tags)]
|
||
return filtered_choices
|
||
|
||
def filter_models(filter=None):
|
||
filtered_checkpoint_choices = filter_choices(checkpoint_choices, filter, tags["checkpoints"])
|
||
return gr.Dropdown.update(choices=filtered_checkpoint_choices)
|
||
|
||
def filter_embeddings(filter=None):
|
||
filtered_embedding_choices = filter_choices(embedding_choices, filter, tags["embeddings"])
|
||
return gr.Dropdown.update(choices=filtered_embedding_choices)
|
||
|
||
def filter_hypernetworks(filter=None):
|
||
filtered_hypernetwork_choices = filter_choices(hypernetwork_choices, filter, tags["hypernetworks"])
|
||
return gr.Dropdown.update(choices=filtered_hypernetwork_choices)
|
||
|
||
def filter_loras(filter=None):
|
||
filtered_lora_choices = filter_choices(lora_choices, filter, tags["loras"])
|
||
return gr.Dropdown.update(choices=filtered_lora_choices)
|
||
|
||
def filter_lycorii(filter=None):
|
||
filtered_lycoris_choices = filter_choices(lycoris_choices, filter, tags["lycoris"])
|
||
return gr.Dropdown.update(choices=filtered_lycoris_choices)
|
||
|
||
def update_checkpoint(name):
|
||
# update the selected preview for checkpoint tab
|
||
new_choice = find_choice(checkpoint_choices, name)
|
||
return new_choice, *show_model_preview(new_choice)
|
||
|
||
def update_embedding(name):
|
||
# update the selected preview for embedding tab
|
||
new_choice = find_choice(embedding_choices, name)
|
||
return new_choice, *show_embedding_preview(new_choice)
|
||
|
||
def update_hypernetwork(name):
|
||
# update the selected preview for hypernetwork tab
|
||
new_choice = find_choice(hypernetwork_choices, name)
|
||
return new_choice, *show_hypernetwork_preview(new_choice)
|
||
|
||
def update_lora(name):
|
||
# update the selected preview for lora tab
|
||
new_choice = find_choice(lora_choices, name)
|
||
return new_choice, *show_lora_preview(new_choice)
|
||
|
||
def update_lycorii(name):
|
||
# update the selected preview for LyCORIS tab
|
||
new_choice = find_choice(lycoris_choices, name)
|
||
return new_choice, *show_lycoris_preview(new_choice)
|
||
|
||
def find_choice(list, name):
|
||
# clean the name from the list and match a choice to the model
|
||
# TODO there could be name collisions here that may need to be handled in the future.
|
||
for choice in list:
|
||
cleaned_name = clean_modelname(choice)
|
||
if cleaned_name == name:
|
||
return choice
|
||
return name
|
||
|
||
def create_html_iframe(file, is_in_a1111_dir):
|
||
if is_in_a1111_dir:
|
||
# escape special URL characters from the filename
|
||
encoded_file_path = urllib.parse.quote(file, safe='/:\\')
|
||
# create the iframe html code
|
||
html_code = f'<iframe class="sdmpxd-iframe" src="file={encoded_file_path}"></iframe>'
|
||
else:
|
||
html_code = ""
|
||
# the html file isnt located in the a1111 directory so load the html file as a base64 string instead of linking to it
|
||
with open(file, 'rb') as html_file:
|
||
html_data = base64.b64encode(html_file.read()).decode()
|
||
html_code = f'<iframe class="sdmpxd-iframe" src="data:text/html;charset=UTF-8;base64,{html_data}"></iframe>'
|
||
return html_code
|
||
|
||
def extract_civitai_image_key(url):
|
||
pattern = r"https?://(?:image(?:cache)?\.civitai\.com)/xG1nkqKTMzGDvpLrqFT7WA/([a-f0-9-]+)"
|
||
match = re.match(pattern, url)
|
||
if match:
|
||
return match.group(1)
|
||
return None
|
||
|
||
def convert_image_to_base64(url):
|
||
# Only cache the image if setting is on
|
||
if not shared.opts.model_preview_xd_cache_images_civitai_info:
|
||
return url
|
||
|
||
# Get the image key from the URL
|
||
image_key = extract_civitai_image_key(url)
|
||
|
||
if image_key is None:
|
||
# Can't find the image key, the given url isn't in an expected form, just return the input URL
|
||
return url
|
||
|
||
# Construct the cache directory path
|
||
cache_directory = os.path.join(current_extension_directory, 'civit_cache')
|
||
|
||
# Check if the cache directory exists, if not, create it
|
||
if not os.path.exists(cache_directory):
|
||
os.makedirs(cache_directory)
|
||
|
||
# Construct the image path within the cache directory
|
||
image_path = os.path.join(cache_directory, image_key)
|
||
|
||
# Check if the image file already exists in the cache directory
|
||
if os.path.isfile(image_path):
|
||
# If it exists, read the base64 data from the file
|
||
with open(image_path, "r") as f:
|
||
base64_data_uri = f.read()
|
||
f.close()
|
||
# Return the cached uri
|
||
return base64_data_uri
|
||
else:
|
||
# If the image file doesn't exist in the cache directory, download it
|
||
response = requests.get(url)
|
||
|
||
# Check if the request was successful
|
||
if response.status_code == 200:
|
||
image_data = response.content
|
||
else:
|
||
# If not successful, return the input URL
|
||
return url
|
||
|
||
try:
|
||
# Attempt to open the image using PIL
|
||
image = Image.open(BytesIO(image_data))
|
||
# Encode the image data to base64
|
||
base64_image = base64.b64encode(image_data).decode('utf-8')
|
||
except Image.UnidentifiedImageError:
|
||
# If the image format is not recognized, return the input URL
|
||
return url
|
||
|
||
# Determine the image format
|
||
if image.format:
|
||
image_format = image.format
|
||
else:
|
||
image_format = "PNG"
|
||
|
||
# Construct the base64 data URI
|
||
base64_data_uri = f"data:image/{image_format};base64,{base64_image}"
|
||
|
||
# Write the base64 data to a file in the cache directory
|
||
with open(image_path, "w") as f:
|
||
f.write(base64_data_uri)
|
||
f.close
|
||
|
||
print(f"SD Model Preview caching image {image_path}")
|
||
|
||
return base64_data_uri
|
||
|
||
def create_civitai_info_html(file):
|
||
# initialize the info object
|
||
data = {}
|
||
|
||
# read the civitai.info file
|
||
if os.path.isfile(file):
|
||
with open(file, 'r') as f:
|
||
data = json.load(f)
|
||
f.close()
|
||
|
||
# Sanitize the HTML content of the description properties
|
||
data['description'] = sanitize_html(data.get('description', ''))
|
||
if 'model' in data:
|
||
data['model']['description'] = sanitize_html(data['model'].get('description', ''))
|
||
|
||
# build the html
|
||
civitai_info_html = [f"""<div class='civitai-info'>
|
||
<h1 id="ci-name">{data.get('name','')}</h1>
|
||
<ul>
|
||
<li><strong>ID:</strong> <span id="ci-id">{data.get('id','')}</span></li>
|
||
<li><strong>Model ID:</strong> <a id="ci-modelId" href="https://civitai.com/models/{data.get('modelId','')}" target="_blank">{data.get('modelId','')}</a></li>
|
||
<li><strong>Created At:</strong> <span id="ci-createdAt">{data.get('createdAt','')}</span></li>
|
||
<li><strong>Updated At:</strong> <span id="ci-updatedAt">{data.get('updatedAt','')}</span></li>
|
||
<li><strong>Base Model:</strong> <span id="ci-baseModel">{data.get('baseModel','')}</span></li>
|
||
<li><strong>Trained Words:</strong> <span id="ci-trainedWords">{"None Specified" if (not data.get('trainedWords',None) or len(data.get('trainedWords',[])) == 0) else "<ul><li>" + "</li><li>".join(data.get('trainedWords',[])) + "</li></ul>"}</span></li>
|
||
<li><strong>Early Access Time Frame:</strong> <span id="ci-earlyAccessTimeFrame">{data.get('earlyAccessTimeFrame','')}</span></li>
|
||
</ul>
|
||
<details open>
|
||
<summary><strong>Description:</strong></summary>
|
||
<div id="ci-description" class="description">{data.get('description','')}</div>
|
||
</details>
|
||
<details>
|
||
<summary><strong>Stats:</strong></summary>
|
||
<ul>
|
||
<li><strong>Download Count:</strong> <span id="ci-downloadCount">{data.get('stats',{}).get('downloadCount','')}</span></li>
|
||
<li><strong>Rating Count:</strong> <span id="ci-ratingCount"{data.get('stats',{}).get('ratingCount','')}></span></li>
|
||
<li><strong>Rating:</strong> <span id="ci-rating">{data.get('stats',{}).get('rating','')}</span></li>
|
||
</ul>
|
||
</details>
|
||
<details>
|
||
<summary><strong>Model Information:</strong></summary>
|
||
<ul>
|
||
<li><strong>Name:</strong> <span id="ci-modelName">{data.get('model',{}).get('name','')}</span></li>
|
||
<li><strong>Type:</strong> <span id="ci-modelType">{data.get('model',{}).get('type','')}</span></li>
|
||
<li><strong>NSFW:</strong> <span id="ci-modelNsfw">{data.get('model',{}).get('nsfw','')}</span></li>
|
||
<li><strong>POI:</strong> <span id="ci-modelPoi">{data.get('model',{}).get('poi','')}</span></li>
|
||
<li><strong>Description:</strong> <div id="ci-modelDescription" class="description">{data.get('model',{}).get('description','')}</div></li>
|
||
</ul>
|
||
</details>
|
||
<details>
|
||
<summary><strong>Files:</strong></summary>
|
||
"""]
|
||
|
||
for i, data_file in enumerate(data.get('files',[])):
|
||
civitai_info_html.append(f"""<details>
|
||
<summary><strong id="ci-fileName-{i}">{data_file.get('name','')}</strong></summary>
|
||
<ul>
|
||
<li><strong>ID:</strong> <span id="ci-fileId-{i}">{data_file.get('id','')}</span></li>
|
||
<li><strong>Size (KB):</strong> <span id="ci-fileSizeKB-{i}">{data_file.get('sizeKB','')}</span></li>
|
||
<li><strong>Type:</strong> <span id="ci-fileType-{i}">{data_file.get('type','')}</span></li>
|
||
<li><strong>Format:</strong> <span id="ci-fileFormat-{i}">{data_file.get('metadata',{}).get('format','')}</span></li>
|
||
<li><strong>Fp:</strong> <span id="ci-fileFp-{i}">{data_file.get('metadata',{}).get('fp','')}</span></li>
|
||
<li><strong>Size:</strong> <span id="ci-fileSize-{i}">{data_file.get('metadata',{}).get('size','')}</span></li>
|
||
<li><strong>Pickle Scan Result:</strong> <span id="ci-pickleScanResult-{i}">{data_file.get('pickleScanResult','')}</span></li>
|
||
<li><strong>Pickle Scan Message:</strong> <span id="ci-pickleScanMessage-{i}">{data_file.get('pickleScanMessage','')}</span></li>
|
||
<li><strong>Virus Scan Result:</strong> <span id="ci-virusScanResult-{i}">{data_file.get('virusScanResult','')}</span></li>
|
||
<li><strong>Scanned At:</strong> <span id="ci-scannedAt-{i}">{data_file.get('scannedAt','')}</span></li>
|
||
<li><strong>Download URL:</strong> <a id="ci-downloadUrl-{i}" href="{data_file.get('downloadUrl','')}" target="_blank">{data_file.get('downloadUrl','')}</a></li>
|
||
</ul>
|
||
</details>
|
||
""")
|
||
|
||
civitai_info_html.append("""</details>
|
||
<br>
|
||
<div id="ci-images" class="img-container-set">
|
||
""")
|
||
|
||
for i, image in enumerate(data.get('images',[])):
|
||
|
||
# Get the meta data object from the image
|
||
meta_data = image.get('meta', None)
|
||
|
||
# Initialize html meta list as not found incase its empty
|
||
meta_list_items = "<li>No Meta Data Found</li>"
|
||
|
||
image_url = convert_image_to_base64(image.get('url',''))
|
||
|
||
civitai_info_html.append(f"""<div class='img-prop-container'><div class='img-container'>
|
||
<img id="ci-image-{i}" src="{image_url}" onclick="imageZoomIn(event)" />
|
||
""")
|
||
|
||
# if there is prompt/meta data
|
||
if meta_data:
|
||
# Create the HTML list of all the meta data keys
|
||
meta_list_items = "\n".join([f"<li><strong>{key}:</strong> {meta_data.get(key,'')}</li>" for key in meta_data])
|
||
|
||
# Build the meta data string that will be copied when you press the copy button
|
||
meta_tags = list(meta_data.keys())
|
||
meta_out = []
|
||
if "prompt" in meta_tags:
|
||
meta_out.append(f"{image['meta']['prompt']}\n")
|
||
if "negativePrompt" in meta_tags:
|
||
meta_out.append(f"Negative prompt: {image['meta']['negativePrompt']}\n")
|
||
for i, tag in enumerate(meta_tags):
|
||
if tag == "cfgScale":
|
||
# Add the cfgScale meta data to the output string
|
||
meta_out.append(f"CFG scale: {image['meta']['cfgScale']}, ")
|
||
elif tag != "prompt" and tag != "negativePrompt" and tag != "resources" and tag != "hashes":
|
||
# Add the other meta data to the output string, convert the tag to Proper case
|
||
meta_out.append(re.sub(r'\b\w', lambda x: x.group(0).upper(), tag, count=1) + ": " + str(image['meta'][tag]) + ", ")
|
||
# Remove any trailing commas or whitespace
|
||
meta_out_string = "".join(meta_out).rstrip(", ")
|
||
|
||
# Add the button and an invisible textarea that will let you copy the meta data as a prompt
|
||
if meta_out_string.strip() != "":
|
||
civitai_info_html.append('<div class="img-meta-ico" title="Copy Metadata" onclick="metaDataCopy(event)"></div>')
|
||
civitai_info_html.append(f'<textarea class="img-meta">{meta_out_string}</textarea>')
|
||
|
||
civitai_info_html.append(f"""</div>
|
||
<details class='img-properties-list'>
|
||
<summary><strong>Properties:</strong></summary>
|
||
<ul>
|
||
<li><strong>URL:</strong> <a id="ci-image-URL-{i}" href="{image.get('url','')}" target="_blank">{image.get('url','')}</a></li>
|
||
<li><strong>NSFW:</strong> <span id="ci-image-nsfw-{i}">{image.get('nsfw','')}</span></li>
|
||
<li><strong>Meta:</strong>
|
||
<ul id="ci-image-meta-{i}">
|
||
{meta_list_items}
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
</details>
|
||
</div>
|
||
""")
|
||
|
||
civitai_info_html.append("</div></div>")
|
||
return "".join(civitai_info_html)
|
||
|
||
def create_html_img(file, is_in_a1111_dir):
|
||
# create the html to display an image along with its meta data
|
||
image = Image.open(file)
|
||
# load the image to memory (needed for getting the meta data)
|
||
image.load()
|
||
# get the prompt data
|
||
metadata, _ = images.read_info_from_image(image)
|
||
|
||
# set default order to 0
|
||
order = 0
|
||
# if strict naming is on, search the file name for a number at the end of the file and use that for its order
|
||
if shared.opts.model_preview_xd_name_matching == "Strict":
|
||
# get the file name without extension
|
||
file_name, file_extension = os.path.splitext(os.path.basename(file))
|
||
# search for '{anything}.{number}' in the file name and return the number
|
||
image_number = re.search(".*\.(\d+)$", file_name)
|
||
order = int(image_number.group(1)) if image_number else 0
|
||
|
||
if is_in_a1111_dir:
|
||
# escape special URL characters from the filename
|
||
encoded_file_path = urllib.parse.quote(file, safe='/:\\')
|
||
# create the html for the image
|
||
html_code = f'<div class="img-container" style="order:{order}"><img src=file={encoded_file_path} onclick="imageZoomIn(event)" />'
|
||
else:
|
||
# linking to the image wont work so convert it to a base64 byte string
|
||
with open(file, "rb") as img_file:
|
||
img_data = base64.b64encode(img_file.read()).decode()
|
||
# create the html for the image
|
||
html_code = f'<div class="img-container" style="order:{order}"><img src="data:image/{image.format};base64,{img_data}" onclick="imageZoomIn(event)" />'
|
||
|
||
# if the image has prompt data in the meta data also add some elements to support copying the prompt to clipboard
|
||
if metadata is not None and metadata.strip() != "":
|
||
html_code += '<div class="img-meta-ico" title="Copy Metadata" onclick="metaDataCopy(event)"></div>'
|
||
html_code += f'<textarea class="img-meta">{metadata}</textarea>'
|
||
html_code += "</div>\n"
|
||
# return the html code
|
||
return html_code
|
||
|
||
def search_and_display_previews(model_name, paths):
|
||
html_generic_pattern = re.compile(r'^.*(?i:\.' + html_ext_pattern + r')$')
|
||
civitai_generic_pattern = re.compile(r'^.*(?i:\.' + civitai_ext_pattern + r')$')
|
||
md_generic_pattern = re.compile(r'^.*(?i:\.' + md_ext_pattern + r')$')
|
||
txt_generic_pattern = re.compile(r'^.*(?i:\.' + txt_ext_pattern + r')$')
|
||
prompts_generic_pattern = re.compile(r'^.*(?i:\.' + prompts_ext_pattern + r')$')
|
||
img_generic_pattern = re.compile(r'^.*(?i:\.' + img_ext_pattern + r')$')
|
||
# create patters for the supported preview file types
|
||
# `model_name` will be the name of the model to check for preview files for
|
||
if shared.opts.model_preview_xd_name_matching == "Strict" or shared.opts.model_preview_xd_name_matching == "Index":
|
||
# strict naming is intended to avoid name collision between 'checkpoint1000' and 'checkpoint10000'.
|
||
# Using a loose naming rule preview files for 'checkpoint10000' would show up for 'checkpoint1000'
|
||
# The rules for strict naming are:
|
||
# HTML previews should follow {model}.html example 'checkpoint1000.html'
|
||
html_pattern = re.compile(r'^' + re.escape(model_name) + r'(?i:\.' + html_ext_pattern+ r')$')
|
||
# Civitai info files should follow {model}.civitai.info
|
||
civitai_pattern = re.compile(r'^' + re.escape(model_name) + r'(?i:\.' + civitai_ext_pattern+ r')$')
|
||
# Markdown previews should follow {model}.md example 'checkpoint1000.md'
|
||
md_pattern = re.compile(r'^' + re.escape(model_name) + r'(?i:\.' + md_ext_pattern+ r')$')
|
||
# Prompt lists should follow {model}.prompt example 'checkpoint1000.prompt'
|
||
prompts_pattern = re.compile(r'^' + re.escape(model_name) + r'(?i:\.' + prompts_ext_pattern + r')$')
|
||
# Text files previews should follow {model}.txt example 'checkpoint1000.txt'
|
||
txt_pattern = re.compile(r'^' + re.escape(model_name) + r'(?i:\.' + txt_ext_pattern+ r')$')
|
||
# Images previews should follow {model}.{extension} or {model}.preview.{extension} or {model}.{number}.{extension} or {model}.preview.{number}.{extension}
|
||
# example 1 'checkpoint1000.png'
|
||
# example 2 'checkpoint1000.preview.jpg'
|
||
# example 3 'checkpoint1000.1.jpeg'
|
||
# example 4 'checkpoint1000.preview.1.webp'
|
||
img_pattern = re.compile(r'^' + re.escape(model_name) + r'(?i:(?:\.preview)?(?:\.\d+)?\.' + img_ext_pattern + r')$')
|
||
elif shared.opts.model_preview_xd_name_matching == "Folder":
|
||
# use a folder name matching that only requires the model name to show up somewhere in the folder path not the file name name
|
||
html_pattern = html_generic_pattern
|
||
civitai_pattern = civitai_generic_pattern
|
||
md_pattern = md_generic_pattern
|
||
txt_pattern = txt_generic_pattern
|
||
prompts_pattern = prompts_generic_pattern
|
||
img_pattern = img_generic_pattern
|
||
else:
|
||
# use a loose name matching that only requires the model name to show up somewhere in the file name
|
||
html_pattern = re.compile(r'^.*' + re.escape(model_name) + r'.*(?i:\.' + html_ext_pattern + r')$')
|
||
civitai_pattern = re.compile(r'^.*' + re.escape(model_name) + r'.*(?i:\.' + civitai_ext_pattern + r')$')
|
||
md_pattern = re.compile(r'^.*' + re.escape(model_name) + r'.*(?i:\.' + md_ext_pattern + r')$')
|
||
txt_pattern = re.compile(r'^.*' + re.escape(model_name) + r'.*(?i:\.' + txt_ext_pattern + r')$')
|
||
prompts_pattern = re.compile(r'^.*' + re.escape(model_name) + r'.*(?i:\.' + prompts_ext_pattern + r')$')
|
||
img_pattern = re.compile(r'^.*' + re.escape(model_name) + r'.*(?i:\.' + img_ext_pattern + r')$')
|
||
|
||
# an array to hold the image html code
|
||
html_code_list = []
|
||
# if a text file is found
|
||
found_txt_file = None
|
||
# if a markdown file is found
|
||
md_file = None
|
||
# if a prompts file is found
|
||
prompts_file = None
|
||
# if an html file is found the iframe
|
||
html_file_frame = None
|
||
# if an civitai.info file is found the generated html
|
||
civitai_info_html = None
|
||
|
||
# if a text file is found
|
||
generic_found_txt_file = None
|
||
# if a markdown file is found
|
||
generic_md_file = None
|
||
# if a prompts file is found
|
||
generic_prompts_file = None
|
||
# if an html file is found the iframe
|
||
generic_html_file_frame = None
|
||
# if an civitai.info file is found the generated html
|
||
generic_civitai_info_html = None
|
||
|
||
# get the current directory so we can convert absolute paths to relative paths if we need to
|
||
current_directory = os.getcwd()
|
||
|
||
# support the ability to check multiple paths
|
||
for path in paths:
|
||
# loop through all files in the path and any subdirectories
|
||
for dirpath, dirnames, filenames in os.walk(path, followlinks=True):
|
||
# get a list of all parent directories
|
||
directories = dirpath.split(os.path.sep)
|
||
# if we are not using folder match mode look for files normally otherwise we are using folder match mode so make sure at least one parent directory is equal to the name of the model
|
||
if shared.opts.model_preview_xd_name_matching != "Folder" or (shared.opts.model_preview_xd_name_matching == "Folder" and model_name in directories):
|
||
# sort the file names using a natural sort algorithm
|
||
sorted_filenames = sorted(filenames, key=natural_order_number)
|
||
# if we are using index matching mode check to see if there is an index file, if there is read it and compile a regex that matches all the models listed in the file to use later
|
||
index_has_model = False
|
||
index_models_pattern = None
|
||
if shared.opts.model_preview_xd_name_matching == "Index":
|
||
index_txt_filename = next((filename for filename in sorted_filenames if filename.lower() == "index.txt"), None)
|
||
if index_txt_filename is not None:
|
||
index_txt_path = os.path.join(dirpath, index_txt_filename)
|
||
output_text = ""
|
||
with open(index_txt_path, "r", encoding="utf8") as file:
|
||
output_text = file.read()
|
||
index_models = [model.strip() for model in output_text.replace(",", "\n").splitlines()]
|
||
index_has_model = model_name in index_models
|
||
index_models = [re.escape(model) for model in index_models if model != model_name]
|
||
if len(index_models) > 0:
|
||
index_models_pattern = re.compile(r'^(?:' + r'|'.join(index_models) + r')(?i:(?:\.preview)?(?:\.\d+)?\.' + all_ext_pattern + r')$')
|
||
# check each file to see if it is a preview file
|
||
for filename in sorted_filenames:
|
||
file_path = os.path.join(dirpath, filename)
|
||
# check if the path is a subdirectory of the install directory
|
||
is_in_a1111_dir = is_in_directory(current_directory, file_path)
|
||
img_file = None
|
||
# if we are using index matching, find all preview files that match the regex compiled earlier
|
||
if shared.opts.model_preview_xd_name_matching == "Index":
|
||
if (index_models_pattern is not None and index_models_pattern.match(filename)) or filename.lower() == "index.txt":
|
||
# ignore preview files that strictly match any of the other models in the index file
|
||
continue
|
||
if index_has_model:
|
||
if html_generic_pattern.match(filename):
|
||
# there can only be one html file, if one was already found it is replaced
|
||
generic_html_file_frame = create_html_iframe(file_path, is_in_a1111_dir)
|
||
if civitai_generic_pattern.match(filename):
|
||
# there can only be one civitai.info file, if one was already found it is replaced
|
||
generic_civitai_info_html = create_civitai_info_html(file_path)
|
||
if md_generic_pattern.match(filename):
|
||
# there can only be one markdown file, if one was already found it is replaced
|
||
generic_md_file = file_path
|
||
if prompts_generic_pattern.match(filename):
|
||
# there can only be one prompts file, if one was already found it is replaced
|
||
generic_prompts_file = file_path
|
||
if img_generic_pattern.match(filename):
|
||
# there can be many images, even spread across the multiple paths
|
||
img_file = file_path
|
||
if txt_generic_pattern.match(filename):
|
||
# there can only be one text file, if one was already found it is replaced
|
||
generic_found_txt_file = file_path
|
||
# perform the normal file matching rules for the matching mode determined at the beginning of this function
|
||
if html_pattern.match(filename):
|
||
# there can only be one html file, if one was already found it is replaced
|
||
html_file_frame = create_html_iframe(file_path, is_in_a1111_dir)
|
||
if civitai_pattern.match(filename):
|
||
# there can only be one civitai.info file, if one was already found it is replaced
|
||
civitai_info_html = create_civitai_info_html(file_path)
|
||
if md_pattern.match(filename):
|
||
# there can only be one markdown file, if one was already found it is replaced
|
||
md_file = file_path
|
||
if prompts_pattern.match(filename):
|
||
# there can only be one prompts file, if one was already found it is replaced
|
||
prompts_file = file_path
|
||
if img_pattern.match(filename):
|
||
# there can be many images, even spread across the multiple paths
|
||
img_file = file_path
|
||
if txt_pattern.match(filename):
|
||
# there can only be one text file, if one was already found it is replaced
|
||
found_txt_file = file_path
|
||
|
||
# if this file was an image file append the image to the html code list
|
||
if img_file is not None:
|
||
html_code_list.append(create_html_img(img_file, is_in_a1111_dir))
|
||
|
||
# if a generic preview file was found but not a specific one, use the generic one
|
||
if html_file_frame is None and generic_html_file_frame is not None:
|
||
html_file_frame = generic_html_file_frame
|
||
if civitai_info_html is None and generic_civitai_info_html is not None:
|
||
civitai_info_html = generic_civitai_info_html
|
||
if md_file is None and generic_md_file is not None:
|
||
md_file = generic_md_file
|
||
if prompts_file is None and generic_prompts_file is not None:
|
||
prompts_file = generic_prompts_file
|
||
if found_txt_file is None and generic_found_txt_file is not None:
|
||
found_txt_file = generic_found_txt_file
|
||
|
||
# if an html file was found, ignore other txt, md, or image preview files and return the html file and prompt file if available
|
||
if html_file_frame is not None:
|
||
return html_file_frame, None, prompts_file, None
|
||
|
||
# if an civitai.info file was found, ignore other txt, md, or image preview files and return the html created and prompt file if available
|
||
if civitai_info_html is not None:
|
||
return civitai_info_html, None, prompts_file, None
|
||
|
||
# if there were images found, wrap the images in a container div
|
||
html_code_output = '<div class="img-container-set">' + ''.join(html_code_list) + '</div>' if len(html_code_list) > 0 else None
|
||
|
||
# return the all preview files found
|
||
return html_code_output, md_file, prompts_file, found_txt_file
|
||
|
||
def get_checkpoints_dirs():
|
||
# create list of directories
|
||
|
||
# use the default directory as a fallback for people who want to keep their models outside of the automatic1111 directory but their preview files inside
|
||
default_dir = os.path.join('models','Stable-diffusion') # models/Stable-diffusion
|
||
directories = [default_dir] if os.path.exists(default_dir) and os.path.isdir(default_dir) else []
|
||
|
||
# add the directory expected by automatic1111 for this type of model (it may be the same as the above model so only add it if its not already added)
|
||
set_dir = shared.cmd_opts.ckpt_dir
|
||
if set_dir is not None and os.path.exists(set_dir) and os.path.isdir(set_dir) and not is_dir_in_list(directories, set_dir):
|
||
# WARNING: html files and markdown files that link to local files outside of the automatic1111 directory will not work correctly
|
||
directories.append(set_dir)
|
||
return directories
|
||
|
||
def get_embedding_dirs():
|
||
# create list of directories
|
||
|
||
# use the default directory as a fallback for people who want to keep their models outside of the automatic1111 directory but their preview files inside
|
||
directories = ['embeddings', os.path.join('models','embeddings')] # support the Vladmandic fork by also adding models/embeddings as a default location
|
||
directories = list(filter(lambda x: os.path.exists(x), directories))
|
||
|
||
# add the directory expected by automatic1111 for this type of model (it may be the same as the above model so only add it if its not already added)
|
||
set_dir = shared.cmd_opts.embeddings_dir
|
||
if set_dir is not None and os.path.exists(set_dir) and os.path.isdir(set_dir) and not is_dir_in_list(directories, set_dir):
|
||
# WARNING: html files and markdown files that link to local files outside of the automatic1111 directory will not work correctly
|
||
directories.append(set_dir)
|
||
return directories
|
||
|
||
def get_hypernetwork_dirs():
|
||
# create list of directories
|
||
|
||
# use the default directory as a fallback for people who want to keep their models outside of the automatic1111 directory but their preview files inside
|
||
default_dir = os.path.join('models','hypernetworks') # models/hypernetworks
|
||
directories = [default_dir] if os.path.exists(default_dir) and os.path.isdir(default_dir) else []
|
||
|
||
# add the directory expected by automatic1111 for this type of model (it may be the same as the above model so only add it if its not already added)
|
||
set_dir = shared.cmd_opts.hypernetwork_dir
|
||
if set_dir is not None and os.path.exists(set_dir) and os.path.isdir(set_dir) and not is_dir_in_list(directories, set_dir):
|
||
# WARNING: html files and markdown files that link to local files outside of the automatic1111 directory will not work correctly
|
||
directories.append(set_dir)
|
||
return directories
|
||
|
||
def get_lora_dirs():
|
||
# create list of directories
|
||
directories = []
|
||
|
||
# add models/lora directory as a fallback for people who want to keep their models outside of the automatic1111 directory but their preview files inside
|
||
default_dir = os.path.join("models","Lora") # models/Lora
|
||
if os.path.exists(default_dir) and os.path.isdir(default_dir):
|
||
directories.append(default_dir)
|
||
# add directories from the builtin lora extension if exists
|
||
set_dir = shared.cmd_opts.lora_dir
|
||
if set_dir is not None and os.path.exists(set_dir) and os.path.isdir(set_dir) and not is_dir_in_list(directories, set_dir):
|
||
# WARNING: html files and markdown files that link to local files outside of the automatic1111 directory will not work correctly
|
||
directories.append(set_dir)
|
||
# add directories from the third party lora extension if exists
|
||
if additional_networks is not None:
|
||
# use the same pattern as the additional_networks.py extension to build up a list of paths to check for lora models and preview files
|
||
set_dir = additional_networks.lora_models_dir
|
||
if set_dir is not None and os.path.exists(set_dir) and os.path.isdir(set_dir) and not is_dir_in_list(directories, set_dir):
|
||
directories.append(set_dir)
|
||
extra_lora_path = shared.opts.data.get("additional_networks_extra_lora_path", None)
|
||
if extra_lora_path and os.path.exists(extra_lora_path) and os.path.isdir(extra_lora_path) and not is_dir_in_list(directories, extra_lora_path):
|
||
directories.append(extra_lora_path)
|
||
# add models/LyCORIS if exists to support backwards compatibility with people who still have their LyCORIS models in a seperate folder
|
||
lycoris_dir = os.path.join("models","LyCORIS") # models/LyCORIS
|
||
if os.path.exists(lycoris_dir) and os.path.isdir(lycoris_dir):
|
||
directories.append(lycoris_dir)
|
||
return directories
|
||
|
||
def get_lycoris_dirs():
|
||
# create list of directories
|
||
directories = []
|
||
|
||
# add directories from the third party lycoris extension if exists
|
||
if lycoris_module is not None:
|
||
# add models/LyCORIS just in case to the list of directories
|
||
default_dir = os.path.join("models","LyCORIS") # models/LyCORIS
|
||
if os.path.exists(default_dir) and os.path.isdir(default_dir):
|
||
directories.append(default_dir)
|
||
# add directories from the third party lycoris extension if exists
|
||
set_dir = shared.cmd_opts.lyco_dir
|
||
if set_dir is not None and os.path.exists(set_dir) and os.path.isdir(set_dir) and not is_dir_in_list(directories, set_dir):
|
||
# WARNING: html files and markdown files that link to local files outside of the automatic1111 directory will not work correctly
|
||
directories.append(set_dir)
|
||
|
||
return directories
|
||
|
||
def show_model_preview(modelname=None):
|
||
# get preview for the model
|
||
return show_preview(modelname, get_checkpoints_dirs(), "checkpoints")
|
||
|
||
def show_embedding_preview(modelname=None):
|
||
# get preview for the model
|
||
return show_preview(modelname, get_embedding_dirs(), "embeddings")
|
||
|
||
def show_hypernetwork_preview(modelname=None):
|
||
# get preview for the model
|
||
return show_preview(modelname, get_hypernetwork_dirs(), "hypernetworks")
|
||
|
||
def show_lora_preview(modelname=None):
|
||
# get preview for the model
|
||
return show_preview(modelname, get_lora_dirs(), "loras")
|
||
|
||
def show_lycoris_preview(modelname=None):
|
||
# get preview for a LyCORIS
|
||
return show_preview(modelname, get_lycoris_dirs(), "lycoris")
|
||
|
||
def show_preview(modelname, paths, tags_key):
|
||
if modelname is None or len(modelname) == 0 or paths is None or len(paths) == 0:
|
||
txt_update = gr.Textbox.update(value=None, visible=False)
|
||
md_update = gr.Textbox.update(value=None, visible=False)
|
||
prompts_list_update = gr.CheckboxGroup.update(visible=False)
|
||
prompts_button_update = gr.Button.update(visible=False)
|
||
html_update = gr.HTML.update(value='', visible=False)
|
||
tags_html = gr.HTML.update(value='', visible=False)
|
||
return prompts_list_update, prompts_button_update, txt_update, md_update, html_update, tags_html
|
||
|
||
# remove the hash if exists, the extension, and if the string is a path just return the file name
|
||
name = clean_modelname(modelname)
|
||
# get the preview data
|
||
html_code, found_md_file, found_prompts_file, found_txt_file = search_and_display_previews(name, paths)
|
||
preview_html = '' if html_code is None else html_code
|
||
|
||
# if a text file was found update the gradio text element
|
||
if found_txt_file:
|
||
output_text = ""
|
||
with open(found_txt_file, "r", encoding="utf8") as file:
|
||
for line in file:
|
||
output_text = f'{output_text}{line.strip()}\n'
|
||
txt_update = gr.Textbox.update(value=output_text, visible=True)
|
||
else:
|
||
txt_update = gr.Textbox.update(value=None, visible=False)
|
||
|
||
# if a markdown file was found update the gradio markdown element
|
||
if found_md_file:
|
||
output_text = ""
|
||
with open(found_md_file, "r", encoding="utf8") as file:
|
||
output_text = file.read()
|
||
md_update = gr.Textbox.update(value=output_text, visible=True)
|
||
else:
|
||
md_update = gr.Textbox.update(value=None, visible=False)
|
||
|
||
# if a prompt file was found update the gradio prompts list
|
||
if found_prompts_file:
|
||
prompts: list[str] = list()
|
||
try:
|
||
with open(found_prompts_file, newline='') as csvfile:
|
||
reader = csv.reader(csvfile)
|
||
for row in reader:
|
||
for prompt in row:
|
||
if prompt not in prompts:
|
||
prompts.append(prompt)
|
||
finally:
|
||
prompts_list_update = gr.CheckboxGroup.update(visible=True, choices=prompts, value=prompts)
|
||
prompts_button_update = gr.Button.update(visible=True)
|
||
else:
|
||
prompts_list_update = gr.CheckboxGroup.update(visible=False)
|
||
prompts_button_update = gr.Button.update(visible=False)
|
||
|
||
# if images were found or an HTML file was found update the gradio html element
|
||
if html_code:
|
||
html_update = gr.HTML.update(value=preview_html, visible=True)
|
||
else:
|
||
html_update = gr.HTML.update(value='', visible=False)
|
||
|
||
# if nothing was found display a message that nothing was found
|
||
if found_txt_file is None and found_md_file is None and (html_code is None or len(html_code) == 0):
|
||
html_update = gr.HTML.update(value="<span style='margin-left: 1em;'>No Preview Found</span>", visible=True)
|
||
|
||
# get the tags from the tags object and create a span for them
|
||
found_tags = tags[tags_key].get(modelname, None)
|
||
if found_tags is not None:
|
||
tags_html = gr.HTML.update(value=f'<div class="footer-tags">{found_tags}</div>', visible=True)
|
||
else:
|
||
tags_html = gr.HTML.update(value='', visible=False)
|
||
return prompts_list_update, prompts_button_update, txt_update, md_update, html_update, tags_html
|
||
|
||
def create_tab(tab_label, tab_id_key, list_choices, show_preview_fn, filter_fn, refresh_fn, update_selected_fn):
|
||
# create a tab for model previews
|
||
with gr.Tab(tab_label, elem_id=f"model_preview_xd_{tab_label.lower()}_tab", elem_classes="model_preview_xd_tab"):
|
||
with gr.Row(elem_id=f"{tab_id_key}_modelpreview_xd_control_row", elem_classes="modelpreview_xd_control_row"):
|
||
list = gr.Dropdown(label="Model", choices=list_choices, interactive=True, elem_id=f"{tab_id_key}_mp2_preview_model_list", elem_classes="mp2_preview_model_list")
|
||
filter_input = gr.Textbox(label="Filter", value="", elem_id=f"{tab_id_key}_modelpreview_xd_filter_text", elem_classes="modelpreview_xd_filter_text")
|
||
with gr.Row(elem_id=f"{tab_id_key}_modelpreview_xd_hidden_ui", elem_classes="modelpreview_xd_hidden_ui"):
|
||
refresh_list = gr.Button(value=refresh_symbol, elem_id=f"{tab_id_key}_modelpreview_xd_refresh_sd_model", elem_classes="modelpreview_xd_refresh_sd_model")
|
||
update_model_input = gr.Textbox(value="", elem_id=f"{tab_id_key}_modelpreview_xd_update_sd_model_text", elem_classes="modelpreview_xd_update_sd_model_text")
|
||
update_model_button = gr.Button(value=update_symbol, elem_id=f"{tab_id_key}_modelpreview_xd_update_sd_model", elem_classes="modelpreview_xd_update_sd_model")
|
||
with gr.Row(elem_id=f"{tab_id_key}_modelpreview_xd_prompts_row", elem_classes="modelpreview_xd_prompts_row"):
|
||
prompts_list = gr.Checkboxgroup(label="Prompts", visible=False, interactive=True, elem_id=f"{tab_id_key}_modelpreview_xd_prompts_list", elem_classes="modelpreview_xd_prompts_list")
|
||
prompts_copy_button = gr.Button(value="Copy", visible=False, interactive=True, elem_id=f"{tab_id_key}_modelpreview_xd_prompts_copy_button", elem_classes="modelpreview_xd_prompts_copy_button")
|
||
with gr.Row(elem_id=f"{tab_id_key}_modelpreview_xd_notes_row", elem_classes="modelpreview_xd_notes_row"):
|
||
notes_text_area = gr.Textbox(label='Notes', interactive=False, lines=1, visible=False, elem_id=f"{tab_id_key}_modelpreview_xd_update_sd_model_text_area", elem_classes="modelpreview_xd_update_sd_model_text_area")
|
||
with gr.Row(elem_id=f"{tab_id_key}_modelpreview_xd_html_row", elem_classes="modelpreview_xd_html_row"):
|
||
with gr.Row(elem_id=f"{tab_id_key}_modelpreview_xd_flexcolumn_row", elem_classes="modelpreview_xd_flexcolumn_row"):
|
||
preview_html = gr.HTML(elem_id=f"{tab_id_key}_modelpreview_xd_html_div", elem_classes="modelpreview_xd_html_div", visible=False)
|
||
preview_md = gr.Markdown(elem_id=f"{tab_id_key}_modelpreview_xd_markdown_div", elem_classes="modelpreview_xd_markdown_div", visible=False)
|
||
with gr.Row(elem_id=f"{tab_id_key}_modelpreview_xd_tags_row", elem_classes="modelpreview_xd_tags_row"):
|
||
preview_tags = gr.HTML(elem_id=f"{tab_id_key}_modelpreview_xd_tags_div", elem_classes="modelpreview_xd_tags_div", visible=False)
|
||
|
||
list.change(
|
||
fn=show_preview_fn,
|
||
inputs=[
|
||
list,
|
||
],
|
||
outputs=[
|
||
prompts_list,
|
||
prompts_copy_button,
|
||
notes_text_area,
|
||
preview_md,
|
||
preview_html,
|
||
preview_tags
|
||
]
|
||
)
|
||
|
||
filter_input.change(
|
||
fn=filter_fn,
|
||
inputs=[
|
||
filter_input,
|
||
],
|
||
outputs=[
|
||
list,
|
||
]
|
||
)
|
||
|
||
refresh_list.click(
|
||
fn=refresh_fn,
|
||
inputs=[
|
||
list,
|
||
filter_input,
|
||
],
|
||
outputs=[
|
||
list,
|
||
prompts_list,
|
||
prompts_copy_button,
|
||
notes_text_area,
|
||
preview_md,
|
||
preview_html,
|
||
preview_tags
|
||
]
|
||
)
|
||
|
||
update_model_button.click(
|
||
fn=update_selected_fn,
|
||
inputs=[
|
||
update_model_input,
|
||
],
|
||
outputs=[
|
||
list,
|
||
prompts_list,
|
||
prompts_copy_button,
|
||
notes_text_area,
|
||
preview_md,
|
||
preview_html,
|
||
preview_tags
|
||
]
|
||
)
|
||
|
||
prompts_copy_button.click(
|
||
fn=None,
|
||
inputs=[prompts_list],
|
||
_js="(x) => copyToClipboard(x)",
|
||
)
|
||
|
||
def on_ui_tabs():
|
||
global additional_networks, additional_networks_builtin
|
||
# import/update the lora module
|
||
additional_networks = import_lora_module()
|
||
additional_networks_builtin = import_lora_module_builtin()
|
||
lycoris_module = import_lycoris_module()
|
||
|
||
# create a gradio block
|
||
with gr.Blocks() as modelpreview_interface:
|
||
|
||
limitHeight = shared.opts.model_preview_xd_limit_sizing
|
||
columnView = shared.opts.model_preview_xd_column_view
|
||
|
||
gr.HTML(elem_id='modelpreview_xd_setting', value='<script id="modelpreview_xd_setting_json" type="application/json">{ "LimitSize": ' + ( "true" if limitHeight else "false" ) + ', "ColumnView": ' + ( "true" if columnView else "false" ) + ' }</script>', visible=False)
|
||
|
||
# create a tab for the checkpoint previews
|
||
create_tab("Checkpoints", "cp",
|
||
list_all_models(),
|
||
show_model_preview,
|
||
filter_models,
|
||
refresh_models,
|
||
update_checkpoint)
|
||
create_tab("Embeddings", "em",
|
||
list_all_embeddings(),
|
||
show_embedding_preview,
|
||
filter_embeddings,
|
||
refresh_embeddings,
|
||
update_embedding)
|
||
create_tab("Hypernetwork", "hn",
|
||
list_all_hypernetworks(),
|
||
show_hypernetwork_preview,
|
||
filter_hypernetworks,
|
||
refresh_hypernetworks,
|
||
update_hypernetwork)
|
||
|
||
# create a tab for the lora previews if the module was loaded
|
||
if additional_networks is not None or additional_networks_builtin is not None:
|
||
create_tab("Lora", "lo",
|
||
list_all_loras(),
|
||
show_lora_preview,
|
||
filter_loras,
|
||
refresh_loras,
|
||
update_lora)
|
||
|
||
# create a tab for the LyCORIS previews if the module was loaded
|
||
if lycoris_module is not None:
|
||
create_tab("LyCORIS", "ly",
|
||
list_all_lycorii(),
|
||
show_lycoris_preview,
|
||
filter_lycorii,
|
||
refresh_lycorii,
|
||
update_lycorii)
|
||
|
||
return (modelpreview_interface, "Model Previews", "modelpreview_xd_interface"),
|
||
|
||
def on_ui_settings():
|
||
section = ('model_preview_xd', "Model Preview XD")
|
||
shared.opts.add_option("model_preview_xd_name_matching", shared.OptionInfo("Loose", "Name matching rule for preview files", gr.Radio, {"choices": ["Loose", "Strict", "Folder", "Index"]}, section=section).info("Requires UI Reload").html("""
|
||
<ul style='margin-left: 1.5em'>
|
||
<li><strong>Loose</strong> - Use a loose naming scheme for matching preview files. Your preview files must contain the model name somewhere in their file name. If your model is named <strong>'model.ckpt'</strong> your preview files must be named in the following manner:
|
||
<ul style='margin-left: 2em'>
|
||
<li>my<strong>model</strong>.html</li>
|
||
<li><strong>model</strong>_markdown.md</li>
|
||
<li>trigger_words_for_<strong>model</strong>.txt</li>
|
||
<li><strong>model</strong>-image.webp</li>
|
||
<li><strong>model</strong>.preview.png</li>
|
||
<li>my3D<strong>model</strong>.jpg</li>
|
||
<li><strong>model</strong>ling.jpeg</li>
|
||
</ul>
|
||
</li>
|
||
<li><strong>Strict</strong> - Use a strict naming scheme for matching preview files. If your model is named <strong>'model.ckpt'</strong> your preview files must be named in the following manner:
|
||
<ul style='margin-left: 2em'>
|
||
<li><strong>model</strong>.html</li>
|
||
<li><strong>model</strong>.md</li>
|
||
<li><strong>model</strong>.txt</li>
|
||
<li><strong>model</strong>.webp</li>
|
||
<li><strong>model</strong>.preview.png</li>
|
||
<li><strong>model</strong>.3.jpg</li>
|
||
<li><strong>model</strong>.preview.4.jpeg</li>
|
||
</ul>
|
||
</li>
|
||
<li><strong>Folder</strong> - Use folder name matching. Will look for a folder within your model directory that matches your model's name (case sensitive) and will show any preview files found within that folder or any subfolders of that folder. If your model is named <strong>'mymodel.ckpt'</strong> all preview files located in <strong>'/mymodel/'</strong> will be shown.</li>
|
||
<li><strong>Index</strong> - If a folder contains a file <strong>'index.txt'</strong> that lists model names, any preview files in that folder regardless of name will be associated with each model in the index file. This allows you to share preview files among a number of models. This matching mode will also match any file named similar to the <strong>'Strict'</strong> matching mode to allow you to still specify preview files for specific models.</li>
|
||
</ul>"""))
|
||
shared.opts.add_option("model_preview_xd_limit_sizing", shared.OptionInfo(True, "Limit the height of previews to the height of the browser window", section=section).info(".html preview files are always limited regardless of this setting. Requires UI Reload"))
|
||
shared.opts.add_option("model_preview_xd_column_view", shared.OptionInfo(False, "Column view", section=section).info("This is only recommended if you use .txt files. Left column will have model select, .txt and .prompt preview data. Right column will have preview images and .md preview data, or .civitai.info preview data or .html preview data. Requires UI Reload"))
|
||
shared.opts.add_option("model_preview_xd_cache_images_civitai_info", shared.OptionInfo(False, "Cache images from .civitai.info previews", section=section).info("Saves files to extension folder."))
|
||
|
||
script_callbacks.on_ui_settings(on_ui_settings)
|
||
script_callbacks.on_ui_tabs(on_ui_tabs) |