NagisaNao commited on
Commit
b8ecd45
1 Parent(s): e560c51
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
+ )