ui_common.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import csv
  2. import dataclasses
  3. import json
  4. import html
  5. import os
  6. import gradio as gr
  7. from modules import call_queue, shared, ui_tempdir, util
  8. from modules.infotext_utils import image_from_url_text
  9. import modules.images
  10. from modules.ui_components import ToolButton
  11. import modules.infotext_utils as parameters_copypaste
  12. folder_symbol = '\U0001f4c2' # 📂
  13. refresh_symbol = '\U0001f504' # 🔄
  14. def update_generation_info(generation_info, html_info, img_index):
  15. try:
  16. generation_info = json.loads(generation_info)
  17. if img_index < 0 or img_index >= len(generation_info["infotexts"]):
  18. return html_info, gr.update()
  19. return plaintext_to_html(generation_info["infotexts"][img_index]), gr.update()
  20. except Exception:
  21. pass
  22. # if the json parse or anything else fails, just return the old html_info
  23. return html_info, gr.update()
  24. def plaintext_to_html(text, classname=None):
  25. content = "<br>\n".join(html.escape(x) for x in text.split('\n'))
  26. return f"<p class='{classname}'>{content}</p>" if classname else f"<p>{content}</p>"
  27. def update_logfile(logfile_path, fields):
  28. """Update a logfile from old format to new format to maintain CSV integrity."""
  29. with open(logfile_path, "r", encoding="utf8", newline="") as file:
  30. reader = csv.reader(file)
  31. rows = list(reader)
  32. # blank file: leave it as is
  33. if not rows:
  34. return
  35. # file is already synced, do nothing
  36. if len(rows[0]) == len(fields):
  37. return
  38. rows[0] = fields
  39. # append new fields to each row as empty values
  40. for row in rows[1:]:
  41. while len(row) < len(fields):
  42. row.append("")
  43. with open(logfile_path, "w", encoding="utf8", newline="") as file:
  44. writer = csv.writer(file)
  45. writer.writerows(rows)
  46. def save_files(js_data, images, do_make_zip, index):
  47. filenames = []
  48. fullfns = []
  49. parsed_infotexts = []
  50. # quick dictionary to class object conversion. Its necessary due apply_filename_pattern requiring it
  51. class MyObject:
  52. def __init__(self, d=None):
  53. if d is not None:
  54. for key, value in d.items():
  55. setattr(self, key, value)
  56. data = json.loads(js_data)
  57. p = MyObject(data)
  58. path = shared.opts.outdir_save
  59. save_to_dirs = shared.opts.use_save_to_dirs_for_ui
  60. extension: str = shared.opts.samples_format
  61. start_index = 0
  62. if index > -1 and shared.opts.save_selected_only and (index >= data["index_of_first_image"]): # ensures we are looking at a specific non-grid picture, and we have save_selected_only
  63. images = [images[index]]
  64. start_index = index
  65. os.makedirs(shared.opts.outdir_save, exist_ok=True)
  66. fields = [
  67. "prompt",
  68. "seed",
  69. "width",
  70. "height",
  71. "sampler",
  72. "cfgs",
  73. "steps",
  74. "filename",
  75. "negative_prompt",
  76. "sd_model_name",
  77. "sd_model_hash",
  78. ]
  79. logfile_path = os.path.join(shared.opts.outdir_save, "log.csv")
  80. # NOTE: ensure csv integrity when fields are added by
  81. # updating headers and padding with delimiters where needed
  82. if os.path.exists(logfile_path):
  83. update_logfile(logfile_path, fields)
  84. with open(logfile_path, "a", encoding="utf8", newline='') as file:
  85. at_start = file.tell() == 0
  86. writer = csv.writer(file)
  87. if at_start:
  88. writer.writerow(fields)
  89. for image_index, filedata in enumerate(images, start_index):
  90. image = image_from_url_text(filedata)
  91. is_grid = image_index < p.index_of_first_image
  92. p.batch_index = image_index-1
  93. parameters = parameters_copypaste.parse_generation_parameters(data["infotexts"][image_index], [])
  94. parsed_infotexts.append(parameters)
  95. fullfn, txt_fullfn = modules.images.save_image(image, path, "", seed=parameters['Seed'], prompt=parameters['Prompt'], extension=extension, info=p.infotexts[image_index], grid=is_grid, p=p, save_to_dirs=save_to_dirs)
  96. filename = os.path.relpath(fullfn, path)
  97. filenames.append(filename)
  98. fullfns.append(fullfn)
  99. if txt_fullfn:
  100. filenames.append(os.path.basename(txt_fullfn))
  101. fullfns.append(txt_fullfn)
  102. writer.writerow([parsed_infotexts[0]['Prompt'], parsed_infotexts[0]['Seed'], data["width"], data["height"], data["sampler_name"], data["cfg_scale"], data["steps"], filenames[0], parsed_infotexts[0]['Negative prompt'], data["sd_model_name"], data["sd_model_hash"]])
  103. # Make Zip
  104. if do_make_zip:
  105. p.all_seeds = [parameters['Seed'] for parameters in parsed_infotexts]
  106. namegen = modules.images.FilenameGenerator(p, parsed_infotexts[0]['Seed'], parsed_infotexts[0]['Prompt'], image, True)
  107. zip_filename = namegen.apply(shared.opts.grid_zip_filename_pattern or "[datetime]_[[model_name]]_[seed]-[seed_last]")
  108. zip_filepath = os.path.join(path, f"{zip_filename}.zip")
  109. from zipfile import ZipFile
  110. with ZipFile(zip_filepath, "w") as zip_file:
  111. for i in range(len(fullfns)):
  112. with open(fullfns[i], mode="rb") as f:
  113. zip_file.writestr(filenames[i], f.read())
  114. fullfns.insert(0, zip_filepath)
  115. return gr.File.update(value=fullfns, visible=True), plaintext_to_html(f"Saved: {filenames[0]}")
  116. @dataclasses.dataclass
  117. class OutputPanel:
  118. gallery = None
  119. generation_info = None
  120. infotext = None
  121. html_log = None
  122. button_upscale = None
  123. def create_output_panel(tabname, outdir, toprow=None):
  124. res = OutputPanel()
  125. def open_folder(f, images=None, index=None):
  126. if shared.cmd_opts.hide_ui_dir_config:
  127. return
  128. try:
  129. if 'Sub' in shared.opts.open_dir_button_choice:
  130. image_dir = os.path.split(images[index]["name"].rsplit('?', 1)[0])[0]
  131. if 'temp' in shared.opts.open_dir_button_choice or not ui_tempdir.is_gradio_temp_path(image_dir):
  132. f = image_dir
  133. except Exception:
  134. pass
  135. util.open_folder(f)
  136. with gr.Column(elem_id=f"{tabname}_results"):
  137. if toprow:
  138. toprow.create_inline_toprow_image()
  139. with gr.Column(variant='panel', elem_id=f"{tabname}_results_panel"):
  140. with gr.Group(elem_id=f"{tabname}_gallery_container"):
  141. res.gallery = gr.Gallery(label='Output', show_label=False, elem_id=f"{tabname}_gallery", columns=4, preview=True, height=shared.opts.gallery_height or None)
  142. with gr.Row(elem_id=f"image_buttons_{tabname}", elem_classes="image-buttons"):
  143. open_folder_button = ToolButton(folder_symbol, elem_id=f'{tabname}_open_folder', visible=not shared.cmd_opts.hide_ui_dir_config, tooltip="Open images output directory.")
  144. if tabname != "extras":
  145. save = ToolButton('💾', elem_id=f'save_{tabname}', tooltip=f"Save the image to a dedicated directory ({shared.opts.outdir_save}).")
  146. save_zip = ToolButton('🗃️', elem_id=f'save_zip_{tabname}', tooltip=f"Save zip archive with images to a dedicated directory ({shared.opts.outdir_save})")
  147. buttons = {
  148. 'img2img': ToolButton('🖼️', elem_id=f'{tabname}_send_to_img2img', tooltip="Send image and generation parameters to img2img tab."),
  149. 'inpaint': ToolButton('🎨️', elem_id=f'{tabname}_send_to_inpaint', tooltip="Send image and generation parameters to img2img inpaint tab."),
  150. 'extras': ToolButton('📐', elem_id=f'{tabname}_send_to_extras', tooltip="Send image and generation parameters to extras tab.")
  151. }
  152. if tabname == 'txt2img':
  153. res.button_upscale = ToolButton('✨', elem_id=f'{tabname}_upscale', tooltip="Create an upscaled version of the current image using hires fix settings.")
  154. open_folder_button.click(
  155. fn=lambda images, index: open_folder(shared.opts.outdir_samples or outdir, images, index),
  156. _js="(y, w) => [y, selected_gallery_index()]",
  157. inputs=[
  158. res.gallery,
  159. open_folder_button, # placeholder for index
  160. ],
  161. outputs=[],
  162. )
  163. if tabname != "extras":
  164. download_files = gr.File(None, file_count="multiple", interactive=False, show_label=False, visible=False, elem_id=f'download_files_{tabname}')
  165. with gr.Group():
  166. res.infotext = gr.HTML(elem_id=f'html_info_{tabname}', elem_classes="infotext")
  167. res.html_log = gr.HTML(elem_id=f'html_log_{tabname}', elem_classes="html-log")
  168. res.generation_info = gr.Textbox(visible=False, elem_id=f'generation_info_{tabname}')
  169. if tabname == 'txt2img' or tabname == 'img2img':
  170. generation_info_button = gr.Button(visible=False, elem_id=f"{tabname}_generation_info_button")
  171. generation_info_button.click(
  172. fn=update_generation_info,
  173. _js="function(x, y, z){ return [x, y, selected_gallery_index()] }",
  174. inputs=[res.generation_info, res.infotext, res.infotext],
  175. outputs=[res.infotext, res.infotext],
  176. show_progress=False,
  177. )
  178. save.click(
  179. fn=call_queue.wrap_gradio_call(save_files),
  180. _js="(x, y, z, w) => [x, y, false, selected_gallery_index()]",
  181. inputs=[
  182. res.generation_info,
  183. res.gallery,
  184. res.infotext,
  185. res.infotext,
  186. ],
  187. outputs=[
  188. download_files,
  189. res.html_log,
  190. ],
  191. show_progress=False,
  192. )
  193. save_zip.click(
  194. fn=call_queue.wrap_gradio_call(save_files),
  195. _js="(x, y, z, w) => [x, y, true, selected_gallery_index()]",
  196. inputs=[
  197. res.generation_info,
  198. res.gallery,
  199. res.infotext,
  200. res.infotext,
  201. ],
  202. outputs=[
  203. download_files,
  204. res.html_log,
  205. ]
  206. )
  207. else:
  208. res.generation_info = gr.HTML(elem_id=f'html_info_x_{tabname}')
  209. res.infotext = gr.HTML(elem_id=f'html_info_{tabname}', elem_classes="infotext")
  210. res.html_log = gr.HTML(elem_id=f'html_log_{tabname}')
  211. paste_field_names = []
  212. if tabname == "txt2img":
  213. paste_field_names = modules.scripts.scripts_txt2img.paste_field_names
  214. elif tabname == "img2img":
  215. paste_field_names = modules.scripts.scripts_img2img.paste_field_names
  216. for paste_tabname, paste_button in buttons.items():
  217. parameters_copypaste.register_paste_params_button(parameters_copypaste.ParamBinding(
  218. paste_button=paste_button, tabname=paste_tabname, source_tabname="txt2img" if tabname == "txt2img" else None, source_image_component=res.gallery,
  219. paste_field_names=paste_field_names
  220. ))
  221. return res
  222. def create_refresh_button(refresh_component, refresh_method, refreshed_args, elem_id):
  223. refresh_components = refresh_component if isinstance(refresh_component, list) else [refresh_component]
  224. label = None
  225. for comp in refresh_components:
  226. label = getattr(comp, 'label', None)
  227. if label is not None:
  228. break
  229. def refresh():
  230. refresh_method()
  231. args = refreshed_args() if callable(refreshed_args) else refreshed_args
  232. for k, v in args.items():
  233. for comp in refresh_components:
  234. setattr(comp, k, v)
  235. return [gr.update(**(args or {})) for _ in refresh_components] if len(refresh_components) > 1 else gr.update(**(args or {}))
  236. refresh_button = ToolButton(value=refresh_symbol, elem_id=elem_id, tooltip=f"{label}: refresh" if label else "Refresh")
  237. refresh_button.click(
  238. fn=refresh,
  239. inputs=[],
  240. outputs=refresh_components
  241. )
  242. return refresh_button
  243. def setup_dialog(button_show, dialog, *, button_close=None):
  244. """Sets up the UI so that the dialog (gr.Box) is invisible, and is only shown when buttons_show is clicked, in a fullscreen modal window."""
  245. dialog.visible = False
  246. button_show.click(
  247. fn=lambda: gr.update(visible=True),
  248. inputs=[],
  249. outputs=[dialog],
  250. ).then(fn=None, _js="function(){ popupId('" + dialog.elem_id + "'); }")
  251. if button_close:
  252. button_close.click(fn=None, _js="closePopup")