:3
Browse files
extensions/Additional-Networks/fix/metadata_editor.py
ADDED
@@ -0,0 +1,623 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import sys
|
4 |
+
import io
|
5 |
+
import base64
|
6 |
+
import platform
|
7 |
+
import subprocess as sp
|
8 |
+
from PIL import PngImagePlugin, Image
|
9 |
+
|
10 |
+
from modules import shared
|
11 |
+
import gradio as gr
|
12 |
+
|
13 |
+
import modules.ui
|
14 |
+
from modules.ui_components import ToolButton
|
15 |
+
import modules.extras
|
16 |
+
import modules.generation_parameters_copypaste as parameters_copypaste
|
17 |
+
|
18 |
+
from scripts import safetensors_hack, model_util
|
19 |
+
from scripts.model_util import MAX_MODEL_COUNT
|
20 |
+
|
21 |
+
|
22 |
+
folder_symbol = "\U0001f4c2" # 📂
|
23 |
+
keycap_symbols = [
|
24 |
+
"\u0031\ufe0f\u20e3", # 1️⃣
|
25 |
+
"\u0032\ufe0f\u20e3", # 2️⃣
|
26 |
+
"\u0033\ufe0f\u20e3", # 3️⃣
|
27 |
+
"\u0034\ufe0f\u20e3", # 4️⃣
|
28 |
+
"\u0035\ufe0f\u20e3", # 5️⃣
|
29 |
+
"\u0036\ufe0f\u20e3", # 6️⃣
|
30 |
+
"\u0037\ufe0f\u20e3", # 7️⃣
|
31 |
+
"\u0038\ufe0f\u20e3", # 8️
|
32 |
+
"\u0039\ufe0f\u20e3", # 9️
|
33 |
+
"\u1f51f", # 🔟
|
34 |
+
]
|
35 |
+
|
36 |
+
|
37 |
+
def write_webui_model_preview_image(model_path, image):
|
38 |
+
basename, ext = os.path.splitext(model_path)
|
39 |
+
preview_path = f"{basename}.png"
|
40 |
+
|
41 |
+
# Copy any text-only metadata
|
42 |
+
use_metadata = False
|
43 |
+
metadata = PngImagePlugin.PngInfo()
|
44 |
+
for key, value in image.info.items():
|
45 |
+
if isinstance(key, str) and isinstance(value, str):
|
46 |
+
metadata.add_text(key, value)
|
47 |
+
use_metadata = True
|
48 |
+
|
49 |
+
image.save(preview_path, "PNG", pnginfo=(metadata if use_metadata else None))
|
50 |
+
|
51 |
+
|
52 |
+
def delete_webui_model_preview_image(model_path):
|
53 |
+
basename, ext = os.path.splitext(model_path)
|
54 |
+
preview_paths = [f"{basename}.preview.png", f"{basename}.png"]
|
55 |
+
|
56 |
+
for preview_path in preview_paths:
|
57 |
+
if os.path.isfile(preview_path):
|
58 |
+
os.unlink(preview_path)
|
59 |
+
|
60 |
+
|
61 |
+
def decode_base64_to_pil(encoding):
|
62 |
+
if encoding.startswith("data:image/"):
|
63 |
+
encoding = encoding.split(";")[1].split(",")[1]
|
64 |
+
return Image.open(io.BytesIO(base64.b64decode(encoding)))
|
65 |
+
|
66 |
+
|
67 |
+
def encode_pil_to_base64(image):
|
68 |
+
with io.BytesIO() as output_bytes:
|
69 |
+
# Copy any text-only metadata
|
70 |
+
use_metadata = False
|
71 |
+
metadata = PngImagePlugin.PngInfo()
|
72 |
+
for key, value in image.info.items():
|
73 |
+
if isinstance(key, str) and isinstance(value, str):
|
74 |
+
metadata.add_text(key, value)
|
75 |
+
use_metadata = True
|
76 |
+
|
77 |
+
image.save(output_bytes, "PNG", pnginfo=(metadata if use_metadata else None))
|
78 |
+
bytes_data = output_bytes.getvalue()
|
79 |
+
return base64.b64encode(bytes_data)
|
80 |
+
|
81 |
+
|
82 |
+
def open_folder(f):
|
83 |
+
if not os.path.exists(f):
|
84 |
+
print(f'Folder "{f}" does not exist. After you create an image, the folder will be created.')
|
85 |
+
return
|
86 |
+
elif not os.path.isdir(f):
|
87 |
+
print(
|
88 |
+
f"""
|
89 |
+
WARNING
|
90 |
+
An open_folder request was made with an argument that is not a folder.
|
91 |
+
This could be an error or a malicious attempt to run code on your computer.
|
92 |
+
Requested path was: {f}
|
93 |
+
""",
|
94 |
+
file=sys.stderr,
|
95 |
+
)
|
96 |
+
return
|
97 |
+
|
98 |
+
if not shared.cmd_opts.hide_ui_dir_config:
|
99 |
+
path = os.path.normpath(f)
|
100 |
+
if platform.system() == "Windows":
|
101 |
+
os.startfile(path)
|
102 |
+
elif platform.system() == "Darwin":
|
103 |
+
sp.Popen(["open", path])
|
104 |
+
elif "microsoft-standard-WSL2" in platform.uname().release:
|
105 |
+
sp.Popen(["wsl-open", path])
|
106 |
+
else:
|
107 |
+
sp.Popen(["xdg-open", path])
|
108 |
+
|
109 |
+
|
110 |
+
def copy_metadata_to_all(module, model_path, copy_dir, same_session_only, missing_meta_only, cover_image):
|
111 |
+
"""
|
112 |
+
Given a model with metadata, copies that metadata to all models in copy_dir.
|
113 |
+
|
114 |
+
:str module: Module name ("LoRA")
|
115 |
+
:str model: Model key in lora_models ("MyModel(123456abcdef)")
|
116 |
+
:str copy_dir: Directory to copy to
|
117 |
+
:bool same_session_only: Only copy to modules with the same ss_session_id
|
118 |
+
:bool missing_meta_only: Only copy to modules that are missing user metadata
|
119 |
+
:Optional[Image] cover_image: Cover image to embed in the file as base64
|
120 |
+
:returns: gr.HTML.update()
|
121 |
+
"""
|
122 |
+
if model_path == "None":
|
123 |
+
return "No model selected."
|
124 |
+
|
125 |
+
if not os.path.isfile(model_path):
|
126 |
+
return f"Model path not found: {model_path}"
|
127 |
+
|
128 |
+
model_path = os.path.realpath(model_path)
|
129 |
+
|
130 |
+
if os.path.splitext(model_path)[1] != ".safetensors":
|
131 |
+
return "Model is not in .safetensors format."
|
132 |
+
|
133 |
+
if not os.path.isdir(copy_dir):
|
134 |
+
return "Please provide a directory containing models in .safetensors format."
|
135 |
+
|
136 |
+
print(f"[MetadataEditor] Copying metadata to models in {copy_dir}.")
|
137 |
+
metadata = model_util.read_model_metadata(model_path, module)
|
138 |
+
count = 0
|
139 |
+
for entry in os.scandir(copy_dir):
|
140 |
+
if entry.is_file():
|
141 |
+
path = os.path.realpath(os.path.join(copy_dir, entry.name))
|
142 |
+
if path != model_path and model_util.is_safetensors(path):
|
143 |
+
if same_session_only:
|
144 |
+
other_metadata = safetensors_hack.read_metadata(path)
|
145 |
+
if missing_meta_only and other_metadata.get("ssmd_display_name", "").strip():
|
146 |
+
print(f"[MetadataEditor] Skipping {path} as it already has metadata")
|
147 |
+
continue
|
148 |
+
|
149 |
+
session_id = metadata.get("ss_session_id", None)
|
150 |
+
other_session_id = other_metadata.get("ss_session_id", None)
|
151 |
+
if session_id is None or other_session_id is None or session_id != other_session_id:
|
152 |
+
continue
|
153 |
+
|
154 |
+
updates = {
|
155 |
+
"ssmd_cover_images": "[]",
|
156 |
+
"ssmd_display_name": "",
|
157 |
+
"ssmd_version": "",
|
158 |
+
"ssmd_keywords": "",
|
159 |
+
"ssmd_author": "",
|
160 |
+
"ssmd_source": "",
|
161 |
+
"ssmd_description": "",
|
162 |
+
"ssmd_rating": "0",
|
163 |
+
"ssmd_tags": "",
|
164 |
+
}
|
165 |
+
|
166 |
+
for k, v in metadata.items():
|
167 |
+
if k.startswith("ssmd_") and k != "ssmd_cover_images":
|
168 |
+
updates[k] = v
|
169 |
+
|
170 |
+
model_util.write_model_metadata(path, module, updates)
|
171 |
+
count += 1
|
172 |
+
|
173 |
+
print(f"[MetadataEditor] Updated {count} models in directory {copy_dir}.")
|
174 |
+
return f"Updated {count} models in directory {copy_dir}."
|
175 |
+
|
176 |
+
|
177 |
+
def load_cover_image(model_path, metadata):
|
178 |
+
"""
|
179 |
+
Loads a cover image either from embedded metadata or an image file with
|
180 |
+
.preview.png/.png format
|
181 |
+
"""
|
182 |
+
cover_images = json.loads(metadata.get("ssmd_cover_images", "[]"))
|
183 |
+
cover_image = None
|
184 |
+
if len(cover_images) > 0:
|
185 |
+
print("[MetadataEditor] Loading embedded cover image.")
|
186 |
+
cover_image = decode_base64_to_pil(cover_images[0])
|
187 |
+
else:
|
188 |
+
basename, ext = os.path.splitext(model_path)
|
189 |
+
|
190 |
+
preview_paths = [f"{basename}.preview.png", f"{basename}.png"]
|
191 |
+
|
192 |
+
for preview_path in preview_paths:
|
193 |
+
if os.path.isfile(preview_path):
|
194 |
+
print(f"[MetadataEditor] Loading webui preview image: {preview_path}")
|
195 |
+
cover_image = Image.open(preview_path)
|
196 |
+
|
197 |
+
return cover_image
|
198 |
+
|
199 |
+
|
200 |
+
# Dummy value since gr.Dataframe cannot handle an empty list
|
201 |
+
# https://github.com/gradio-app/gradio/issues/3182
|
202 |
+
unknown_folders = ["(Unknown)", 0, 0, 0]
|
203 |
+
|
204 |
+
|
205 |
+
def refresh_metadata(module, model_path):
|
206 |
+
"""
|
207 |
+
Reads metadata from the model on disk and updates all Gradio components
|
208 |
+
"""
|
209 |
+
if model_path == "None":
|
210 |
+
return {}, None, "", "", "", "", "", 0, "", "", "", "", "", {}, [unknown_folders]
|
211 |
+
|
212 |
+
if not os.path.isfile(model_path):
|
213 |
+
return (
|
214 |
+
{"info": f"Model path not found: {model_path}"},
|
215 |
+
None,
|
216 |
+
"",
|
217 |
+
"",
|
218 |
+
"",
|
219 |
+
"",
|
220 |
+
"",
|
221 |
+
0,
|
222 |
+
"",
|
223 |
+
"",
|
224 |
+
"",
|
225 |
+
"",
|
226 |
+
"",
|
227 |
+
{},
|
228 |
+
[unknown_folders],
|
229 |
+
)
|
230 |
+
|
231 |
+
if os.path.splitext(model_path)[1] != ".safetensors":
|
232 |
+
return (
|
233 |
+
{"info": "Model is not in .safetensors format."},
|
234 |
+
None,
|
235 |
+
"",
|
236 |
+
"",
|
237 |
+
"",
|
238 |
+
"",
|
239 |
+
"",
|
240 |
+
0,
|
241 |
+
"",
|
242 |
+
"",
|
243 |
+
"",
|
244 |
+
"",
|
245 |
+
"",
|
246 |
+
{},
|
247 |
+
[unknown_folders],
|
248 |
+
)
|
249 |
+
|
250 |
+
metadata = model_util.read_model_metadata(model_path, module)
|
251 |
+
|
252 |
+
if metadata is None:
|
253 |
+
training_params = {}
|
254 |
+
metadata = {}
|
255 |
+
else:
|
256 |
+
training_params = {k: v for k, v in metadata.items() if k.startswith("ss_")}
|
257 |
+
|
258 |
+
cover_image = load_cover_image(model_path, metadata)
|
259 |
+
|
260 |
+
display_name = metadata.get("ssmd_display_name", "")
|
261 |
+
author = metadata.get("ssmd_author", "")
|
262 |
+
# version = metadata.get("ssmd_version", "")
|
263 |
+
source = metadata.get("ssmd_source", "")
|
264 |
+
keywords = metadata.get("ssmd_keywords", "")
|
265 |
+
description = metadata.get("ssmd_description", "")
|
266 |
+
rating = int(metadata.get("ssmd_rating", "0"))
|
267 |
+
tags = metadata.get("ssmd_tags", "")
|
268 |
+
model_hash = metadata.get("sshs_model_hash", model_util.cache("hashes").get(model_path, {}).get("model", ""))
|
269 |
+
legacy_hash = metadata.get("sshs_legacy_hash", model_util.cache("hashes").get(model_path, {}).get("legacy", ""))
|
270 |
+
|
271 |
+
top_tags = {}
|
272 |
+
if "ss_tag_frequency" in training_params:
|
273 |
+
tag_frequency = json.loads(training_params.pop("ss_tag_frequency"))
|
274 |
+
count_max = 0
|
275 |
+
for dir, frequencies in tag_frequency.items():
|
276 |
+
for tag, count in frequencies.items():
|
277 |
+
tag = tag.strip()
|
278 |
+
existing = top_tags.get(tag, 0)
|
279 |
+
top_tags[tag] = count + existing
|
280 |
+
if len(top_tags) > 0:
|
281 |
+
top_tags = dict(sorted(top_tags.items(), key=lambda x: x[1], reverse=True))
|
282 |
+
|
283 |
+
count_max = max(top_tags.values())
|
284 |
+
top_tags = {k: float(v / count_max) for k, v in top_tags.items()}
|
285 |
+
|
286 |
+
dataset_folders = []
|
287 |
+
if "ss_dataset_dirs" in training_params:
|
288 |
+
dataset_dirs = json.loads(training_params.pop("ss_dataset_dirs"))
|
289 |
+
for dir, counts in dataset_dirs.items():
|
290 |
+
img_count = int(counts["img_count"])
|
291 |
+
n_repeats = int(counts["n_repeats"])
|
292 |
+
dataset_folders.append([dir, img_count, n_repeats, img_count * n_repeats])
|
293 |
+
if dataset_folders:
|
294 |
+
dataset_folders.append(
|
295 |
+
["(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)]
|
296 |
+
)
|
297 |
+
else:
|
298 |
+
dataset_folders.append(unknown_folders)
|
299 |
+
|
300 |
+
return (
|
301 |
+
training_params,
|
302 |
+
cover_image,
|
303 |
+
display_name,
|
304 |
+
author,
|
305 |
+
source,
|
306 |
+
keywords,
|
307 |
+
description,
|
308 |
+
rating,
|
309 |
+
tags,
|
310 |
+
model_hash,
|
311 |
+
legacy_hash,
|
312 |
+
model_path,
|
313 |
+
os.path.dirname(model_path),
|
314 |
+
top_tags,
|
315 |
+
dataset_folders,
|
316 |
+
)
|
317 |
+
|
318 |
+
|
319 |
+
def save_metadata(module, model_path, cover_image, display_name, author, source, keywords, description, rating, tags):
|
320 |
+
"""
|
321 |
+
Writes metadata from the Gradio components to the model file
|
322 |
+
"""
|
323 |
+
if model_path == "None":
|
324 |
+
return "No model selected.", "", ""
|
325 |
+
|
326 |
+
if not os.path.isfile(model_path):
|
327 |
+
return f"file not found: {model_path}", "", ""
|
328 |
+
|
329 |
+
if os.path.splitext(model_path)[1] != ".safetensors":
|
330 |
+
return "Model is not in .safetensors format", "", ""
|
331 |
+
|
332 |
+
metadata = safetensors_hack.read_metadata(model_path)
|
333 |
+
model_hash = safetensors_hack.hash_file(model_path)
|
334 |
+
legacy_hash = model_util.get_legacy_hash(metadata, model_path)
|
335 |
+
|
336 |
+
# TODO: Support multiple images
|
337 |
+
# Blocked on gradio not having a gallery upload option
|
338 |
+
# https://github.com/gradio-app/gradio/issues/1379
|
339 |
+
cover_images = []
|
340 |
+
if cover_image is not None:
|
341 |
+
cover_images.append(encode_pil_to_base64(cover_image).decode("ascii"))
|
342 |
+
|
343 |
+
# NOTE: User-specified metadata should NOT be prefixed with "ss_". This is
|
344 |
+
# to maintain backwards compatibility with the old hashing method. "ss_"
|
345 |
+
# should be used for training parameters that will never be manually
|
346 |
+
# updated on the model.
|
347 |
+
updates = {
|
348 |
+
"ssmd_cover_images": json.dumps(cover_images),
|
349 |
+
"ssmd_display_name": display_name,
|
350 |
+
"ssmd_author": author,
|
351 |
+
# "ssmd_version": version,
|
352 |
+
"ssmd_source": source,
|
353 |
+
"ssmd_keywords": keywords,
|
354 |
+
"ssmd_description": description,
|
355 |
+
"ssmd_rating": rating,
|
356 |
+
"ssmd_tags": tags,
|
357 |
+
"sshs_model_hash": model_hash,
|
358 |
+
"sshs_legacy_hash": legacy_hash,
|
359 |
+
}
|
360 |
+
|
361 |
+
model_util.write_model_metadata(model_path, module, updates)
|
362 |
+
if cover_image is None:
|
363 |
+
delete_webui_model_preview_image(model_path)
|
364 |
+
else:
|
365 |
+
write_webui_model_preview_image(model_path, cover_image)
|
366 |
+
|
367 |
+
model_name = os.path.basename(model_path)
|
368 |
+
return f"Model saved: {model_name}", model_hash, legacy_hash
|
369 |
+
|
370 |
+
|
371 |
+
model_name_filter = ""
|
372 |
+
|
373 |
+
|
374 |
+
def get_filtered_model_paths(s):
|
375 |
+
# newer Gradio seems to show None in the list?
|
376 |
+
# if not s:
|
377 |
+
# return ["None"] + list(model_util.lora_models.values())
|
378 |
+
# return ["None"] + [v for v in model_util.lora_models.values() if v and s in v.lower()]
|
379 |
+
if not s:
|
380 |
+
l = list(model_util.lora_models.values())
|
381 |
+
else:
|
382 |
+
l = [v for v in model_util.lora_models.values() if v and s in v.lower()]
|
383 |
+
l = [v for v in l if v] # remove None
|
384 |
+
l = ["None"] + l
|
385 |
+
return l
|
386 |
+
|
387 |
+
def get_filtered_model_paths_global():
|
388 |
+
global model_name_filter
|
389 |
+
return get_filtered_model_paths(model_name_filter)
|
390 |
+
|
391 |
+
|
392 |
+
def setup_ui(addnet_paste_params):
|
393 |
+
"""
|
394 |
+
:dict addnet_paste_params: Dictionary of txt2img/img2img controls for each model weight slider,
|
395 |
+
for sending module and model to them from the metadata editor
|
396 |
+
"""
|
397 |
+
can_edit = False
|
398 |
+
|
399 |
+
with gr.Row(equal_height=True):
|
400 |
+
# Lefthand column
|
401 |
+
with gr.Column(variant="panel"):
|
402 |
+
# Module and model selector
|
403 |
+
with gr.Row():
|
404 |
+
model_filter = gr.Textbox("", label="Model path filter", placeholder="Filter models by path name")
|
405 |
+
|
406 |
+
def update_model_filter(s):
|
407 |
+
global model_name_filter
|
408 |
+
model_name_filter = s.strip().lower()
|
409 |
+
|
410 |
+
model_filter.change(update_model_filter, inputs=[model_filter], outputs=[])
|
411 |
+
with gr.Row():
|
412 |
+
module = gr.Dropdown(
|
413 |
+
["LoRA"],
|
414 |
+
label="Network module",
|
415 |
+
value="LoRA",
|
416 |
+
interactive=True,
|
417 |
+
elem_id="additional_networks_metadata_editor_module",
|
418 |
+
)
|
419 |
+
model = gr.Dropdown(
|
420 |
+
get_filtered_model_paths_global(),
|
421 |
+
label="Model",
|
422 |
+
value="None",
|
423 |
+
interactive=True,
|
424 |
+
elem_id="additional_networks_metadata_editor_model",
|
425 |
+
)
|
426 |
+
modules.ui.create_refresh_button(
|
427 |
+
model, model_util.update_models, lambda: {"choices": get_filtered_model_paths_global()}, "refresh_lora_models"
|
428 |
+
)
|
429 |
+
|
430 |
+
def submit_model_filter(s):
|
431 |
+
global model_name_filter
|
432 |
+
model_name_filter = s
|
433 |
+
paths = get_filtered_model_paths(s)
|
434 |
+
return gr.Dropdown.update(choices=paths, value="None")
|
435 |
+
|
436 |
+
model_filter.submit(submit_model_filter, inputs=[model_filter], outputs=[model])
|
437 |
+
|
438 |
+
# Model hashes and path
|
439 |
+
with gr.Row():
|
440 |
+
model_hash = gr.Textbox("", label="Model hash", interactive=False)
|
441 |
+
legacy_hash = gr.Textbox("", label="Legacy hash", interactive=False)
|
442 |
+
with gr.Row():
|
443 |
+
model_path = gr.Textbox("", label="Model path", interactive=False)
|
444 |
+
open_folder_button = ToolButton(
|
445 |
+
value=folder_symbol,
|
446 |
+
elem_id="hidden_element" if shared.cmd_opts.hide_ui_dir_config else "open_folder_metadata_editor",
|
447 |
+
)
|
448 |
+
|
449 |
+
# Send to txt2img/img2img buttons
|
450 |
+
for tabname in ["txt2img", "img2img"]:
|
451 |
+
with gr.Row():
|
452 |
+
with gr.Box():
|
453 |
+
with gr.Row():
|
454 |
+
gr.HTML(f"Send to {tabname}:")
|
455 |
+
for i in range(MAX_MODEL_COUNT):
|
456 |
+
send_to_button = ToolButton(
|
457 |
+
value=keycap_symbols[i], elem_id=f"additional_networks_send_to_{tabname}_{i}"
|
458 |
+
)
|
459 |
+
send_to_button.click(
|
460 |
+
fn=lambda modu, mod: (modu, model_util.find_closest_lora_model_name(mod) or "None"),
|
461 |
+
inputs=[module, model],
|
462 |
+
outputs=[addnet_paste_params[tabname][i]["module"], addnet_paste_params[tabname][i]["model"]],
|
463 |
+
)
|
464 |
+
send_to_button.click(fn=None, _js=f"addnet_switch_to_{tabname}", inputs=None, outputs=None)
|
465 |
+
|
466 |
+
# "Copy metadata to other models" panel
|
467 |
+
with gr.Row():
|
468 |
+
with gr.Column():
|
469 |
+
gr.HTML(value="Copy metadata to other models in directory")
|
470 |
+
copy_metadata_dir = gr.Textbox(
|
471 |
+
"",
|
472 |
+
label="Containing directory",
|
473 |
+
placeholder="All models in this directory will receive the selected model's metadata",
|
474 |
+
)
|
475 |
+
with gr.Row():
|
476 |
+
copy_same_session = gr.Checkbox(True, label="Only copy to models with same session ID")
|
477 |
+
copy_no_metadata = gr.Checkbox(True, label="Only copy to models with no metadata")
|
478 |
+
copy_metadata_button = gr.Button("Copy Metadata", variant="primary")
|
479 |
+
|
480 |
+
# Center column, metadata viewer/editor
|
481 |
+
with gr.Column():
|
482 |
+
with gr.Row():
|
483 |
+
display_name = gr.Textbox(value="", label="Name", placeholder="Display name for this model", interactive=can_edit)
|
484 |
+
author = gr.Textbox(value="", label="Author", placeholder="Author of this model", interactive=can_edit)
|
485 |
+
with gr.Row():
|
486 |
+
keywords = gr.Textbox(
|
487 |
+
value="", label="Keywords", placeholder="Activation keywords, comma-separated", interactive=can_edit
|
488 |
+
)
|
489 |
+
with gr.Row():
|
490 |
+
description = gr.Textbox(
|
491 |
+
value="",
|
492 |
+
label="Description",
|
493 |
+
placeholder="Model description/readme/notes/instructions",
|
494 |
+
lines=15,
|
495 |
+
interactive=can_edit,
|
496 |
+
)
|
497 |
+
with gr.Row():
|
498 |
+
source = gr.Textbox(
|
499 |
+
value="", label="Source", placeholder="Source URL where this model could be found", interactive=can_edit
|
500 |
+
)
|
501 |
+
with gr.Row():
|
502 |
+
rating = gr.Slider(minimum=0, maximum=10, step=1, label="Rating", value=0, interactive=can_edit)
|
503 |
+
tags = gr.Textbox(
|
504 |
+
value="",
|
505 |
+
label="Tags",
|
506 |
+
placeholder='Comma-separated list of tags ("artist, style, character, 2d, 3d...")',
|
507 |
+
lines=2,
|
508 |
+
interactive=can_edit,
|
509 |
+
)
|
510 |
+
with gr.Row():
|
511 |
+
editing_enabled = gr.Checkbox(label="Editing Enabled", value=can_edit)
|
512 |
+
with gr.Row():
|
513 |
+
save_metadata_button = gr.Button("Save Metadata", variant="primary", interactive=can_edit)
|
514 |
+
with gr.Row():
|
515 |
+
save_output = gr.HTML("")
|
516 |
+
|
517 |
+
# Righthand column, cover image and training parameters view
|
518 |
+
with gr.Column():
|
519 |
+
# Cover image
|
520 |
+
with gr.Row():
|
521 |
+
cover_image = gr.Image(
|
522 |
+
label="Cover image",
|
523 |
+
elem_id="additional_networks_cover_image",
|
524 |
+
source="upload",
|
525 |
+
interactive=can_edit,
|
526 |
+
type="pil",
|
527 |
+
image_mode="RGBA",
|
528 |
+
height=480
|
529 |
+
)
|
530 |
+
|
531 |
+
# Image parameters
|
532 |
+
with gr.Accordion("Image Parameters", open=False):
|
533 |
+
with gr.Row():
|
534 |
+
info2 = gr.HTML()
|
535 |
+
with gr.Row():
|
536 |
+
try:
|
537 |
+
send_to_buttons = parameters_copypaste.create_buttons(["txt2img", "img2img", "inpaint", "extras"])
|
538 |
+
except:
|
539 |
+
pass
|
540 |
+
|
541 |
+
# Training info, below cover image
|
542 |
+
with gr.Accordion("Training info", open=False):
|
543 |
+
# Top tags used
|
544 |
+
with gr.Row():
|
545 |
+
max_top_tags = int(shared.opts.data.get("additional_networks_max_top_tags", 20))
|
546 |
+
most_frequent_tags = gr.Label(value={}, label="Most frequent tags in captions", num_top_classes=max_top_tags)
|
547 |
+
|
548 |
+
# Dataset folders
|
549 |
+
with gr.Row():
|
550 |
+
max_dataset_folders = int(shared.opts.data.get("additional_networks_max_dataset_folders", 20))
|
551 |
+
dataset_folders = gr.Dataframe(
|
552 |
+
headers=["Name", "Image Count", "Repeats", "Total Images"],
|
553 |
+
datatype=["str", "number", "number", "number"],
|
554 |
+
label="Dataset folder structure",
|
555 |
+
max_rows=max_dataset_folders,
|
556 |
+
col_count=(4, "fixed"),
|
557 |
+
)
|
558 |
+
|
559 |
+
# Training Parameters
|
560 |
+
with gr.Row():
|
561 |
+
metadata_view = gr.JSON(value={}, label="Training parameters")
|
562 |
+
|
563 |
+
# Hidden/internal
|
564 |
+
with gr.Row(visible=False):
|
565 |
+
info1 = gr.HTML()
|
566 |
+
img_file_info = gr.Textbox(label="Generate Info", interactive=False, lines=6)
|
567 |
+
|
568 |
+
open_folder_button.click(fn=lambda p: open_folder(os.path.dirname(p)), inputs=[model_path], outputs=[])
|
569 |
+
copy_metadata_button.click(
|
570 |
+
fn=copy_metadata_to_all,
|
571 |
+
inputs=[module, model, copy_metadata_dir, copy_same_session, copy_no_metadata, cover_image],
|
572 |
+
outputs=[save_output],
|
573 |
+
)
|
574 |
+
|
575 |
+
def update_editing(enabled):
|
576 |
+
"""
|
577 |
+
Enable/disable components based on "Editing Enabled" status
|
578 |
+
"""
|
579 |
+
updates = [gr.Textbox.update(interactive=enabled)] * 6
|
580 |
+
updates.append(gr.Image.update(interactive=enabled))
|
581 |
+
updates.append(gr.Slider.update(interactive=enabled))
|
582 |
+
updates.append(gr.Button.update(interactive=enabled))
|
583 |
+
return updates
|
584 |
+
|
585 |
+
editing_enabled.change(
|
586 |
+
fn=update_editing,
|
587 |
+
inputs=[editing_enabled],
|
588 |
+
outputs=[display_name, author, source, keywords, description, tags, cover_image, rating, save_metadata_button],
|
589 |
+
)
|
590 |
+
|
591 |
+
cover_image.change(fn=modules.extras.run_pnginfo, inputs=[cover_image], outputs=[info1, img_file_info, info2])
|
592 |
+
|
593 |
+
try:
|
594 |
+
parameters_copypaste.bind_buttons(send_to_buttons, cover_image, img_file_info)
|
595 |
+
except:
|
596 |
+
pass
|
597 |
+
|
598 |
+
model.change(
|
599 |
+
refresh_metadata,
|
600 |
+
inputs=[module, model],
|
601 |
+
outputs=[
|
602 |
+
metadata_view,
|
603 |
+
cover_image,
|
604 |
+
display_name,
|
605 |
+
author,
|
606 |
+
source,
|
607 |
+
keywords,
|
608 |
+
description,
|
609 |
+
rating,
|
610 |
+
tags,
|
611 |
+
model_hash,
|
612 |
+
legacy_hash,
|
613 |
+
model_path,
|
614 |
+
copy_metadata_dir,
|
615 |
+
most_frequent_tags,
|
616 |
+
dataset_folders,
|
617 |
+
],
|
618 |
+
)
|
619 |
+
save_metadata_button.click(
|
620 |
+
save_metadata,
|
621 |
+
inputs=[module, model, cover_image, display_name, author, source, keywords, description, rating, tags],
|
622 |
+
outputs=[save_output, model_hash, legacy_hash],
|
623 |
+
)
|