scripts.py 12.1 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
from modules import shared, paths, script_callbacks, extensions
11 12 13

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
        You can modify the processing object (p) here, inject hooks, etc.
        args contains all values returned by components from ui()
        """

        pass

A
Artem Zagidulin 已提交
73 74 75 76 77 78 79
    def process_one(self, p, *args):
        """
        Same as process(), but called for every iteration
        """

        pass

A
AUTOMATIC 已提交
80 81 82 83
    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()
84 85 86 87
        """

        pass

A
AUTOMATIC 已提交
88
    def describe(self):
89
        """unused"""
A
AUTOMATIC 已提交
90 91
        return ""

92

93 94 95 96 97 98 99 100 101 102 103
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 已提交
104
scripts_data = []
105 106 107 108 109 110 111 112 113 114 115 116
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)))

117 118
    for ext in extensions.active():
        scripts_list += ext.list_files(scriptdirname, extension)
119

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

122
    return scripts_list
123

124

A
AUTOMATIC 已提交
125 126 127
def list_files_with_name(filename):
    res = []

128
    dirs = [paths.script_path] + [ext.path for ext in extensions.active()]
A
AUTOMATIC 已提交
129 130 131 132 133 134 135 136 137 138 139 140

    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


141 142 143 144 145 146 147 148
def load_scripts():
    global current_basedir
    scripts_data.clear()
    script_callbacks.clear_callbacks()

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

    syspath = sys.path
149

150
    for scriptfile in sorted(scripts_list):
151
        try:
152 153 154 155 156
            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:
157 158
                text = file.read()

159
            from types import ModuleType
160 161
            compiled = compile(text, scriptfile.path, 'exec')
            module = ModuleType(scriptfile.filename)
162 163 164 165
            exec(compiled, module.__dict__)

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

        except Exception:
169
            print(f"Error loading script: {scriptfile.filename}", file=sys.stderr)
170
            print(traceback.format_exc(), file=sys.stderr)
171

172 173 174 175
        finally:
            sys.path = syspath
            current_basedir = paths.script_path

176 177 178

def wrap_call(func, filename, funcname, *args, default=None, **kwargs):
    try:
A
AUTOMATIC 已提交
179
        res = func(*args, **kwargs)
180 181
        return res
    except Exception:
A
AUTOMATIC 已提交
182
        print(f"Error calling: {filename}/{funcname}", file=sys.stderr)
183 184 185 186 187
        print(traceback.format_exc(), file=sys.stderr)

    return default


A
AUTOMATIC 已提交
188 189 190
class ScriptRunner:
    def __init__(self):
        self.scripts = []
191 192
        self.selectable_scripts = []
        self.alwayson_scripts = []
ふぁ 已提交
193
        self.titles = []
194
        self.infotext_fields = []
A
AUTOMATIC 已提交
195 196

    def setup_ui(self, is_img2img):
197
        for script_class, path, basedir in scripts_data:
A
AUTOMATIC 已提交
198 199 200
            script = script_class()
            script.filename = path

201
            visibility = script.show(is_img2img)
A
AUTOMATIC 已提交
202

203 204 205 206
            if visibility == AlwaysVisible:
                self.scripts.append(script)
                self.alwayson_scripts.append(script)
                script.alwayson = True
A
AUTOMATIC 已提交
207

208 209 210
            elif visibility:
                self.scripts.append(script)
                self.selectable_scripts.append(script)
A
AUTOMATIC 已提交
211

212 213 214 215
        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 已提交
216

217
        def create_script_ui(script, inputs, inputs_alwayson):
A
AUTOMATIC 已提交
218
            script.args_from = len(inputs)
O
OWKenobi 已提交
219
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
220 221 222 223

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

            if controls is None:
224
                return
A
AUTOMATIC 已提交
225

A
AUTOMATIC 已提交
226
            for control in controls:
D
DepFA 已提交
227
                control.custom_script_source = os.path.basename(script.filename)
228 229 230 231 232
                if not script.alwayson:
                    control.visible = False

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

A
AUTOMATIC 已提交
234
            inputs += controls
235
            inputs_alwayson += [script.alwayson for _ in controls]
A
AUTOMATIC 已提交
236
            script.args_to = len(inputs)
A
AUTOMATIC 已提交
237

238 239 240 241
        for script in self.alwayson_scripts:
            with gr.Group():
                create_script_ui(script, inputs, inputs_alwayson)

X
xmodar 已提交
242
        dropdown = gr.Dropdown(label="Script", elem_id="script_list", choices=["None"] + self.titles, value="None", type="index")
243 244 245 246 247 248
        dropdown.save_to_config = True
        inputs[0] = dropdown

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

A
AUTOMATIC 已提交
249
        def select_script(script_index):
250 251
            if 0 < script_index <= len(self.selectable_scripts):
                script = self.selectable_scripts[script_index-1]
A
AUTOMATIC 已提交
252 253 254 255 256
                args_from = script.args_from
                args_to = script.args_to
            else:
                args_from = 0
                args_to = 0
A
AUTOMATIC 已提交
257

258
            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 已提交
259

ふぁ 已提交
260
        def init_field(title):
ふぁ 已提交
261
            if title == 'None':
ふぁ 已提交
262 263
                return
            script_index = self.titles.index(title)
264
            script = self.selectable_scripts[script_index]
ふぁ 已提交
265 266 267 268
            for i in range(script.args_from, script.args_to):
                inputs[i].visible = True

        dropdown.init_field = init_field
A
AUTOMATIC 已提交
269 270 271 272 273
        dropdown.change(
            fn=select_script,
            inputs=[dropdown],
            outputs=inputs
        )
A
AUTOMATIC 已提交
274

A
AUTOMATIC 已提交
275
        return inputs
A
AUTOMATIC 已提交
276

A
AUTOMATIC 已提交
277 278
    def run(self, p: StableDiffusionProcessing, *args):
        script_index = args[0]
A
AUTOMATIC 已提交
279

A
AUTOMATIC 已提交
280 281
        if script_index == 0:
            return None
A
AUTOMATIC 已提交
282

283
        script = self.selectable_scripts[script_index-1]
A
AUTOMATIC 已提交
284

A
AUTOMATIC 已提交
285 286
        if script is None:
            return None
A
AUTOMATIC 已提交
287

A
AUTOMATIC 已提交
288 289
        script_args = args[script.args_from:script.args_to]
        processed = script.run(p, *script_args)
A
AUTOMATIC 已提交
290

291 292
        shared.total_tqdm.clear()

A
AUTOMATIC 已提交
293
        return processed
A
AUTOMATIC 已提交
294

A
AUTOMATIC 已提交
295
    def process(self, p):
296 297 298 299 300
        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 已提交
301 302 303
                print(f"Error running process: {script.filename}", file=sys.stderr)
                print(traceback.format_exc(), file=sys.stderr)

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

A
AUTOMATIC 已提交
313 314 315 316 317 318 319
    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)
320 321
                print(traceback.format_exc(), file=sys.stderr)

A
AUTOMATIC 已提交
322
    def reload_sources(self, cache):
D
DepFA 已提交
323 324 325 326 327 328
        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 已提交
329

D
DepFA 已提交
330
                from types import ModuleType
D
DepFA 已提交
331

A
AUTOMATIC 已提交
332 333 334 335 336 337
                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 已提交
338 339 340 341 342 343 344

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

346

A
AUTOMATIC 已提交
347 348
scripts_txt2img = ScriptRunner()
scripts_img2img = ScriptRunner()
D
DepFA 已提交
349

350

D
DepFA 已提交
351
def reload_script_body_only():
A
AUTOMATIC 已提交
352 353 354
    cache = {}
    scripts_txt2img.reload_sources(cache)
    scripts_img2img.reload_sources(cache)
D
DepFA 已提交
355

D
DepFA 已提交
356

357
def reload_scripts():
D
DepFA 已提交
358
    global scripts_txt2img, scripts_img2img
D
DepFA 已提交
359

360
    load_scripts()
D
DepFA 已提交
361

D
DepFA 已提交
362 363
    scripts_txt2img = ScriptRunner()
    scripts_img2img = ScriptRunner()
364