options.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import os
  2. import json
  3. import sys
  4. from dataclasses import dataclass
  5. import gradio as gr
  6. from modules import errors
  7. from modules.shared_cmd_options import cmd_opts
  8. from modules.paths_internal import script_path
  9. class OptionInfo:
  10. def __init__(self, default=None, label="", component=None, component_args=None, onchange=None, section=None, refresh=None, comment_before='', comment_after='', infotext=None, restrict_api=False, category_id=None):
  11. self.default = default
  12. self.label = label
  13. self.component = component
  14. self.component_args = component_args
  15. self.onchange = onchange
  16. self.section = section
  17. self.category_id = category_id
  18. self.refresh = refresh
  19. self.do_not_save = False
  20. self.comment_before = comment_before
  21. """HTML text that will be added after label in UI"""
  22. self.comment_after = comment_after
  23. """HTML text that will be added before label in UI"""
  24. self.infotext = infotext
  25. self.restrict_api = restrict_api
  26. """If True, the setting will not be accessible via API"""
  27. def link(self, label, url):
  28. self.comment_before += f"[<a href='{url}' target='_blank'>{label}</a>]"
  29. return self
  30. def js(self, label, js_func):
  31. self.comment_before += f"[<a onclick='{js_func}(); return false'>{label}</a>]"
  32. return self
  33. def info(self, info):
  34. self.comment_after += f"<span class='info'>({info})</span>"
  35. return self
  36. def html(self, html):
  37. self.comment_after += html
  38. return self
  39. def needs_restart(self):
  40. self.comment_after += " <span class='info'>(requires restart)</span>"
  41. return self
  42. def needs_reload_ui(self):
  43. self.comment_after += " <span class='info'>(requires Reload UI)</span>"
  44. return self
  45. class OptionHTML(OptionInfo):
  46. def __init__(self, text):
  47. super().__init__(str(text).strip(), label='', component=lambda **kwargs: gr.HTML(elem_classes="settings-info", **kwargs))
  48. self.do_not_save = True
  49. def options_section(section_identifier, options_dict):
  50. for v in options_dict.values():
  51. if len(section_identifier) == 2:
  52. v.section = section_identifier
  53. elif len(section_identifier) == 3:
  54. v.section = section_identifier[0:2]
  55. v.category_id = section_identifier[2]
  56. return options_dict
  57. options_builtin_fields = {"data_labels", "data", "restricted_opts", "typemap"}
  58. class Options:
  59. typemap = {int: float}
  60. def __init__(self, data_labels: dict[str, OptionInfo], restricted_opts):
  61. self.data_labels = data_labels
  62. self.data = {k: v.default for k, v in self.data_labels.items() if not v.do_not_save}
  63. self.restricted_opts = restricted_opts
  64. def __setattr__(self, key, value):
  65. if key in options_builtin_fields:
  66. return super(Options, self).__setattr__(key, value)
  67. if self.data is not None:
  68. if key in self.data or key in self.data_labels:
  69. # Check that settings aren't globally frozen
  70. assert not cmd_opts.freeze_settings, "changing settings is disabled"
  71. # Get the info related to the setting being changed
  72. info = self.data_labels.get(key, None)
  73. if info.do_not_save:
  74. return
  75. # Restrict component arguments
  76. comp_args = info.component_args if info else None
  77. if isinstance(comp_args, dict) and comp_args.get('visible', True) is False:
  78. raise RuntimeError(f"not possible to set '{key}' because it is restricted")
  79. # Check that this section isn't frozen
  80. if cmd_opts.freeze_settings_in_sections is not None:
  81. frozen_sections = list(map(str.strip, cmd_opts.freeze_settings_in_sections.split(','))) # Trim whitespace from section names
  82. section_key = info.section[0]
  83. section_name = info.section[1]
  84. assert section_key not in frozen_sections, f"not possible to set '{key}' because settings in section '{section_name}' ({section_key}) are frozen with --freeze-settings-in-sections"
  85. # Check that this section of the settings isn't frozen
  86. if cmd_opts.freeze_specific_settings is not None:
  87. frozen_keys = list(map(str.strip, cmd_opts.freeze_specific_settings.split(','))) # Trim whitespace from setting keys
  88. assert key not in frozen_keys, f"not possible to set '{key}' because this setting is frozen with --freeze-specific-settings"
  89. # Check shorthand option which disables editing options in "saving-paths"
  90. if cmd_opts.hide_ui_dir_config and key in self.restricted_opts:
  91. raise RuntimeError(f"not possible to set '{key}' because it is restricted with --hide_ui_dir_config")
  92. self.data[key] = value
  93. return
  94. return super(Options, self).__setattr__(key, value)
  95. def __getattr__(self, item):
  96. if item in options_builtin_fields:
  97. return super(Options, self).__getattribute__(item)
  98. if self.data is not None:
  99. if item in self.data:
  100. return self.data[item]
  101. if item in self.data_labels:
  102. return self.data_labels[item].default
  103. return super(Options, self).__getattribute__(item)
  104. def set(self, key, value, is_api=False, run_callbacks=True):
  105. """sets an option and calls its onchange callback, returning True if the option changed and False otherwise"""
  106. oldval = self.data.get(key, None)
  107. if oldval == value:
  108. return False
  109. option = self.data_labels[key]
  110. if option.do_not_save:
  111. return False
  112. if is_api and option.restrict_api:
  113. return False
  114. try:
  115. setattr(self, key, value)
  116. except RuntimeError:
  117. return False
  118. if run_callbacks and option.onchange is not None:
  119. try:
  120. option.onchange()
  121. except Exception as e:
  122. errors.display(e, f"changing setting {key} to {value}")
  123. setattr(self, key, oldval)
  124. return False
  125. return True
  126. def get_default(self, key):
  127. """returns the default value for the key"""
  128. data_label = self.data_labels.get(key)
  129. if data_label is None:
  130. return None
  131. return data_label.default
  132. def save(self, filename):
  133. assert not cmd_opts.freeze_settings, "saving settings is disabled"
  134. with open(filename, "w", encoding="utf8") as file:
  135. json.dump(self.data, file, indent=4, ensure_ascii=False)
  136. def same_type(self, x, y):
  137. if x is None or y is None:
  138. return True
  139. type_x = self.typemap.get(type(x), type(x))
  140. type_y = self.typemap.get(type(y), type(y))
  141. return type_x == type_y
  142. def load(self, filename):
  143. try:
  144. with open(filename, "r", encoding="utf8") as file:
  145. self.data = json.load(file)
  146. except FileNotFoundError:
  147. self.data = {}
  148. except Exception:
  149. errors.report(f'\nCould not load settings\nThe config file "{filename}" is likely corrupted\nIt has been moved to the "tmp/config.json"\nReverting config to default\n\n''', exc_info=True)
  150. os.replace(filename, os.path.join(script_path, "tmp", "config.json"))
  151. self.data = {}
  152. # 1.6.0 VAE defaults
  153. if self.data.get('sd_vae_as_default') is not None and self.data.get('sd_vae_overrides_per_model_preferences') is None:
  154. self.data['sd_vae_overrides_per_model_preferences'] = not self.data.get('sd_vae_as_default')
  155. # 1.1.1 quicksettings list migration
  156. if self.data.get('quicksettings') is not None and self.data.get('quicksettings_list') is None:
  157. self.data['quicksettings_list'] = [i.strip() for i in self.data.get('quicksettings').split(',')]
  158. # 1.4.0 ui_reorder
  159. if isinstance(self.data.get('ui_reorder'), str) and self.data.get('ui_reorder') and "ui_reorder_list" not in self.data:
  160. self.data['ui_reorder_list'] = [i.strip() for i in self.data.get('ui_reorder').split(',')]
  161. bad_settings = 0
  162. for k, v in self.data.items():
  163. info = self.data_labels.get(k, None)
  164. if info is not None and not self.same_type(info.default, v):
  165. print(f"Warning: bad setting value: {k}: {v} ({type(v).__name__}; expected {type(info.default).__name__})", file=sys.stderr)
  166. bad_settings += 1
  167. if bad_settings > 0:
  168. print(f"The program is likely to not work with bad settings.\nSettings file: {filename}\nEither fix the file, or delete it and restart.", file=sys.stderr)
  169. def onchange(self, key, func, call=True):
  170. item = self.data_labels.get(key)
  171. item.onchange = func
  172. if call:
  173. func()
  174. def dumpjson(self):
  175. d = {k: self.data.get(k, v.default) for k, v in self.data_labels.items()}
  176. d["_comments_before"] = {k: v.comment_before for k, v in self.data_labels.items() if v.comment_before is not None}
  177. d["_comments_after"] = {k: v.comment_after for k, v in self.data_labels.items() if v.comment_after is not None}
  178. item_categories = {}
  179. for item in self.data_labels.values():
  180. if item.section[0] is None:
  181. continue
  182. category = categories.mapping.get(item.category_id)
  183. category = "Uncategorized" if category is None else category.label
  184. if category not in item_categories:
  185. item_categories[category] = item.section[1]
  186. # _categories is a list of pairs: [section, category]. Each section (a setting page) will get a special heading above it with the category as text.
  187. d["_categories"] = [[v, k] for k, v in item_categories.items()] + [["Defaults", "Other"]]
  188. return json.dumps(d)
  189. def add_option(self, key, info):
  190. self.data_labels[key] = info
  191. if key not in self.data and not info.do_not_save:
  192. self.data[key] = info.default
  193. def reorder(self):
  194. """Reorder settings so that:
  195. - all items related to section always go together
  196. - all sections belonging to a category go together
  197. - sections inside a category are ordered alphabetically
  198. - categories are ordered by creation order
  199. Category is a superset of sections: for category "postprocessing" there could be multiple sections: "face restoration", "upscaling".
  200. This function also changes items' category_id so that all items belonging to a section have the same category_id.
  201. """
  202. category_ids = {}
  203. section_categories = {}
  204. settings_items = self.data_labels.items()
  205. for _, item in settings_items:
  206. if item.section not in section_categories:
  207. section_categories[item.section] = item.category_id
  208. for _, item in settings_items:
  209. item.category_id = section_categories.get(item.section)
  210. for category_id in categories.mapping:
  211. if category_id not in category_ids:
  212. category_ids[category_id] = len(category_ids)
  213. def sort_key(x):
  214. item: OptionInfo = x[1]
  215. category_order = category_ids.get(item.category_id, len(category_ids))
  216. section_order = item.section[1]
  217. return category_order, section_order
  218. self.data_labels = dict(sorted(settings_items, key=sort_key))
  219. def cast_value(self, key, value):
  220. """casts an arbitrary to the same type as this setting's value with key
  221. Example: cast_value("eta_noise_seed_delta", "12") -> returns 12 (an int rather than str)
  222. """
  223. if value is None:
  224. return None
  225. default_value = self.data_labels[key].default
  226. if default_value is None:
  227. default_value = getattr(self, key, None)
  228. if default_value is None:
  229. return None
  230. expected_type = type(default_value)
  231. if expected_type == bool and value == "False":
  232. value = False
  233. else:
  234. value = expected_type(value)
  235. return value
  236. @dataclass
  237. class OptionsCategory:
  238. id: str
  239. label: str
  240. class OptionsCategories:
  241. def __init__(self):
  242. self.mapping = {}
  243. def register_category(self, category_id, label):
  244. if category_id in self.mapping:
  245. return category_id
  246. self.mapping[category_id] = OptionsCategory(category_id, label)
  247. categories = OptionsCategories()