scripts.py 12.4 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
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
    """A gr.Group component that has all script's UI inside it"""
    group = None

23 24 25 26
    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
    """
27 28

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

31 32
        raise NotImplementedError()

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

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

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

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

        pass

75
    def process_batch(self, p, *args, **kwargs):
A
Artem Zagidulin 已提交
76
        """
77 78 79 80 81 82 83
        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 已提交
84 85 86 87
        """

        pass

A
AUTOMATIC 已提交
88 89 90 91
    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()
92 93 94 95
        """

        pass

A
AUTOMATIC 已提交
96
    def describe(self):
97
        """unused"""
A
AUTOMATIC 已提交
98 99
        return ""

100

101 102 103 104 105 106 107 108 109 110 111
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 已提交
112
scripts_data = []
113 114 115 116 117 118 119 120 121 122 123 124
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)))

125 126
    for ext in extensions.active():
        scripts_list += ext.list_files(scriptdirname, extension)
127

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

130
    return scripts_list
131

132

A
AUTOMATIC 已提交
133 134 135
def list_files_with_name(filename):
    res = []

136
    dirs = [paths.script_path] + [ext.path for ext in extensions.active()]
A
AUTOMATIC 已提交
137 138 139 140 141 142 143 144 145 146 147 148

    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


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

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

    syspath = sys.path
157

158
    for scriptfile in sorted(scripts_list):
159
        try:
160 161 162 163 164
            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:
165 166
                text = file.read()

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

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

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

180 181 182 183
        finally:
            sys.path = syspath
            current_basedir = paths.script_path

184 185 186

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

    return default


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

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

209
            visibility = script.show(is_img2img)
A
AUTOMATIC 已提交
210

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

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

220 221 222 223
        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 已提交
224

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

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

            if controls is None:
232
                return
A
AUTOMATIC 已提交
233

A
AUTOMATIC 已提交
234
            for control in controls:
D
DepFA 已提交
235
                control.custom_script_source = os.path.basename(script.filename)
236 237 238

            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
        for script in self.alwayson_scripts:
245
            with gr.Group() as group:
246 247
                create_script_ui(script, inputs, inputs_alwayson)

248 249
            script.group = group

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

        for script in self.selectable_scripts:
255 256 257 258
            with gr.Group(visible=False) as group:
                create_script_ui(script, inputs, inputs_alwayson)

            script.group = group
259

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

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

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

ふぁ 已提交
268
            if title == 'None':
ふぁ 已提交
269
                return
270

ふぁ 已提交
271
            script_index = self.titles.index(title)
272
            self.selectable_scripts[script_index].group.visible = True
ふぁ 已提交
273 274

        dropdown.init_field = init_field
275

A
AUTOMATIC 已提交
276 277 278
        dropdown.change(
            fn=select_script,
            inputs=[dropdown],
279
            outputs=[script.group for script in self.selectable_scripts]
A
AUTOMATIC 已提交
280
        )
A
AUTOMATIC 已提交
281

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

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

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

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

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

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

298 299
        shared.total_tqdm.clear()

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

A
AUTOMATIC 已提交
302
    def process(self, p):
303 304 305 306 307
        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 已提交
308 309 310
                print(f"Error running process: {script.filename}", file=sys.stderr)
                print(traceback.format_exc(), file=sys.stderr)

311
    def process_batch(self, p, **kwargs):
A
Artem Zagidulin 已提交
312 313 314
        for script in self.alwayson_scripts:
            try:
                script_args = p.script_args[script.args_from:script.args_to]
315
                script.process_batch(p, *script_args, **kwargs)
A
Artem Zagidulin 已提交
316
            except Exception:
317
                print(f"Error running process_batch: {script.filename}", file=sys.stderr)
A
Artem Zagidulin 已提交
318 319
                print(traceback.format_exc(), file=sys.stderr)

A
AUTOMATIC 已提交
320 321 322 323 324 325 326
    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)
327 328
                print(traceback.format_exc(), file=sys.stderr)

A
AUTOMATIC 已提交
329
    def reload_sources(self, cache):
D
DepFA 已提交
330 331 332 333 334 335
        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 已提交
336

D
DepFA 已提交
337
                from types import ModuleType
D
DepFA 已提交
338

A
AUTOMATIC 已提交
339 340 341 342 343 344
                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 已提交
345 346 347 348 349 350 351

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

353

A
AUTOMATIC 已提交
354 355
scripts_txt2img = ScriptRunner()
scripts_img2img = ScriptRunner()
D
DepFA 已提交
356

357

D
DepFA 已提交
358
def reload_script_body_only():
A
AUTOMATIC 已提交
359 360 361
    cache = {}
    scripts_txt2img.reload_sources(cache)
    scripts_img2img.reload_sources(cache)
D
DepFA 已提交
362

D
DepFA 已提交
363

364
def reload_scripts():
D
DepFA 已提交
365
    global scripts_txt2img, scripts_img2img
D
DepFA 已提交
366

367
    load_scripts()
D
DepFA 已提交
368

D
DepFA 已提交
369 370
    scripts_txt2img = ScriptRunner()
    scripts_img2img = ScriptRunner()
371