stable-diffusion-webui
754 строки · 28.8 Кб
1import functools
2import os.path
3import urllib.parse
4from pathlib import Path
5from typing import Optional, Union
6from dataclasses import dataclass
7
8from modules import shared, ui_extra_networks_user_metadata, errors, extra_networks, util
9from modules.images import read_info_from_image, save_image_with_geninfo
10import gradio as gr
11import json
12import html
13from fastapi.exceptions import HTTPException
14
15from modules.infotext_utils import image_from_url_text
16
17extra_pages = []
18allowed_dirs = set()
19default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"]
20
21@functools.cache
22def allowed_preview_extensions_with_extra(extra_extensions=None):
23return set(default_allowed_preview_extensions) | set(extra_extensions or [])
24
25
26def allowed_preview_extensions():
27return allowed_preview_extensions_with_extra((shared.opts.samples_format, ))
28
29
30@dataclass
31class ExtraNetworksItem:
32"""Wrapper for dictionaries representing ExtraNetworks items."""
33item: dict
34
35
36def get_tree(paths: Union[str, list[str]], items: dict[str, ExtraNetworksItem]) -> dict:
37"""Recursively builds a directory tree.
38
39Args:
40paths: Path or list of paths to directories. These paths are treated as roots from which
41the tree will be built.
42items: A dictionary associating filepaths to an ExtraNetworksItem instance.
43
44Returns:
45The result directory tree.
46"""
47if isinstance(paths, (str,)):
48paths = [paths]
49
50def _get_tree(_paths: list[str], _root: str):
51_res = {}
52for path in _paths:
53relpath = os.path.relpath(path, _root)
54if os.path.isdir(path):
55dir_items = os.listdir(path)
56# Ignore empty directories.
57if not dir_items:
58continue
59dir_tree = _get_tree([os.path.join(path, x) for x in dir_items], _root)
60# We only want to store non-empty folders in the tree.
61if dir_tree:
62_res[relpath] = dir_tree
63else:
64if path not in items:
65continue
66# Add the ExtraNetworksItem to the result.
67_res[relpath] = items[path]
68return _res
69
70res = {}
71# Handle each root directory separately.
72# Each root WILL have a key/value at the root of the result dict though
73# the value can be an empty dict if the directory is empty. We want these
74# placeholders for empty dirs so we can inform the user later.
75for path in paths:
76root = os.path.dirname(path)
77relpath = os.path.relpath(path, root)
78# Wrap the path in a list since that is what the `_get_tree` expects.
79res[relpath] = _get_tree([path], root)
80if res[relpath]:
81# We need to pull the inner path out one for these root dirs.
82res[relpath] = res[relpath][relpath]
83
84return res
85
86def register_page(page):
87"""registers extra networks page for the UI; recommend doing it in on_before_ui() callback for extensions"""
88
89extra_pages.append(page)
90allowed_dirs.clear()
91allowed_dirs.update(set(sum([x.allowed_directories_for_previews() for x in extra_pages], [])))
92
93
94def fetch_file(filename: str = ""):
95from starlette.responses import FileResponse
96
97if not os.path.isfile(filename):
98raise HTTPException(status_code=404, detail="File not found")
99
100if not any(Path(x).absolute() in Path(filename).absolute().parents for x in allowed_dirs):
101raise ValueError(f"File cannot be fetched: {filename}. Must be in one of directories registered by extra pages.")
102
103ext = os.path.splitext(filename)[1].lower()[1:]
104if ext not in allowed_preview_extensions():
105raise ValueError(f"File cannot be fetched: {filename}. Extensions allowed: {allowed_preview_extensions()}.")
106
107# would profit from returning 304
108return FileResponse(filename, headers={"Accept-Ranges": "bytes"})
109
110
111def get_metadata(page: str = "", item: str = ""):
112from starlette.responses import JSONResponse
113
114page = next(iter([x for x in extra_pages if x.name == page]), None)
115if page is None:
116return JSONResponse({})
117
118metadata = page.metadata.get(item)
119if metadata is None:
120return JSONResponse({})
121
122return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)})
123
124
125def get_single_card(page: str = "", tabname: str = "", name: str = ""):
126from starlette.responses import JSONResponse
127
128page = next(iter([x for x in extra_pages if x.name == page]), None)
129
130try:
131item = page.create_item(name, enable_filter=False)
132page.items[name] = item
133except Exception as e:
134errors.display(e, "creating item for extra network")
135item = page.items.get(name)
136
137page.read_user_metadata(item, use_cache=False)
138item_html = page.create_item_html(tabname, item, shared.html("extra-networks-card.html"))
139
140return JSONResponse({"html": item_html})
141
142
143def add_pages_to_demo(app):
144app.add_api_route("/sd_extra_networks/thumb", fetch_file, methods=["GET"])
145app.add_api_route("/sd_extra_networks/metadata", get_metadata, methods=["GET"])
146app.add_api_route("/sd_extra_networks/get-single-card", get_single_card, methods=["GET"])
147
148
149def quote_js(s):
150s = s.replace('\\', '\\\\')
151s = s.replace('"', '\\"')
152return f'"{s}"'
153
154class ExtraNetworksPage:
155def __init__(self, title):
156self.title = title
157self.name = title.lower()
158# This is the actual name of the extra networks tab (not txt2img/img2img).
159self.extra_networks_tabname = self.name.replace(" ", "_")
160self.allow_prompt = True
161self.allow_negative_prompt = False
162self.metadata = {}
163self.items = {}
164self.lister = util.MassFileLister()
165# HTML Templates
166self.pane_tpl = shared.html("extra-networks-pane.html")
167self.card_tpl = shared.html("extra-networks-card.html")
168self.btn_tree_tpl = shared.html("extra-networks-tree-button.html")
169self.btn_copy_path_tpl = shared.html("extra-networks-copy-path-button.html")
170self.btn_metadata_tpl = shared.html("extra-networks-metadata-button.html")
171self.btn_edit_item_tpl = shared.html("extra-networks-edit-item-button.html")
172
173def refresh(self):
174pass
175
176def read_user_metadata(self, item, use_cache=True):
177filename = item.get("filename", None)
178metadata = extra_networks.get_user_metadata(filename, lister=self.lister if use_cache else None)
179
180desc = metadata.get("description", None)
181if desc is not None:
182item["description"] = desc
183
184item["user_metadata"] = metadata
185
186def link_preview(self, filename):
187quoted_filename = urllib.parse.quote(filename.replace('\\', '/'))
188mtime, _ = self.lister.mctime(filename)
189return f"./sd_extra_networks/thumb?filename={quoted_filename}&mtime={mtime}"
190
191def search_terms_from_path(self, filename, possible_directories=None):
192abspath = os.path.abspath(filename)
193for parentdir in (possible_directories if possible_directories is not None else self.allowed_directories_for_previews()):
194parentdir = os.path.dirname(os.path.abspath(parentdir))
195if abspath.startswith(parentdir):
196return os.path.relpath(abspath, parentdir)
197
198return ""
199
200def create_item_html(
201self,
202tabname: str,
203item: dict,
204template: Optional[str] = None,
205) -> Union[str, dict]:
206"""Generates HTML for a single ExtraNetworks Item.
207
208Args:
209tabname: The name of the active tab.
210item: Dictionary containing item information.
211template: Optional template string to use.
212
213Returns:
214If a template is passed: HTML string generated for this item.
215Can be empty if the item is not meant to be shown.
216If no template is passed: A dictionary containing the generated item's attributes.
217"""
218preview = item.get("preview", None)
219style_height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else ''
220style_width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else ''
221style_font_size = f"font-size: {shared.opts.extra_networks_card_text_scale*100}%;"
222card_style = style_height + style_width + style_font_size
223background_image = f'<img src="{html.escape(preview)}" class="preview" loading="lazy">' if preview else ''
224
225onclick = item.get("onclick", None)
226if onclick is None:
227# Don't quote prompt/neg_prompt since they are stored as js strings already.
228onclick_js_tpl = "cardClicked('{tabname}', {prompt}, {neg_prompt}, {allow_neg});"
229onclick = onclick_js_tpl.format(
230**{
231"tabname": tabname,
232"prompt": item["prompt"],
233"neg_prompt": item.get("negative_prompt", "''"),
234"allow_neg": str(self.allow_negative_prompt).lower(),
235}
236)
237onclick = html.escape(onclick)
238
239btn_copy_path = self.btn_copy_path_tpl.format(**{"filename": item["filename"]})
240btn_metadata = ""
241metadata = item.get("metadata")
242if metadata:
243btn_metadata = self.btn_metadata_tpl.format(
244**{
245"extra_networks_tabname": self.extra_networks_tabname,
246"name": html.escape(item["name"]),
247}
248)
249btn_edit_item = self.btn_edit_item_tpl.format(
250**{
251"tabname": tabname,
252"extra_networks_tabname": self.extra_networks_tabname,
253"name": html.escape(item["name"]),
254}
255)
256
257local_path = ""
258filename = item.get("filename", "")
259for reldir in self.allowed_directories_for_previews():
260absdir = os.path.abspath(reldir)
261
262if filename.startswith(absdir):
263local_path = filename[len(absdir):]
264
265# if this is true, the item must not be shown in the default view, and must instead only be
266# shown when searching for it
267if shared.opts.extra_networks_hidden_models == "Always":
268search_only = False
269else:
270search_only = "/." in local_path or "\\." in local_path
271
272if search_only and shared.opts.extra_networks_hidden_models == "Never":
273return ""
274
275sort_keys = " ".join(
276[
277f'data-sort-{k}="{html.escape(str(v))}"'
278for k, v in item.get("sort_keys", {}).items()
279]
280).strip()
281
282search_terms_html = ""
283search_term_template = "<span class='hidden {class}'>{search_term}</span>"
284for search_term in item.get("search_terms", []):
285search_terms_html += search_term_template.format(
286**{
287"class": f"search_terms{' search_only' if search_only else ''}",
288"search_term": search_term,
289}
290)
291
292description = (item.get("description", "") or "" if shared.opts.extra_networks_card_show_desc else "")
293if not shared.opts.extra_networks_card_description_is_html:
294description = html.escape(description)
295
296# Some items here might not be used depending on HTML template used.
297args = {
298"background_image": background_image,
299"card_clicked": onclick,
300"copy_path_button": btn_copy_path,
301"description": description,
302"edit_button": btn_edit_item,
303"local_preview": quote_js(item["local_preview"]),
304"metadata_button": btn_metadata,
305"name": html.escape(item["name"]),
306"prompt": item.get("prompt", None),
307"save_card_preview": html.escape(f"return saveCardPreview(event, '{tabname}', '{item['local_preview']}');"),
308"search_only": " search_only" if search_only else "",
309"search_terms": search_terms_html,
310"sort_keys": sort_keys,
311"style": card_style,
312"tabname": tabname,
313"extra_networks_tabname": self.extra_networks_tabname,
314}
315
316if template:
317return template.format(**args)
318else:
319return args
320
321def create_tree_dir_item_html(
322self,
323tabname: str,
324dir_path: str,
325content: Optional[str] = None,
326) -> Optional[str]:
327"""Generates HTML for a directory item in the tree.
328
329The generated HTML is of the format:
330```html
331<li class="tree-list-item tree-list-item--has-subitem">
332<div class="tree-list-content tree-list-content-dir"></div>
333<ul class="tree-list tree-list--subgroup">
334{content}
335</ul>
336</li>
337```
338
339Args:
340tabname: The name of the active tab.
341dir_path: Path to the directory for this item.
342content: Optional HTML string that will be wrapped by this <ul>.
343
344Returns:
345HTML formatted string.
346"""
347if not content:
348return None
349
350btn = self.btn_tree_tpl.format(
351**{
352"search_terms": "",
353"subclass": "tree-list-content-dir",
354"tabname": tabname,
355"extra_networks_tabname": self.extra_networks_tabname,
356"onclick_extra": "",
357"data_path": dir_path,
358"data_hash": "",
359"action_list_item_action_leading": "<i class='tree-list-item-action-chevron'></i>",
360"action_list_item_visual_leading": "🗀",
361"action_list_item_label": os.path.basename(dir_path),
362"action_list_item_visual_trailing": "",
363"action_list_item_action_trailing": "",
364}
365)
366ul = f"<ul class='tree-list tree-list--subgroup' hidden>{content}</ul>"
367return (
368"<li class='tree-list-item tree-list-item--has-subitem' data-tree-entry-type='dir'>"
369f"{btn}{ul}"
370"</li>"
371)
372
373def create_tree_file_item_html(self, tabname: str, file_path: str, item: dict) -> str:
374"""Generates HTML for a file item in the tree.
375
376The generated HTML is of the format:
377```html
378<li class="tree-list-item tree-list-item--subitem">
379<span data-filterable-item-text hidden></span>
380<div class="tree-list-content tree-list-content-file"></div>
381</li>
382```
383
384Args:
385tabname: The name of the active tab.
386file_path: The path to the file for this item.
387item: Dictionary containing the item information.
388
389Returns:
390HTML formatted string.
391"""
392item_html_args = self.create_item_html(tabname, item)
393action_buttons = "".join(
394[
395item_html_args["copy_path_button"],
396item_html_args["metadata_button"],
397item_html_args["edit_button"],
398]
399)
400action_buttons = f"<div class=\"button-row\">{action_buttons}</div>"
401btn = self.btn_tree_tpl.format(
402**{
403"search_terms": "",
404"subclass": "tree-list-content-file",
405"tabname": tabname,
406"extra_networks_tabname": self.extra_networks_tabname,
407"onclick_extra": item_html_args["card_clicked"],
408"data_path": file_path,
409"data_hash": item["shorthash"],
410"action_list_item_action_leading": "<i class='tree-list-item-action-chevron'></i>",
411"action_list_item_visual_leading": "🗎",
412"action_list_item_label": item["name"],
413"action_list_item_visual_trailing": "",
414"action_list_item_action_trailing": action_buttons,
415}
416)
417return (
418"<li class='tree-list-item tree-list-item--subitem' data-tree-entry-type='file'>"
419f"{btn}"
420"</li>"
421)
422
423def create_tree_view_html(self, tabname: str) -> str:
424"""Generates HTML for displaying folders in a tree view.
425
426Args:
427tabname: The name of the active tab.
428
429Returns:
430HTML string generated for this tree view.
431"""
432res = ""
433
434# Setup the tree dictionary.
435roots = self.allowed_directories_for_previews()
436tree_items = {v["filename"]: ExtraNetworksItem(v) for v in self.items.values()}
437tree = get_tree([os.path.abspath(x) for x in roots], items=tree_items)
438
439if not tree:
440return res
441
442def _build_tree(data: Optional[dict[str, ExtraNetworksItem]] = None) -> Optional[str]:
443"""Recursively builds HTML for a tree.
444
445Args:
446data: Dictionary representing a directory tree. Can be NoneType.
447Data keys should be absolute paths from the root and values
448should be subdirectory trees or an ExtraNetworksItem.
449
450Returns:
451If data is not None: HTML string
452Else: None
453"""
454if not data:
455return None
456
457# Lists for storing <li> items html for directories and files separately.
458_dir_li = []
459_file_li = []
460
461for k, v in sorted(data.items(), key=lambda x: shared.natural_sort_key(x[0])):
462if isinstance(v, (ExtraNetworksItem,)):
463_file_li.append(self.create_tree_file_item_html(tabname, k, v.item))
464else:
465_dir_li.append(self.create_tree_dir_item_html(tabname, k, _build_tree(v)))
466
467# Directories should always be displayed before files so we order them here.
468return "".join(_dir_li) + "".join(_file_li)
469
470# Add each root directory to the tree.
471for k, v in sorted(tree.items(), key=lambda x: shared.natural_sort_key(x[0])):
472item_html = self.create_tree_dir_item_html(tabname, k, _build_tree(v))
473# Only add non-empty entries to the tree.
474if item_html is not None:
475res += item_html
476
477return f"<ul class='tree-list tree-list--tree'>{res}</ul>"
478
479def create_card_view_html(self, tabname: str, *, none_message) -> str:
480"""Generates HTML for the network Card View section for a tab.
481
482This HTML goes into the `extra-networks-pane.html` <div> with
483`id='{tabname}_{extra_networks_tabname}_cards`.
484
485Args:
486tabname: The name of the active tab.
487none_message: HTML text to show when there are no cards.
488
489Returns:
490HTML formatted string.
491"""
492res = ""
493for item in self.items.values():
494res += self.create_item_html(tabname, item, self.card_tpl)
495
496if res == "":
497dirs = "".join([f"<li>{x}</li>" for x in self.allowed_directories_for_previews()])
498res = none_message or shared.html("extra-networks-no-cards.html").format(dirs=dirs)
499
500return res
501
502def create_html(self, tabname, *, empty=False):
503"""Generates an HTML string for the current pane.
504
505The generated HTML uses `extra-networks-pane.html` as a template.
506
507Args:
508tabname: The name of the active tab.
509empty: create an empty HTML page with no items
510
511Returns:
512HTML formatted string.
513"""
514self.lister.reset()
515self.metadata = {}
516
517items_list = [] if empty else self.list_items()
518self.items = {x["name"]: x for x in items_list}
519
520# Populate the instance metadata for each item.
521for item in self.items.values():
522metadata = item.get("metadata")
523if metadata:
524self.metadata[item["name"]] = metadata
525
526if "user_metadata" not in item:
527self.read_user_metadata(item)
528
529data_sortdir = shared.opts.extra_networks_card_order
530data_sortmode = shared.opts.extra_networks_card_order_field.lower().replace("sort", "").replace(" ", "_").rstrip("_").strip()
531data_sortkey = f"{data_sortmode}-{data_sortdir}-{len(self.items)}"
532tree_view_btn_extra_class = ""
533tree_view_div_extra_class = "hidden"
534if shared.opts.extra_networks_tree_view_default_enabled:
535tree_view_btn_extra_class = "extra-network-control--enabled"
536tree_view_div_extra_class = ""
537
538return self.pane_tpl.format(
539**{
540"tabname": tabname,
541"extra_networks_tabname": self.extra_networks_tabname,
542"data_sortmode": data_sortmode,
543"data_sortkey": data_sortkey,
544"data_sortdir": data_sortdir,
545"tree_view_btn_extra_class": tree_view_btn_extra_class,
546"tree_view_div_extra_class": tree_view_div_extra_class,
547"tree_html": self.create_tree_view_html(tabname),
548"items_html": self.create_card_view_html(tabname, none_message="Loading..." if empty else None),
549}
550)
551
552def create_item(self, name, index=None):
553raise NotImplementedError()
554
555def list_items(self):
556raise NotImplementedError()
557
558def allowed_directories_for_previews(self):
559return []
560
561def get_sort_keys(self, path):
562"""
563List of default keys used for sorting in the UI.
564"""
565pth = Path(path)
566mtime, ctime = self.lister.mctime(path)
567return {
568"date_created": int(mtime),
569"date_modified": int(ctime),
570"name": pth.name.lower(),
571"path": str(pth).lower(),
572}
573
574def find_preview(self, path):
575"""
576Find a preview PNG for a given path (without extension) and call link_preview on it.
577"""
578
579potential_files = sum([[f"{path}.{ext}", f"{path}.preview.{ext}"] for ext in allowed_preview_extensions()], [])
580
581for file in potential_files:
582if self.lister.exists(file):
583return self.link_preview(file)
584
585return None
586
587def find_description(self, path):
588"""
589Find and read a description file for a given path (without extension).
590"""
591for file in [f"{path}.txt", f"{path}.description.txt"]:
592if not self.lister.exists(file):
593continue
594
595try:
596with open(file, "r", encoding="utf-8", errors="replace") as f:
597return f.read()
598except OSError:
599pass
600return None
601
602def create_user_metadata_editor(self, ui, tabname):
603return ui_extra_networks_user_metadata.UserMetadataEditor(ui, tabname, self)
604
605
606def initialize():
607extra_pages.clear()
608
609
610def register_default_pages():
611from modules.ui_extra_networks_textual_inversion import ExtraNetworksPageTextualInversion
612from modules.ui_extra_networks_hypernets import ExtraNetworksPageHypernetworks
613from modules.ui_extra_networks_checkpoints import ExtraNetworksPageCheckpoints
614register_page(ExtraNetworksPageTextualInversion())
615register_page(ExtraNetworksPageHypernetworks())
616register_page(ExtraNetworksPageCheckpoints())
617
618
619class ExtraNetworksUi:
620def __init__(self):
621self.pages = None
622"""gradio HTML components related to extra networks' pages"""
623
624self.page_contents = None
625"""HTML content of the above; empty initially, filled when extra pages have to be shown"""
626
627self.stored_extra_pages = None
628
629self.button_save_preview = None
630self.preview_target_filename = None
631
632self.tabname = None
633
634
635def pages_in_preferred_order(pages):
636tab_order = [x.lower().strip() for x in shared.opts.ui_extra_networks_tab_reorder.split(",")]
637
638def tab_name_score(name):
639name = name.lower()
640for i, possible_match in enumerate(tab_order):
641if possible_match in name:
642return i
643
644return len(pages)
645
646tab_scores = {page.name: (tab_name_score(page.name), original_index) for original_index, page in enumerate(pages)}
647
648return sorted(pages, key=lambda x: tab_scores[x.name])
649
650
651def create_ui(interface: gr.Blocks, unrelated_tabs, tabname):
652ui = ExtraNetworksUi()
653ui.pages = []
654ui.pages_contents = []
655ui.user_metadata_editors = []
656ui.stored_extra_pages = pages_in_preferred_order(extra_pages.copy())
657ui.tabname = tabname
658
659related_tabs = []
660
661for page in ui.stored_extra_pages:
662with gr.Tab(page.title, elem_id=f"{tabname}_{page.extra_networks_tabname}", elem_classes=["extra-page"]) as tab:
663with gr.Column(elem_id=f"{tabname}_{page.extra_networks_tabname}_prompts", elem_classes=["extra-page-prompts"]):
664pass
665
666elem_id = f"{tabname}_{page.extra_networks_tabname}_cards_html"
667page_elem = gr.HTML(page.create_html(tabname, empty=True), elem_id=elem_id)
668ui.pages.append(page_elem)
669editor = page.create_user_metadata_editor(ui, tabname)
670editor.create_ui()
671ui.user_metadata_editors.append(editor)
672related_tabs.append(tab)
673
674ui.button_save_preview = gr.Button('Save preview', elem_id=f"{tabname}_save_preview", visible=False)
675ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=f"{tabname}_preview_filename", visible=False)
676
677for tab in unrelated_tabs:
678tab.select(fn=None, _js=f"function(){{extraNetworksUnrelatedTabSelected('{tabname}');}}", inputs=[], outputs=[], show_progress=False)
679
680for page, tab in zip(ui.stored_extra_pages, related_tabs):
681jscode = (
682"function(){{"
683f"extraNetworksTabSelected('{tabname}', '{tabname}_{page.extra_networks_tabname}_prompts', {str(page.allow_prompt).lower()}, {str(page.allow_negative_prompt).lower()}, '{tabname}_{page.extra_networks_tabname}');"
684f"applyExtraNetworkFilter('{tabname}_{page.extra_networks_tabname}');"
685"}}"
686)
687tab.select(fn=None, _js=jscode, inputs=[], outputs=[], show_progress=False)
688
689def refresh():
690for pg in ui.stored_extra_pages:
691pg.refresh()
692create_html()
693return ui.pages_contents
694
695button_refresh = gr.Button("Refresh", elem_id=f"{tabname}_{page.extra_networks_tabname}_extra_refresh_internal", visible=False)
696button_refresh.click(fn=refresh, inputs=[], outputs=ui.pages).then(fn=lambda: None, _js="function(){ " + f"applyExtraNetworkFilter('{tabname}_{page.extra_networks_tabname}');" + " }")
697
698def create_html():
699ui.pages_contents = [pg.create_html(ui.tabname) for pg in ui.stored_extra_pages]
700
701def pages_html():
702if not ui.pages_contents:
703create_html()
704return ui.pages_contents
705
706interface.load(fn=pages_html, inputs=[], outputs=ui.pages)
707
708return ui
709
710
711def path_is_parent(parent_path, child_path):
712parent_path = os.path.abspath(parent_path)
713child_path = os.path.abspath(child_path)
714
715return child_path.startswith(parent_path)
716
717
718def setup_ui(ui, gallery):
719def save_preview(index, images, filename):
720# this function is here for backwards compatibility and likely will be removed soon
721
722if len(images) == 0:
723print("There is no image in gallery to save as a preview.")
724return [page.create_html(ui.tabname) for page in ui.stored_extra_pages]
725
726index = int(index)
727index = 0 if index < 0 else index
728index = len(images) - 1 if index >= len(images) else index
729
730img_info = images[index if index >= 0 else 0]
731image = image_from_url_text(img_info)
732geninfo, items = read_info_from_image(image)
733
734is_allowed = False
735for extra_page in ui.stored_extra_pages:
736if any(path_is_parent(x, filename) for x in extra_page.allowed_directories_for_previews()):
737is_allowed = True
738break
739
740assert is_allowed, f'writing to {filename} is not allowed'
741
742save_image_with_geninfo(image, geninfo, filename)
743
744return [page.create_html(ui.tabname) for page in ui.stored_extra_pages]
745
746ui.button_save_preview.click(
747fn=save_preview,
748_js="function(x, y, z){return [selected_gallery_index(), y, z]}",
749inputs=[ui.preview_target_filename, gallery, ui.preview_target_filename],
750outputs=[*ui.pages]
751)
752
753for editor in ui.user_metadata_editors:
754editor.setup_ui(gallery)
755