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

import gradio as gr

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

AlwaysVisible = object()

A
AUTOMATIC 已提交
13

14 15
class Script:
    filename = None
A
AUTOMATIC 已提交
16 17
    args_from = None
    args_to = None
18 19
    alwayson = False

20 21 22
    is_txt2img = False
    is_img2img = False

23 24 25
    """A gr.Group component that has all script's UI inside it"""
    group = None

26 27 28 29
    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
    """
30 31

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

34 35
        raise NotImplementedError()

A
AUTOMATIC 已提交
36
    def ui(self, is_img2img):
37 38 39 40 41
        """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 已提交
42 43
        pass

A
AUTOMATIC 已提交
44
    def show(self, is_img2img):
45 46 47 48 49 50 51 52 53
        """
        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 已提交
54 55
        return True

56 57 58 59 60 61 62 63 64 65 66
    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 已提交
67 68
        raise NotImplementedError()

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

        pass

78
    def process_batch(self, p, *args, **kwargs):
A
Artem Zagidulin 已提交
79
        """
80 81 82 83 84 85 86
        Same as process(), but called for every batch.

        **kwargs will have those items:
          - batch_number - index of current batch, from 0 to number of batches-1
          - prompts - list of prompts for current batch; you can change contents of this list but changing the number of entries will likely break things
          - seeds - list of seeds for current batch
          - subseeds - list of subseeds for current batch
A
Artem Zagidulin 已提交
87 88 89 90
        """

        pass

A
AUTOMATIC 已提交
91 92 93 94
    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()
95 96 97 98
        """

        pass

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
    def before_component(self, component, **kwargs):
        """
        Called before a component is created.
        Use elem_id/label fields of kwargs to figure out which component it is.
        This can be useful to inject your own components somewhere in the middle of vanilla UI.
        You can return created components in the ui() function to add them to the list of arguments for your processing functions
        """

        pass

    def after_component(self, component, **kwargs):
        """
        Called after a component is created. Same as above.
        """

        pass

A
AUTOMATIC 已提交
116
    def describe(self):
117
        """unused"""
A
AUTOMATIC 已提交
118 119
        return ""

120

121 122 123 124 125 126 127 128 129 130 131
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 已提交
132
scripts_data = []
133 134 135 136 137 138 139 140 141 142 143 144
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)))

145 146
    for ext in extensions.active():
        scripts_list += ext.list_files(scriptdirname, extension)
147

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

150
    return scripts_list
151

152

A
AUTOMATIC 已提交
153 154 155
def list_files_with_name(filename):
    res = []

156
    dirs = [paths.script_path] + [ext.path for ext in extensions.active()]
A
AUTOMATIC 已提交
157 158 159 160 161 162

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

        path = os.path.join(dirpath, filename)
T
Tong Zeng 已提交
163
        if os.path.isfile(path):
A
AUTOMATIC 已提交
164 165 166 167 168
            res.append(path)

    return res


169 170 171 172 173 174 175 176
def load_scripts():
    global current_basedir
    scripts_data.clear()
    script_callbacks.clear_callbacks()

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

    syspath = sys.path
177

178
    for scriptfile in sorted(scripts_list):
179
        try:
180 181 182 183
            if scriptfile.basedir != paths.script_path:
                sys.path = [scriptfile.basedir] + sys.path
            current_basedir = scriptfile.basedir

184
            module = script_loading.load_module(scriptfile.path)
185 186 187

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

        except Exception:
191
            print(f"Error loading script: {scriptfile.filename}", file=sys.stderr)
192
            print(traceback.format_exc(), file=sys.stderr)
193

194 195 196 197
        finally:
            sys.path = syspath
            current_basedir = paths.script_path

198 199 200

def wrap_call(func, filename, funcname, *args, default=None, **kwargs):
    try:
A
AUTOMATIC 已提交
201
        res = func(*args, **kwargs)
202 203
        return res
    except Exception:
A
AUTOMATIC 已提交
204
        print(f"Error calling: {filename}/{funcname}", file=sys.stderr)
205 206 207 208 209
        print(traceback.format_exc(), file=sys.stderr)

    return default


A
AUTOMATIC 已提交
210 211 212
class ScriptRunner:
    def __init__(self):
        self.scripts = []
213 214
        self.selectable_scripts = []
        self.alwayson_scripts = []
ふぁ 已提交
215
        self.titles = []
216
        self.infotext_fields = []
A
AUTOMATIC 已提交
217

218 219 220 221 222
    def initialize_scripts(self, is_img2img):
        self.scripts.clear()
        self.alwayson_scripts.clear()
        self.selectable_scripts.clear()

223
        for script_class, path, basedir in scripts_data:
A
AUTOMATIC 已提交
224 225
            script = script_class()
            script.filename = path
226 227
            script.is_txt2img = not is_img2img
            script.is_img2img = is_img2img
A
AUTOMATIC 已提交
228

229
            visibility = script.show(script.is_img2img)
A
AUTOMATIC 已提交
230

231 232 233 234
            if visibility == AlwaysVisible:
                self.scripts.append(script)
                self.alwayson_scripts.append(script)
                script.alwayson = True
A
AUTOMATIC 已提交
235

236 237 238
            elif visibility:
                self.scripts.append(script)
                self.selectable_scripts.append(script)
A
AUTOMATIC 已提交
239

240
    def setup_ui(self):
241 242 243 244
        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 已提交
245

246
        def create_script_ui(script, inputs, inputs_alwayson):
A
AUTOMATIC 已提交
247
            script.args_from = len(inputs)
O
OWKenobi 已提交
248
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
249

250
            controls = wrap_call(script.ui, script.filename, "ui", script.is_img2img)
A
AUTOMATIC 已提交
251 252

            if controls is None:
253
                return
A
AUTOMATIC 已提交
254

A
AUTOMATIC 已提交
255
            for control in controls:
D
DepFA 已提交
256
                control.custom_script_source = os.path.basename(script.filename)
257 258 259

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

A
AUTOMATIC 已提交
261
            inputs += controls
262
            inputs_alwayson += [script.alwayson for _ in controls]
A
AUTOMATIC 已提交
263
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
264

265
        for script in self.alwayson_scripts:
266
            with gr.Group() as group:
267 268
                create_script_ui(script, inputs, inputs_alwayson)

269 270
            script.group = group

X
xmodar 已提交
271
        dropdown = gr.Dropdown(label="Script", elem_id="script_list", choices=["None"] + self.titles, value="None", type="index")
272 273 274 275
        dropdown.save_to_config = True
        inputs[0] = dropdown

        for script in self.selectable_scripts:
276 277 278 279
            with gr.Group(visible=False) as group:
                create_script_ui(script, inputs, inputs_alwayson)

            script.group = group
280

A
AUTOMATIC 已提交
281
        def select_script(script_index):
282
            selected_script = self.selectable_scripts[script_index - 1] if script_index>0 else None
A
AUTOMATIC 已提交
283

284
            return [gr.update(visible=selected_script == s) for s in self.selectable_scripts]
A
AUTOMATIC 已提交
285

ふぁ 已提交
286
        def init_field(title):
287 288
            """called when an initial value is set from ui-config.json to show script's UI components"""

ふぁ 已提交
289
            if title == 'None':
ふぁ 已提交
290
                return
291

ふぁ 已提交
292
            script_index = self.titles.index(title)
293
            self.selectable_scripts[script_index].group.visible = True
ふぁ 已提交
294 295

        dropdown.init_field = init_field
296

A
AUTOMATIC 已提交
297 298 299
        dropdown.change(
            fn=select_script,
            inputs=[dropdown],
300
            outputs=[script.group for script in self.selectable_scripts]
A
AUTOMATIC 已提交
301
        )
A
AUTOMATIC 已提交
302

A
AUTOMATIC 已提交
303
        return inputs
A
AUTOMATIC 已提交
304

A
AUTOMATIC 已提交
305 306
    def run(self, p: StableDiffusionProcessing, *args):
        script_index = args[0]
A
AUTOMATIC 已提交
307

A
AUTOMATIC 已提交
308 309
        if script_index == 0:
            return None
A
AUTOMATIC 已提交
310

311
        script = self.selectable_scripts[script_index-1]
A
AUTOMATIC 已提交
312

A
AUTOMATIC 已提交
313 314
        if script is None:
            return None
A
AUTOMATIC 已提交
315

A
AUTOMATIC 已提交
316 317
        script_args = args[script.args_from:script.args_to]
        processed = script.run(p, *script_args)
A
AUTOMATIC 已提交
318

319 320
        shared.total_tqdm.clear()

A
AUTOMATIC 已提交
321
        return processed
A
AUTOMATIC 已提交
322

A
AUTOMATIC 已提交
323
    def process(self, p):
324 325 326 327 328
        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 已提交
329 330 331
                print(f"Error running process: {script.filename}", file=sys.stderr)
                print(traceback.format_exc(), file=sys.stderr)

332
    def process_batch(self, p, **kwargs):
A
Artem Zagidulin 已提交
333 334 335
        for script in self.alwayson_scripts:
            try:
                script_args = p.script_args[script.args_from:script.args_to]
336
                script.process_batch(p, *script_args, **kwargs)
A
Artem Zagidulin 已提交
337
            except Exception:
338
                print(f"Error running process_batch: {script.filename}", file=sys.stderr)
A
Artem Zagidulin 已提交
339 340
                print(traceback.format_exc(), file=sys.stderr)

A
AUTOMATIC 已提交
341 342 343 344 345 346 347
    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)
348 349
                print(traceback.format_exc(), file=sys.stderr)

350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
    def before_component(self, component, **kwargs):
        for script in self.scripts:
            try:
                script.before_component(component, **kwargs)
            except Exception:
                print(f"Error running before_component: {script.filename}", file=sys.stderr)
                print(traceback.format_exc(), file=sys.stderr)

    def after_component(self, component, **kwargs):
        for script in self.scripts:
            try:
                script.after_component(component, **kwargs)
            except Exception:
                print(f"Error running after_component: {script.filename}", file=sys.stderr)
                print(traceback.format_exc(), file=sys.stderr)

A
AUTOMATIC 已提交
366
    def reload_sources(self, cache):
D
DepFA 已提交
367
        for si, script in list(enumerate(self.scripts)):
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
            args_from = script.args_from
            args_to = script.args_to
            filename = script.filename

            module = cache.get(filename, None)
            if module is None:
                module = script_loading.load_module(script.filename)
                cache[filename] = module

            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 已提交
383

384

A
AUTOMATIC 已提交
385 386
scripts_txt2img = ScriptRunner()
scripts_img2img = ScriptRunner()
387
scripts_current: ScriptRunner = None
D
DepFA 已提交
388

389

D
DepFA 已提交
390
def reload_script_body_only():
A
AUTOMATIC 已提交
391 392 393
    cache = {}
    scripts_txt2img.reload_sources(cache)
    scripts_img2img.reload_sources(cache)
D
DepFA 已提交
394

D
DepFA 已提交
395

396
def reload_scripts():
D
DepFA 已提交
397
    global scripts_txt2img, scripts_img2img
D
DepFA 已提交
398

399
    load_scripts()
D
DepFA 已提交
400

D
DepFA 已提交
401 402
    scripts_txt2img = ScriptRunner()
    scripts_img2img = ScriptRunner()
403

404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422

def IOComponent_init(self, *args, **kwargs):
    if scripts_current is not None:
        scripts_current.before_component(self, **kwargs)

    script_callbacks.before_component_callback(self, **kwargs)

    res = original_IOComponent_init(self, *args, **kwargs)

    script_callbacks.after_component_callback(self, **kwargs)

    if scripts_current is not None:
        scripts_current.after_component(self, **kwargs)

    return res


original_IOComponent_init = gr.components.IOComponent.__init__
gr.components.IOComponent.__init__ = IOComponent_init