2024-01-04 05:21:53 +08:00
import os
2023-08-09 15:25:35 +08:00
import json
import sys
2023-11-26 22:56:16 +08:00
from dataclasses import dataclass
2023-08-09 15:25:35 +08:00
import gradio as gr
from modules import errors
from modules . shared_cmd_options import cmd_opts
2024-01-04 05:21:53 +08:00
from modules . paths_internal import script_path
2023-08-09 15:25:35 +08:00
class OptionInfo :
2023-11-26 22:56:16 +08:00
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 ) :
2023-08-09 15:25:35 +08:00
self . default = default
self . label = label
self . component = component
self . component_args = component_args
self . onchange = onchange
self . section = section
2023-11-26 22:56:16 +08:00
self . category_id = category_id
2023-08-09 15:25:35 +08:00
self . refresh = refresh
self . do_not_save = False
self . comment_before = comment_before
""" HTML text that will be added after label in UI """
self . comment_after = comment_after
""" HTML text that will be added before label in UI """
2023-08-10 21:42:26 +08:00
self . infotext = infotext
2023-08-21 12:59:57 +08:00
self . restrict_api = restrict_api
""" If True, the setting will not be accessible via API """
2023-08-09 15:25:35 +08:00
def link ( self , label , url ) :
self . comment_before + = f " [<a href= ' { url } ' target= ' _blank ' > { label } </a>] "
return self
def js ( self , label , js_func ) :
self . comment_before + = f " [<a onclick= ' { js_func } (); return false ' > { label } </a>] "
return self
def info ( self , info ) :
self . comment_after + = f " <span class= ' info ' >( { info } )</span> "
return self
def html ( self , html ) :
self . comment_after + = html
return self
def needs_restart ( self ) :
self . comment_after + = " <span class= ' info ' >(requires restart)</span> "
return self
def needs_reload_ui ( self ) :
self . comment_after + = " <span class= ' info ' >(requires Reload UI)</span> "
return self
class OptionHTML ( OptionInfo ) :
def __init__ ( self , text ) :
super ( ) . __init__ ( str ( text ) . strip ( ) , label = ' ' , component = lambda * * kwargs : gr . HTML ( elem_classes = " settings-info " , * * kwargs ) )
self . do_not_save = True
def options_section ( section_identifier , options_dict ) :
for v in options_dict . values ( ) :
2023-11-26 22:56:16 +08:00
if len ( section_identifier ) == 2 :
v . section = section_identifier
elif len ( section_identifier ) == 3 :
v . section = section_identifier [ 0 : 2 ]
v . category_id = section_identifier [ 2 ]
2023-08-09 15:25:35 +08:00
return options_dict
options_builtin_fields = { " data_labels " , " data " , " restricted_opts " , " typemap " }
class Options :
typemap = { int : float }
2023-08-21 12:59:57 +08:00
def __init__ ( self , data_labels : dict [ str , OptionInfo ] , restricted_opts ) :
2023-08-09 15:25:35 +08:00
self . data_labels = data_labels
2023-11-26 16:36:17 +08:00
self . data = { k : v . default for k , v in self . data_labels . items ( ) if not v . do_not_save }
2023-08-09 15:25:35 +08:00
self . restricted_opts = restricted_opts
def __setattr__ ( self , key , value ) :
if key in options_builtin_fields :
return super ( Options , self ) . __setattr__ ( key , value )
if self . data is not None :
if key in self . data or key in self . data_labels :
2023-10-30 00:05:01 +08:00
2023-10-29 23:40:58 +08:00
# Check that settings aren't globally frozen
2023-08-09 15:25:35 +08:00
assert not cmd_opts . freeze_settings , " changing settings is disabled "
2023-10-29 23:40:58 +08:00
# Get the info related to the setting being changed
2023-08-09 15:25:35 +08:00
info = self . data_labels . get ( key , None )
if info . do_not_save :
return
2023-10-29 23:40:58 +08:00
# Restrict component arguments
2023-08-09 15:25:35 +08:00
comp_args = info . component_args if info else None
if isinstance ( comp_args , dict ) and comp_args . get ( ' visible ' , True ) is False :
2023-10-29 23:40:58 +08:00
raise RuntimeError ( f " not possible to set ' { key } ' because it is restricted " )
2023-08-09 15:25:35 +08:00
2023-10-29 23:40:58 +08:00
# Check that this section isn't frozen
if cmd_opts . freeze_settings_in_sections is not None :
frozen_sections = list ( map ( str . strip , cmd_opts . freeze_settings_in_sections . split ( ' , ' ) ) ) # Trim whitespace from section names
section_key = info . section [ 0 ]
section_name = info . section [ 1 ]
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 "
# Check that this section of the settings isn't frozen
if cmd_opts . freeze_specific_settings is not None :
frozen_keys = list ( map ( str . strip , cmd_opts . freeze_specific_settings . split ( ' , ' ) ) ) # Trim whitespace from setting keys
assert key not in frozen_keys , f " not possible to set ' { key } ' because this setting is frozen with --freeze-specific-settings "
# Check shorthand option which disables editing options in "saving-paths"
2023-08-09 15:25:35 +08:00
if cmd_opts . hide_ui_dir_config and key in self . restricted_opts :
2023-10-29 23:40:58 +08:00
raise RuntimeError ( f " not possible to set ' { key } ' because it is restricted with --hide_ui_dir_config " )
2023-08-09 15:25:35 +08:00
self . data [ key ] = value
return
return super ( Options , self ) . __setattr__ ( key , value )
def __getattr__ ( self , item ) :
if item in options_builtin_fields :
return super ( Options , self ) . __getattribute__ ( item )
if self . data is not None :
if item in self . data :
return self . data [ item ]
if item in self . data_labels :
return self . data_labels [ item ] . default
return super ( Options , self ) . __getattribute__ ( item )
2023-08-21 13:58:15 +08:00
def set ( self , key , value , is_api = False , run_callbacks = True ) :
2023-08-09 15:25:35 +08:00
""" sets an option and calls its onchange callback, returning True if the option changed and False otherwise """
oldval = self . data . get ( key , None )
if oldval == value :
return False
2023-08-21 12:59:57 +08:00
option = self . data_labels [ key ]
if option . do_not_save :
return False
if is_api and option . restrict_api :
2023-08-09 15:25:35 +08:00
return False
try :
setattr ( self , key , value )
except RuntimeError :
return False
2023-08-21 13:58:15 +08:00
if run_callbacks and option . onchange is not None :
2023-08-09 15:25:35 +08:00
try :
2023-08-21 12:59:57 +08:00
option . onchange ( )
2023-08-09 15:25:35 +08:00
except Exception as e :
errors . display ( e , f " changing setting { key } to { value } " )
setattr ( self , key , oldval )
return False
return True
def get_default ( self , key ) :
""" returns the default value for the key """
data_label = self . data_labels . get ( key )
if data_label is None :
return None
return data_label . default
def save ( self , filename ) :
assert not cmd_opts . freeze_settings , " saving settings is disabled "
with open ( filename , " w " , encoding = " utf8 " ) as file :
2023-11-26 20:55:50 +08:00
json . dump ( self . data , file , indent = 4 , ensure_ascii = False )
2023-08-09 15:25:35 +08:00
def same_type ( self , x , y ) :
if x is None or y is None :
return True
type_x = self . typemap . get ( type ( x ) , type ( x ) )
type_y = self . typemap . get ( type ( y ) , type ( y ) )
return type_x == type_y
def load ( self , filename ) :
2024-01-04 05:21:53 +08:00
try :
with open ( filename , " r " , encoding = " utf8 " ) as file :
self . data = json . load ( file )
2024-01-26 16:16:53 +08:00
except FileNotFoundError :
self . data = { }
2024-01-04 05:21:53 +08:00
except Exception :
errors . report ( f ' \n Could not load settings \n The config file " { filename } " is likely corrupted \n It has been moved to the " tmp/config.json " \n Reverting config to default \n \n ' ' ' , exc_info = True )
os . replace ( filename , os . path . join ( script_path , " tmp " , " config.json " ) )
self . data = { }
2023-08-09 15:25:35 +08:00
# 1.6.0 VAE defaults
if self . data . get ( ' sd_vae_as_default ' ) is not None and self . data . get ( ' sd_vae_overrides_per_model_preferences ' ) is None :
self . data [ ' sd_vae_overrides_per_model_preferences ' ] = not self . data . get ( ' sd_vae_as_default ' )
# 1.1.1 quicksettings list migration
if self . data . get ( ' quicksettings ' ) is not None and self . data . get ( ' quicksettings_list ' ) is None :
self . data [ ' quicksettings_list ' ] = [ i . strip ( ) for i in self . data . get ( ' quicksettings ' ) . split ( ' , ' ) ]
# 1.4.0 ui_reorder
if isinstance ( self . data . get ( ' ui_reorder ' ) , str ) and self . data . get ( ' ui_reorder ' ) and " ui_reorder_list " not in self . data :
self . data [ ' ui_reorder_list ' ] = [ i . strip ( ) for i in self . data . get ( ' ui_reorder ' ) . split ( ' , ' ) ]
bad_settings = 0
for k , v in self . data . items ( ) :
info = self . data_labels . get ( k , None )
if info is not None and not self . same_type ( info . default , v ) :
print ( f " Warning: bad setting value: { k } : { v } ( { type ( v ) . __name__ } ; expected { type ( info . default ) . __name__ } ) " , file = sys . stderr )
bad_settings + = 1
if bad_settings > 0 :
print ( f " The program is likely to not work with bad settings. \n Settings file: { filename } \n Either fix the file, or delete it and restart. " , file = sys . stderr )
def onchange ( self , key , func , call = True ) :
item = self . data_labels . get ( key )
item . onchange = func
if call :
func ( )
def dumpjson ( self ) :
d = { k : self . data . get ( k , v . default ) for k , v in self . data_labels . items ( ) }
d [ " _comments_before " ] = { k : v . comment_before for k , v in self . data_labels . items ( ) if v . comment_before is not None }
d [ " _comments_after " ] = { k : v . comment_after for k , v in self . data_labels . items ( ) if v . comment_after is not None }
2023-11-26 22:56:16 +08:00
item_categories = { }
for item in self . data_labels . values ( ) :
category = categories . mapping . get ( item . category_id )
category = " Uncategorized " if category is None else category . label
if category not in item_categories :
item_categories [ category ] = item . section [ 1 ]
# _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.
d [ " _categories " ] = [ [ v , k ] for k , v in item_categories . items ( ) ] + [ [ " Defaults " , " Other " ] ]
2023-08-09 15:25:35 +08:00
return json . dumps ( d )
def add_option ( self , key , info ) :
self . data_labels [ key ] = info
2023-11-26 16:36:17 +08:00
if key not in self . data and not info . do_not_save :
2023-09-12 21:43:35 +08:00
self . data [ key ] = info . default
2023-08-09 15:25:35 +08:00
def reorder ( self ) :
2023-11-26 22:56:16 +08:00
""" Reorder settings so that:
- all items related to section always go together
- all sections belonging to a category go together
- sections inside a category are ordered alphabetically
- categories are ordered by creation order
Category is a superset of sections : for category " postprocessing " there could be multiple sections : " face restoration " , " upscaling " .
This function also changes items ' category_id so that all items belonging to a section have the same category_id.
"""
category_ids = { }
section_categories = { }
2023-08-09 15:25:35 +08:00
settings_items = self . data_labels . items ( )
for _ , item in settings_items :
2023-11-26 22:56:16 +08:00
if item . section not in section_categories :
section_categories [ item . section ] = item . category_id
for _ , item in settings_items :
item . category_id = section_categories . get ( item . section )
for category_id in categories . mapping :
if category_id not in category_ids :
category_ids [ category_id ] = len ( category_ids )
2023-08-09 15:25:35 +08:00
2023-11-26 22:56:16 +08:00
def sort_key ( x ) :
item : OptionInfo = x [ 1 ]
category_order = category_ids . get ( item . category_id , len ( category_ids ) )
section_order = item . section [ 1 ]
return category_order , section_order
self . data_labels = dict ( sorted ( settings_items , key = sort_key ) )
2023-08-09 15:25:35 +08:00
def cast_value ( self , key , value ) :
""" casts an arbitrary to the same type as this setting ' s value with key
Example : cast_value ( " eta_noise_seed_delta " , " 12 " ) - > returns 12 ( an int rather than str )
"""
if value is None :
return None
default_value = self . data_labels [ key ] . default
if default_value is None :
default_value = getattr ( self , key , None )
if default_value is None :
return None
expected_type = type ( default_value )
if expected_type == bool and value == " False " :
value = False
else :
value = expected_type ( value )
return value
2023-11-26 22:56:16 +08:00
@dataclass
class OptionsCategory :
id : str
label : str
class OptionsCategories :
def __init__ ( self ) :
self . mapping = { }
def register_category ( self , category_id , label ) :
if category_id in self . mapping :
return category_id
self . mapping [ category_id ] = OptionsCategory ( category_id , label )
categories = OptionsCategories ( )