Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
automatic1111
GitHub Repository: automatic1111/stable-diffusion-webui
Path: blob/master/modules/options.py
3055 views
1
import os
2
import json
3
import sys
4
from dataclasses import dataclass
5
6
import gradio as gr
7
8
from modules import errors
9
from modules.shared_cmd_options import cmd_opts
10
from modules.paths_internal import script_path
11
12
13
class OptionInfo:
14
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):
15
self.default = default
16
self.label = label
17
self.component = component
18
self.component_args = component_args
19
self.onchange = onchange
20
self.section = section
21
self.category_id = category_id
22
self.refresh = refresh
23
self.do_not_save = False
24
25
self.comment_before = comment_before
26
"""HTML text that will be added after label in UI"""
27
28
self.comment_after = comment_after
29
"""HTML text that will be added before label in UI"""
30
31
self.infotext = infotext
32
33
self.restrict_api = restrict_api
34
"""If True, the setting will not be accessible via API"""
35
36
def link(self, label, url):
37
self.comment_before += f"[<a href='{url}' target='_blank'>{label}</a>]"
38
return self
39
40
def js(self, label, js_func):
41
self.comment_before += f"[<a onclick='{js_func}(); return false'>{label}</a>]"
42
return self
43
44
def info(self, info):
45
self.comment_after += f"<span class='info'>({info})</span>"
46
return self
47
48
def html(self, html):
49
self.comment_after += html
50
return self
51
52
def needs_restart(self):
53
self.comment_after += " <span class='info'>(requires restart)</span>"
54
return self
55
56
def needs_reload_ui(self):
57
self.comment_after += " <span class='info'>(requires Reload UI)</span>"
58
return self
59
60
61
class OptionHTML(OptionInfo):
62
def __init__(self, text):
63
super().__init__(str(text).strip(), label='', component=lambda **kwargs: gr.HTML(elem_classes="settings-info", **kwargs))
64
65
self.do_not_save = True
66
67
68
def options_section(section_identifier, options_dict):
69
for v in options_dict.values():
70
if len(section_identifier) == 2:
71
v.section = section_identifier
72
elif len(section_identifier) == 3:
73
v.section = section_identifier[0:2]
74
v.category_id = section_identifier[2]
75
76
return options_dict
77
78
79
options_builtin_fields = {"data_labels", "data", "restricted_opts", "typemap"}
80
81
82
class Options:
83
typemap = {int: float}
84
85
def __init__(self, data_labels: dict[str, OptionInfo], restricted_opts):
86
self.data_labels = data_labels
87
self.data = {k: v.default for k, v in self.data_labels.items() if not v.do_not_save}
88
self.restricted_opts = restricted_opts
89
90
def __setattr__(self, key, value):
91
if key in options_builtin_fields:
92
return super(Options, self).__setattr__(key, value)
93
94
if self.data is not None:
95
if key in self.data or key in self.data_labels:
96
97
# Check that settings aren't globally frozen
98
assert not cmd_opts.freeze_settings, "changing settings is disabled"
99
100
# Get the info related to the setting being changed
101
info = self.data_labels.get(key, None)
102
if info.do_not_save:
103
return
104
105
# Restrict component arguments
106
comp_args = info.component_args if info else None
107
if isinstance(comp_args, dict) and comp_args.get('visible', True) is False:
108
raise RuntimeError(f"not possible to set '{key}' because it is restricted")
109
110
# Check that this section isn't frozen
111
if cmd_opts.freeze_settings_in_sections is not None:
112
frozen_sections = list(map(str.strip, cmd_opts.freeze_settings_in_sections.split(','))) # Trim whitespace from section names
113
section_key = info.section[0]
114
section_name = info.section[1]
115
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"
116
117
# Check that this section of the settings isn't frozen
118
if cmd_opts.freeze_specific_settings is not None:
119
frozen_keys = list(map(str.strip, cmd_opts.freeze_specific_settings.split(','))) # Trim whitespace from setting keys
120
assert key not in frozen_keys, f"not possible to set '{key}' because this setting is frozen with --freeze-specific-settings"
121
122
# Check shorthand option which disables editing options in "saving-paths"
123
if cmd_opts.hide_ui_dir_config and key in self.restricted_opts:
124
raise RuntimeError(f"not possible to set '{key}' because it is restricted with --hide_ui_dir_config")
125
126
self.data[key] = value
127
return
128
129
return super(Options, self).__setattr__(key, value)
130
131
def __getattr__(self, item):
132
if item in options_builtin_fields:
133
return super(Options, self).__getattribute__(item)
134
135
if self.data is not None:
136
if item in self.data:
137
return self.data[item]
138
139
if item in self.data_labels:
140
return self.data_labels[item].default
141
142
return super(Options, self).__getattribute__(item)
143
144
def set(self, key, value, is_api=False, run_callbacks=True):
145
"""sets an option and calls its onchange callback, returning True if the option changed and False otherwise"""
146
147
oldval = self.data.get(key, None)
148
if oldval == value:
149
return False
150
151
option = self.data_labels[key]
152
if option.do_not_save:
153
return False
154
155
if is_api and option.restrict_api:
156
return False
157
158
try:
159
setattr(self, key, value)
160
except RuntimeError:
161
return False
162
163
if run_callbacks and option.onchange is not None:
164
try:
165
option.onchange()
166
except Exception as e:
167
errors.display(e, f"changing setting {key} to {value}")
168
setattr(self, key, oldval)
169
return False
170
171
return True
172
173
def get_default(self, key):
174
"""returns the default value for the key"""
175
176
data_label = self.data_labels.get(key)
177
if data_label is None:
178
return None
179
180
return data_label.default
181
182
def save(self, filename):
183
assert not cmd_opts.freeze_settings, "saving settings is disabled"
184
185
with open(filename, "w", encoding="utf8") as file:
186
json.dump(self.data, file, indent=4, ensure_ascii=False)
187
188
def same_type(self, x, y):
189
if x is None or y is None:
190
return True
191
192
type_x = self.typemap.get(type(x), type(x))
193
type_y = self.typemap.get(type(y), type(y))
194
195
return type_x == type_y
196
197
def load(self, filename):
198
try:
199
with open(filename, "r", encoding="utf8") as file:
200
self.data = json.load(file)
201
except FileNotFoundError:
202
self.data = {}
203
except Exception:
204
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)
205
os.replace(filename, os.path.join(script_path, "tmp", "config.json"))
206
self.data = {}
207
# 1.6.0 VAE defaults
208
if self.data.get('sd_vae_as_default') is not None and self.data.get('sd_vae_overrides_per_model_preferences') is None:
209
self.data['sd_vae_overrides_per_model_preferences'] = not self.data.get('sd_vae_as_default')
210
211
# 1.1.1 quicksettings list migration
212
if self.data.get('quicksettings') is not None and self.data.get('quicksettings_list') is None:
213
self.data['quicksettings_list'] = [i.strip() for i in self.data.get('quicksettings').split(',')]
214
215
# 1.4.0 ui_reorder
216
if isinstance(self.data.get('ui_reorder'), str) and self.data.get('ui_reorder') and "ui_reorder_list" not in self.data:
217
self.data['ui_reorder_list'] = [i.strip() for i in self.data.get('ui_reorder').split(',')]
218
219
bad_settings = 0
220
for k, v in self.data.items():
221
info = self.data_labels.get(k, None)
222
if info is not None and not self.same_type(info.default, v):
223
print(f"Warning: bad setting value: {k}: {v} ({type(v).__name__}; expected {type(info.default).__name__})", file=sys.stderr)
224
bad_settings += 1
225
226
if bad_settings > 0:
227
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)
228
229
def onchange(self, key, func, call=True):
230
item = self.data_labels.get(key)
231
item.onchange = func
232
233
if call:
234
func()
235
236
def dumpjson(self):
237
d = {k: self.data.get(k, v.default) for k, v in self.data_labels.items()}
238
d["_comments_before"] = {k: v.comment_before for k, v in self.data_labels.items() if v.comment_before is not None}
239
d["_comments_after"] = {k: v.comment_after for k, v in self.data_labels.items() if v.comment_after is not None}
240
241
item_categories = {}
242
for item in self.data_labels.values():
243
if item.section[0] is None:
244
continue
245
246
category = categories.mapping.get(item.category_id)
247
category = "Uncategorized" if category is None else category.label
248
if category not in item_categories:
249
item_categories[category] = item.section[1]
250
251
# _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.
252
d["_categories"] = [[v, k] for k, v in item_categories.items()] + [["Defaults", "Other"]]
253
254
return json.dumps(d)
255
256
def add_option(self, key, info):
257
self.data_labels[key] = info
258
if key not in self.data and not info.do_not_save:
259
self.data[key] = info.default
260
261
def reorder(self):
262
"""Reorder settings so that:
263
- all items related to section always go together
264
- all sections belonging to a category go together
265
- sections inside a category are ordered alphabetically
266
- categories are ordered by creation order
267
268
Category is a superset of sections: for category "postprocessing" there could be multiple sections: "face restoration", "upscaling".
269
270
This function also changes items' category_id so that all items belonging to a section have the same category_id.
271
"""
272
273
category_ids = {}
274
section_categories = {}
275
276
settings_items = self.data_labels.items()
277
for _, item in settings_items:
278
if item.section not in section_categories:
279
section_categories[item.section] = item.category_id
280
281
for _, item in settings_items:
282
item.category_id = section_categories.get(item.section)
283
284
for category_id in categories.mapping:
285
if category_id not in category_ids:
286
category_ids[category_id] = len(category_ids)
287
288
def sort_key(x):
289
item: OptionInfo = x[1]
290
category_order = category_ids.get(item.category_id, len(category_ids))
291
section_order = item.section[1]
292
293
return category_order, section_order
294
295
self.data_labels = dict(sorted(settings_items, key=sort_key))
296
297
def cast_value(self, key, value):
298
"""casts an arbitrary to the same type as this setting's value with key
299
Example: cast_value("eta_noise_seed_delta", "12") -> returns 12 (an int rather than str)
300
"""
301
302
if value is None:
303
return None
304
305
default_value = self.data_labels[key].default
306
if default_value is None:
307
default_value = getattr(self, key, None)
308
if default_value is None:
309
return None
310
311
expected_type = type(default_value)
312
if expected_type == bool and value == "False":
313
value = False
314
else:
315
value = expected_type(value)
316
317
return value
318
319
320
@dataclass
321
class OptionsCategory:
322
id: str
323
label: str
324
325
class OptionsCategories:
326
def __init__(self):
327
self.mapping = {}
328
329
def register_category(self, category_id, label):
330
if category_id in self.mapping:
331
return category_id
332
333
self.mapping[category_id] = OptionsCategory(category_id, label)
334
335
336
categories = OptionsCategories()
337
338