scripts.py 12.0 KB
Newer Older
1 2 3
import os
import sys
import traceback
4
from collections import namedtuple
5

A
AUTOMATIC 已提交
6
import modules.ui as ui
7 8
import gradio as gr

A
AUTOMATIC 已提交
9
from modules.processing import StableDiffusionProcessing
10 11 12 13
from modules import shared, paths, script_callbacks

AlwaysVisible = object()

A
AUTOMATIC 已提交
14

15 16
class Script:
    filename = None
A
AUTOMATIC 已提交
17 18
    args_from = None
    args_to = None
19 20 21 22 23 24
    alwayson = False

    infotext_fields = None
    """if set in ui(), this is a list of pairs of gradio component + text; the text will be used when
    parsing infotext to set the value for the component; see ui.py's txt2img_paste_fields for an example
    """
25 26

    def title(self):
27 28
        """this function should return the title of the script. This is what will be displayed in the dropdown menu."""

29 30
        raise NotImplementedError()

A
AUTOMATIC 已提交
31
    def ui(self, is_img2img):
32 33 34 35 36
        """this function should create gradio UI elements. See https://gradio.app/docs/#components
        The return value should be an array of all components that are used in processing.
        Values of those returned componenbts will be passed to run() and process() functions.
        """

A
AUTOMATIC 已提交
37 38
        pass

A
AUTOMATIC 已提交
39
    def show(self, is_img2img):
40 41 42 43 44 45 46 47 48
        """
        is_img2img is True if this function is called for the img2img interface, and Fasle otherwise

        This function should return:
         - False if the script should not be shown in UI at all
         - True if the script should be shown in UI if it's scelected in the scripts drowpdown
         - script.AlwaysVisible if the script should be shown in UI at all times
         """

A
AUTOMATIC 已提交
49 50
        return True

51 52 53 54 55 56 57 58 59 60 61
    def run(self, p, *args):
        """
        This function is called if the script has been selected in the script dropdown.
        It must do all processing and return the Processed object with results, same as
        one returned by processing.process_images.

        Usually the processing is done by calling the processing.process_images function.

        args contains all values returned by components from ui()
        """

A
AUTOMATIC 已提交
62 63
        raise NotImplementedError()

64 65 66
    def process(self, p, *args):
        """
        This function is called before processing begins for AlwaysVisible scripts.
A
AUTOMATIC 已提交
67 68 69 70 71 72 73 74 75 76
        You can modify the processing object (p) here, inject hooks, etc.
        args contains all values returned by components from ui()
        """

        pass

    def postprocess(self, p, processed, *args):
        """
        This function is called after processing ends for AlwaysVisible scripts.
        args contains all values returned by components from ui()
77 78 79 80
        """

        pass

A
AUTOMATIC 已提交
81
    def describe(self):
82
        """unused"""
A
AUTOMATIC 已提交
83 84
        return ""

85

86 87 88 89 90 91 92 93 94 95 96
current_basedir = paths.script_path


def basedir():
    """returns the base directory for the current script. For scripts in the main scripts directory,
    this is the main directory (where webui.py resides), and for scripts in extensions directory
    (ie extensions/aesthetic/script/aesthetic.py), this is extension's directory (extensions/aesthetic)
    """
    return current_basedir


A
AUTOMATIC 已提交
97
scripts_data = []
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
ScriptFile = namedtuple("ScriptFile", ["basedir", "filename", "path"])
ScriptClassData = namedtuple("ScriptClassData", ["script_class", "path", "basedir"])


def list_scripts(scriptdirname, extension):
    scripts_list = []

    basedir = os.path.join(paths.script_path, scriptdirname)
    if os.path.exists(basedir):
        for filename in sorted(os.listdir(basedir)):
            scripts_list.append(ScriptFile(paths.script_path, filename, os.path.join(basedir, filename)))

    extdir = os.path.join(paths.script_path, "extensions")
    if os.path.exists(extdir):
        for dirname in sorted(os.listdir(extdir)):
            dirpath = os.path.join(extdir, dirname)
A
AUTOMATIC 已提交
114 115 116
            scriptdirpath = os.path.join(dirpath, scriptdirname)

            if not os.path.isdir(scriptdirpath):
117
                continue
118

A
AUTOMATIC 已提交
119 120
            for filename in sorted(os.listdir(scriptdirpath)):
                scripts_list.append(ScriptFile(dirpath, filename, os.path.join(scriptdirpath, filename)))
121

122
    scripts_list = [x for x in scripts_list if os.path.splitext(x.path)[1].lower() == extension and os.path.isfile(x.path)]
123

124
    return scripts_list
125

126

A
AUTOMATIC 已提交
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
def list_files_with_name(filename):
    res = []

    dirs = [paths.script_path]

    extdir = os.path.join(paths.script_path, "extensions")
    if os.path.exists(extdir):
        dirs += [os.path.join(extdir, d) for d in sorted(os.listdir(extdir))]

    for dirpath in dirs:
        if not os.path.isdir(dirpath):
            continue

        path = os.path.join(dirpath, filename)
        if os.path.isfile(filename):
            res.append(path)

    return res


147 148 149 150 151 152 153 154
def load_scripts():
    global current_basedir
    scripts_data.clear()
    script_callbacks.clear_callbacks()

    scripts_list = list_scripts("scripts", ".py")

    syspath = sys.path
155

156
    for scriptfile in sorted(scripts_list):
157
        try:
158 159 160 161 162
            if scriptfile.basedir != paths.script_path:
                sys.path = [scriptfile.basedir] + sys.path
            current_basedir = scriptfile.basedir

            with open(scriptfile.path, "r", encoding="utf8") as file:
163 164
                text = file.read()

165
            from types import ModuleType
166 167
            compiled = compile(text, scriptfile.path, 'exec')
            module = ModuleType(scriptfile.filename)
168 169 170 171
            exec(compiled, module.__dict__)

            for key, script_class in module.__dict__.items():
                if type(script_class) == type and issubclass(script_class, Script):
172
                    scripts_data.append(ScriptClassData(script_class, scriptfile.path, scriptfile.basedir))
173 174

        except Exception:
175
            print(f"Error loading script: {scriptfile.filename}", file=sys.stderr)
176
            print(traceback.format_exc(), file=sys.stderr)
177

178 179 180 181
        finally:
            sys.path = syspath
            current_basedir = paths.script_path

182 183 184

def wrap_call(func, filename, funcname, *args, default=None, **kwargs):
    try:
A
AUTOMATIC 已提交
185
        res = func(*args, **kwargs)
186 187
        return res
    except Exception:
A
AUTOMATIC 已提交
188
        print(f"Error calling: {filename}/{funcname}", file=sys.stderr)
189 190 191 192 193
        print(traceback.format_exc(), file=sys.stderr)

    return default


A
AUTOMATIC 已提交
194 195 196
class ScriptRunner:
    def __init__(self):
        self.scripts = []
197 198
        self.selectable_scripts = []
        self.alwayson_scripts = []
ふぁ 已提交
199
        self.titles = []
200
        self.infotext_fields = []
A
AUTOMATIC 已提交
201 202

    def setup_ui(self, is_img2img):
203
        for script_class, path, basedir in scripts_data:
A
AUTOMATIC 已提交
204 205 206
            script = script_class()
            script.filename = path

207
            visibility = script.show(is_img2img)
A
AUTOMATIC 已提交
208

209 210 211 212
            if visibility == AlwaysVisible:
                self.scripts.append(script)
                self.alwayson_scripts.append(script)
                script.alwayson = True
A
AUTOMATIC 已提交
213

214 215 216
            elif visibility:
                self.scripts.append(script)
                self.selectable_scripts.append(script)
A
AUTOMATIC 已提交
217

218 219 220 221
        self.titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.selectable_scripts]

        inputs = [None]
        inputs_alwayson = [True]
A
AUTOMATIC 已提交
222

223
        def create_script_ui(script, inputs, inputs_alwayson):
A
AUTOMATIC 已提交
224
            script.args_from = len(inputs)
O
OWKenobi 已提交
225
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
226 227 228 229

            controls = wrap_call(script.ui, script.filename, "ui", is_img2img)

            if controls is None:
230
                return
A
AUTOMATIC 已提交
231

A
AUTOMATIC 已提交
232
            for control in controls:
D
DepFA 已提交
233
                control.custom_script_source = os.path.basename(script.filename)
234 235 236 237 238
                if not script.alwayson:
                    control.visible = False

            if script.infotext_fields is not None:
                self.infotext_fields += script.infotext_fields
A
AUTOMATIC 已提交
239

A
AUTOMATIC 已提交
240
            inputs += controls
241
            inputs_alwayson += [script.alwayson for _ in controls]
A
AUTOMATIC 已提交
242
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
243

244 245 246 247
        for script in self.alwayson_scripts:
            with gr.Group():
                create_script_ui(script, inputs, inputs_alwayson)

X
xmodar 已提交
248
        dropdown = gr.Dropdown(label="Script", elem_id="script_list", choices=["None"] + self.titles, value="None", type="index")
249 250 251 252 253 254
        dropdown.save_to_config = True
        inputs[0] = dropdown

        for script in self.selectable_scripts:
            create_script_ui(script, inputs, inputs_alwayson)

A
AUTOMATIC 已提交
255
        def select_script(script_index):
256 257
            if 0 < script_index <= len(self.selectable_scripts):
                script = self.selectable_scripts[script_index-1]
A
AUTOMATIC 已提交
258 259 260 261 262
                args_from = script.args_from
                args_to = script.args_to
            else:
                args_from = 0
                args_to = 0
A
AUTOMATIC 已提交
263

264
            return [ui.gr_show(True if i == 0 else args_from <= i < args_to or is_alwayson) for i, is_alwayson in enumerate(inputs_alwayson)]
A
AUTOMATIC 已提交
265

ふぁ 已提交
266
        def init_field(title):
ふぁ 已提交
267
            if title == 'None':
ふぁ 已提交
268 269
                return
            script_index = self.titles.index(title)
270
            script = self.selectable_scripts[script_index]
ふぁ 已提交
271 272 273 274
            for i in range(script.args_from, script.args_to):
                inputs[i].visible = True

        dropdown.init_field = init_field
A
AUTOMATIC 已提交
275 276 277 278 279
        dropdown.change(
            fn=select_script,
            inputs=[dropdown],
            outputs=inputs
        )
A
AUTOMATIC 已提交
280

A
AUTOMATIC 已提交
281
        return inputs
A
AUTOMATIC 已提交
282

A
AUTOMATIC 已提交
283 284
    def run(self, p: StableDiffusionProcessing, *args):
        script_index = args[0]
A
AUTOMATIC 已提交
285

A
AUTOMATIC 已提交
286 287
        if script_index == 0:
            return None
A
AUTOMATIC 已提交
288

289
        script = self.selectable_scripts[script_index-1]
A
AUTOMATIC 已提交
290

A
AUTOMATIC 已提交
291 292
        if script is None:
            return None
A
AUTOMATIC 已提交
293

A
AUTOMATIC 已提交
294 295
        script_args = args[script.args_from:script.args_to]
        processed = script.run(p, *script_args)
A
AUTOMATIC 已提交
296

297 298
        shared.total_tqdm.clear()

A
AUTOMATIC 已提交
299
        return processed
A
AUTOMATIC 已提交
300

A
AUTOMATIC 已提交
301
    def process(self, p):
302 303 304 305 306
        for script in self.alwayson_scripts:
            try:
                script_args = p.script_args[script.args_from:script.args_to]
                script.process(p, *script_args)
            except Exception:
A
AUTOMATIC 已提交
307 308 309 310 311 312 313 314 315 316
                print(f"Error running process: {script.filename}", file=sys.stderr)
                print(traceback.format_exc(), file=sys.stderr)

    def postprocess(self, p, processed):
        for script in self.alwayson_scripts:
            try:
                script_args = p.script_args[script.args_from:script.args_to]
                script.postprocess(p, processed, *script_args)
            except Exception:
                print(f"Error running postprocess: {script.filename}", file=sys.stderr)
317 318
                print(traceback.format_exc(), file=sys.stderr)

A
AUTOMATIC 已提交
319
    def reload_sources(self, cache):
D
DepFA 已提交
320 321 322 323 324 325
        for si, script in list(enumerate(self.scripts)):
            with open(script.filename, "r", encoding="utf8") as file:
                args_from = script.args_from
                args_to = script.args_to
                filename = script.filename
                text = file.read()
D
DepFA 已提交
326

D
DepFA 已提交
327
                from types import ModuleType
D
DepFA 已提交
328

A
AUTOMATIC 已提交
329 330 331 332 333 334
                module = cache.get(filename, None)
                if module is None:
                    compiled = compile(text, filename, 'exec')
                    module = ModuleType(script.filename)
                    exec(compiled, module.__dict__)
                    cache[filename] = module
D
DepFA 已提交
335 336 337 338 339 340 341

                for key, script_class in module.__dict__.items():
                    if type(script_class) == type and issubclass(script_class, Script):
                        self.scripts[si] = script_class()
                        self.scripts[si].filename = filename
                        self.scripts[si].args_from = args_from
                        self.scripts[si].args_to = args_to
A
AUTOMATIC 已提交
342

343

A
AUTOMATIC 已提交
344 345
scripts_txt2img = ScriptRunner()
scripts_img2img = ScriptRunner()
D
DepFA 已提交
346

347

D
DepFA 已提交
348
def reload_script_body_only():
A
AUTOMATIC 已提交
349 350 351
    cache = {}
    scripts_txt2img.reload_sources(cache)
    scripts_img2img.reload_sources(cache)
D
DepFA 已提交
352

D
DepFA 已提交
353

354
def reload_scripts():
D
DepFA 已提交
355
    global scripts_txt2img, scripts_img2img
D
DepFA 已提交
356

357
    load_scripts()
D
DepFA 已提交
358

D
DepFA 已提交
359 360
    scripts_txt2img = ScriptRunner()
    scripts_img2img = ScriptRunner()
361