| | import os |
| | import json |
| | import sys |
| | import io |
| | import base64 |
| | import platform |
| | import subprocess as sp |
| | from PIL import PngImagePlugin, Image |
| |
|
| | from modules import shared |
| | import gradio as gr |
| |
|
| | import modules.ui |
| | from modules.ui_components import ToolButton |
| | import modules.extras |
| | import modules.generation_parameters_copypaste as parameters_copypaste |
| |
|
| | from scripts import safetensors_hack, model_util |
| | from scripts.model_util import MAX_MODEL_COUNT |
| |
|
| |
|
| | folder_symbol = '\U0001f4c2' |
| | keycap_symbols = [ |
| | '\u0031\ufe0f\u20e3', |
| | '\u0032\ufe0f\u20e3', |
| | '\u0033\ufe0f\u20e3', |
| | '\u0034\ufe0f\u20e3', |
| | '\u0035\ufe0f\u20e3', |
| | '\u0036\ufe0f\u20e3', |
| | '\u0037\ufe0f\u20e3', |
| | '\u0038\ufe0f\u20e3', |
| | '\u0039\ufe0f\u20e3', |
| | '\u1f51f' |
| | ] |
| |
|
| |
|
| | def write_webui_model_preview_image(model_path, image): |
| | basename, ext = os.path.splitext(model_path) |
| | preview_path = f"{basename}.png" |
| |
|
| | |
| | use_metadata = False |
| | metadata = PngImagePlugin.PngInfo() |
| | for key, value in image.info.items(): |
| | if isinstance(key, str) and isinstance(value, str): |
| | metadata.add_text(key, value) |
| | use_metadata = True |
| |
|
| | image.save(preview_path, "PNG", pnginfo=(metadata if use_metadata else None)) |
| |
|
| |
|
| | def delete_webui_model_preview_image(model_path): |
| | basename, ext = os.path.splitext(model_path) |
| | preview_paths = [f"{basename}.preview.png", f"{basename}.png"] |
| |
|
| | for preview_path in preview_paths: |
| | if os.path.isfile(preview_path): |
| | os.unlink(preview_path) |
| |
|
| |
|
| | def decode_base64_to_pil(encoding): |
| | if encoding.startswith("data:image/"): |
| | encoding = encoding.split(";")[1].split(",")[1] |
| | return Image.open(io.BytesIO(base64.b64decode(encoding))) |
| |
|
| |
|
| | def encode_pil_to_base64(image): |
| | with io.BytesIO() as output_bytes: |
| |
|
| | |
| | use_metadata = False |
| | metadata = PngImagePlugin.PngInfo() |
| | for key, value in image.info.items(): |
| | if isinstance(key, str) and isinstance(value, str): |
| | metadata.add_text(key, value) |
| | use_metadata = True |
| |
|
| | image.save( |
| | output_bytes, "PNG", pnginfo=(metadata if use_metadata else None) |
| | ) |
| | bytes_data = output_bytes.getvalue() |
| | return base64.b64encode(bytes_data) |
| |
|
| |
|
| | def open_folder(f): |
| | if not os.path.exists(f): |
| | print(f'Folder "{f}" does not exist. After you create an image, the folder will be created.') |
| | return |
| | elif not os.path.isdir(f): |
| | print(f""" |
| | WARNING |
| | An open_folder request was made with an argument that is not a folder. |
| | This could be an error or a malicious attempt to run code on your computer. |
| | Requested path was: {f} |
| | """, file=sys.stderr) |
| | return |
| |
|
| | if not shared.cmd_opts.hide_ui_dir_config: |
| | path = os.path.normpath(f) |
| | if platform.system() == "Windows": |
| | os.startfile(path) |
| | elif platform.system() == "Darwin": |
| | sp.Popen(["open", path]) |
| | elif "microsoft-standard-WSL2" in platform.uname().release: |
| | sp.Popen(["wsl-open", path]) |
| | else: |
| | sp.Popen(["xdg-open", path]) |
| |
|
| |
|
| | def copy_metadata_to_all(module, model_path, copy_dir, same_session_only, missing_meta_only, cover_image): |
| | """ |
| | Given a model with metadata, copies that metadata to all models in copy_dir. |
| | |
| | :str module: Module name ("LoRA") |
| | :str model: Model key in lora_models ("MyModel(123456abcdef)") |
| | :str copy_dir: Directory to copy to |
| | :bool same_session_only: Only copy to modules with the same ss_session_id |
| | :bool missing_meta_only: Only copy to modules that are missing user metadata |
| | :Optional[Image] cover_image: Cover image to embed in the file as base64 |
| | :returns: gr.HTML.update() |
| | """ |
| | if model_path == "None": |
| | return "No model selected." |
| |
|
| | if not os.path.isfile(model_path): |
| | return f"Model path not found: {model_path}" |
| |
|
| | model_path = os.path.realpath(model_path) |
| |
|
| | if os.path.splitext(model_path)[1] != ".safetensors": |
| | return "Model is not in .safetensors format." |
| |
|
| | if not os.path.isdir(copy_dir): |
| | return "Please provide a directory containing models in .safetensors format." |
| |
|
| | print(f"[MetadataEditor] Copying metadata to models in {copy_dir}.") |
| | metadata = model_util.read_model_metadata(model_path, module) |
| | count = 0 |
| | for entry in os.scandir(copy_dir): |
| | if entry.is_file(): |
| | path = os.path.realpath(os.path.join(copy_dir, entry.name)) |
| | if path != model_path and model_util.is_safetensors(path): |
| | if same_session_only: |
| | other_metadata = safetensors_hack.read_metadata(path) |
| | if missing_meta_only and other_metadata.get("ssmd_display_name", "").strip(): |
| | print(f"[MetadataEditor] Skipping {path} as it already has metadata") |
| | continue |
| |
|
| | session_id = metadata.get("ss_session_id", None) |
| | other_session_id = other_metadata.get("ss_session_id", None) |
| | if session_id is None or other_session_id is None or session_id != other_session_id: |
| | continue |
| |
|
| | updates = { |
| | "ssmd_cover_images": "[]", |
| | "ssmd_display_name": "", |
| | "ssmd_version": "", |
| | "ssmd_keywords": "", |
| | "ssmd_author": "", |
| | "ssmd_source": "", |
| | "ssmd_description": "", |
| | "ssmd_rating": "0", |
| | "ssmd_tags": "", |
| | } |
| |
|
| | for k, v in metadata.items(): |
| | if k.startswith("ssmd_") and k != "ssmd_cover_images": |
| | updates[k] = v |
| |
|
| | model_util.write_model_metadata(path, module, updates) |
| | count += 1 |
| |
|
| | print(f"[MetadataEditor] Updated {count} models in directory {copy_dir}.") |
| | return f"Updated {count} models in directory {copy_dir}." |
| |
|
| |
|
| | def load_cover_image(model_path, metadata): |
| | """ |
| | Loads a cover image either from embedded metadata or an image file with |
| | .preview.png/.png format |
| | """ |
| | cover_images = json.loads(metadata.get("ssmd_cover_images", "[]")) |
| | cover_image = None |
| | if len(cover_images) > 0: |
| | print("[MetadataEditor] Loading embedded cover image.") |
| | cover_image = decode_base64_to_pil(cover_images[0]) |
| | else: |
| | basename, ext = os.path.splitext(model_path) |
| |
|
| | preview_paths = [f"{basename}.preview.png", f"{basename}.png"] |
| |
|
| | for preview_path in preview_paths: |
| | if os.path.isfile(preview_path): |
| | print(f"[MetadataEditor] Loading webui preview image: {preview_path}") |
| | cover_image = Image.open(preview_path) |
| |
|
| | return cover_image |
| |
|
| |
|
| | |
| | |
| | unknown_folders = ["(Unknown)", 0, 0, 0] |
| |
|
| |
|
| | def refresh_metadata(module, model_path): |
| | """ |
| | Reads metadata from the model on disk and updates all Gradio components |
| | """ |
| | if model_path == "None": |
| | return {}, None, "", "", "", "", "", 0, "", "", "", "", "", {}, [unknown_folders] |
| |
|
| | if not os.path.isfile(model_path): |
| | return {"info": f"Model path not found: {model_path}"}, None, "", "", "", "", "", 0, "", "", "", "", "", {}, [unknown_folders] |
| |
|
| | if os.path.splitext(model_path)[1] != ".safetensors": |
| | return {"info": "Model is not in .safetensors format."}, None, "", "", "", "", "", 0, "", "", "", "", "", {}, [unknown_folders] |
| |
|
| | metadata = model_util.read_model_metadata(model_path, module) |
| |
|
| | if metadata is None: |
| | training_params = {} |
| | metadata = {} |
| | else: |
| | training_params = {k: v for k, v in metadata.items() if k.startswith("ss_")} |
| |
|
| | cover_image = load_cover_image(model_path, metadata) |
| |
|
| | display_name = metadata.get("ssmd_display_name", "") |
| | author = metadata.get("ssmd_author", "") |
| | |
| | source = metadata.get("ssmd_source", "") |
| | keywords = metadata.get("ssmd_keywords", "") |
| | description = metadata.get("ssmd_description", "") |
| | rating = int(metadata.get("ssmd_rating", "0")) |
| | tags = metadata.get("ssmd_tags", "") |
| | model_hash = metadata.get("sshs_model_hash", model_util.cache("hashes").get(model_path, {}).get("model", "")) |
| | legacy_hash = metadata.get("sshs_legacy_hash", model_util.cache("hashes").get(model_path, {}).get("legacy", "")) |
| |
|
| | top_tags = {} |
| | if "ss_tag_frequency" in training_params: |
| | tag_frequency = json.loads(training_params.pop("ss_tag_frequency")) |
| | count_max = 0 |
| | for dir, frequencies in tag_frequency.items(): |
| | for tag, count in frequencies.items(): |
| | tag = tag.strip() |
| | existing = top_tags.get(tag, 0) |
| | top_tags[tag] = count + existing |
| | if len(top_tags) > 0: |
| | top_tags = dict(sorted(top_tags.items(), key=lambda x: x[1], reverse=True)) |
| |
|
| | count_max = max(top_tags.values()) |
| | top_tags = {k: float(v / count_max) for k, v in top_tags.items()} |
| |
|
| | dataset_folders = [] |
| | if "ss_dataset_dirs" in training_params: |
| | dataset_dirs = json.loads(training_params.pop("ss_dataset_dirs")) |
| | for dir, counts in dataset_dirs.items(): |
| | img_count = int(counts["img_count"]) |
| | n_repeats = int(counts["n_repeats"]) |
| | dataset_folders.append([dir, img_count, n_repeats, img_count * n_repeats]) |
| | if dataset_folders: |
| | dataset_folders.append(["(Total)", sum(r[1] for r in dataset_folders), sum(r[2] for r in dataset_folders), sum(r[3] for r in dataset_folders)]) |
| | else: |
| | dataset_folders.append(unknown_folders) |
| |
|
| | return training_params, cover_image, display_name, author, source, keywords, description, rating, tags, model_hash, legacy_hash, model_path, os.path.dirname(model_path), top_tags, dataset_folders |
| |
|
| |
|
| | def save_metadata(module, model_path, cover_image, display_name, author, source, keywords, description, rating, tags): |
| | """ |
| | Writes metadata from the Gradio components to the model file |
| | """ |
| | if model_path == "None": |
| | return "No model selected.", "", "" |
| |
|
| | if not os.path.isfile(model_path): |
| | return f"file not found: {model_path}", "", "" |
| |
|
| | if os.path.splitext(model_path)[1] != ".safetensors": |
| | return "Model is not in .safetensors format", "", "" |
| |
|
| | metadata = safetensors_hack.read_metadata(model_path) |
| | model_hash = safetensors_hack.hash_file(model_path) |
| | legacy_hash = model_util.get_legacy_hash(metadata, model_path) |
| |
|
| | |
| | |
| | |
| | cover_images = [] |
| | if cover_image is not None: |
| | cover_images.append(encode_pil_to_base64(cover_image).decode("ascii")) |
| |
|
| | |
| | |
| | |
| | |
| | updates = { |
| | "ssmd_cover_images": json.dumps(cover_images), |
| | "ssmd_display_name": display_name, |
| | "ssmd_author": author, |
| | |
| | "ssmd_source": source, |
| | "ssmd_keywords": keywords, |
| | "ssmd_description": description, |
| | "ssmd_rating": rating, |
| | "ssmd_tags": tags, |
| | "sshs_model_hash": model_hash, |
| | "sshs_legacy_hash": legacy_hash |
| | } |
| |
|
| | model_util.write_model_metadata(model_path, module, updates) |
| | if cover_image is None: |
| | delete_webui_model_preview_image(model_path) |
| | else: |
| | write_webui_model_preview_image(model_path, cover_image) |
| |
|
| | model_name = os.path.basename(model_path) |
| | return f"Model saved: {model_name}", model_hash, legacy_hash |
| |
|
| |
|
| | model_name_filter = "" |
| |
|
| |
|
| | def get_filtered_model_paths(s): |
| | if not s: |
| | return ["None"] + list(model_util.lora_models.values()) |
| |
|
| | return ["None"] + [v for v in model_util.lora_models.values() if v and s in v.lower()] |
| |
|
| |
|
| | def get_filtered_model_paths_global(): |
| | global model_name_filter |
| | return get_filtered_model_paths(model_name_filter) |
| |
|
| |
|
| | def setup_ui(addnet_paste_params): |
| | """ |
| | :dict addnet_paste_params: Dictionary of txt2img/img2img controls for each model weight slider, |
| | for sending module and model to them from the metadata editor |
| | """ |
| | can_edit = False |
| |
|
| | with gr.Row().style(equal_height=False): |
| |
|
| | |
| | with gr.Column(variant='panel'): |
| |
|
| | |
| | with gr.Row(): |
| | model_filter = gr.Textbox("", label="Model path filter", placeholder="Filter models by path name") |
| | def update_model_filter(s): |
| | global model_name_filter |
| | model_name_filter = s.strip().lower() |
| | model_filter.change(update_model_filter, inputs=[model_filter], outputs=[]) |
| | with gr.Row(): |
| | module = gr.Dropdown(["LoRA"], label="Network module", value="LoRA", interactive=True, elem_id="additional_networks_metadata_editor_module") |
| | model = gr.Dropdown(get_filtered_model_paths_global(), label="Model", value="None", interactive=True, |
| | elem_id="additional_networks_metadata_editor_model") |
| | modules.ui.create_refresh_button(model, model_util.update_models, lambda: {"choices": get_filtered_model_paths_global()}, "refresh_lora_models") |
| |
|
| | def submit_model_filter(s): |
| | global model_name_filter |
| | model_name_filter = s |
| | paths = get_filtered_model_paths(s) |
| | return gr.Dropdown.update(choices=paths, value="None") |
| | model_filter.submit(submit_model_filter, inputs=[model_filter], outputs=[model]) |
| |
|
| | |
| | with gr.Row(): |
| | model_hash = gr.Textbox("", label="Model hash", interactive=False) |
| | legacy_hash = gr.Textbox("", label="Legacy hash", interactive=False) |
| | with gr.Row(): |
| | model_path = gr.Textbox("", label="Model path", interactive=False) |
| | open_folder_button = ToolButton(value=folder_symbol, elem_id="hidden_element" if shared.cmd_opts.hide_ui_dir_config else "open_folder_metadata_editor") |
| |
|
| | |
| | for tabname in ["txt2img", "img2img"]: |
| | with gr.Row(): |
| | with gr.Box(): |
| | with gr.Row(): |
| | gr.HTML(f"Send to {tabname}:") |
| | for i in range(MAX_MODEL_COUNT): |
| | send_to_button = ToolButton(value=keycap_symbols[i], elem_id=f"additional_networks_send_to_{tabname}_{i}") |
| | send_to_button.click(fn=lambda modu, mod: (modu, model_util.find_closest_lora_model_name(mod) or "None"), inputs=[module, model], outputs=[addnet_paste_params[tabname][i]["module"], addnet_paste_params[tabname][i]["model"]]) |
| | send_to_button.click(fn=None,_js=f"addnet_switch_to_{tabname}", inputs=None, outputs=None) |
| |
|
| | |
| | with gr.Row(): |
| | with gr.Column(): |
| | gr.HTML(value="Copy metadata to other models in directory") |
| | copy_metadata_dir = gr.Textbox("", label="Containing directory", placeholder="All models in this directory will receive the selected model's metadata") |
| | with gr.Row(): |
| | copy_same_session = gr.Checkbox(True, label="Only copy to models with same session ID") |
| | copy_no_metadata = gr.Checkbox(True, label="Only copy to models with no metadata") |
| | copy_metadata_button = gr.Button("Copy Metadata", variant="primary") |
| |
|
| | |
| | with gr.Column(): |
| | with gr.Row(): |
| | display_name = gr.Textbox(value="", label="Name", placeholder="Display name for this model", interactive=can_edit) |
| | author = gr.Textbox(value="", label="Author", placeholder="Author of this model", interactive=can_edit) |
| | with gr.Row(): |
| | keywords = gr.Textbox(value="", label="Keywords", placeholder="Activation keywords, comma-separated", interactive=can_edit) |
| | with gr.Row(): |
| | description = gr.Textbox(value="", label="Description", placeholder="Model description/readme/notes/instructions", lines=15, interactive=can_edit) |
| | with gr.Row(): |
| | source = gr.Textbox(value="", label="Source", placeholder="Source URL where this model could be found", interactive=can_edit) |
| | with gr.Row(): |
| | rating = gr.Slider(minimum=0, maximum=10, step=1, label="Rating", value=0, interactive=can_edit) |
| | tags = gr.Textbox(value="", label="Tags", placeholder="Comma-separated list of tags (\"artist, style, character, 2d, 3d...\")", lines=2, interactive=can_edit) |
| | with gr.Row(): |
| | editing_enabled = gr.Checkbox(label="Editing Enabled", value=can_edit) |
| | with gr.Row(): |
| | save_metadata_button = gr.Button("Save Metadata", variant="primary", interactive=can_edit) |
| | with gr.Row(): |
| | save_output = gr.HTML("") |
| |
|
| | |
| | with gr.Column(): |
| |
|
| | |
| | with gr.Row(): |
| | cover_image = gr.Image(label="Cover image", elem_id="additional_networks_cover_image", source="upload", interactive=can_edit, type="pil", image_mode="RGBA").style(height=480) |
| |
|
| | |
| | with gr.Accordion("Image Parameters", open=False): |
| | with gr.Row(): |
| | info2 = gr.HTML() |
| | with gr.Row(): |
| | try: |
| | send_to_buttons = parameters_copypaste.create_buttons(["txt2img", "img2img", "inpaint", "extras"]) |
| | except: |
| | pass |
| |
|
| | |
| | with gr.Accordion("Training info", open=False): |
| |
|
| | |
| | with gr.Row(): |
| | max_top_tags = int(shared.opts.data.get("additional_networks_max_top_tags", 20)) |
| | most_frequent_tags = gr.Label(value={}, label="Most frequent tags in captions", num_top_classes=max_top_tags) |
| |
|
| | |
| | with gr.Row(): |
| | max_dataset_folders = int(shared.opts.data.get("additional_networks_max_dataset_folders", 20)) |
| | dataset_folders = gr.Dataframe( |
| | headers=["Name", "Image Count", "Repeats", "Total Images"], |
| | datatype=["str", "number", "number", "number"], |
| | label="Dataset folder structure", |
| | max_rows=max_dataset_folders, |
| | col_count=(4, "fixed")) |
| |
|
| | |
| | with gr.Row(): |
| | metadata_view = gr.JSON(value={}, label="Training parameters") |
| |
|
| | |
| | with gr.Row(visible=False): |
| | info1 = gr.HTML() |
| | img_file_info = gr.Textbox(label="Generate Info", interactive=False, lines=6) |
| |
|
| | open_folder_button.click(fn=lambda p: open_folder(os.path.dirname(p)), inputs=[model_path], outputs=[]) |
| | copy_metadata_button.click(fn=copy_metadata_to_all, inputs=[module, model, copy_metadata_dir, copy_same_session, copy_no_metadata, cover_image], outputs=[save_output]) |
| |
|
| | def update_editing(enabled): |
| | """ |
| | Enable/disable components based on "Editing Enabled" status |
| | """ |
| | updates = [gr.Textbox.update(interactive=enabled)] * 6 |
| | updates.append(gr.Image.update(interactive=enabled)) |
| | updates.append(gr.Slider.update(interactive=enabled)) |
| | updates.append(gr.Button.update(interactive=enabled)) |
| | return updates |
| | editing_enabled.change(fn=update_editing, inputs=[editing_enabled], outputs=[display_name, author, source, keywords, description, tags, cover_image, rating, save_metadata_button]) |
| |
|
| | cover_image.change(fn=modules.extras.run_pnginfo, inputs=[cover_image], outputs=[info1, img_file_info, info2]) |
| |
|
| | try: |
| | parameters_copypaste.bind_buttons(send_to_buttons, cover_image, img_file_info) |
| | except: |
| | pass |
| |
|
| | model.change(refresh_metadata, inputs=[module, model], outputs=[metadata_view, cover_image, display_name, author, source, keywords, description, rating, tags, model_hash, legacy_hash, model_path, copy_metadata_dir, most_frequent_tags, dataset_folders]) |
| | save_metadata_button.click(save_metadata, inputs=[module, model, cover_image, display_name, author, source, keywords, description, rating, tags], outputs=[save_output, model_hash, legacy_hash]) |
| |
|