Path: blob/master/modules/options.py
3055 views
import os1import json2import sys3from dataclasses import dataclass45import gradio as gr67from modules import errors8from modules.shared_cmd_options import cmd_opts9from modules.paths_internal import script_path101112class OptionInfo:13def __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):14self.default = default15self.label = label16self.component = component17self.component_args = component_args18self.onchange = onchange19self.section = section20self.category_id = category_id21self.refresh = refresh22self.do_not_save = False2324self.comment_before = comment_before25"""HTML text that will be added after label in UI"""2627self.comment_after = comment_after28"""HTML text that will be added before label in UI"""2930self.infotext = infotext3132self.restrict_api = restrict_api33"""If True, the setting will not be accessible via API"""3435def link(self, label, url):36self.comment_before += f"[<a href='{url}' target='_blank'>{label}</a>]"37return self3839def js(self, label, js_func):40self.comment_before += f"[<a onclick='{js_func}(); return false'>{label}</a>]"41return self4243def info(self, info):44self.comment_after += f"<span class='info'>({info})</span>"45return self4647def html(self, html):48self.comment_after += html49return self5051def needs_restart(self):52self.comment_after += " <span class='info'>(requires restart)</span>"53return self5455def needs_reload_ui(self):56self.comment_after += " <span class='info'>(requires Reload UI)</span>"57return self585960class OptionHTML(OptionInfo):61def __init__(self, text):62super().__init__(str(text).strip(), label='', component=lambda **kwargs: gr.HTML(elem_classes="settings-info", **kwargs))6364self.do_not_save = True656667def options_section(section_identifier, options_dict):68for v in options_dict.values():69if len(section_identifier) == 2:70v.section = section_identifier71elif len(section_identifier) == 3:72v.section = section_identifier[0:2]73v.category_id = section_identifier[2]7475return options_dict767778options_builtin_fields = {"data_labels", "data", "restricted_opts", "typemap"}798081class Options:82typemap = {int: float}8384def __init__(self, data_labels: dict[str, OptionInfo], restricted_opts):85self.data_labels = data_labels86self.data = {k: v.default for k, v in self.data_labels.items() if not v.do_not_save}87self.restricted_opts = restricted_opts8889def __setattr__(self, key, value):90if key in options_builtin_fields:91return super(Options, self).__setattr__(key, value)9293if self.data is not None:94if key in self.data or key in self.data_labels:9596# Check that settings aren't globally frozen97assert not cmd_opts.freeze_settings, "changing settings is disabled"9899# Get the info related to the setting being changed100info = self.data_labels.get(key, None)101if info.do_not_save:102return103104# Restrict component arguments105comp_args = info.component_args if info else None106if isinstance(comp_args, dict) and comp_args.get('visible', True) is False:107raise RuntimeError(f"not possible to set '{key}' because it is restricted")108109# Check that this section isn't frozen110if cmd_opts.freeze_settings_in_sections is not None:111frozen_sections = list(map(str.strip, cmd_opts.freeze_settings_in_sections.split(','))) # Trim whitespace from section names112section_key = info.section[0]113section_name = info.section[1]114assert 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"115116# Check that this section of the settings isn't frozen117if cmd_opts.freeze_specific_settings is not None:118frozen_keys = list(map(str.strip, cmd_opts.freeze_specific_settings.split(','))) # Trim whitespace from setting keys119assert key not in frozen_keys, f"not possible to set '{key}' because this setting is frozen with --freeze-specific-settings"120121# Check shorthand option which disables editing options in "saving-paths"122if cmd_opts.hide_ui_dir_config and key in self.restricted_opts:123raise RuntimeError(f"not possible to set '{key}' because it is restricted with --hide_ui_dir_config")124125self.data[key] = value126return127128return super(Options, self).__setattr__(key, value)129130def __getattr__(self, item):131if item in options_builtin_fields:132return super(Options, self).__getattribute__(item)133134if self.data is not None:135if item in self.data:136return self.data[item]137138if item in self.data_labels:139return self.data_labels[item].default140141return super(Options, self).__getattribute__(item)142143def set(self, key, value, is_api=False, run_callbacks=True):144"""sets an option and calls its onchange callback, returning True if the option changed and False otherwise"""145146oldval = self.data.get(key, None)147if oldval == value:148return False149150option = self.data_labels[key]151if option.do_not_save:152return False153154if is_api and option.restrict_api:155return False156157try:158setattr(self, key, value)159except RuntimeError:160return False161162if run_callbacks and option.onchange is not None:163try:164option.onchange()165except Exception as e:166errors.display(e, f"changing setting {key} to {value}")167setattr(self, key, oldval)168return False169170return True171172def get_default(self, key):173"""returns the default value for the key"""174175data_label = self.data_labels.get(key)176if data_label is None:177return None178179return data_label.default180181def save(self, filename):182assert not cmd_opts.freeze_settings, "saving settings is disabled"183184with open(filename, "w", encoding="utf8") as file:185json.dump(self.data, file, indent=4, ensure_ascii=False)186187def same_type(self, x, y):188if x is None or y is None:189return True190191type_x = self.typemap.get(type(x), type(x))192type_y = self.typemap.get(type(y), type(y))193194return type_x == type_y195196def load(self, filename):197try:198with open(filename, "r", encoding="utf8") as file:199self.data = json.load(file)200except FileNotFoundError:201self.data = {}202except Exception:203errors.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)204os.replace(filename, os.path.join(script_path, "tmp", "config.json"))205self.data = {}206# 1.6.0 VAE defaults207if self.data.get('sd_vae_as_default') is not None and self.data.get('sd_vae_overrides_per_model_preferences') is None:208self.data['sd_vae_overrides_per_model_preferences'] = not self.data.get('sd_vae_as_default')209210# 1.1.1 quicksettings list migration211if self.data.get('quicksettings') is not None and self.data.get('quicksettings_list') is None:212self.data['quicksettings_list'] = [i.strip() for i in self.data.get('quicksettings').split(',')]213214# 1.4.0 ui_reorder215if isinstance(self.data.get('ui_reorder'), str) and self.data.get('ui_reorder') and "ui_reorder_list" not in self.data:216self.data['ui_reorder_list'] = [i.strip() for i in self.data.get('ui_reorder').split(',')]217218bad_settings = 0219for k, v in self.data.items():220info = self.data_labels.get(k, None)221if info is not None and not self.same_type(info.default, v):222print(f"Warning: bad setting value: {k}: {v} ({type(v).__name__}; expected {type(info.default).__name__})", file=sys.stderr)223bad_settings += 1224225if bad_settings > 0:226print(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)227228def onchange(self, key, func, call=True):229item = self.data_labels.get(key)230item.onchange = func231232if call:233func()234235def dumpjson(self):236d = {k: self.data.get(k, v.default) for k, v in self.data_labels.items()}237d["_comments_before"] = {k: v.comment_before for k, v in self.data_labels.items() if v.comment_before is not None}238d["_comments_after"] = {k: v.comment_after for k, v in self.data_labels.items() if v.comment_after is not None}239240item_categories = {}241for item in self.data_labels.values():242if item.section[0] is None:243continue244245category = categories.mapping.get(item.category_id)246category = "Uncategorized" if category is None else category.label247if category not in item_categories:248item_categories[category] = item.section[1]249250# _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.251d["_categories"] = [[v, k] for k, v in item_categories.items()] + [["Defaults", "Other"]]252253return json.dumps(d)254255def add_option(self, key, info):256self.data_labels[key] = info257if key not in self.data and not info.do_not_save:258self.data[key] = info.default259260def reorder(self):261"""Reorder settings so that:262- all items related to section always go together263- all sections belonging to a category go together264- sections inside a category are ordered alphabetically265- categories are ordered by creation order266267Category is a superset of sections: for category "postprocessing" there could be multiple sections: "face restoration", "upscaling".268269This function also changes items' category_id so that all items belonging to a section have the same category_id.270"""271272category_ids = {}273section_categories = {}274275settings_items = self.data_labels.items()276for _, item in settings_items:277if item.section not in section_categories:278section_categories[item.section] = item.category_id279280for _, item in settings_items:281item.category_id = section_categories.get(item.section)282283for category_id in categories.mapping:284if category_id not in category_ids:285category_ids[category_id] = len(category_ids)286287def sort_key(x):288item: OptionInfo = x[1]289category_order = category_ids.get(item.category_id, len(category_ids))290section_order = item.section[1]291292return category_order, section_order293294self.data_labels = dict(sorted(settings_items, key=sort_key))295296def cast_value(self, key, value):297"""casts an arbitrary to the same type as this setting's value with key298Example: cast_value("eta_noise_seed_delta", "12") -> returns 12 (an int rather than str)299"""300301if value is None:302return None303304default_value = self.data_labels[key].default305if default_value is None:306default_value = getattr(self, key, None)307if default_value is None:308return None309310expected_type = type(default_value)311if expected_type == bool and value == "False":312value = False313else:314value = expected_type(value)315316return value317318319@dataclass320class OptionsCategory:321id: str322label: str323324class OptionsCategories:325def __init__(self):326self.mapping = {}327328def register_category(self, category_id, label):329if category_id in self.mapping:330return category_id331332self.mapping[category_id] = OptionsCategory(category_id, label)333334335categories = OptionsCategories()336337338